From c24478e8bc2785e7f032721d5777e339c21afc74 Mon Sep 17 00:00:00 2001 From: alm Date: Fri, 16 Feb 2024 18:53:25 +0200 Subject: [PATCH 001/936] docs: fix link in Contributing.md (#3845) --- Contributing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Contributing.md b/Contributing.md index 332645542d7..dad543856cc 100644 --- a/Contributing.md +++ b/Contributing.md @@ -35,7 +35,7 @@ Helping others often reveals bugs, documentation weaknesses, and missing APIs. I ### Implement issues ready for development -Issues where the solution is clear and work is not in progress use the [needs-implementer](https://github.com/PyO3/pyo3/issues?q=is%3Aissue+is%3Aopen+label%3Aneeds-implemeter) label. +Issues where the solution is clear and work is not in progress use the [needs-implementer](https://github.com/PyO3/pyo3/issues?q=is%3Aissue+is%3Aopen+label%3Aneeds-implementer) label. Don't be afraid if the solution is not clear to you! The core PyO3 contributors will be happy to mentor you through any questions you have to help you write the solution. From 940804fe0d311e087098c4638083258b100093f5 Mon Sep 17 00:00:00 2001 From: Lily Foote Date: Sat, 17 Feb 2024 00:27:45 +0000 Subject: [PATCH 002/936] Pyerr value bound (#3820) * Implement PyErr::value_bound * Use PyErr::value_bound in conversions * Implement PyErr::from_value_bound * Remove unnecessary clone * Return a reference from PyErr::value_bound * Avoid clone in PyErr::from_value_bound * Use PyErr::from_value_bound instead of from_value * Remove unnecessary .as_borrowed() calls * Remove unused import * Simplify UnraisableCapture.hook * Use Bound::from_owned_ptr_or_opt in fn cause * Update PyErrState::lazy to take Py This is easier to work with elsewhere than `&PyAny` or `Bound<'py, PyAny>`. * Add Bound PyUnicodeDecodeError constructors * Update PyErr::from_value_bound to take Bound * Simplify PyErr::from_value * Simplify PyErr::value * Remove unnecessary reference * Simplify Pyerr::cause implementation * Simplify PyUnicodeDecodeError::new_bound --- guide/src/python_from_rust.md | 2 +- src/conversions/anyhow.rs | 4 +- src/conversions/chrono.rs | 8 ++-- src/conversions/eyre.rs | 4 +- src/coroutine.rs | 2 +- src/err/err_state.rs | 10 +++-- src/err/impls.rs | 6 ++- src/err/mod.rs | 83 +++++++++++++++++++++++------------ src/exceptions.rs | 67 +++++++++++++++++++++++----- src/impl_/extract_argument.rs | 7 ++- src/tests/common.rs | 4 +- src/types/string.rs | 40 ++++++++++------- src/types/traceback.rs | 6 +-- tests/test_coroutine.rs | 2 +- 14 files changed, 165 insertions(+), 80 deletions(-) diff --git a/guide/src/python_from_rust.md b/guide/src/python_from_rust.md index 1ac1a7c217a..0b412c871c7 100644 --- a/guide/src/python_from_rust.md +++ b/guide/src/python_from_rust.md @@ -479,7 +479,7 @@ class House(object): } Err(e) => { house - .call_method1("__exit__", (e.get_type_bound(py), e.value(py), e.traceback_bound(py))) + .call_method1("__exit__", (e.get_type_bound(py), e.value_bound(py), e.traceback_bound(py))) .unwrap(); } } diff --git a/src/conversions/anyhow.rs b/src/conversions/anyhow.rs index 453799c6e8b..a490bd4ce31 100644 --- a/src/conversions/anyhow.rs +++ b/src/conversions/anyhow.rs @@ -149,7 +149,7 @@ mod test_anyhow { Python::with_gil(|py| { let locals = [("err", pyerr)].into_py_dict_bound(py); let pyerr = py.run_bound("raise err", None, Some(&locals)).unwrap_err(); - assert_eq!(pyerr.value(py).to_string(), expected_contents); + assert_eq!(pyerr.value_bound(py).to_string(), expected_contents); }) } @@ -166,7 +166,7 @@ mod test_anyhow { Python::with_gil(|py| { let locals = [("err", pyerr)].into_py_dict_bound(py); let pyerr = py.run_bound("raise err", None, Some(&locals)).unwrap_err(); - assert_eq!(pyerr.value(py).to_string(), expected_contents); + assert_eq!(pyerr.value_bound(py).to_string(), expected_contents); }) } diff --git a/src/conversions/chrono.rs b/src/conversions/chrono.rs index d67818e645c..6737c16f8cf 100644 --- a/src/conversions/chrono.rs +++ b/src/conversions/chrono.rs @@ -590,7 +590,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(py).repr().unwrap().to_string(); + let msg = res.value_bound(py).repr().unwrap().to_string(); assert_eq!(msg, "TypeError(\"zoneinfo.ZoneInfo(key='Europe/London') is not a fixed offset timezone\")"); }); } @@ -605,7 +605,7 @@ mod tests { // Now test that converting a PyDateTime with tzinfo to a NaiveDateTime fails let res: PyResult = py_datetime.extract(); assert_eq!( - res.unwrap_err().value(py).repr().unwrap().to_string(), + res.unwrap_err().value_bound(py).repr().unwrap().to_string(), "TypeError('expected a datetime without tzinfo')" ); }); @@ -620,14 +620,14 @@ mod tests { // Now test that converting a PyDateTime with tzinfo to a NaiveDateTime fails let res: PyResult> = py_datetime.extract(); assert_eq!( - res.unwrap_err().value(py).repr().unwrap().to_string(), + res.unwrap_err().value_bound(py).repr().unwrap().to_string(), "TypeError('expected a datetime with non-None tzinfo')" ); // Now test that converting a PyDateTime with tzinfo to a NaiveDateTime fails let res: PyResult> = py_datetime.extract(); assert_eq!( - res.unwrap_err().value(py).repr().unwrap().to_string(), + res.unwrap_err().value_bound(py).repr().unwrap().to_string(), "TypeError('expected a datetime with non-None tzinfo')" ); }); diff --git a/src/conversions/eyre.rs b/src/conversions/eyre.rs index d25a10af9ee..64ed4a0320f 100644 --- a/src/conversions/eyre.rs +++ b/src/conversions/eyre.rs @@ -154,7 +154,7 @@ mod tests { Python::with_gil(|py| { let locals = [("err", pyerr)].into_py_dict_bound(py); let pyerr = py.run_bound("raise err", None, Some(&locals)).unwrap_err(); - assert_eq!(pyerr.value(py).to_string(), expected_contents); + assert_eq!(pyerr.value_bound(py).to_string(), expected_contents); }) } @@ -171,7 +171,7 @@ mod tests { Python::with_gil(|py| { let locals = [("err", pyerr)].into_py_dict_bound(py); let pyerr = py.run_bound("raise err", None, Some(&locals)).unwrap_err(); - assert_eq!(pyerr.value(py).to_string(), expected_contents); + assert_eq!(pyerr.value_bound(py).to_string(), expected_contents); }) } diff --git a/src/coroutine.rs b/src/coroutine.rs index c4b5ddf3185..3a0df983076 100644 --- a/src/coroutine.rs +++ b/src/coroutine.rs @@ -78,7 +78,7 @@ impl Coroutine { (Some(exc), Some(cb)) => cb.throw(exc.as_ref(py)), (Some(exc), None) => { self.close(); - return Err(PyErr::from_value(exc.as_ref(py))); + return Err(PyErr::from_value_bound(exc.into_bound(py))); } (None, _) => {} } diff --git a/src/err/err_state.rs b/src/err/err_state.rs index 863b276307e..eb03d8b9ff2 100644 --- a/src/err/err_state.rs +++ b/src/err/err_state.rs @@ -101,19 +101,20 @@ where } impl PyErrState { - pub(crate) fn lazy(ptype: &PyAny, args: impl PyErrArguments + 'static) -> Self { - let ptype = ptype.into(); + pub(crate) fn lazy(ptype: Py, args: impl PyErrArguments + 'static) -> Self { PyErrState::Lazy(Box::new(move |py| PyErrStateLazyFnOutput { ptype, pvalue: args.arguments(py), })) } - pub(crate) fn normalized(pvalue: &PyBaseException) -> Self { + pub(crate) fn normalized(pvalue: Bound<'_, PyBaseException>) -> Self { + #[cfg(not(Py_3_12))] + use crate::types::any::PyAnyMethods; + Self::Normalized(PyErrStateNormalized { #[cfg(not(Py_3_12))] ptype: pvalue.get_type().into(), - pvalue: pvalue.into(), #[cfg(not(Py_3_12))] ptraceback: unsafe { Py::from_owned_ptr_or_opt( @@ -121,6 +122,7 @@ impl PyErrState { ffi::PyException_GetTraceback(pvalue.as_ptr()), ) }, + pvalue: pvalue.into(), }) } diff --git a/src/err/impls.rs b/src/err/impls.rs index f49ac3eb1a8..f14a839b471 100644 --- a/src/err/impls.rs +++ b/src/err/impls.rs @@ -124,6 +124,8 @@ mod tests { #[test] fn io_errors() { + use crate::types::any::PyAnyMethods; + let check_err = |kind, expected_ty| { Python::with_gil(|py| { let rust_err = io::Error::new(kind, "some error msg"); @@ -139,8 +141,8 @@ mod tests { let py_err_recovered_from_rust_err: PyErr = rust_err_from_py_err.into(); assert!(py_err_recovered_from_rust_err - .value(py) - .is(py_error_clone.value(py))); // It should be the same exception + .value_bound(py) + .is(py_error_clone.value_bound(py))); // It should be the same exception }) }; diff --git a/src/err/mod.rs b/src/err/mod.rs index b5e72a71bf9..b5cd02c0954 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -187,7 +187,19 @@ impl PyErr { where A: PyErrArguments + Send + Sync + 'static, { - PyErr::from_state(PyErrState::lazy(ty, args)) + PyErr::from_state(PyErrState::lazy(ty.into(), args)) + } + + /// 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" + ) + )] + pub fn from_value(obj: &PyAny) -> PyErr { + PyErr::from_value_bound(obj.as_borrowed().to_owned()) } /// Creates a new PyErr. @@ -201,33 +213,38 @@ impl PyErr { /// # Examples /// ```rust /// use pyo3::prelude::*; + /// use pyo3::PyTypeInfo; /// use pyo3::exceptions::PyTypeError; - /// use pyo3::types::{PyType, PyString}; + /// use pyo3::types::PyString; /// /// Python::with_gil(|py| { /// // Case #1: Exception object - /// let err = PyErr::from_value(PyTypeError::new_err("some type error").value(py)); + /// let err = PyErr::from_value_bound(PyTypeError::new_err("some type error") + /// .value_bound(py).clone().into_any()); /// assert_eq!(err.to_string(), "TypeError: some type error"); /// /// // Case #2: Exception type - /// let err = PyErr::from_value(PyType::new::(py)); + /// let err = PyErr::from_value_bound(PyTypeError::type_object_bound(py).into_any()); /// assert_eq!(err.to_string(), "TypeError: "); /// /// // Case #3: Invalid exception value - /// let err = PyErr::from_value(PyString::new_bound(py, "foo").as_gil_ref()); + /// let err = PyErr::from_value_bound(PyString::new_bound(py, "foo").into_any()); /// assert_eq!( /// err.to_string(), /// "TypeError: exceptions must derive from BaseException" /// ); /// }); /// ``` - pub fn from_value(obj: &PyAny) -> PyErr { - let state = if let Ok(obj) = obj.downcast::() { - PyErrState::normalized(obj) - } else { - // Assume obj is Type[Exception]; let later normalization handle if this - // is not the case - PyErrState::lazy(obj, obj.py().None()) + pub fn from_value_bound(obj: Bound<'_, PyAny>) -> PyErr { + let state = match obj.downcast_into::() { + Ok(obj) => PyErrState::normalized(obj), + Err(err) => { + // Assume obj is Type[Exception]; let later normalization handle if this + // is not the case + let obj = err.into_inner(); + let py = obj.py(); + PyErrState::lazy(obj.into_py(py), py.None()) + } }; PyErr::from_state(state) @@ -260,6 +277,18 @@ impl PyErr { self.normalized(py).ptype(py) } + /// 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" + ) + )] + 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 @@ -270,11 +299,11 @@ impl PyErr { /// Python::with_gil(|py| { /// let err: PyErr = PyTypeError::new_err(("some type error",)); /// assert!(err.is_instance_of::(py)); - /// assert_eq!(err.value(py).to_string(), "some type error"); + /// assert_eq!(err.value_bound(py).to_string(), "some type error"); /// }); /// ``` - pub fn value<'py>(&'py self, py: Python<'py>) -> &'py PyBaseException { - self.normalized(py).pvalue.as_ref(py) + pub fn value_bound<'py>(&self, py: Python<'py>) -> &Bound<'py, PyBaseException> { + self.normalized(py).pvalue.bind(py) } /// Consumes self to take ownership of the exception value contained in this error. @@ -527,7 +556,7 @@ impl PyErr { pub fn display(&self, py: Python<'_>) { #[cfg(Py_3_12)] unsafe { - ffi::PyErr_DisplayException(self.value(py).as_ptr()) + ffi::PyErr_DisplayException(self.value_bound(py).as_ptr()) } #[cfg(not(Py_3_12))] @@ -540,7 +569,7 @@ impl PyErr { let type_bound = self.get_type_bound(py); ffi::PyErr_Display( type_bound.as_ptr(), - self.value(py).as_ptr(), + self.value_bound(py).as_ptr(), traceback .as_ref() .map_or(std::ptr::null_mut(), |traceback| traceback.as_ptr()), @@ -785,7 +814,7 @@ impl PyErr { /// 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(py).is(err_clone.value(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)), @@ -800,15 +829,14 @@ impl PyErr { /// Return the cause (either an exception instance, or None, set by `raise ... from ...`) /// associated with the exception, as accessible from Python through `__cause__`. pub fn cause(&self, py: Python<'_>) -> Option { - let value = self.value(py); - let obj = - unsafe { py.from_owned_ptr_or_opt::(ffi::PyException_GetCause(value.as_ptr())) }; - obj.map(Self::from_value) + use crate::ffi_ptr_ext::FfiPtrExt; + unsafe { ffi::PyException_GetCause(self.value_bound(py).as_ptr()).assume_owned_or_opt(py) } + .map(Self::from_value_bound) } /// Set the cause associated with the exception, pass `None` to clear it. pub fn set_cause(&self, py: Python<'_>, cause: Option) { - let value = self.value(py); + let value = self.value_bound(py); let cause = cause.map(|err| err.into_value(py)); unsafe { // PyException_SetCause _steals_ a reference to cause, so must use .into_ptr() @@ -868,7 +896,7 @@ impl std::fmt::Debug for PyErr { Python::with_gil(|py| { f.debug_struct("PyErr") .field("type", &self.get_type_bound(py)) - .field("value", self.value(py)) + .field("value", self.value_bound(py)) .field("traceback", &self.traceback_bound(py)) .finish() }) @@ -877,8 +905,9 @@ impl std::fmt::Debug for PyErr { impl std::fmt::Display for PyErr { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use crate::types::string::PyStringMethods; Python::with_gil(|py| { - let value = self.value(py); + let value = self.value_bound(py); let type_name = value.get_type().qualname().map_err(|_| std::fmt::Error)?; write!(f, "{}", type_name)?; if let Ok(s) = value.str() { @@ -1043,7 +1072,6 @@ impl_signed_integer!(isize); mod tests { use super::PyErrState; use crate::exceptions::{self, PyTypeError, PyValueError}; - use crate::types::any::PyAnyMethods; use crate::{PyErr, PyNativeType, PyTypeInfo, Python}; #[test] @@ -1237,6 +1265,7 @@ mod tests { #[test] fn warnings() { + use crate::types::any::PyAnyMethods; // Note: although the warning filter is interpreter global, keeping the // GIL locked should prevent effects to be visible to other testing // threads. @@ -1284,7 +1313,7 @@ mod tests { ) .unwrap_err(); assert!(err - .value(py) + .value_bound(py) .getattr("args") .unwrap() .get_item(0) diff --git a/src/exceptions.rs b/src/exceptions.rs index bcbe91b90c7..e7b6407e721 100644 --- a/src/exceptions.rs +++ b/src/exceptions.rs @@ -9,7 +9,7 @@ //! yourself to import Python classes that are ultimately derived from //! `BaseException`. -use crate::{ffi, PyResult, Python}; +use crate::{ffi, Bound, PyResult, Python}; use std::ffi::CStr; use std::ops; use std::os::raw::c_char; @@ -22,6 +22,7 @@ macro_rules! impl_exception_boilerplate { impl ::std::convert::From<&$name> for $crate::PyErr { #[inline] fn from(err: &$name) -> $crate::PyErr { + #[allow(deprecated)] $crate::PyErr::from_value(err) } } @@ -613,7 +614,14 @@ impl_windows_native_exception!( ); impl PyUnicodeDecodeError { - /// Creates a Python `UnicodeDecodeError`. + /// 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" + ) + )] pub fn new<'p>( py: Python<'p>, encoding: &CStr, @@ -621,16 +629,47 @@ impl PyUnicodeDecodeError { range: ops::Range, reason: &CStr, ) -> PyResult<&'p PyUnicodeDecodeError> { + Ok(PyUnicodeDecodeError::new_bound(py, encoding, input, range, reason)?.into_gil_ref()) + } + + /// Creates a Python `UnicodeDecodeError`. + pub fn new_bound<'p>( + py: Python<'p>, + encoding: &CStr, + input: &[u8], + range: ops::Range, + reason: &CStr, + ) -> PyResult> { + use crate::ffi_ptr_ext::FfiPtrExt; + use crate::py_result_ext::PyResultExt; unsafe { - py.from_owned_ptr_or_err(ffi::PyUnicodeDecodeError_Create( + ffi::PyUnicodeDecodeError_Create( encoding.as_ptr(), input.as_ptr() as *const c_char, input.len() as ffi::Py_ssize_t, range.start as ffi::Py_ssize_t, range.end as ffi::Py_ssize_t, reason.as_ptr(), - )) + ) + .assume_owned_or_err(py) } + .downcast_into() + } + + /// Deprecated form of [`PyUnicodeDecodeError::new_utf8_bound`]. + #[cfg_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" + ) + )] + 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. @@ -646,7 +685,7 @@ impl PyUnicodeDecodeError { /// Python::with_gil(|py| { /// let invalid_utf8 = b"fo\xd8o"; /// let err = std::str::from_utf8(invalid_utf8).expect_err("should be invalid utf8"); - /// let decode_err = PyUnicodeDecodeError::new_utf8(py, invalid_utf8, err)?; + /// let decode_err = PyUnicodeDecodeError::new_utf8_bound(py, invalid_utf8, err)?; /// assert_eq!( /// decode_err.to_string(), /// "'utf-8' codec can't decode byte 0xd8 in position 2: invalid utf-8" @@ -654,13 +693,13 @@ impl PyUnicodeDecodeError { /// Ok(()) /// }) /// # } - pub fn new_utf8<'p>( + pub fn new_utf8_bound<'p>( py: Python<'p>, input: &[u8], err: std::str::Utf8Error, - ) -> PyResult<&'p PyUnicodeDecodeError> { + ) -> PyResult> { let pos = err.valid_up_to(); - PyUnicodeDecodeError::new( + PyUnicodeDecodeError::new_bound( py, CStr::from_bytes_with_nul(b"utf-8\0").unwrap(), input, @@ -745,7 +784,7 @@ macro_rules! test_exception { assert!(err.is_instance_of::<$exc_ty>(py)); - let value: &$exc_ty = err.value(py).downcast().unwrap(); + let value: &$exc_ty = err.value_bound(py).clone().into_gil_ref().downcast().unwrap(); assert!(value.source().is_none()); err.set_cause(py, Some($crate::exceptions::PyValueError::new_err("a cause"))); @@ -1025,14 +1064,14 @@ mod tests { #[cfg_attr(invalid_from_utf8_lint, allow(invalid_from_utf8))] let err = std::str::from_utf8(invalid_utf8).expect_err("should be invalid utf8"); Python::with_gil(|py| { - let decode_err = PyUnicodeDecodeError::new_utf8(py, invalid_utf8, err).unwrap(); + let decode_err = PyUnicodeDecodeError::new_utf8_bound(py, invalid_utf8, err).unwrap(); assert_eq!( format!("{:?}", decode_err), "UnicodeDecodeError('utf-8', b'fo\\xd8o', 2, 3, 'invalid utf-8')" ); // Restoring should preserve the same error - let e: PyErr = decode_err.into(); + let e = PyErr::from_value_bound(decode_err.into_any()); e.restore(py); assert_eq!( @@ -1081,7 +1120,11 @@ mod tests { let invalid_utf8 = b"fo\xd8o"; #[cfg_attr(invalid_from_utf8_lint, allow(invalid_from_utf8))] let err = std::str::from_utf8(invalid_utf8).expect_err("should be invalid utf8"); - PyErr::from_value(PyUnicodeDecodeError::new_utf8(py, invalid_utf8, err).unwrap()) + PyErr::from_value_bound( + PyUnicodeDecodeError::new_utf8_bound(py, invalid_utf8, err) + .unwrap() + .into_any(), + ) }); test_exception!(PyUnicodeEncodeError, |py| py .eval_bound("chr(40960).encode('ascii')", None, None) diff --git a/src/impl_/extract_argument.rs b/src/impl_/extract_argument.rs index ff1c2436e38..d9487359cf4 100644 --- a/src/impl_/extract_argument.rs +++ b/src/impl_/extract_argument.rs @@ -167,8 +167,11 @@ pub fn from_py_with_with_default<'a, 'py, T>( pub fn argument_extraction_error(py: Python<'_>, arg_name: &str, error: PyErr) -> PyErr { use crate::types::any::PyAnyMethods; if error.get_type_bound(py).is(py.get_type::()) { - let remapped_error = - PyTypeError::new_err(format!("argument '{}': {}", arg_name, error.value(py))); + let remapped_error = PyTypeError::new_err(format!( + "argument '{}': {}", + arg_name, + error.value_bound(py) + )); remapped_error.set_cause(py, error.cause(py)); remapped_error } else { diff --git a/src/tests/common.rs b/src/tests/common.rs index ed3972f16a6..5eec60949bc 100644 --- a/src/tests/common.rs +++ b/src/tests/common.rs @@ -73,8 +73,8 @@ mod inner { #[cfg(all(feature = "macros", Py_3_8))] #[pymethods(crate = "pyo3")] impl UnraisableCapture { - pub fn hook(&mut self, unraisable: &PyAny) { - let err = PyErr::from_value(unraisable.getattr("exc_value").unwrap()); + pub fn hook(&mut self, unraisable: Bound<'_, PyAny>) { + let err = PyErr::from_value_bound(unraisable.getattr("exc_value").unwrap()); let instance = unraisable.getattr("object").unwrap(); self.capture = Some((err, instance.into())); } diff --git a/src/types/string.rs b/src/types/string.rs index 2b2635715d3..2e17f909df5 100644 --- a/src/types/string.rs +++ b/src/types/string.rs @@ -73,9 +73,9 @@ impl<'a> PyStringData<'a> { match self { Self::Ucs1(data) => match str::from_utf8(data) { Ok(s) => Ok(Cow::Borrowed(s)), - Err(e) => Err(crate::PyErr::from_value(PyUnicodeDecodeError::new_utf8( - py, data, e, - )?)), + Err(e) => Err(crate::PyErr::from_value_bound( + PyUnicodeDecodeError::new_utf8_bound(py, data, e)?.into_any(), + )), }, Self::Ucs2(data) => match String::from_utf16(data) { Ok(s) => Ok(Cow::Owned(s)), @@ -83,24 +83,30 @@ impl<'a> PyStringData<'a> { let mut message = e.to_string().as_bytes().to_vec(); message.push(0); - Err(crate::PyErr::from_value(PyUnicodeDecodeError::new( - py, - CStr::from_bytes_with_nul(b"utf-16\0").unwrap(), - self.as_bytes(), - 0..self.as_bytes().len(), - CStr::from_bytes_with_nul(&message).unwrap(), - )?)) + Err(crate::PyErr::from_value_bound( + PyUnicodeDecodeError::new_bound( + py, + CStr::from_bytes_with_nul(b"utf-16\0").unwrap(), + self.as_bytes(), + 0..self.as_bytes().len(), + CStr::from_bytes_with_nul(&message).unwrap(), + )? + .into_any(), + )) } }, Self::Ucs4(data) => match data.iter().map(|&c| std::char::from_u32(c)).collect() { Some(s) => Ok(Cow::Owned(s)), - None => Err(crate::PyErr::from_value(PyUnicodeDecodeError::new( - py, - CStr::from_bytes_with_nul(b"utf-32\0").unwrap(), - self.as_bytes(), - 0..self.as_bytes().len(), - CStr::from_bytes_with_nul(b"error converting utf-32\0").unwrap(), - )?)), + None => Err(crate::PyErr::from_value_bound( + PyUnicodeDecodeError::new_bound( + py, + CStr::from_bytes_with_nul(b"utf-32\0").unwrap(), + self.as_bytes(), + 0..self.as_bytes().len(), + CStr::from_bytes_with_nul(b"error converting utf-32\0").unwrap(), + )? + .into_any(), + )), }, } } diff --git a/src/types/traceback.rs b/src/types/traceback.rs index b97e2ed0093..19f42d4b64c 100644 --- a/src/types/traceback.rs +++ b/src/types/traceback.rs @@ -150,9 +150,9 @@ except Exception as e: Some(&locals), ) .unwrap(); - let err = PyErr::from_value(locals.get_item("err").unwrap().unwrap().into_gil_ref()); - let traceback = err.value(py).getattr("__traceback__").unwrap(); - assert!(err.traceback_bound(py).unwrap().is(traceback)); + let err = PyErr::from_value_bound(locals.get_item("err").unwrap().unwrap()); + let traceback = err.value_bound(py).getattr("__traceback__").unwrap(); + assert!(err.traceback_bound(py).unwrap().is(&traceback)); }) } diff --git a/tests/test_coroutine.rs b/tests/test_coroutine.rs index 549d54aa5c4..5954b9794e7 100644 --- a/tests/test_coroutine.rs +++ b/tests/test_coroutine.rs @@ -137,7 +137,7 @@ fn cancelled_coroutine() { ) .unwrap_err(); assert_eq!( - err.value(gil).get_type().qualname().unwrap(), + err.value_bound(gil).get_type().qualname().unwrap(), "CancelledError" ); }) From 65cf5808d9b0fb62676cddbb526e46fef6ae77c6 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sat, 17 Feb 2024 10:35:28 +0000 Subject: [PATCH 003/936] docs: add note about mapping to dangling pointer with `Bound` API (#3805) --- guide/src/migration.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/guide/src/migration.md b/guide/src/migration.md index 936784897bd..33b465de8dc 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -224,6 +224,22 @@ let gil_ref: &PyAny = ...; let bound: &Bound = &gil_ref.as_borrowed(); ``` +> Because of the ownership changes, code which uses `.as_ptr()` to convert `&PyAny` and other GIL Refs to a `*mut pyo3_ffi::PyObject` should take care to avoid creating dangling pointers now that `Bound` carries ownership. +> +> For example, the following pattern with `Option<&PyAny>` can easily create a dangling pointer when migrating to the `Bound` smart pointer: +> +> ```rust,ignore +> let opt: Option<&PyAny> = ...; +> let p: *mut ffi::PyObject = opt.map_or(std::ptr::null_mut(), |any| any.as_ptr()); +> ``` +> +> The correct way to migrate this code is to use `.as_ref()` to avoid dropping the `Bound` in the `map_or` closure: +> +> ```rust,ignore +> let opt: Option> = ...; +> let p: *mut ffi::PyObject = opt.as_ref().map_or(std::ptr::null_mut(), Bound::as_ptr); +> ``` + #### Migrating `FromPyObject` implementations `FromPyObject` has had a new method `extract_bound` which takes `&Bound<'py, PyAny>` as an argument instead of `&PyAny`. Both `extract` and `extract_bound` have been given default implementations in terms of the other, to avoid breaking code immediately on update to 0.21. From eb90b81d4481767a49a04242785f1f2f46b007d6 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sat, 17 Feb 2024 10:57:53 +0000 Subject: [PATCH 004/936] always use a Python iterator for sets and frozensets (#3849) * always use a Python iterator for sets and frozensets * add newsfragment --- newsfragments/3849.added.md | 1 + newsfragments/3849.changed.md | 1 + src/types/frozenset.rs | 115 ++++++++---------------------- src/types/set.rs | 129 ++++++++-------------------------- 4 files changed, 62 insertions(+), 184 deletions(-) create mode 100644 newsfragments/3849.added.md create mode 100644 newsfragments/3849.changed.md diff --git a/newsfragments/3849.added.md b/newsfragments/3849.added.md new file mode 100644 index 00000000000..8aa9a55df03 --- /dev/null +++ b/newsfragments/3849.added.md @@ -0,0 +1 @@ +Implement `ExactSizeIterator` for `set` and `frozenset` iterators on `abi3` feature. diff --git a/newsfragments/3849.changed.md b/newsfragments/3849.changed.md new file mode 100644 index 00000000000..ee8dfbf5b2c --- /dev/null +++ b/newsfragments/3849.changed.md @@ -0,0 +1 @@ +`PySet` and `PyFrozenSet` iterators now always iterate the equivalent of `iter(set)`. (A "fast path" with no noticeable performance benefit was removed.) diff --git a/src/types/frozenset.rs b/src/types/frozenset.rs index fa56a9c69f7..3be3f0ed0a1 100644 --- a/src/types/frozenset.rs +++ b/src/types/frozenset.rs @@ -1,4 +1,3 @@ -#[cfg(Py_LIMITED_API)] use crate::types::PyIterator; use crate::{ err::{self, PyErr, PyResult}, @@ -205,6 +204,13 @@ impl<'py> Iterator for PyFrozenSetIterator<'py> { } } +impl ExactSizeIterator for PyFrozenSetIterator<'_> { + #[inline] + fn len(&self) -> usize { + self.0.len() + } +} + impl<'py> IntoIterator for &'py PyFrozenSet { type Item = &'py PyAny; type IntoIter = PyFrozenSetIterator<'py>; @@ -224,90 +230,42 @@ impl<'py> IntoIterator for Bound<'py, PyFrozenSet> { } } -#[cfg(Py_LIMITED_API)] -mod impl_ { - use super::*; - - /// PyO3 implementation of an iterator for a Python `set` object. - pub struct BoundFrozenSetIterator<'p> { - it: Bound<'p, PyIterator>, - } - - impl<'py> BoundFrozenSetIterator<'py> { - pub(super) fn new(frozenset: Bound<'py, PyFrozenSet>) -> Self { - Self { - it: PyIterator::from_bound_object(&frozenset).unwrap(), - } - } - } - - impl<'py> Iterator for BoundFrozenSetIterator<'py> { - type Item = Bound<'py, super::PyAny>; - - /// Advances the iterator and returns the next value. - #[inline] - fn next(&mut self) -> Option { - self.it.next().map(Result::unwrap) - } - } +/// PyO3 implementation of an iterator for a Python `frozenset` object. +pub struct BoundFrozenSetIterator<'p> { + it: Bound<'p, PyIterator>, + // Remaining elements in the frozenset + remaining: usize, } -#[cfg(not(Py_LIMITED_API))] -mod impl_ { - use super::*; - - /// PyO3 implementation of an iterator for a Python `frozenset` object. - pub struct BoundFrozenSetIterator<'py> { - set: Bound<'py, PyFrozenSet>, - pos: ffi::Py_ssize_t, - } - - impl<'py> BoundFrozenSetIterator<'py> { - pub(super) fn new(set: Bound<'py, PyFrozenSet>) -> Self { - Self { set, pos: 0 } +impl<'py> BoundFrozenSetIterator<'py> { + pub(super) fn new(set: Bound<'py, PyFrozenSet>) -> Self { + Self { + it: PyIterator::from_bound_object(&set).unwrap(), + remaining: set.len(), } } +} - impl<'py> Iterator for BoundFrozenSetIterator<'py> { - type Item = Bound<'py, PyAny>; - - #[inline] - fn next(&mut self) -> Option { - unsafe { - let mut key: *mut ffi::PyObject = std::ptr::null_mut(); - let mut hash: ffi::Py_hash_t = 0; - if ffi::_PySet_NextEntry(self.set.as_ptr(), &mut self.pos, &mut key, &mut hash) != 0 - { - // _PySet_NextEntry returns borrowed object; for safety must make owned (see #890) - Some(key.assume_borrowed(self.set.py()).to_owned()) - } else { - None - } - } - } +impl<'py> Iterator for BoundFrozenSetIterator<'py> { + type Item = Bound<'py, super::PyAny>; - #[inline] - fn size_hint(&self) -> (usize, Option) { - let len = self.len(); - (len, Some(len)) - } + /// Advances the iterator and returns the next value. + fn next(&mut self) -> Option { + self.remaining = self.remaining.saturating_sub(1); + self.it.next().map(Result::unwrap) } - impl<'py> ExactSizeIterator for BoundFrozenSetIterator<'py> { - fn len(&self) -> usize { - self.set.len().saturating_sub(self.pos as usize) - } + fn size_hint(&self) -> (usize, Option) { + (self.remaining, Some(self.remaining)) } +} - impl<'py> ExactSizeIterator for PyFrozenSetIterator<'py> { - fn len(&self) -> usize { - self.0.len() - } +impl<'py> ExactSizeIterator for BoundFrozenSetIterator<'py> { + fn len(&self) -> usize { + self.remaining } } -pub use impl_::*; - #[inline] pub(crate) fn new_from_iter( py: Python<'_>, @@ -387,7 +345,6 @@ mod tests { } #[test] - #[cfg(not(Py_LIMITED_API))] fn test_frozenset_iter_size_hint() { Python::with_gil(|py| { let set = PyFrozenSet::new(py, &[1]).unwrap(); @@ -402,18 +359,6 @@ mod tests { }); } - #[test] - #[cfg(Py_LIMITED_API)] - fn test_frozenset_iter_size_hint() { - Python::with_gil(|py| { - let set = PyFrozenSet::new(py, &[1]).unwrap(); - let iter = set.iter(); - - // No known bounds - assert_eq!(iter.size_hint(), (0, None)); - }); - } - #[test] fn test_frozenset_builder() { use super::PyFrozenSetBuilder; diff --git a/src/types/set.rs b/src/types/set.rs index 3204e71f3e5..0c1733527cf 100644 --- a/src/types/set.rs +++ b/src/types/set.rs @@ -1,4 +1,3 @@ -#[cfg(Py_LIMITED_API)] use crate::types::PyIterator; use crate::{ err::{self, PyErr, PyResult}, @@ -277,6 +276,12 @@ impl<'py> Iterator for PySetIterator<'py> { } } +impl ExactSizeIterator for PySetIterator<'_> { + fn len(&self) -> usize { + self.0.len() + } +} + impl<'py> IntoIterator for &'py PySet { type Item = &'py PyAny; type IntoIter = PySetIterator<'py>; @@ -304,104 +309,43 @@ impl<'py> IntoIterator for Bound<'py, PySet> { } } -#[cfg(Py_LIMITED_API)] -mod impl_ { - use super::*; - - /// PyO3 implementation of an iterator for a Python `set` object. - pub struct BoundSetIterator<'p> { - it: Bound<'p, PyIterator>, - } - - impl<'py> BoundSetIterator<'py> { - pub(super) fn new(set: Bound<'py, PySet>) -> Self { - Self { - it: PyIterator::from_bound_object(&set).unwrap(), - } - } - } - - impl<'py> Iterator for BoundSetIterator<'py> { - type Item = Bound<'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.it.next().map(Result::unwrap) - } - } +/// PyO3 implementation of an iterator for a Python `set` object. +pub struct BoundSetIterator<'p> { + it: Bound<'p, PyIterator>, + // Remaining elements in the set. This is fine to store because + // Python will error if the set changes size during iteration. + remaining: usize, } -#[cfg(not(Py_LIMITED_API))] -mod impl_ { - use super::*; - - /// PyO3 implementation of an iterator for a Python `set` object. - pub struct BoundSetIterator<'py> { - set: Bound<'py, super::PySet>, - pos: ffi::Py_ssize_t, - used: ffi::Py_ssize_t, - } - - impl<'py> BoundSetIterator<'py> { - pub(super) fn new(set: Bound<'py, PySet>) -> Self { - let used = unsafe { ffi::PySet_Size(set.as_ptr()) }; - BoundSetIterator { set, pos: 0, used } +impl<'py> BoundSetIterator<'py> { + pub(super) fn new(set: Bound<'py, PySet>) -> Self { + Self { + it: PyIterator::from_bound_object(&set).unwrap(), + remaining: set.len(), } } +} - impl<'py> Iterator for BoundSetIterator<'py> { - type Item = Bound<'py, super::PyAny>; - - /// Advances the iterator and returns the next value. - /// - /// # Panics - /// - /// If PyO3 detects that the set is mutated during iteration, it will panic. - #[inline] - fn next(&mut self) -> Option { - unsafe { - let len = ffi::PySet_Size(self.set.as_ptr()); - assert_eq!(self.used, len, "Set changed size during iteration"); - - let mut key: *mut ffi::PyObject = std::ptr::null_mut(); - let mut hash: ffi::Py_hash_t = 0; - if ffi::_PySet_NextEntry(self.set.as_ptr(), &mut self.pos, &mut key, &mut hash) != 0 - { - // _PySet_NextEntry returns borrowed object; for safety must make owned (see #890) - Some(key.assume_borrowed(self.set.py()).to_owned()) - } else { - None - } - } - } +impl<'py> Iterator for BoundSetIterator<'py> { + type Item = Bound<'py, super::PyAny>; - #[inline] - fn size_hint(&self) -> (usize, Option) { - let len = self.len(); - (len, Some(len)) - } + /// Advances the iterator and returns the next value. + fn next(&mut self) -> Option { + self.remaining = self.remaining.saturating_sub(1); + self.it.next().map(Result::unwrap) } - impl<'py> ExactSizeIterator for BoundSetIterator<'py> { - fn len(&self) -> usize { - self.set.len().saturating_sub(self.pos as usize) - } + fn size_hint(&self) -> (usize, Option) { + (self.remaining, Some(self.remaining)) } +} - impl<'py> ExactSizeIterator for PySetIterator<'py> { - fn len(&self) -> usize { - self.0.len() - } +impl<'py> ExactSizeIterator for BoundSetIterator<'py> { + fn len(&self) -> usize { + self.remaining } } -pub use impl_::*; - #[inline] pub(crate) fn new_from_iter( py: Python<'_>, @@ -571,7 +515,6 @@ mod tests { } #[test] - #[cfg(not(Py_LIMITED_API))] fn test_set_iter_size_hint() { Python::with_gil(|py| { let set = PySet::new(py, &[1]).unwrap(); @@ -585,16 +528,4 @@ mod tests { assert_eq!(iter.size_hint(), (0, Some(0))); }); } - - #[test] - #[cfg(Py_LIMITED_API)] - fn test_set_iter_size_hint() { - Python::with_gil(|py| { - let set = PySet::new(py, &[1]).unwrap(); - let iter = set.iter(); - - // No known bounds - assert_eq!(iter.size_hint(), (0, None)); - }); - } } From 1d8d81db2d3ab99b5ffde5cdac93cc7605ce8d44 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sat, 17 Feb 2024 13:21:41 +0100 Subject: [PATCH 005/936] port `PyFrozenSetBuilder` to `Bound` API (#3850) --- src/types/frozenset.rs | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/types/frozenset.rs b/src/types/frozenset.rs index 3be3f0ed0a1..909e1276a57 100644 --- a/src/types/frozenset.rs +++ b/src/types/frozenset.rs @@ -11,7 +11,7 @@ use std::ptr; /// Allows building a Python `frozenset` one item at a time pub struct PyFrozenSetBuilder<'py> { - py_frozen_set: &'py PyFrozenSet, + py_frozen_set: Bound<'py, PyFrozenSet>, } impl<'py> PyFrozenSetBuilder<'py> { @@ -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(py)?, + py_frozen_set: PyFrozenSet::empty_bound(py)?, }) } @@ -29,17 +29,29 @@ impl<'py> PyFrozenSetBuilder<'py> { where K: ToPyObject, { - fn inner(frozenset: &PyFrozenSet, key: PyObject) -> PyResult<()> { + fn inner(frozenset: &Bound<'_, PyFrozenSet>, key: PyObject) -> PyResult<()> { err::error_on_minusone(frozenset.py(), unsafe { ffi::PySet_Add(frozenset.as_ptr(), key.as_ptr()) }) } - inner(self.py_frozen_set, key.to_object(self.py_frozen_set.py())) + inner(&self.py_frozen_set, key.to_object(self.py_frozen_set.py())) } - /// Finish building the set and take ownership of its current value + /// Deprecated form of [`PyFrozenSetBuilder::finalize_bound`] + #[cfg_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" + ) + )] 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 } } @@ -372,7 +384,7 @@ mod tests { builder.add(2).unwrap(); // finalize it - let set = builder.finalize(); + let set = builder.finalize_bound(); assert!(set.contains(1).unwrap()); assert!(set.contains(2).unwrap()); From c33d330b18164bf1afedd0e07f40fa27740eb94a Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sat, 17 Feb 2024 14:40:54 +0100 Subject: [PATCH 006/936] deprecate `PyFrozenSet::empty` (#3851) --- src/types/frozenset.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/types/frozenset.rs b/src/types/frozenset.rs index 909e1276a57..15f892588e5 100644 --- a/src/types/frozenset.rs +++ b/src/types/frozenset.rs @@ -104,6 +104,13 @@ 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" + ) + )] pub fn empty(py: Python<'_>) -> PyResult<&'_ PyFrozenSet> { Self::empty_bound(py).map(Bound::into_gil_ref) } From 5f42c02e4f9ef969f9e71b9c2e456c1bc36d93f1 Mon Sep 17 00:00:00 2001 From: Lily Foote Date: Sat, 17 Feb 2024 17:04:35 +0000 Subject: [PATCH 007/936] Remove stray " character from docstring (#3852) --- src/instance.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/instance.rs b/src/instance.rs index ff553c44324..2db63d06a43 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -766,7 +766,7 @@ impl IntoPy for Borrowed<'_, '_, 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. -/// As you can only get this by acquiring the GIL, `Py<...>` "implements [`Send`] and [`Sync`]. +/// As you can only get this by acquiring the GIL, `Py<...>` implements [`Send`] and [`Sync`]. /// /// [`Rc`]: std::rc::Rc /// [`RefCell`]: std::cell::RefCell From 0dd568d397b18c476c25a68157f1805d666f6bdd Mon Sep 17 00:00:00 2001 From: Lily Foote Date: Sun, 18 Feb 2024 00:09:56 +0000 Subject: [PATCH 008/936] Use the new bound API instead of .as_ref(py) (#3853) * Use the new bound API instead of .as_ref(py) * Move import into a nested scope * Use to_cow instead of to_str for compatibility `to_str` is not available before Python 3.10 on the limited api. * Relax &self lifetimes * Use Bound<'py, PyAny> in test Mapping signatures * Use .as_bytes(py) * Simplify ThrowCallback::throw signature * Avoid .as_any call with Py api instead of Bound --- src/conversions/std/ipaddr.rs | 3 ++- src/conversions/std/osstr.rs | 10 ++++------ src/conversions/std/slice.rs | 4 ++-- src/conversions/std/string.rs | 7 ++++--- src/coroutine.rs | 8 ++++---- src/coroutine/cancel.rs | 6 +++--- src/coroutine/waker.rs | 11 +++++++---- src/err/mod.rs | 6 +++--- src/instance.rs | 10 +++++----- src/pycell.rs | 4 ++-- src/types/capsule.rs | 6 +++--- tests/test_class_basics.rs | 2 +- tests/test_class_conversion.rs | 4 ++-- tests/test_field_cfg.rs | 10 ++++++++-- tests/test_inheritance.rs | 2 +- tests/test_mapping.rs | 4 ++-- tests/test_proto_methods.rs | 28 ++++++++++++++-------------- tests/test_pyself.rs | 4 ++-- tests/test_sequence.rs | 2 +- tests/test_various.rs | 2 +- 20 files changed, 71 insertions(+), 62 deletions(-) diff --git a/src/conversions/std/ipaddr.rs b/src/conversions/std/ipaddr.rs index 84aacab43a2..5d030b445d8 100755 --- a/src/conversions/std/ipaddr.rs +++ b/src/conversions/std/ipaddr.rs @@ -87,7 +87,8 @@ mod test_ipaddr { }; let pyobj = ip.into_py(py); - let repr = pyobj.as_ref(py).repr().unwrap().to_string_lossy(); + let repr = pyobj.bind(py).repr().unwrap(); + let repr = repr.to_string_lossy(); assert_eq!(repr, format!("{}('{}')", py_cls, ip)); let ip2: IpAddr = pyobj.extract(py).unwrap(); diff --git a/src/conversions/std/osstr.rs b/src/conversions/std/osstr.rs index be337959b5f..b9382688589 100644 --- a/src/conversions/std/osstr.rs +++ b/src/conversions/std/osstr.rs @@ -68,13 +68,11 @@ impl FromPyObject<'_> for OsString { // Create an OsStr view into the raw bytes from Python #[cfg(target_os = "wasi")] - let os_str: &OsStr = std::os::wasi::ffi::OsStrExt::from_bytes( - fs_encoded_bytes.as_ref(ob.py()).as_bytes(), - ); + let os_str: &OsStr = + std::os::wasi::ffi::OsStrExt::from_bytes(fs_encoded_bytes.as_bytes(ob.py())); #[cfg(not(target_os = "wasi"))] - let os_str: &OsStr = std::os::unix::ffi::OsStrExt::from_bytes( - fs_encoded_bytes.as_ref(ob.py()).as_bytes(), - ); + let os_str: &OsStr = + std::os::unix::ffi::OsStrExt::from_bytes(fs_encoded_bytes.as_bytes(ob.py())); Ok(os_str.to_os_string()) } diff --git a/src/conversions/std/slice.rs b/src/conversions/std/slice.rs index 2d46abe2953..dad27f081cf 100644 --- a/src/conversions/std/slice.rs +++ b/src/conversions/std/slice.rs @@ -94,10 +94,10 @@ mod tests { .unwrap_err(); let cow = Cow::<[u8]>::Borrowed(b"foobar").to_object(py); - assert!(cow.as_ref(py).is_instance_of::()); + assert!(cow.bind(py).is_instance_of::()); let cow = Cow::<[u8]>::Owned(b"foobar".to_vec()).to_object(py); - assert!(cow.as_ref(py).is_instance_of::()); + assert!(cow.bind(py).is_instance_of::()); }); } } diff --git a/src/conversions/std/string.rs b/src/conversions/std/string.rs index ac29d80491c..60e8ff7eb53 100644 --- a/src/conversions/std/string.rs +++ b/src/conversions/std/string.rs @@ -159,6 +159,7 @@ impl FromPyObject<'_> for char { #[cfg(test)] mod tests { + use crate::types::any::PyAnyMethods; use crate::Python; use crate::{IntoPy, PyObject, ToPyObject}; use std::borrow::Cow; @@ -200,7 +201,7 @@ mod tests { let s = "Hello Python"; let py_string = s.to_object(py); - let s2: &str = py_string.as_ref(py).extract().unwrap(); + let s2: &str = py_string.bind(py).extract().unwrap(); assert_eq!(s, s2); }) } @@ -210,7 +211,7 @@ mod tests { Python::with_gil(|py| { let ch = '😃'; let py_string = ch.to_object(py); - let ch2: char = py_string.as_ref(py).extract().unwrap(); + let ch2: char = py_string.bind(py).extract().unwrap(); assert_eq!(ch, ch2); }) } @@ -220,7 +221,7 @@ mod tests { Python::with_gil(|py| { let s = "Hello Python"; let py_string = s.to_object(py); - let err: crate::PyResult = py_string.as_ref(py).extract(); + let err: crate::PyResult = py_string.bind(py).extract(); assert!(err .unwrap_err() .to_string() diff --git a/src/coroutine.rs b/src/coroutine.rs index 3a0df983076..b24197ad593 100644 --- a/src/coroutine.rs +++ b/src/coroutine.rs @@ -14,8 +14,8 @@ use crate::{ coroutine::{cancel::ThrowCallback, waker::AsyncioWaker}, exceptions::{PyAttributeError, PyRuntimeError, PyStopIteration}, panic::PanicException, - types::{PyIterator, PyString}, - IntoPy, Py, PyAny, PyErr, PyNativeType, PyObject, PyResult, Python, + types::{string::PyStringMethods, PyIterator, PyString}, + IntoPy, Py, PyAny, PyErr, PyObject, PyResult, Python, }; pub(crate) mod cancel; @@ -75,7 +75,7 @@ impl Coroutine { }; // reraise thrown exception it match (throw, &self.throw_callback) { - (Some(exc), Some(cb)) => cb.throw(exc.as_ref(py)), + (Some(exc), Some(cb)) => cb.throw(exc), (Some(exc), None) => { self.close(); return Err(PyErr::from_value_bound(exc.into_bound(py))); @@ -135,7 +135,7 @@ impl Coroutine { #[getter] fn __qualname__(&self, py: Python<'_>) -> PyResult> { match (&self.name, &self.qualname_prefix) { - (Some(name), Some(prefix)) => Ok(format!("{}.{}", prefix, name.as_ref(py).to_str()?) + (Some(name), Some(prefix)) => Ok(format!("{}.{}", prefix, name.bind(py).to_cow()?) .as_str() .into_py(py)), (Some(name), None) => Ok(name.clone_ref(py)), diff --git a/src/coroutine/cancel.rs b/src/coroutine/cancel.rs index 7828986c11e..2b10fb9a438 100644 --- a/src/coroutine/cancel.rs +++ b/src/coroutine/cancel.rs @@ -1,4 +1,4 @@ -use crate::{PyAny, PyObject}; +use crate::{Py, PyAny, PyObject}; use parking_lot::Mutex; use std::future::Future; use std::pin::Pin; @@ -68,9 +68,9 @@ impl Future for Cancelled<'_> { pub struct ThrowCallback(Arc>); impl ThrowCallback { - pub(super) fn throw(&self, exc: &PyAny) { + pub(super) fn throw(&self, exc: Py) { let mut inner = self.0.lock(); - inner.exception = Some(exc.into()); + inner.exception = Some(exc); if let Some(waker) = inner.waker.take() { waker.wake(); } diff --git a/src/coroutine/waker.rs b/src/coroutine/waker.rs index c46654896fc..096146f8292 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, 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; @@ -25,10 +25,13 @@ impl AsyncioWaker { self.0.take(); } - pub(super) fn initialize_future<'a>(&'a self, py: Python<'a>) -> PyResult> { + pub(super) fn initialize_future<'py>( + &self, + py: Python<'py>, + ) -> PyResult>> { let init = || LoopAndFuture::new(py).map(Some); let loop_and_future = self.0.get_or_try_init(py, init)?.as_ref(); - Ok(loop_and_future.map(|LoopAndFuture { future, .. }| future.as_ref(py))) + Ok(loop_and_future.map(|LoopAndFuture { future, .. }| future.bind(py))) } } @@ -74,7 +77,7 @@ impl LoopAndFuture { let call_soon_threadsafe = self.event_loop.call_method1( py, intern!(py, "call_soon_threadsafe"), - (release_waiter, self.future.as_ref(py)), + (release_waiter, self.future.bind(py)), ); if let Err(err) = call_soon_threadsafe { // `call_soon_threadsafe` will raise if the event loop is closed; diff --git a/src/err/mod.rs b/src/err/mod.rs index b5cd02c0954..0c2637a7389 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -2,6 +2,7 @@ use crate::instance::Bound; use crate::panic::PanicException; use crate::type_object::PyTypeInfo; use crate::types::any::PyAnyMethods; +use crate::types::string::PyStringMethods; use crate::types::{PyTraceback, PyType}; use crate::{ exceptions::{self, PyBaseException}, @@ -403,7 +404,7 @@ impl PyErr { if ptype.as_ptr() == PanicException::type_object_raw(py).cast() { let msg = pvalue .as_ref() - .and_then(|obj| obj.as_ref(py).str().ok()) + .and_then(|obj| obj.bind(py).str().ok()) .map(|py_str| py_str.to_string_lossy().into()) .unwrap_or_else(|| String::from("Unwrapped panic from Python code")); @@ -425,7 +426,7 @@ impl PyErr { #[cfg(Py_3_12)] fn _take(py: Python<'_>) -> Option { let state = PyErrStateNormalized::take(py)?; - let pvalue = state.pvalue.as_ref(py); + let pvalue = state.pvalue.bind(py); if pvalue.get_type().as_ptr() == PanicException::type_object_raw(py).cast() { let msg: String = pvalue .str() @@ -905,7 +906,6 @@ impl std::fmt::Debug for PyErr { impl std::fmt::Display for PyErr { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - use crate::types::string::PyStringMethods; Python::with_gil(|py| { let value = self.value_bound(py); let type_name = value.get_type().qualname().map_err(|_| std::fmt::Error)?; diff --git a/src/instance.rs b/src/instance.rs index 2db63d06a43..cb803cc6c4d 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -989,7 +989,7 @@ where /// Panics if the value is currently mutably borrowed. For a non-panicking variant, use /// [`try_borrow`](#method.try_borrow). pub fn borrow<'py>(&'py self, py: Python<'py>) -> PyRef<'py, T> { - self.as_ref(py).borrow() + self.bind(py).borrow() } /// Mutably borrows the value `T`. @@ -1028,7 +1028,7 @@ where where T: PyClass, { - self.as_ref(py).borrow_mut() + self.bind(py).borrow_mut() } /// Attempts to immutably borrow the value `T`, returning an error if the value is currently mutably borrowed. @@ -1042,7 +1042,7 @@ where /// Equivalent to `self.as_ref(py).borrow_mut()` - /// see [`PyCell::try_borrow`](crate::pycell::PyCell::try_borrow). pub fn try_borrow<'py>(&'py self, py: Python<'py>) -> Result, PyBorrowError> { - self.as_ref(py).try_borrow() + self.bind(py).try_borrow() } /// Attempts to mutably borrow the value `T`, returning an error if the value is currently borrowed. @@ -1060,7 +1060,7 @@ where where T: PyClass, { - self.as_ref(py).try_borrow_mut() + self.bind(py).try_borrow_mut() } /// Provide an immutable borrow of the value `T` without acquiring the GIL. @@ -1667,7 +1667,7 @@ where T::AsRefTarget: std::fmt::Display, { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - Python::with_gil(|py| std::fmt::Display::fmt(self.as_ref(py), f)) + Python::with_gil(|py| std::fmt::Display::fmt(self.bind(py), f)) } } diff --git a/src/pycell.rs b/src/pycell.rs index 3744c55f67d..8409c5cc679 100644 --- a/src/pycell.rs +++ b/src/pycell.rs @@ -96,7 +96,7 @@ //! //! // We borrow the guard and then dereference //! // it to get a mutable reference to Number -//! let mut guard: PyRefMut<'_, Number> = n.as_ref(py).borrow_mut(); +//! let mut guard: PyRefMut<'_, Number> = n.bind(py).borrow_mut(); //! let n_mutable: &mut Number = &mut *guard; //! //! n_mutable.increment(); @@ -105,7 +105,7 @@ //! // `PyRefMut` before borrowing again. //! drop(guard); //! -//! let n_immutable: &Number = &n.as_ref(py).borrow(); +//! let n_immutable: &Number = &n.bind(py).borrow(); //! assert_eq!(n_immutable.inner, 1); //! //! Ok(()) diff --git a/src/types/capsule.rs b/src/types/capsule.rs index 30e77391171..466315ad6c7 100644 --- a/src/types/capsule.rs +++ b/src/types/capsule.rs @@ -491,7 +491,7 @@ mod tests { }); Python::with_gil(|py| { - let f = unsafe { cap.as_ref(py).reference:: u32>() }; + let f = unsafe { cap.bind(py).reference:: u32>() }; assert_eq!(f(123), 123); }); } @@ -555,7 +555,7 @@ mod tests { }); Python::with_gil(|py| { - let ctx: &Vec = unsafe { cap.as_ref(py).reference() }; + let ctx: &Vec = unsafe { cap.bind(py).reference() }; assert_eq!(ctx, &[1, 2, 3, 4]); }) } @@ -574,7 +574,7 @@ mod tests { }); Python::with_gil(|py| { - let ctx_ptr: *mut c_void = cap.as_ref(py).context().unwrap(); + 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]); }) diff --git a/tests/test_class_basics.rs b/tests/test_class_basics.rs index ff3555c3107..d19d106d206 100644 --- a/tests/test_class_basics.rs +++ b/tests/test_class_basics.rs @@ -235,7 +235,7 @@ fn test_unsendable() -> PyResult<()> { // Accessing the value inside this thread should not panic let caught_panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| -> PyResult<_> { - assert_eq!(obj.as_ref(py).getattr("value")?.extract::()?, 5); + assert_eq!(obj.getattr(py, "value")?.extract::(py)?, 5); Ok(()) })) .is_err(); diff --git a/tests/test_class_conversion.rs b/tests/test_class_conversion.rs index 27a8f604b3f..217d2d59d45 100644 --- a/tests/test_class_conversion.rs +++ b/tests/test_class_conversion.rs @@ -89,7 +89,7 @@ fn test_polymorphic_container_stores_sub_class() { .unwrap() .to_object(py); - p.as_ref(py) + p.bind(py) .setattr( "inner", PyCell::new( @@ -116,7 +116,7 @@ fn test_polymorphic_container_does_not_accept_other_types() { .unwrap() .to_object(py); - let setattr = |value: PyObject| p.as_ref(py).setattr("inner", value); + let setattr = |value: PyObject| p.bind(py).setattr("inner", value); assert!(setattr(1i32.into_py(py)).is_err()); assert!(setattr(py.None()).is_err()); diff --git a/tests/test_field_cfg.rs b/tests/test_field_cfg.rs index bd671641e5b..be400f2d749 100644 --- a/tests/test_field_cfg.rs +++ b/tests/test_field_cfg.rs @@ -22,8 +22,14 @@ fn test_cfg() { Python::with_gil(|py| { let cfg = CfgClass { b: 3 }; let py_cfg = Py::new(py, cfg).unwrap(); - assert!(py_cfg.as_ref(py).getattr("a").is_err()); - let b: u32 = py_cfg.as_ref(py).getattr("b").unwrap().extract().unwrap(); + assert!(py_cfg.bind(py).as_any().getattr("a").is_err()); + let b: u32 = py_cfg + .bind(py) + .as_any() + .getattr("b") + .unwrap() + .extract() + .unwrap(); assert_eq!(b, 3); }); } diff --git a/tests/test_inheritance.rs b/tests/test_inheritance.rs index 6c33dec6a9f..cd728c77bfe 100644 --- a/tests/test_inheritance.rs +++ b/tests/test_inheritance.rs @@ -247,7 +247,7 @@ mod inheriting_native_type { let item = &py.eval_bound("object()", None, None).unwrap(); assert_eq!(item.get_refcnt(), 1); - dict_sub.as_ref(py).set_item("foo", item).unwrap(); + dict_sub.bind(py).as_any().set_item("foo", item).unwrap(); assert_eq!(item.get_refcnt(), 2); drop(dict_sub); diff --git a/tests/test_mapping.rs b/tests/test_mapping.rs index 9c99d56467f..9a80ab491a1 100644 --- a/tests/test_mapping.rs +++ b/tests/test_mapping.rs @@ -123,7 +123,7 @@ fn mapping_is_not_sequence() { PyMapping::register::(py).unwrap(); - assert!(m.as_ref(py).downcast::().is_ok()); - assert!(m.as_ref(py).downcast::().is_err()); + assert!(m.bind(py).as_any().downcast::().is_ok()); + assert!(m.bind(py).as_any().downcast::().is_err()); }); } diff --git a/tests/test_proto_methods.rs b/tests/test_proto_methods.rs index 872777bf706..3151fbe5be7 100644 --- a/tests/test_proto_methods.rs +++ b/tests/test_proto_methods.rs @@ -191,20 +191,20 @@ pub struct Mapping { #[pymethods] impl Mapping { fn __len__(&self, py: Python<'_>) -> usize { - self.values.as_ref(py).len() + self.values.bind(py).len() } - fn __getitem__<'a>(&'a self, key: &'a PyAny) -> PyResult<&'a PyAny> { - let any: &PyAny = self.values.as_ref(key.py()).as_ref(); + fn __getitem__<'py>(&self, key: &Bound<'py, PyAny>) -> PyResult> { + let any: &Bound<'py, PyAny> = self.values.bind(key.py()); any.get_item(key) } - fn __setitem__(&self, key: &PyAny, value: &PyAny) -> PyResult<()> { - self.values.as_ref(key.py()).set_item(key, value) + fn __setitem__<'py>(&self, key: &Bound<'py, PyAny>, value: &Bound<'py, PyAny>) -> PyResult<()> { + self.values.bind(key.py()).set_item(key, value) } - fn __delitem__(&self, key: &PyAny) -> PyResult<()> { - self.values.as_ref(key.py()).del_item(key) + fn __delitem__(&self, key: &Bound<'_, PyAny>) -> PyResult<()> { + self.values.bind(key.py()).del_item(key) } } @@ -221,7 +221,7 @@ fn mapping() { ) .unwrap(); - let mapping: &PyMapping = inst.as_ref(py).downcast().unwrap(); + let mapping: &Bound<'_, PyMapping> = inst.bind(py).as_any().downcast().unwrap(); py_assert!(py, inst, "len(inst) == 0"); @@ -323,7 +323,7 @@ fn sequence() { let inst = Py::new(py, Sequence { values: vec![] }).unwrap(); - let sequence: &PySequence = inst.as_ref(py).downcast().unwrap(); + let sequence: &Bound<'_, PySequence> = inst.bind(py).as_any().downcast().unwrap(); py_assert!(py, inst, "len(inst) == 0"); @@ -350,16 +350,16 @@ fn sequence() { // indices. assert!(sequence.len().is_err()); // however regular python len() works thanks to mp_len slot - assert_eq!(inst.as_ref(py).len().unwrap(), 0); + assert_eq!(inst.bind(py).as_any().len().unwrap(), 0); py_run!(py, inst, "inst.append(0)"); sequence.set_item(0, 5).unwrap(); - assert_eq!(inst.as_ref(py).len().unwrap(), 1); + assert_eq!(inst.bind(py).as_any().len().unwrap(), 1); assert_eq!(sequence.get_item(0).unwrap().extract::().unwrap(), 5); sequence.del_item(0).unwrap(); - assert_eq!(inst.as_ref(py).len().unwrap(), 0); + assert_eq!(inst.bind(py).as_any().len().unwrap(), 0); }); } @@ -658,10 +658,10 @@ impl OnceFuture { fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { slf } - fn __next__<'py>(&'py mut self, py: Python<'py>) -> Option<&'py PyAny> { + fn __next__<'py>(&mut self, py: Python<'py>) -> Option<&Bound<'py, PyAny>> { if !self.polled { self.polled = true; - Some(self.future.as_ref(py)) + Some(self.future.bind(py)) } else { None } diff --git a/tests/test_pyself.rs b/tests/test_pyself.rs index 7abec02b0a2..e14d117a639 100644 --- a/tests/test_pyself.rs +++ b/tests/test_pyself.rs @@ -63,12 +63,12 @@ impl Iter { } fn __next__(mut slf: PyRefMut<'_, Self>) -> PyResult> { - let bytes = slf.keys.as_ref(slf.py()).as_bytes(); + let bytes = slf.keys.bind(slf.py()).as_bytes(); match bytes.get(slf.idx) { Some(&b) => { slf.idx += 1; let py = slf.py(); - let reader = slf.reader.as_ref(py); + let reader = slf.reader.bind(py); let reader_ref = reader.try_borrow()?; let res = reader_ref .inner diff --git a/tests/test_sequence.rs b/tests/test_sequence.rs index d3c84b76c8c..f18323dbdeb 100644 --- a/tests/test_sequence.rs +++ b/tests/test_sequence.rs @@ -276,7 +276,7 @@ fn test_generic_list_set() { .items .iter() .zip(&[1u32, 2, 3]) - .all(|(a, b)| a.as_ref(py).eq(&b.into_py(py)).unwrap())); + .all(|(a, b)| a.bind(py).eq(&b.into_py(py)).unwrap())); }); } diff --git a/tests/test_various.rs b/tests/test_various.rs index d0df8d4a04e..a2ca12fbbda 100644 --- a/tests/test_various.rs +++ b/tests/test_various.rs @@ -31,7 +31,7 @@ fn mut_ref_arg() { let inst2 = Py::new(py, MutRefArg { n: 0 }).unwrap(); py_run!(py, inst1 inst2, "inst1.set_other(inst2)"); - let inst2 = inst2.as_ref(py).borrow(); + let inst2 = inst2.bind(py).borrow(); assert_eq!(inst2.n, 100); }); } From 1d295a12a022e7ef977224bb209d3a0c6dbac2fc Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sun, 18 Feb 2024 02:11:43 +0100 Subject: [PATCH 009/936] port `PyObject::downcast` to `Bound` API (#3856) * port `PyObject::downcast` to `Bound` API * relax traits bounds for unchecked variant in `Bound` API * deprecate `Python::(checked_)cast_as` * reword deprecation warning --- src/conversions/hashbrown.rs | 4 +-- src/conversions/indexmap.rs | 4 +-- src/conversions/std/array.rs | 2 +- src/conversions/std/map.rs | 8 ++--- src/err/mod.rs | 9 ++++++ src/instance.rs | 61 +++++++++++++++++++++++++++++------- src/marker.rs | 14 +++++++++ src/types/mapping.rs | 20 ++++++------ src/types/none.rs | 4 +-- src/types/tuple.rs | 4 +-- 10 files changed, 95 insertions(+), 35 deletions(-) diff --git a/src/conversions/hashbrown.rs b/src/conversions/hashbrown.rs index 7e57d332d91..c9b6c61c6eb 100644 --- a/src/conversions/hashbrown.rs +++ b/src/conversions/hashbrown.rs @@ -121,7 +121,7 @@ mod tests { map.insert(1, 1); let m = map.to_object(py); - let py_map: &PyDict = m.downcast(py).unwrap(); + let py_map = m.downcast_bound::(py).unwrap(); assert!(py_map.len() == 1); assert!( @@ -143,7 +143,7 @@ mod tests { map.insert(1, 1); let m: PyObject = map.into_py(py); - let py_map: &PyDict = m.downcast(py).unwrap(); + let py_map = m.downcast_bound::(py).unwrap(); assert!(py_map.len() == 1); assert!( diff --git a/src/conversions/indexmap.rs b/src/conversions/indexmap.rs index ec7ed5de557..5a8cd3d6951 100644 --- a/src/conversions/indexmap.rs +++ b/src/conversions/indexmap.rs @@ -149,7 +149,7 @@ mod test_indexmap { map.insert(1, 1); let m = map.to_object(py); - let py_map: &PyDict = m.downcast(py).unwrap(); + let py_map = m.downcast_bound::(py).unwrap(); assert!(py_map.len() == 1); assert!( @@ -175,7 +175,7 @@ mod test_indexmap { map.insert(1, 1); let m: PyObject = map.into_py(py); - let py_map: &PyDict = m.downcast(py).unwrap(); + let py_map = m.downcast_bound::(py).unwrap(); assert!(py_map.len() == 1); assert!( diff --git a/src/conversions/std/array.rs b/src/conversions/std/array.rs index 016ab0a643d..d901ff4e59d 100644 --- a/src/conversions/std/array.rs +++ b/src/conversions/std/array.rs @@ -239,7 +239,7 @@ mod tests { Python::with_gil(|py| { let array: [Foo; 8] = [Foo, Foo, Foo, Foo, Foo, Foo, Foo, Foo]; let pyobject = array.into_py(py); - let list: &PyList = pyobject.downcast(py).unwrap(); + let list = pyobject.downcast_bound::(py).unwrap(); let _cell: &crate::PyCell = list.get_item(4).unwrap().extract().unwrap(); }); } diff --git a/src/conversions/std/map.rs b/src/conversions/std/map.rs index 700617ec17a..8c6835d7215 100644 --- a/src/conversions/std/map.rs +++ b/src/conversions/std/map.rs @@ -121,7 +121,7 @@ mod tests { map.insert(1, 1); let m = map.to_object(py); - let py_map: &PyDict = m.downcast(py).unwrap(); + let py_map = m.downcast_bound::(py).unwrap(); assert!(py_map.len() == 1); assert!( @@ -144,7 +144,7 @@ mod tests { map.insert(1, 1); let m = map.to_object(py); - let py_map: &PyDict = m.downcast(py).unwrap(); + let py_map = m.downcast_bound::(py).unwrap(); assert!(py_map.len() == 1); assert!( @@ -167,7 +167,7 @@ mod tests { map.insert(1, 1); let m: PyObject = map.into_py(py); - let py_map: &PyDict = m.downcast(py).unwrap(); + let py_map = m.downcast_bound::(py).unwrap(); assert!(py_map.len() == 1); assert!( @@ -189,7 +189,7 @@ mod tests { map.insert(1, 1); let m: PyObject = map.into_py(py); - let py_map: &PyDict = m.downcast(py).unwrap(); + let py_map = m.downcast_bound::(py).unwrap(); assert!(py_map.len() == 1); assert!( diff --git a/src/err/mod.rs b/src/err/mod.rs index 0c2637a7389..cf74739db87 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -62,6 +62,15 @@ impl<'a> PyDowncastError<'a> { to: to.into(), } } + + /// Compatibility API to convert the Bound variant `DowncastError` into the + /// gil-ref variant + pub(crate) fn from_downcast_err(DowncastError { from, to }: DowncastError<'a, 'a>) -> Self { + Self { + from: from.as_gil_ref(), + to, + } + } } /// Error that indicates a failure to convert a PyAny to a more specific Python type. diff --git a/src/instance.rs b/src/instance.rs index cb803cc6c4d..821aad81e58 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -7,8 +7,8 @@ use crate::types::any::PyAnyMethods; use crate::types::string::PyStringMethods; use crate::types::{PyDict, PyString, PyTuple}; use crate::{ - ffi, AsPyPointer, FromPyObject, IntoPy, PyAny, PyClass, PyClassInitializer, PyRef, PyRefMut, - PyTypeInfo, Python, ToPyObject, + ffi, AsPyPointer, DowncastError, FromPyObject, IntoPy, PyAny, PyClass, PyClassInitializer, + PyRef, PyRefMut, PyTypeInfo, Python, ToPyObject, }; use crate::{gil, PyTypeCheck}; use std::marker::PhantomData; @@ -1686,6 +1686,23 @@ impl std::fmt::Debug for Py { 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" + ) + )] + #[inline] + pub fn downcast<'py, T>(&'py self, py: Python<'py>) -> Result<&'py T, PyDowncastError<'py>> + where + T: PyTypeCheck, + { + self.downcast_bound::(py) + .map(Bound::as_gil_ref) + .map_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 @@ -1703,8 +1720,8 @@ impl PyObject { /// Python::with_gil(|py| { /// let any: PyObject = PyDict::new_bound(py).into(); /// - /// assert!(any.downcast::(py).is_ok()); - /// assert!(any.downcast::(py).is_err()); + /// assert!(any.downcast_bound::(py).is_ok()); + /// assert!(any.downcast_bound::(py).is_err()); /// }); /// ``` /// @@ -1725,9 +1742,9 @@ impl PyObject { /// Python::with_gil(|py| { /// let class: PyObject = Py::new(py, Class { i: 0 }).unwrap().into_py(py); /// - /// let class_cell: &PyCell = class.downcast(py)?; + /// let class_bound = class.downcast_bound::(py)?; /// - /// class_cell.borrow_mut().i += 1; + /// class_bound.borrow_mut().i += 1; /// /// // Alternatively you can get a `PyRefMut` directly /// let class_ref: PyRefMut<'_, Class> = class.extract(py)?; @@ -1737,24 +1754,44 @@ impl PyObject { /// # } /// ``` #[inline] - pub fn downcast<'py, T>(&'py self, py: Python<'py>) -> Result<&'py T, PyDowncastError<'py>> + pub fn downcast_bound<'py, T>( + &self, + py: Python<'py>, + ) -> Result<&Bound<'py, T>, DowncastError<'_, 'py>> where - T: PyTypeCheck, + T: PyTypeCheck, { - self.as_ref(py).downcast() + self.bind(py).downcast() } - /// Casts the PyObject to a concrete Python object type without checking validity. + /// Deprecated form of [`PyObject::downcast_bound_unchecked`] /// /// # Safety /// /// Callers must ensure that the type is valid or risk type confusion. + #[cfg_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" + ) + )] #[inline] - pub unsafe fn downcast_unchecked<'p, T>(&'p self, py: Python<'p>) -> &T + pub unsafe fn downcast_unchecked<'py, T>(&'py self, py: Python<'py>) -> &T where T: HasPyGilRef, { - self.as_ref(py).downcast_unchecked() + self.downcast_bound_unchecked::(py).as_gil_ref() + } + + /// Casts the PyObject to a concrete Python object type without checking validity. + /// + /// # Safety + /// + /// Callers must ensure that the type is valid or risk type confusion. + #[inline] + pub unsafe fn downcast_bound_unchecked<'py, T>(&self, py: Python<'py>) -> &Bound<'py, T> { + self.bind(py).downcast_unchecked() } } diff --git a/src/marker.rs b/src/marker.rs index fc84ca54c90..5321eddb2d5 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -826,6 +826,13 @@ 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 = "part of the deprecated GIL Ref API; to migrate 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>> where T: PyTypeCheck, @@ -839,6 +846,13 @@ 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 = "part of the deprecated GIL Ref API; to migrate 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, diff --git a/src/types/mapping.rs b/src/types/mapping.rs index 81bb2e63912..86c605f8bed 100644 --- a/src/types/mapping.rs +++ b/src/types/mapping.rs @@ -299,13 +299,13 @@ mod tests { Python::with_gil(|py| { let mut v = HashMap::new(); let ob = v.to_object(py); - let mapping: &PyMapping = ob.downcast(py).unwrap(); + let mapping = ob.downcast_bound::(py).unwrap(); assert_eq!(0, mapping.len().unwrap()); assert!(mapping.is_empty().unwrap()); v.insert(7, 32); let ob = v.to_object(py); - let mapping2: &PyMapping = ob.downcast(py).unwrap(); + let mapping2 = ob.downcast_bound::(py).unwrap(); assert_eq!(1, mapping2.len().unwrap()); assert!(!mapping2.is_empty().unwrap()); }); @@ -317,7 +317,7 @@ mod tests { let mut v = HashMap::new(); v.insert("key0", 1234); let ob = v.to_object(py); - let mapping: &PyMapping = ob.downcast(py).unwrap(); + let mapping = ob.downcast_bound::(py).unwrap(); mapping.set_item("key1", "foo").unwrap(); assert!(mapping.contains("key0").unwrap()); @@ -332,7 +332,7 @@ mod tests { let mut v = HashMap::new(); v.insert(7, 32); let ob = v.to_object(py); - let mapping: &PyMapping = ob.downcast(py).unwrap(); + let mapping = ob.downcast_bound::(py).unwrap(); assert_eq!( 32, mapping.get_item(7i32).unwrap().extract::().unwrap() @@ -350,7 +350,7 @@ mod tests { let mut v = HashMap::new(); v.insert(7, 32); let ob = v.to_object(py); - let mapping: &PyMapping = ob.downcast(py).unwrap(); + let mapping = ob.downcast_bound::(py).unwrap(); assert!(mapping.set_item(7i32, 42i32).is_ok()); // change assert!(mapping.set_item(8i32, 123i32).is_ok()); // insert assert_eq!( @@ -370,7 +370,7 @@ mod tests { let mut v = HashMap::new(); v.insert(7, 32); let ob = v.to_object(py); - let mapping: &PyMapping = ob.downcast(py).unwrap(); + let mapping = ob.downcast_bound::(py).unwrap(); assert!(mapping.del_item(7i32).is_ok()); assert_eq!(0, mapping.len().unwrap()); assert!(mapping @@ -388,12 +388,12 @@ mod tests { v.insert(8, 42); v.insert(9, 123); let ob = v.to_object(py); - let mapping: &PyMapping = ob.downcast(py).unwrap(); + let mapping = ob.downcast_bound::(py).unwrap(); // Can't just compare against a vector of tuples since we don't have a guaranteed ordering. let mut key_sum = 0; let mut value_sum = 0; for el in mapping.items().unwrap().iter().unwrap() { - let tuple = el.unwrap().downcast::().unwrap(); + let tuple = el.unwrap().downcast_into::().unwrap(); key_sum += tuple.get_item(0).unwrap().extract::().unwrap(); value_sum += tuple.get_item(1).unwrap().extract::().unwrap(); } @@ -410,7 +410,7 @@ mod tests { v.insert(8, 42); v.insert(9, 123); let ob = v.to_object(py); - let mapping: &PyMapping = ob.downcast(py).unwrap(); + let mapping = ob.downcast_bound::(py).unwrap(); // Can't just compare against a vector of tuples since we don't have a guaranteed ordering. let mut key_sum = 0; for el in mapping.keys().unwrap().iter().unwrap() { @@ -428,7 +428,7 @@ mod tests { v.insert(8, 42); v.insert(9, 123); let ob = v.to_object(py); - let mapping: &PyMapping = ob.downcast(py).unwrap(); + let mapping = ob.downcast_bound::(py).unwrap(); // Can't just compare against a vector of tuples since we don't have a guaranteed ordering. let mut values_sum = 0; for el in mapping.values().unwrap().iter().unwrap() { diff --git a/src/types/none.rs b/src/types/none.rs index 6460482e5ff..a14af044808 100644 --- a/src/types/none.rs +++ b/src/types/none.rs @@ -103,7 +103,7 @@ mod tests { #[test] fn test_unit_to_object_is_none() { Python::with_gil(|py| { - assert!(().to_object(py).downcast::(py).is_ok()); + assert!(().to_object(py).downcast_bound::(py).is_ok()); }) } @@ -111,7 +111,7 @@ mod tests { fn test_unit_into_py_is_none() { Python::with_gil(|py| { let obj: PyObject = ().into_py(py); - assert!(obj.downcast::(py).is_ok()); + assert!(obj.downcast_bound::(py).is_ok()); }) } diff --git a/src/types/tuple.rs b/src/types/tuple.rs index a8737c256c4..e41fcbf0020 100644 --- a/src/types/tuple.rs +++ b/src/types/tuple.rs @@ -169,7 +169,7 @@ impl PyTuple { /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| -> PyResult<()> { /// let ob = (1, 2, 3).to_object(py); - /// let tuple: &PyTuple = ob.downcast(py).unwrap(); + /// let tuple = ob.downcast_bound::(py).unwrap(); /// let obj = tuple.get_item(0); /// assert_eq!(obj.unwrap().extract::().unwrap(), 1); /// Ok(()) @@ -273,7 +273,7 @@ pub trait PyTupleMethods<'py> { /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| -> PyResult<()> { /// let ob = (1, 2, 3).to_object(py); - /// let tuple: &PyTuple = ob.downcast(py).unwrap(); + /// let tuple = ob.downcast_bound::(py).unwrap(); /// let obj = tuple.get_item(0); /// assert_eq!(obj.unwrap().extract::().unwrap(), 1); /// Ok(()) From f04ad56df465170c1e5f5970c2a41cce96454ff3 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sun, 18 Feb 2024 03:07:48 +0000 Subject: [PATCH 010/936] implement `PyTypeMethods` (#3705) * implement `PyTypeMethods` * introduce `PyType` bound constructors * `from_type_ptr_bound` instead of `from_type_ptr_borrowed` * correct conditional code * just make `from_type_ptr_bound` create an owned `Bound` * correct docstrings Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> * Rework as `PyType::from_borrowed_type_ptr` * correct doc link to `from_borrowed_type_ptr` Co-authored-by: Lily Foote * remove unneeded lifetime name --------- Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> Co-authored-by: Lily Foote --- src/err/err_state.rs | 3 +- src/err/mod.rs | 5 +- src/instance.rs | 3 +- src/prelude.rs | 1 + src/types/any.rs | 12 +-- src/types/boolobject.rs | 5 +- src/types/mod.rs | 2 +- src/types/typeobject.rs | 171 ++++++++++++++++++++++++++++++------- tests/test_class_basics.rs | 2 +- 9 files changed, 155 insertions(+), 49 deletions(-) diff --git a/src/err/err_state.rs b/src/err/err_state.rs index eb03d8b9ff2..9f85296f661 100644 --- a/src/err/err_state.rs +++ b/src/err/err_state.rs @@ -22,9 +22,8 @@ impl PyErrStateNormalized { #[cfg(Py_3_12)] pub(crate) fn ptype<'py>(&self, py: Python<'py>) -> Bound<'py, PyType> { - use crate::instance::PyNativeType; use crate::types::any::PyAnyMethods; - self.pvalue.bind(py).get_type().as_borrowed().to_owned() + self.pvalue.bind(py).get_type() } #[cfg(not(Py_3_12))] diff --git a/src/err/mod.rs b/src/err/mod.rs index cf74739db87..d55c8f45afe 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -2,8 +2,7 @@ use crate::instance::Bound; use crate::panic::PanicException; use crate::type_object::PyTypeInfo; use crate::types::any::PyAnyMethods; -use crate::types::string::PyStringMethods; -use crate::types::{PyTraceback, PyType}; +use crate::types::{string::PyStringMethods, typeobject::PyTypeMethods, PyTraceback, PyType}; use crate::{ exceptions::{self, PyBaseException}, ffi, @@ -280,7 +279,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::(py))); + /// assert!(err.get_type_bound(py).is(&PyType::new_bound::(py))); /// }); /// ``` pub fn get_type_bound<'py>(&self, py: Python<'py>) -> Bound<'py, PyType> { diff --git a/src/instance.rs b/src/instance.rs index 821aad81e58..3e280c43ffe 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -3,8 +3,7 @@ use crate::ffi_ptr_ext::FfiPtrExt; use crate::pycell::{PyBorrowError, PyBorrowMutError, PyCell}; use crate::pyclass::boolean_struct::{False, True}; use crate::type_object::HasPyGilRef; -use crate::types::any::PyAnyMethods; -use crate::types::string::PyStringMethods; +use crate::types::{any::PyAnyMethods, string::PyStringMethods, typeobject::PyTypeMethods}; use crate::types::{PyDict, PyString, PyTuple}; use crate::{ ffi, AsPyPointer, DowncastError, FromPyObject, IntoPy, PyAny, PyClass, PyClassInitializer, diff --git a/src/prelude.rs b/src/prelude.rs index 47951aedcce..3ac239f49e1 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -41,3 +41,4 @@ pub use crate::types::set::PySetMethods; pub use crate::types::string::PyStringMethods; pub use crate::types::traceback::PyTracebackMethods; pub use crate::types::tuple::PyTupleMethods; +pub use crate::types::typeobject::PyTypeMethods; diff --git a/src/types/any.rs b/src/types/any.rs index 33b3d96405a..1cb6bb779f1 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -667,7 +667,7 @@ impl PyAny { /// Returns the Python type object for this object's type. pub fn get_type(&self) -> &PyType { - self.as_borrowed().get_type() + self.as_borrowed().get_type().into_gil_ref() } /// Returns the Python type pointer for this object. @@ -1499,7 +1499,7 @@ pub trait PyAnyMethods<'py> { fn iter(&self) -> PyResult>; /// Returns the Python type object for this object's type. - fn get_type(&self) -> &'py PyType; + fn get_type(&self) -> Bound<'py, PyType>; /// Returns the Python type pointer for this object. fn get_type_ptr(&self) -> *mut ffi::PyTypeObject; @@ -2107,8 +2107,8 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { PyIterator::from_bound_object(self) } - fn get_type(&self) -> &'py PyType { - unsafe { PyType::from_type_ptr(self.py(), ffi::Py_TYPE(self.as_ptr())) } + fn get_type(&self) -> Bound<'py, PyType> { + unsafe { PyType::from_borrowed_type_ptr(self.py(), ffi::Py_TYPE(self.as_ptr())) } } #[inline] @@ -2265,7 +2265,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { #[cfg(not(PyPy))] fn py_super(&self) -> PyResult> { - PySuper::new_bound(&self.get_type().as_borrowed(), self) + PySuper::new_bound(&self.get_type(), self) } } @@ -2286,7 +2286,7 @@ impl<'py> Bound<'py, PyAny> { N: IntoPy>, { let py = self.py(); - let self_type = self.get_type().as_borrowed(); + let self_type = self.get_type(); let attr = if let Ok(attr) = self_type.getattr(attr_name) { attr } else { diff --git a/src/types/boolobject.rs b/src/types/boolobject.rs index c0c4e4ba5c2..01bc14b4431 100644 --- a/src/types/boolobject.rs +++ b/src/types/boolobject.rs @@ -1,8 +1,9 @@ #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; use crate::{ - exceptions::PyTypeError, ffi, ffi_ptr_ext::FfiPtrExt, instance::Bound, Borrowed, FromPyObject, - IntoPy, PyAny, PyNativeType, PyObject, PyResult, Python, ToPyObject, + exceptions::PyTypeError, ffi, ffi_ptr_ext::FfiPtrExt, instance::Bound, + types::typeobject::PyTypeMethods, Borrowed, FromPyObject, IntoPy, PyAny, PyNativeType, + PyObject, PyResult, Python, ToPyObject, }; use super::any::PyAnyMethods; diff --git a/src/types/mod.rs b/src/types/mod.rs index da22b30729f..46909c880bb 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -309,4 +309,4 @@ mod slice; pub(crate) mod string; pub(crate) mod traceback; pub(crate) mod tuple; -mod typeobject; +pub(crate) mod typeobject; diff --git a/src/types/typeobject.rs b/src/types/typeobject.rs index 50eeaa7153d..f0e4a8c0a2c 100644 --- a/src/types/typeobject.rs +++ b/src/types/typeobject.rs @@ -1,5 +1,7 @@ use crate::err::{self, PyResult}; -use crate::{ffi, PyAny, PyTypeInfo, Python}; +use crate::instance::Borrowed; +use crate::types::any::PyAnyMethods; +use crate::{ffi, Bound, PyAny, PyNativeType, PyTypeInfo, Python}; use std::borrow::Cow; #[cfg(not(any(Py_LIMITED_API, PyPy)))] use std::ffi::CStr; @@ -11,38 +13,143 @@ 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. + /// 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" + ) + )] pub fn new(py: Python<'_>) -> &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 { - self.as_ptr() as *mut ffi::PyTypeObject + self.as_borrowed().as_type_ptr() } - /// Retrieves the `PyType` instance for the given FFI pointer. + /// Deprecated form of [`PyType::from_borrowed_type_ptr`]. /// /// # Safety - /// - The pointer must be non-null. - /// - The pointer must be valid for the entire of the lifetime for which the reference is used. + /// + /// - The pointer must a valid non-null reference to a `PyTypeObject`. #[inline] + #[cfg_attr( + not(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 { - py.from_borrowed_ptr(p as *mut ffi::PyObject) + 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() + } + + /// Gets the full name, which includes the module, of the `PyType`. + pub fn name(&self) -> PyResult> { + self.as_borrowed().name() + } + + /// Checks whether `self` is a subclass of `other`. + /// + /// Equivalent to the Python expression `issubclass(self, other)`. + pub fn is_subclass(&self, other: &PyAny) -> PyResult { + self.as_borrowed().is_subclass(&other.as_borrowed()) + } + + /// Checks whether `self` is a subclass of type `T`. + /// + /// Equivalent to the Python expression `issubclass(self, T)`, if the type + /// `T` is known at compile time. + pub fn is_subclass_of(&self) -> PyResult + where + T: PyTypeInfo, + { + self.as_borrowed().is_subclass_of::() + } +} + +/// Implementation of functionality for [`PyType`]. +/// +/// These methods are defined for the `Bound<'py, PyType>` smart pointer, so to use method call +/// syntax these methods are separated into a trait, because stable Rust does not yet support +/// `arbitrary_self_types`. +#[doc(alias = "PyType")] +pub trait PyTypeMethods<'py> { + /// Retrieves the underlying FFI pointer associated with this Python object. + fn as_type_ptr(&self) -> *mut ffi::PyTypeObject; + + /// Gets the full name, which includes the module, of the `PyType`. + fn name(&self) -> PyResult>; + + /// Gets the [qualified name](https://docs.python.org/3/glossary.html#term-qualified-name) of the `PyType`. + fn qualname(&self) -> PyResult; + + /// Checks whether `self` is a subclass of `other`. + /// + /// Equivalent to the Python expression `issubclass(self, other)`. + fn is_subclass(&self, other: &Bound<'_, PyAny>) -> PyResult; + + /// Checks whether `self` is a subclass of type `T`. + /// + /// Equivalent to the Python expression `issubclass(self, T)`, if the type + /// `T` is known at compile time. + fn is_subclass_of(&self) -> PyResult + where + T: PyTypeInfo; +} + +impl<'py> PyTypeMethods<'py> for Bound<'py, PyType> { + /// Retrieves the underlying FFI pointer associated with this Python object. + #[inline] + fn as_type_ptr(&self) -> *mut ffi::PyTypeObject { + self.as_ptr() as *mut ffi::PyTypeObject + } + + /// Gets the name of the `PyType`. + fn name(&self) -> PyResult> { + Borrowed::from(self).name() + } + + fn qualname(&self) -> PyResult { #[cfg(any(Py_LIMITED_API, PyPy, not(Py_3_11)))] let name = self.getattr(intern!(self.py(), "__qualname__"))?.extract(); #[cfg(not(any(Py_LIMITED_API, PyPy, not(Py_3_11))))] let name = { use crate::ffi_ptr_ext::FfiPtrExt; - use crate::types::any::PyAnyMethods; - let obj = unsafe { ffi::PyType_GetQualName(self.as_type_ptr()).assume_owned_or_err(self.py())? }; @@ -53,8 +160,29 @@ impl PyType { name } - /// Gets the full name, which includes the module, of the `PyType`. - pub fn name(&self) -> PyResult> { + /// Checks whether `self` is a subclass of `other`. + /// + /// Equivalent to the Python expression `issubclass(self, other)`. + fn is_subclass(&self, other: &Bound<'_, PyAny>) -> PyResult { + let result = unsafe { ffi::PyObject_IsSubclass(self.as_ptr(), other.as_ptr()) }; + err::error_on_minusone(self.py(), result)?; + Ok(result == 1) + } + + /// Checks whether `self` is a subclass of type `T`. + /// + /// Equivalent to the Python expression `issubclass(self, T)`, if the type + /// `T` is known at compile time. + fn is_subclass_of(&self) -> PyResult + where + T: PyTypeInfo, + { + self.is_subclass(&T::type_object_bound(self.py())) + } +} + +impl<'a> Borrowed<'a, '_, PyType> { + fn name(self) -> PyResult> { #[cfg(not(any(Py_LIMITED_API, PyPy)))] { let ptr = self.as_type_ptr(); @@ -79,33 +207,12 @@ impl PyType { #[cfg(Py_3_11)] let name = { use crate::ffi_ptr_ext::FfiPtrExt; - unsafe { ffi::PyType_GetName(self.as_type_ptr()).assume_owned_or_err(self.py())? } }; Ok(Cow::Owned(format!("{}.{}", module, name))) } } - - /// Checks whether `self` is a subclass of `other`. - /// - /// Equivalent to the Python expression `issubclass(self, other)`. - pub fn is_subclass(&self, other: &PyAny) -> PyResult { - let result = unsafe { ffi::PyObject_IsSubclass(self.as_ptr(), other.as_ptr()) }; - err::error_on_minusone(self.py(), result)?; - Ok(result == 1) - } - - /// Checks whether `self` is a subclass of type `T`. - /// - /// Equivalent to the Python expression `issubclass(self, T)`, if the type - /// `T` is known at compile time. - pub fn is_subclass_of(&self) -> PyResult - where - T: PyTypeInfo, - { - self.is_subclass(T::type_object_bound(self.py()).as_gil_ref()) - } } #[cfg(test)] diff --git a/tests/test_class_basics.rs b/tests/test_class_basics.rs index d19d106d206..1c2b2a5cce3 100644 --- a/tests/test_class_basics.rs +++ b/tests/test_class_basics.rs @@ -230,7 +230,7 @@ impl UnsendableChild { fn test_unsendable() -> PyResult<()> { let obj = Python::with_gil(|py| -> PyResult<_> { - let obj: Py = PyType::new::(py).call1((5,))?.extract()?; + let obj: Py = PyType::new_bound::(py).call1((5,))?.extract()?; // Accessing the value inside this thread should not panic let caught_panic = From 4ce9c35983712b76645864ade4ced46f9a5748f7 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sun, 18 Feb 2024 19:27:19 +0100 Subject: [PATCH 011/936] port `Python::get_type` to `Bound` API (#3846) * port `Python::get_type` to `Bound` API * fix `is_subclass_and_is_instance` FIXME --- guide/src/class.md | 22 +++++++++--------- guide/src/exception.md | 4 ++-- pyo3-macros-backend/src/pyclass.rs | 2 +- src/conversions/chrono.rs | 6 ++--- src/conversions/num_bigint.rs | 8 ++----- src/err/mod.rs | 10 ++++---- src/exceptions.rs | 17 +++++++------- src/impl_/extract_argument.rs | 5 +++- src/macros.rs | 2 +- src/marker.rs | 18 ++++++++++++++- src/pyclass_init.rs | 2 +- src/tests/common.rs | 2 +- src/types/typeobject.rs | 12 ++++++---- tests/test_class_attributes.rs | 16 ++++++------- tests/test_class_basics.rs | 14 +++++------ tests/test_class_new.rs | 37 ++++++++++++++---------------- tests/test_coroutine.rs | 11 +++++---- tests/test_enum.rs | 10 ++++---- tests/test_gc.rs | 30 ++++++++++++------------ tests/test_getter_setter.rs | 2 +- tests/test_inheritance.rs | 26 ++++++++++----------- 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_no_imports.rs | 14 ++++++----- 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 ++-- 33 files changed, 175 insertions(+), 157 deletions(-) diff --git a/guide/src/class.md b/guide/src/class.md index 6ca582ed963..9a2d91c1cb1 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -401,7 +401,7 @@ impl SubSubClass { # pyo3::py_run!(py, subsub, "assert subsub.method3() == 3000"); # let subsub = SubSubClass::factory_method(py, 2).unwrap(); # let subsubsub = SubSubClass::factory_method(py, 3).unwrap(); -# let cls = py.get_type::(); +# let cls = py.get_type_bound::(); # pyo3::py_run!(py, subsub cls, "assert not isinstance(subsub, cls)"); # pyo3::py_run!(py, subsubsub cls, "assert isinstance(subsubsub, cls)"); # }); @@ -497,7 +497,7 @@ impl MyDict { // some custom methods that use `private` here... } # Python::with_gil(|py| { -# let cls = py.get_type::(); +# let cls = py.get_type_bound::(); # pyo3::py_run!(py, cls, "cls(a=1, b=2)") # }); # } @@ -767,7 +767,7 @@ impl MyClass { } Python::with_gil(|py| { - let my_class = py.get_type::(); + let my_class = py.get_type_bound::(); pyo3::py_run!(py, my_class, "assert my_class.my_attribute == 'hello'") }); ``` @@ -1026,7 +1026,7 @@ enum MyEnum { Python::with_gil(|py| { let x = Py::new(py, MyEnum::Variant).unwrap(); let y = Py::new(py, MyEnum::OtherVariant).unwrap(); - let cls = py.get_type::(); + let cls = py.get_type_bound::(); pyo3::py_run!(py, x y cls, r#" assert x == cls.Variant assert y == cls.OtherVariant @@ -1046,7 +1046,7 @@ enum MyEnum { } Python::with_gil(|py| { - let cls = py.get_type::(); + let cls = py.get_type_bound::(); let x = MyEnum::Variant as i32; // The exact value is assigned by the compiler. pyo3::py_run!(py, cls x, r#" assert int(cls.Variant) == x @@ -1068,7 +1068,7 @@ enum MyEnum{ } Python::with_gil(|py| { - let cls = py.get_type::(); + let cls = py.get_type_bound::(); let x = Py::new(py, MyEnum::Variant).unwrap(); pyo3::py_run!(py, cls x, r#" assert repr(x) == 'MyEnum.Variant' @@ -1094,7 +1094,7 @@ impl MyEnum { } Python::with_gil(|py| { - let cls = py.get_type::(); + let cls = py.get_type_bound::(); pyo3::py_run!(py, cls, "assert repr(cls.Answer) == '42'") }) ``` @@ -1111,7 +1111,7 @@ enum MyEnum { Python::with_gil(|py| { let x = Py::new(py, MyEnum::Variant).unwrap(); - let cls = py.get_type::(); + let cls = py.get_type_bound::(); pyo3::py_run!(py, x cls, r#" assert repr(x) == 'RenamedEnum.UPPERCASE' assert x == cls.UPPERCASE @@ -1165,7 +1165,7 @@ enum Shape { 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 cls = py.get_type::(); + let cls = py.get_type_bound::(); pyo3::py_run!(py, circle square cls, r#" assert isinstance(circle, cls) assert isinstance(circle, cls.Circle) @@ -1204,7 +1204,7 @@ enum MyEnum { Python::with_gil(|py| { let x = Py::new(py, MyEnum::Variant { i: 42 }).unwrap(); - let cls = py.get_type::(); + let cls = py.get_type_bound::(); pyo3::py_run!(py, x cls, r#" assert isinstance(x, cls) assert not isinstance(x, cls.Variant) @@ -1308,7 +1308,7 @@ impl pyo3::impl_::pyclass::PyClassImpl for MyClass { } # Python::with_gil(|py| { -# let cls = py.get_type::(); +# let cls = py.get_type_bound::(); # pyo3::py_run!(py, cls, "assert cls.__name__ == 'MyClass'") # }); # } diff --git a/guide/src/exception.md b/guide/src/exception.md index 225118576f7..0be44167760 100644 --- a/guide/src/exception.md +++ b/guide/src/exception.md @@ -24,7 +24,7 @@ use pyo3::exceptions::PyException; create_exception!(mymodule, CustomError, PyException); Python::with_gil(|py| { - let ctx = [("CustomError", py.get_type::())].into_py_dict_bound(py); + let ctx = [("CustomError", py.get_type_bound::())].into_py_dict_bound(py); pyo3::py_run!( py, *ctx, @@ -46,7 +46,7 @@ pyo3::create_exception!(mymodule, CustomError, PyException); #[pymodule] fn mymodule(py: Python<'_>, m: &PyModule) -> PyResult<()> { // ... other elements added to module ... - m.add("CustomError", py.get_type::())?; + m.add("CustomError", py.get_type_bound::())?; Ok(()) } diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 02024011366..7d8c868e3d5 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -1086,7 +1086,7 @@ pub fn gen_complex_enum_variant_attr( let associated_method = quote! { fn #wrapper_ident(py: _pyo3::Python<'_>) -> _pyo3::PyResult<_pyo3::PyObject> { #deprecations - ::std::result::Result::Ok(py.get_type::<#variant_cls>().into()) + ::std::result::Result::Ok(py.get_type_bound::<#variant_cls>().into_any().unbind()) } }; diff --git a/src/conversions/chrono.rs b/src/conversions/chrono.rs index 6737c16f8cf..6878b9d9b1e 100644 --- a/src/conversions/chrono.rs +++ b/src/conversions/chrono.rs @@ -52,9 +52,7 @@ use crate::types::{ }; #[cfg(Py_LIMITED_API)] use crate::{intern, DowncastError}; -use crate::{ - Bound, FromPyObject, IntoPy, PyAny, PyErr, PyNativeType, PyObject, PyResult, Python, ToPyObject, -}; +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, @@ -461,7 +459,7 @@ fn warn_truncated_leap_second(obj: &Bound<'_, PyAny>) { let py = obj.py(); if let Err(e) = PyErr::warn_bound( py, - &py.get_type::().as_borrowed(), + &py.get_type_bound::(), "ignored leap-second, `datetime` does not support leap-seconds", 0, ) { diff --git a/src/conversions/num_bigint.rs b/src/conversions/num_bigint.rs index ff5bd0309bd..3c98a90f092 100644 --- a/src/conversions/num_bigint.rs +++ b/src/conversions/num_bigint.rs @@ -91,12 +91,8 @@ macro_rules! bigint_conversion { } else { None }; - py.get_type::() - .call_method( - "from_bytes", - (bytes_obj, "little"), - kwargs.as_ref().map(crate::Bound::as_gil_ref), - ) + 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() } diff --git a/src/err/mod.rs b/src/err/mod.rs index d55c8f45afe..d922baaad21 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -724,7 +724,7 @@ impl PyErr { /// # use pyo3::prelude::*; /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { - /// let user_warning = py.get_type::().as_borrowed(); + /// let user_warning = py.get_type_bound::(); /// PyErr::warn_bound(py, &user_warning, "I am warning you", 0)?; /// Ok(()) /// }) @@ -1080,7 +1080,7 @@ impl_signed_integer!(isize); mod tests { use super::PyErrState; use crate::exceptions::{self, PyTypeError, PyValueError}; - use crate::{PyErr, PyNativeType, PyTypeInfo, Python}; + use crate::{PyErr, PyTypeInfo, Python}; #[test] fn no_error() { @@ -1278,7 +1278,7 @@ mod tests { // GIL locked should prevent effects to be visible to other testing // threads. Python::with_gil(|py| { - let cls = py.get_type::().as_borrowed(); + let cls = py.get_type_bound::(); // Reset warning filter to default state let warnings = py.import_bound("warnings").unwrap(); @@ -1293,14 +1293,14 @@ mod tests { // Test with raising warnings - .call_method1("simplefilter", ("error", cls)) + .call_method1("simplefilter", ("error", &cls)) .unwrap(); PyErr::warn_bound(py, &cls, "I am warning you", 0).unwrap_err(); // Test with error for an explicit module warnings.call_method0("resetwarnings").unwrap(); warnings - .call_method1("filterwarnings", ("error", "", cls, "pyo3test")) + .call_method1("filterwarnings", ("error", "", &cls, "pyo3test")) .unwrap(); // This has the wrong module and will not raise, just be emitted diff --git a/src/exceptions.rs b/src/exceptions.rs index e7b6407e721..6ae9febc695 100644 --- a/src/exceptions.rs +++ b/src/exceptions.rs @@ -72,7 +72,7 @@ macro_rules! impl_exception_boilerplate { /// import_exception!(socket, gaierror); /// /// Python::with_gil(|py| { -/// let ctx = [("gaierror", py.get_type::())].into_py_dict_bound(py); +/// let ctx = [("gaierror", py.get_type_bound::())].into_py_dict_bound(py); /// pyo3::py_run!(py, *ctx, "import socket; assert gaierror is socket.gaierror"); /// }); /// @@ -160,7 +160,7 @@ macro_rules! import_exception { /// /// #[pymodule] /// fn my_module(py: Python<'_>, m: &PyModule) -> PyResult<()> { -/// m.add("MyError", py.get_type::())?; +/// m.add("MyError", py.get_type_bound::())?; /// m.add_function(wrap_pyfunction!(raise_myerror, py)?)?; /// Ok(()) /// } @@ -168,7 +168,7 @@ macro_rules! import_exception { /// # Python::with_gil(|py| -> PyResult<()> { /// # let fun = wrap_pyfunction!(raise_myerror, py)?; /// # let locals = pyo3::types::PyDict::new_bound(py); -/// # locals.set_item("MyError", py.get_type::())?; +/// # locals.set_item("MyError", py.get_type_bound::())?; /// # locals.set_item("raise_myerror", fun)?; /// # /// # py.run_bound( @@ -241,7 +241,6 @@ macro_rules! create_exception_type_object { impl $name { fn type_object_raw(py: $crate::Python<'_>) -> *mut $crate::ffi::PyTypeObject { use $crate::sync::GILOnceCell; - use $crate::PyNativeType; static TYPE_OBJECT: GILOnceCell<$crate::Py<$crate::types::PyType>> = GILOnceCell::new(); @@ -251,7 +250,7 @@ macro_rules! create_exception_type_object { py, concat!(stringify!($module), ".", stringify!($name)), $doc, - ::std::option::Option::Some(&py.get_type::<$base>().as_borrowed()), + ::std::option::Option::Some(&py.get_type_bound::<$base>()), ::std::option::Option::None, ).expect("Failed to initialize new exception type.") ).as_ptr() as *mut $crate::ffi::PyTypeObject @@ -904,7 +903,7 @@ mod tests { create_exception!(mymodule, CustomError, PyException); Python::with_gil(|py| { - let error_type = py.get_type::(); + let error_type = py.get_type_bound::(); let ctx = [("CustomError", error_type)].into_py_dict_bound(py); let type_description: String = py .eval_bound("str(CustomError)", None, Some(&ctx)) @@ -927,7 +926,7 @@ mod tests { fn custom_exception_dotted_module() { create_exception!(mymodule.exceptions, CustomError, PyException); Python::with_gil(|py| { - let error_type = py.get_type::(); + let error_type = py.get_type_bound::(); let ctx = [("CustomError", error_type)].into_py_dict_bound(py); let type_description: String = py .eval_bound("str(CustomError)", None, Some(&ctx)) @@ -946,7 +945,7 @@ mod tests { create_exception!(mymodule, CustomError, PyException, "Some docs"); Python::with_gil(|py| { - let error_type = py.get_type::(); + let error_type = py.get_type_bound::(); let ctx = [("CustomError", error_type)].into_py_dict_bound(py); let type_description: String = py .eval_bound("str(CustomError)", None, Some(&ctx)) @@ -979,7 +978,7 @@ mod tests { ); Python::with_gil(|py| { - let error_type = py.get_type::(); + let error_type = py.get_type_bound::(); let ctx = [("CustomError", error_type)].into_py_dict_bound(py); let type_description: String = py .eval_bound("str(CustomError)", None, Some(&ctx)) diff --git a/src/impl_/extract_argument.rs b/src/impl_/extract_argument.rs index d9487359cf4..27728c6d46f 100644 --- a/src/impl_/extract_argument.rs +++ b/src/impl_/extract_argument.rs @@ -166,7 +166,10 @@ pub fn from_py_with_with_default<'a, 'py, T>( #[cold] pub fn argument_extraction_error(py: Python<'_>, arg_name: &str, error: PyErr) -> PyErr { use crate::types::any::PyAnyMethods; - if error.get_type_bound(py).is(py.get_type::()) { + if error + .get_type_bound(py) + .is(&py.get_type_bound::()) + { let remapped_error = PyTypeError::new_err(format!( "argument '{}': {}", arg_name, diff --git a/src/macros.rs b/src/macros.rs index d5fbbdc3dfc..0779fb98a4b 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -73,7 +73,7 @@ /// } /// /// Python::with_gil(|py| { -/// let locals = [("C", py.get_type::())].into_py_dict_bound(py); +/// let locals = [("C", py.get_type_bound::())].into_py_dict_bound(py); /// pyo3::py_run!(py, *locals, "c = C()"); /// }); /// ``` diff --git a/src/marker.rs b/src/marker.rs index 5321eddb2d5..4b46d3badbd 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -728,12 +728,28 @@ 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" + ) + )] #[inline] pub fn get_type(self) -> &'py PyType where T: PyTypeInfo, { - T::type_object_bound(self).into_gil_ref() + 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> + where + T: PyTypeInfo, + { + T::type_object_bound(self) } /// Deprecated form of [`Python::import_bound`] diff --git a/src/pyclass_init.rs b/src/pyclass_init.rs index 63761f435bb..ac3aa3ef852 100644 --- a/src/pyclass_init.rs +++ b/src/pyclass_init.rs @@ -122,7 +122,7 @@ impl PyObjectInit for PyNativeTypeInitializer { /// } /// } /// Python::with_gil(|py| { -/// let typeobj = py.get_type::(); +/// let typeobj = py.get_type_bound::(); /// let sub_sub_class = typeobj.call((), None).unwrap(); /// py_run!( /// py, diff --git a/src/tests/common.rs b/src/tests/common.rs index 5eec60949bc..854d73e4d7b 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::()) { + if !err.matches($py, $py.get_type_bound::()) { panic!("Expected {} but got {:?}", stringify!($err), err) } err diff --git a/src/types/typeobject.rs b/src/types/typeobject.rs index f0e4a8c0a2c..4c1b17a2aa8 100644 --- a/src/types/typeobject.rs +++ b/src/types/typeobject.rs @@ -217,22 +217,26 @@ impl<'a> Borrowed<'a, '_, PyType> { #[cfg(test)] mod tests { + use crate::types::typeobject::PyTypeMethods; use crate::types::{PyBool, PyLong}; use crate::Python; #[test] fn test_type_is_subclass() { Python::with_gil(|py| { - let bool_type = py.get_type::(); - let long_type = py.get_type::(); - assert!(bool_type.is_subclass(long_type).unwrap()); + let bool_type = py.get_type_bound::(); + let long_type = py.get_type_bound::(); + assert!(bool_type.is_subclass(&long_type).unwrap()); }); } #[test] fn test_type_is_subclass_of() { Python::with_gil(|py| { - assert!(py.get_type::().is_subclass_of::().unwrap()); + assert!(py + .get_type_bound::() + .is_subclass_of::() + .unwrap()); }); } } diff --git a/tests/test_class_attributes.rs b/tests/test_class_attributes.rs index 906a11c8ea1..9e544211c3c 100644 --- a/tests/test_class_attributes.rs +++ b/tests/test_class_attributes.rs @@ -56,7 +56,7 @@ impl Foo { #[test] fn class_attributes() { Python::with_gil(|py| { - let foo_obj = py.get_type::(); + let foo_obj = py.get_type_bound::(); py_assert!(py, foo_obj, "foo_obj.MY_CONST == 'foobar'"); py_assert!(py, foo_obj, "foo_obj.RENAMED_CONST == 'foobar_2'"); py_assert!(py, foo_obj, "foo_obj.a == 5"); @@ -72,7 +72,7 @@ fn class_attributes() { #[ignore] fn class_attributes_are_immutable() { Python::with_gil(|py| { - let foo_obj = py.get_type::(); + let foo_obj = py.get_type_bound::(); py_expect_exception!(py, foo_obj, "foo_obj.a = 6", PyTypeError); }); } @@ -88,8 +88,8 @@ impl Bar { #[test] fn recursive_class_attributes() { Python::with_gil(|py| { - let foo_obj = py.get_type::(); - let bar_obj = py.get_type::(); + let foo_obj = py.get_type_bound::(); + let bar_obj = py.get_type_bound::(); py_assert!(py, foo_obj, "foo_obj.a_foo.x == 1"); py_assert!(py, foo_obj, "foo_obj.bar.x == 2"); py_assert!(py, bar_obj, "bar_obj.a_foo.x == 3"); @@ -145,7 +145,7 @@ fn test_fallible_class_attribute() { Python::with_gil(|py| { let stderr = CaptureStdErr::new(py).unwrap(); - assert!(std::panic::catch_unwind(|| py.get_type::()).is_err()); + assert!(std::panic::catch_unwind(|| py.get_type_bound::()).is_err()); assert_eq!( stderr.reset().unwrap().trim(), "\ @@ -187,7 +187,7 @@ fn test_renaming_all_struct_fields() { use pyo3::types::PyBool; Python::with_gil(|py| { - let struct_class = py.get_type::(); + let struct_class = py.get_type_bound::(); let struct_obj = struct_class.call0().unwrap(); assert!(struct_obj .setattr("firstField", PyBool::new_bound(py, false)) @@ -220,11 +220,11 @@ macro_rules! test_case { //use pyo3::types::PyInt; Python::with_gil(|py| { - let struct_class = py.get_type::<$struct_name>(); + let struct_class = py.get_type_bound::<$struct_name>(); let struct_obj = struct_class.call0().unwrap(); assert!(struct_obj.setattr($renamed_field_name, 2).is_ok()); let attr = struct_obj.getattr($renamed_field_name).unwrap(); - assert_eq!(2, PyAny::extract::(attr).unwrap()); + assert_eq!(2, attr.extract().unwrap()); }); } }; diff --git a/tests/test_class_basics.rs b/tests/test_class_basics.rs index 1c2b2a5cce3..6d282765c31 100644 --- a/tests/test_class_basics.rs +++ b/tests/test_class_basics.rs @@ -13,7 +13,7 @@ struct EmptyClass {} #[test] fn empty_class() { Python::with_gil(|py| { - let typeobj = py.get_type::(); + let typeobj = py.get_type_bound::(); // By default, don't allow creating instances from python. assert!(typeobj.call((), None).is_err()); @@ -27,7 +27,7 @@ struct UnitClass; #[test] fn unit_class() { Python::with_gil(|py| { - let typeobj = py.get_type::(); + let typeobj = py.get_type_bound::(); // By default, don't allow creating instances from python. assert!(typeobj.call((), None).is_err()); @@ -58,7 +58,7 @@ struct ClassWithDocs { #[test] fn class_with_docstr() { Python::with_gil(|py| { - let typeobj = py.get_type::(); + let typeobj = py.get_type_bound::(); py_run!( py, typeobj, @@ -104,7 +104,7 @@ impl EmptyClass2 { #[test] fn custom_names() { Python::with_gil(|py| { - let typeobj = py.get_type::(); + let typeobj = py.get_type_bound::(); py_assert!(py, typeobj, "typeobj.__name__ == 'CustomName'"); py_assert!(py, typeobj, "typeobj.custom_fn.__name__ == 'custom_fn'"); py_assert!( @@ -137,7 +137,7 @@ impl RawIdents { #[test] fn test_raw_idents() { Python::with_gil(|py| { - let typeobj = py.get_type::(); + let typeobj = py.get_type_bound::(); py_assert!(py, typeobj, "not hasattr(typeobj, 'r#fn')"); py_assert!(py, typeobj, "hasattr(typeobj, 'fn')"); py_assert!(py, typeobj, "hasattr(typeobj, 'type')"); @@ -191,7 +191,7 @@ impl ClassWithObjectField { #[test] fn class_with_object_field() { Python::with_gil(|py| { - let ty = py.get_type::(); + let ty = py.get_type_bound::(); py_assert!(py, ty, "ty(5).value == 5"); py_assert!(py, ty, "ty(None).value == None"); }); @@ -346,7 +346,7 @@ struct TupleClass(#[pyo3(get, set, name = "value")] i32); #[test] fn test_tuple_struct_class() { Python::with_gil(|py| { - let typeobj = py.get_type::(); + let typeobj = py.get_type_bound::(); assert!(typeobj.call((), None).is_err()); py_assert!(py, typeobj, "typeobj.__name__ == 'TupleClass'"); diff --git a/tests/test_class_new.rs b/tests/test_class_new.rs index 59772cc4cf2..b1a0e594799 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::(); + let typeobj = py.get_type_bound::(); assert!(typeobj .call((), None) .unwrap() @@ -29,10 +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).as_gil_ref()) - ) + .call((), Some(&[("some", "kwarg")].into_py_dict_bound(py))) .is_err()); }); } @@ -51,7 +48,7 @@ impl UnitClassWithNew { #[test] fn unit_class_with_new() { Python::with_gil(|py| { - let typeobj = py.get_type::(); + let typeobj = py.get_type_bound::(); assert!(typeobj .call((), None) .unwrap() @@ -74,9 +71,9 @@ impl TupleClassWithNew { #[test] fn tuple_class_with_new() { Python::with_gil(|py| { - let typeobj = py.get_type::(); + let typeobj = py.get_type_bound::(); let wrp = typeobj.call((42,), None).unwrap(); - let obj = wrp.downcast::>().unwrap(); + let obj = wrp.downcast::().unwrap(); let obj_ref = obj.borrow(); assert_eq!(obj_ref.0, 42); }); @@ -99,9 +96,9 @@ impl NewWithOneArg { #[test] fn new_with_one_arg() { Python::with_gil(|py| { - let typeobj = py.get_type::(); + let typeobj = py.get_type_bound::(); let wrp = typeobj.call((42,), None).unwrap(); - let obj = wrp.downcast::>().unwrap(); + let obj = wrp.downcast::().unwrap(); let obj_ref = obj.borrow(); assert_eq!(obj_ref.data, 42); }); @@ -127,12 +124,12 @@ impl NewWithTwoArgs { #[test] fn new_with_two_args() { Python::with_gil(|py| { - let typeobj = py.get_type::(); + let typeobj = py.get_type_bound::(); let wrp = typeobj .call((10, 20), None) .map_err(|e| e.display(py)) .unwrap(); - let obj = wrp.downcast::>().unwrap(); + let obj = wrp.downcast::().unwrap(); let obj_ref = obj.borrow(); assert_eq!(obj_ref.data1, 10); assert_eq!(obj_ref.data2, 20); @@ -158,7 +155,7 @@ impl SuperClass { #[test] fn subclass_new() { Python::with_gil(|py| { - let super_cls = py.get_type::(); + let super_cls = py.get_type_bound::(); let source = pyo3::indoc::indoc!( r#" class Class(SuperClass): @@ -206,7 +203,7 @@ impl NewWithCustomError { #[test] fn new_with_custom_error() { Python::with_gil(|py| { - let typeobj = py.get_type::(); + let typeobj = py.get_type_bound::(); let err = typeobj.call0().unwrap_err(); assert_eq!(err.to_string(), "ValueError: custom error"); }); @@ -247,7 +244,7 @@ impl NewExisting { #[test] fn test_new_existing() { Python::with_gil(|py| { - let typeobj = py.get_type::(); + let typeobj = py.get_type_bound::(); let obj1 = typeobj.call1((0,)).unwrap(); let obj2 = typeobj.call1((0,)).unwrap(); @@ -263,10 +260,10 @@ fn test_new_existing() { assert!(obj5.getattr("num").unwrap().extract::().unwrap() == 2); assert!(obj6.getattr("num").unwrap().extract::().unwrap() == 2); - assert!(obj1.is(obj2)); - assert!(obj3.is(obj4)); - assert!(!obj1.is(obj3)); - assert!(!obj1.is(obj5)); - assert!(!obj5.is(obj6)); + assert!(obj1.is(&obj2)); + assert!(obj3.is(&obj4)); + assert!(!obj1.is(&obj3)); + assert!(!obj1.is(&obj5)); + assert!(!obj5.is(&obj6)); }); } diff --git a/tests/test_coroutine.rs b/tests/test_coroutine.rs index 5954b9794e7..b3b8ba7be1d 100644 --- a/tests/test_coroutine.rs +++ b/tests/test_coroutine.rs @@ -1,6 +1,6 @@ #![cfg(feature = "macros")] #![cfg(not(target_arch = "wasm32"))] -use std::{ops::Deref, task::Poll, thread, time::Duration}; +use std::{task::Poll, thread, time::Duration}; use futures::{channel::oneshot, future::poll_fn, FutureExt}; use pyo3::{ @@ -65,8 +65,11 @@ fn test_coroutine_qualname() { assert coro.__name__ == name and coro.__qualname__ == qualname "#; let locals = [ - ("my_fn", wrap_pyfunction!(my_fn, gil).unwrap().deref()), - ("MyClass", gil.get_type::()), + ( + "my_fn", + wrap_pyfunction!(my_fn, gil).unwrap().as_borrowed().as_any(), + ), + ("MyClass", gil.get_type_bound::().as_any()), ] .into_py_dict_bound(gil); py_run!(gil, *locals, &handle_windows(test)); @@ -286,7 +289,7 @@ fn test_async_method_receiver() { assert False assert asyncio.run(coro3) == 1 "#; - let locals = [("Counter", gil.get_type::())].into_py_dict_bound(gil); + let locals = [("Counter", gil.get_type_bound::())].into_py_dict_bound(gil); py_run!(gil, *locals, test); }) } diff --git a/tests/test_enum.rs b/tests/test_enum.rs index 33e94c241c7..d73316e5512 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::(); + let my_enum = py.get_type_bound::(); let var = Py::new(py, MyEnum::Variant).unwrap(); py_assert!(py, my_enum var, "my_enum.Variant == var"); }) @@ -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::(); + let mynum = py.get_type_bound::(); 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::(); + let mynum = py.get_type_bound::(); py_run!(py, f mynum, "f(mynum.OtherVariant)") }) @@ -83,7 +83,7 @@ enum CustomDiscriminant { fn test_custom_discriminant() { Python::with_gil(|py| { #[allow(non_snake_case)] - let CustomDiscriminant = py.get_type::(); + let CustomDiscriminant = py.get_type_bound::(); let one = Py::new(py, CustomDiscriminant::One).unwrap(); let two = Py::new(py, CustomDiscriminant::Two).unwrap(); py_run!(py, CustomDiscriminant one two, r#" @@ -204,7 +204,7 @@ enum RenameAllVariantsEnum { #[test] fn test_renaming_all_enum_variants() { Python::with_gil(|py| { - let enum_obj = py.get_type::(); + let enum_obj = py.get_type_bound::(); py_assert!(py, enum_obj, "enum_obj.VARIANT_ONE == enum_obj.VARIANT_ONE"); py_assert!(py, enum_obj, "enum_obj.VARIANT_TWO == enum_obj.VARIANT_TWO"); py_assert!( diff --git a/tests/test_gc.rs b/tests/test_gc.rs index 7ed39436062..9e236a27dbf 100644 --- a/tests/test_gc.rs +++ b/tests/test_gc.rs @@ -211,11 +211,11 @@ fn inheritance_with_new_methods_with_drop() { let drop_called2 = Arc::new(AtomicBool::new(false)); Python::with_gil(|py| { - let _typebase = py.get_type::(); - let typeobj = py.get_type::(); + let _typebase = py.get_type_bound::(); + let typeobj = py.get_type_bound::(); let inst = typeobj.call((), None).unwrap(); - let obj: &PyCell = inst.downcast().unwrap(); + let obj = inst.downcast::().unwrap(); let mut obj_ref_mut = obj.borrow_mut(); obj_ref_mut.data = Some(Arc::clone(&drop_called1)); let base: &mut BaseClassWithDrop = obj_ref_mut.as_mut(); @@ -255,8 +255,8 @@ fn gc_during_borrow() { Python::with_gil(|py| { unsafe { // get the traverse function - let ty = py.get_type::().as_type_ptr(); - let traverse = get_type_traverse(ty).unwrap(); + let ty = py.get_type_bound::(); + let traverse = get_type_traverse(ty.as_type_ptr()).unwrap(); // create an object and check that traversing it works normally // when it's not borrowed @@ -303,8 +303,8 @@ impl PartialTraverse { fn traverse_partial() { Python::with_gil(|py| unsafe { // get the traverse function - let ty = py.get_type::().as_type_ptr(); - let traverse = get_type_traverse(ty).unwrap(); + let ty = py.get_type_bound::(); + let traverse = get_type_traverse(ty.as_type_ptr()).unwrap(); // confirm that traversing errors let obj = Py::new(py, PartialTraverse::new(py)).unwrap(); @@ -338,8 +338,8 @@ impl PanickyTraverse { fn traverse_panic() { Python::with_gil(|py| unsafe { // get the traverse function - let ty = py.get_type::().as_type_ptr(); - let traverse = get_type_traverse(ty).unwrap(); + let ty = py.get_type_bound::(); + let traverse = get_type_traverse(ty.as_type_ptr()).unwrap(); // confirm that traversing errors let obj = Py::new(py, PanickyTraverse::new(py)).unwrap(); @@ -361,8 +361,8 @@ impl TriesGILInTraverse { fn tries_gil_in_traverse() { Python::with_gil(|py| unsafe { // get the traverse function - let ty = py.get_type::().as_type_ptr(); - let traverse = get_type_traverse(ty).unwrap(); + let ty = py.get_type_bound::(); + let traverse = get_type_traverse(ty.as_type_ptr()).unwrap(); // confirm that traversing panicks let obj = Py::new(py, TriesGILInTraverse {}).unwrap(); @@ -413,8 +413,8 @@ impl<'a> Traversable for PyRef<'a, HijackedTraverse> { fn traverse_cannot_be_hijacked() { Python::with_gil(|py| unsafe { // get the traverse function - let ty = py.get_type::().as_type_ptr(); - let traverse = get_type_traverse(ty).unwrap(); + let ty = py.get_type_bound::(); + let traverse = get_type_traverse(ty.as_type_ptr()).unwrap(); let cell = PyCell::new(py, HijackedTraverse::new()).unwrap(); let obj = cell.to_object(py); @@ -528,8 +528,8 @@ impl UnsendableTraversal { #[cfg(not(target_arch = "wasm32"))] // We are building wasm Python with pthreads disabled fn unsendable_are_not_traversed_on_foreign_thread() { Python::with_gil(|py| unsafe { - let ty = py.get_type::().as_type_ptr(); - let traverse = get_type_traverse(ty).unwrap(); + let ty = py.get_type_bound::(); + let traverse = get_type_traverse(ty.as_type_ptr()).unwrap(); let obj = Py::new( py, diff --git a/tests/test_getter_setter.rs b/tests/test_getter_setter.rs index b990a82e134..1009ce1bd91 100644 --- a/tests/test_getter_setter.rs +++ b/tests/test_getter_setter.rs @@ -64,7 +64,7 @@ fn class_with_properties() { py_run!(py, inst, "assert inst.get_num() == inst.unwrapped == 42"); py_run!(py, inst, "assert inst.data_list == [42]"); - let d = [("C", py.get_type::())].into_py_dict_bound(py); + let d = [("C", py.get_type_bound::())].into_py_dict_bound(py); py_assert!(py, *d, "C.DATA.__doc__ == 'a getter for data'"); }); } diff --git a/tests/test_inheritance.rs b/tests/test_inheritance.rs index cd728c77bfe..af34974c90b 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::())].into_py_dict_bound(py); + let d = [("SubclassAble", py.get_type_bound::())].into_py_dict_bound(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::(); + let typeobj = py.get_type_bound::(); let inst = typeobj.call((), None).unwrap(); py_run!(py, inst, "assert inst.val1 == 10; assert inst.val2 == 5"); }); @@ -112,16 +112,16 @@ fn mutation_fails() { #[test] fn is_subclass_and_is_instance() { Python::with_gil(|py| { - let sub_ty = py.get_type::(); - let base_ty = py.get_type::(); + let sub_ty = py.get_type_bound::(); + let base_ty = py.get_type_bound::(); assert!(sub_ty.is_subclass_of::().unwrap()); - assert!(sub_ty.is_subclass(base_ty).unwrap()); + assert!(sub_ty.is_subclass(&base_ty).unwrap()); - let obj = PyCell::new(py, SubClass::new()).unwrap(); + let obj = Bound::new(py, SubClass::new()).unwrap().into_any(); assert!(obj.is_instance_of::()); assert!(obj.is_instance_of::()); - assert!(obj.is_instance(sub_ty).unwrap()); - assert!(obj.is_instance(base_ty).unwrap()); + assert!(obj.is_instance(&sub_ty).unwrap()); + assert!(obj.is_instance(&base_ty).unwrap()); }); } @@ -155,7 +155,7 @@ impl SubClass2 { #[test] fn handle_result_in_new() { Python::with_gil(|py| { - let subclass = py.get_type::(); + let subclass = py.get_type_bound::(); py_run!( py, subclass, @@ -274,15 +274,15 @@ mod inheriting_native_type { #[test] fn custom_exception() { Python::with_gil(|py| { - let cls = py.get_type::(); - let dict = [("cls", cls)].into_py_dict_bound(py); + let cls = py.get_type_bound::(); + let dict = [("cls", &cls)].into_py_dict_bound(py); let res = py.run_bound( "e = cls('hello'); assert str(e) == 'hello'; assert e.context == 'Hello :)'; raise e", None, Some(&dict) ); let err = res.unwrap_err(); - assert!(err.matches(py, cls), "{}", err); + assert!(err.matches(py, &cls), "{}", err); // catching the exception in Python also works: py_run!( @@ -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::(); + let SimpleClass = py.get_type_bound::(); py_run!( py, SimpleClass, diff --git a/tests/test_macro_docs.rs b/tests/test_macro_docs.rs index 5e240816cd2..6289626d32e 100644 --- a/tests/test_macro_docs.rs +++ b/tests/test_macro_docs.rs @@ -23,7 +23,7 @@ impl MacroDocs { #[test] fn meth_doc() { Python::with_gil(|py| { - let d = [("C", py.get_type::())].into_py_dict_bound(py); + let d = [("C", py.get_type_bound::())].into_py_dict_bound(py); py_assert!( py, *d, diff --git a/tests/test_macros.rs b/tests/test_macros.rs index c01ba853685..0d2b125b870 100644 --- a/tests/test_macros.rs +++ b/tests/test_macros.rs @@ -73,7 +73,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::(); + let my_base = py.get_type_bound::(); py_assert!(py, my_base, "my_base.__name__ == 'MyClass'"); let my_func = wrap_pyfunction!(my_function_in_macro, py).unwrap(); @@ -83,7 +83,7 @@ fn test_macro_rules_interactions() { "my_func.__text_signature__ == '(a, b=None, *, c=42)'" ); - let renamed_prop = py.get_type::(); + let renamed_prop = py.get_type_bound::(); py_assert!( py, renamed_prop, diff --git a/tests/test_mapping.rs b/tests/test_mapping.rs index 9a80ab491a1..06928e74183 100644 --- a/tests/test_mapping.rs +++ b/tests/test_mapping.rs @@ -69,7 +69,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::())].into_py_dict_bound(py); + let d = [("Mapping", py.get_type_bound::())].into_py_dict_bound(py); py_run!(py, *d, "m = Mapping(['1', '2', '3'])"); d } diff --git a/tests/test_methods.rs b/tests/test_methods.rs index 2114ead25c4..96b40174928 100644 --- a/tests/test_methods.rs +++ b/tests/test_methods.rs @@ -95,7 +95,7 @@ impl ClassMethod { #[test] fn class_method() { Python::with_gil(|py| { - let d = [("C", py.get_type::())].into_py_dict_bound(py); + let d = [("C", py.get_type_bound::())].into_py_dict_bound(py); py_assert!(py, *d, "C.method() == 'ClassMethod.method()!'"); py_assert!(py, *d, "C().method() == 'ClassMethod.method()!'"); py_assert!( @@ -126,7 +126,7 @@ impl ClassMethodWithArgs { #[test] fn class_method_with_args() { Python::with_gil(|py| { - let d = [("C", py.get_type::())].into_py_dict_bound(py); + let d = [("C", py.get_type_bound::())].into_py_dict_bound(py); py_assert!( py, *d, @@ -157,7 +157,7 @@ fn static_method() { Python::with_gil(|py| { assert_eq!(StaticMethod::method(py), "StaticMethod.method()!"); - let d = [("C", py.get_type::())].into_py_dict_bound(py); + let d = [("C", py.get_type_bound::())].into_py_dict_bound(py); py_assert!(py, *d, "C.method() == 'StaticMethod.method()!'"); py_assert!(py, *d, "C().method() == 'StaticMethod.method()!'"); py_assert!(py, *d, "C.method.__doc__ == 'Test static method.'"); @@ -181,7 +181,7 @@ fn static_method_with_args() { Python::with_gil(|py| { assert_eq!(StaticMethodWithArgs::method(py, 1234), "0x4d2"); - let d = [("C", py.get_type::())].into_py_dict_bound(py); + let d = [("C", py.get_type_bound::())].into_py_dict_bound(py); py_assert!(py, *d, "C.method(1337) == '0x539'"); }); } @@ -679,7 +679,7 @@ impl MethDocs { #[test] fn meth_doc() { Python::with_gil(|py| { - let d = [("C", py.get_type::())].into_py_dict_bound(py); + let d = [("C", py.get_type_bound::())].into_py_dict_bound(py); py_assert!(py, *d, "C.__doc__ == 'A class with \"documentation\".'"); py_assert!( py, @@ -866,7 +866,7 @@ impl FromSequence { #[test] fn test_from_sequence() { Python::with_gil(|py| { - let typeobj = py.get_type::(); + let typeobj = py.get_type_bound::(); py_assert!(py, typeobj, "typeobj(range(0, 4)).numbers == [0, 1, 2, 3]"); }); } @@ -946,7 +946,7 @@ impl r#RawIdents { #[test] fn test_raw_idents() { Python::with_gil(|py| { - let raw_idents_type = py.get_type::(); + let raw_idents_type = py.get_type_bound::(); assert_eq!(raw_idents_type.qualname().unwrap(), "RawIdents"); py_run!( py, diff --git a/tests/test_multiple_pymethods.rs b/tests/test_multiple_pymethods.rs index 13baeed3815..308220e78b2 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::(); + let cls = py.get_type_bound::(); py_assert!(py, cls, "cls()() == 'call'"); py_assert!(py, cls, "cls().method() == 'method'"); py_assert!(py, cls, "cls.classmethod() == 'classmethod'"); diff --git a/tests/test_no_imports.rs b/tests/test_no_imports.rs index 4abdcdf2e5b..88932ed282a 100644 --- a/tests/test_no_imports.rs +++ b/tests/test_no_imports.rs @@ -2,6 +2,8 @@ #![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 { @@ -91,13 +93,13 @@ impl BasicClass { fn test_basic() { pyo3::Python::with_gil(|py| { let module = pyo3::wrap_pymodule!(basic_module)(py); - let cls = py.get_type::(); + let cls = py.get_type_bound::(); let d = pyo3::types::IntoPyDict::into_py_dict_bound( [ - ("mod", module.as_ref(py).as_ref()), - ("cls", cls.as_ref()), - ("a", cls.call1((8,)).unwrap()), - ("b", cls.call1(("foo",)).unwrap()), + ("mod", module.bind(py).as_any()), + ("cls", &cls), + ("a", &cls.call1((8,)).unwrap()), + ("b", &cls.call1(("foo",)).unwrap()), ], py, ); @@ -144,7 +146,7 @@ impl NewClassMethod { #[test] fn test_new_class_method() { pyo3::Python::with_gil(|py| { - let cls = py.get_type::(); + let cls = py.get_type_bound::(); pyo3::py_run!(py, cls, "assert cls().cls is cls"); }); } diff --git a/tests/test_proto_methods.rs b/tests/test_proto_methods.rs index 3151fbe5be7..9126555df72 100644 --- a/tests/test_proto_methods.rs +++ b/tests/test_proto_methods.rs @@ -672,7 +672,7 @@ impl OnceFuture { #[cfg(not(target_arch = "wasm32"))] // Won't work without wasm32 event loop (e.g., Pyodide has WebLoop) fn test_await() { Python::with_gil(|py| { - let once = py.get_type::(); + let once = py.get_type_bound::(); let source = r#" import asyncio import sys @@ -725,7 +725,7 @@ impl AsyncIterator { #[cfg(not(target_arch = "wasm32"))] // Won't work without wasm32 event loop (e.g., Pyodide has WebLoop) fn test_anext_aiter() { Python::with_gil(|py| { - let once = py.get_type::(); + let once = py.get_type_bound::(); let source = r#" import asyncio import sys @@ -750,7 +750,7 @@ asyncio.run(main()) .as_borrowed(); globals.set_item("Once", once).unwrap(); globals - .set_item("AsyncIterator", py.get_type::()) + .set_item("AsyncIterator", py.get_type_bound::()) .unwrap(); py.run_bound(source, Some(&globals), None) .map_err(|e| e.display(py)) @@ -793,7 +793,7 @@ impl DescrCounter { #[test] fn descr_getset() { Python::with_gil(|py| { - let counter = py.get_type::(); + let counter = py.get_type_bound::(); let source = pyo3::indoc::indoc!( r#" class Class: diff --git a/tests/test_sequence.rs b/tests/test_sequence.rs index f18323dbdeb..d73d2d110b4 100644 --- a/tests/test_sequence.rs +++ b/tests/test_sequence.rs @@ -106,7 +106,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::())].into_py_dict_bound(py); + let d = [("ByteSequence", py.get_type_bound::())].into_py_dict_bound(py); // Though we can construct `s` in Rust, let's test `__new__` works. py_run!(py, *d, "s = ByteSequence([1, 2, 3])"); d @@ -138,7 +138,7 @@ fn test_setitem() { #[test] fn test_delitem() { Python::with_gil(|py| { - let d = [("ByteSequence", py.get_type::())].into_py_dict_bound(py); + let d = [("ByteSequence", py.get_type_bound::())].into_py_dict_bound(py); py_run!( py, @@ -234,7 +234,7 @@ fn test_repeat() { #[test] fn test_inplace_repeat() { Python::with_gil(|py| { - let d = [("ByteSequence", py.get_type::())].into_py_dict_bound(py); + let d = [("ByteSequence", py.get_type_bound::())].into_py_dict_bound(py); py_run!( py, diff --git a/tests/test_static_slots.rs b/tests/test_static_slots.rs index b01c87cd507..3c270a3154c 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::())].into_py_dict_bound(py); + let d = [("Count5", py.get_type_bound::())].into_py_dict_bound(py); // Though we can construct `s` in Rust, let's test `__new__` works. py_run!(py, *d, "s = Count5()"); d diff --git a/tests/test_super.rs b/tests/test_super.rs index 208290df87b..8dcedf808a5 100644 --- a/tests/test_super.rs +++ b/tests/test_super.rs @@ -44,7 +44,7 @@ impl SubClass { #[test] fn test_call_super_method() { Python::with_gil(|py| { - let cls = py.get_type::(); + let cls = py.get_type_bound::(); pyo3::py_run!( py, cls, diff --git a/tests/test_text_signature.rs b/tests/test_text_signature.rs index 9056ca21e14..b72ad2a2efb 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::(); + let typeobj = py.get_type_bound::(); py_assert!(py, typeobj, "typeobj.__doc__ is None"); py_assert!(py, typeobj, "typeobj.__text_signature__ is None"); @@ -28,7 +28,7 @@ fn class_with_docs() { struct MyClass {} Python::with_gil(|py| { - let typeobj = py.get_type::(); + let typeobj = py.get_type_bound::(); py_assert!(py, typeobj, "typeobj.__doc__ == 'docs line1\\ndocs line2'"); py_assert!(py, typeobj, "typeobj.__text_signature__ is None"); @@ -52,7 +52,7 @@ fn class_with_signature_no_doc() { } Python::with_gil(|py| { - let typeobj = py.get_type::(); + let typeobj = py.get_type_bound::(); py_assert!(py, typeobj, "typeobj.__doc__ == ''"); py_assert!( py, @@ -81,7 +81,7 @@ fn class_with_docs_and_signature() { } Python::with_gil(|py| { - let typeobj = py.get_type::(); + let typeobj = py.get_type_bound::(); py_assert!(py, typeobj, "typeobj.__doc__ == 'docs line1\\ndocs line2'"); py_assert!( @@ -238,7 +238,7 @@ fn test_auto_test_signature_method() { } Python::with_gil(|py| { - let cls = py.get_type::(); + let cls = py.get_type_bound::(); #[cfg(any(not(Py_LIMITED_API), Py_3_10))] py_assert!(py, cls, "cls.__text_signature__ == '(a, b, c)'"); py_assert!( @@ -323,7 +323,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::(); + let cls = py.get_type_bound::(); py_assert!(py, cls, "cls.__text_signature__ == None"); py_assert!(py, cls, "cls.method.__text_signature__ == None"); py_assert!(py, cls, "cls.method_2.__text_signature__ == None"); @@ -383,7 +383,7 @@ fn test_methods() { } Python::with_gil(|py| { - let typeobj = py.get_type::(); + let typeobj = py.get_type_bound::(); py_assert!( py, @@ -424,7 +424,7 @@ fn test_raw_identifiers() { } Python::with_gil(|py| { - let typeobj = py.get_type::(); + let typeobj = py.get_type_bound::(); py_assert!(py, typeobj, "typeobj.__text_signature__ == '()'"); diff --git a/tests/test_variable_arguments.rs b/tests/test_variable_arguments.rs index a1f8bb354cf..8d3cd5d3935 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::(); + let my_obj = py.get_type_bound::(); py_assert!(py, my_obj, "my_obj.test_args() == ()"); py_assert!(py, my_obj, "my_obj.test_args(1) == (1,)"); py_assert!(py, my_obj, "my_obj.test_args(1, 2) == (1, 2)"); @@ -37,7 +37,7 @@ fn variable_args() { #[test] fn variable_kwargs() { Python::with_gil(|py| { - let my_obj = py.get_type::(); + let my_obj = py.get_type_bound::(); py_assert!(py, my_obj, "my_obj.test_kwargs() == None"); py_assert!(py, my_obj, "my_obj.test_kwargs(test=1) == {'test': 1}"); py_assert!( From b4dc854585064a1df6874c9877cb59eededbd590 Mon Sep 17 00:00:00 2001 From: Lily Foote Date: Sun, 18 Feb 2024 22:01:50 +0000 Subject: [PATCH 012/936] Convert LazyTypeObject to use the Bound API (#3855) --- pyo3-macros-backend/src/pyclass.rs | 1 + src/impl_/pyclass/lazy_type_object.rs | 14 +++++++------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 7d8c868e3d5..806df88eee5 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -1283,6 +1283,7 @@ fn impl_pytypeinfo( #[inline] fn type_object_raw(py: _pyo3::Python<'_>) -> *mut _pyo3::ffi::PyTypeObject { + use _pyo3::prelude::PyTypeMethods; #deprecations <#cls as _pyo3::impl_::pyclass::PyClassImpl>::lazy_type_object() diff --git a/src/impl_/pyclass/lazy_type_object.rs b/src/impl_/pyclass/lazy_type_object.rs index af52033a702..1318e1abbbc 100644 --- a/src/impl_/pyclass/lazy_type_object.rs +++ b/src/impl_/pyclass/lazy_type_object.rs @@ -12,7 +12,7 @@ use crate::{ pyclass::{create_type_object, PyClassTypeObject}, sync::{GILOnceCell, GILProtected}, types::PyType, - PyClass, PyErr, PyMethodDefType, PyObject, PyResult, Python, + Bound, PyClass, PyErr, PyMethodDefType, PyObject, PyResult, Python, }; use super::PyClassItemsIter; @@ -46,7 +46,7 @@ impl LazyTypeObject { impl LazyTypeObject { /// Gets the type object contained by this `LazyTypeObject`, initializing it if needed. - pub fn get_or_init<'py>(&'py self, py: Python<'py>) -> &'py PyType { + pub fn get_or_init<'py>(&self, py: Python<'py>) -> &Bound<'py, PyType> { self.get_or_try_init(py).unwrap_or_else(|err| { err.print(py); panic!("failed to create type object for {}", T::NAME) @@ -54,7 +54,7 @@ impl LazyTypeObject { } /// Fallible version of the above. - pub(crate) fn get_or_try_init<'py>(&'py self, py: Python<'py>) -> PyResult<&'py PyType> { + pub(crate) fn get_or_try_init<'py>(&self, py: Python<'py>) -> PyResult<&Bound<'py, PyType>> { self.0 .get_or_try_init(py, create_type_object::, T::NAME, T::items_iter()) } @@ -65,18 +65,18 @@ impl LazyTypeObjectInner { // so that this code is only instantiated once, instead of for every T // like the generic LazyTypeObject methods above. fn get_or_try_init<'py>( - &'py self, + &self, py: Python<'py>, init: fn(Python<'py>) -> PyResult, name: &str, items_iter: PyClassItemsIter, - ) -> PyResult<&'py PyType> { + ) -> PyResult<&Bound<'py, PyType>> { (|| -> PyResult<_> { let type_object = self .value .get_or_try_init(py, || init(py))? .type_object - .as_ref(py); + .bind(py); self.ensure_init(type_object, name, items_iter)?; Ok(type_object) })() @@ -91,7 +91,7 @@ impl LazyTypeObjectInner { fn ensure_init( &self, - type_object: &PyType, + type_object: &Bound<'_, PyType>, name: &str, items_iter: PyClassItemsIter, ) -> PyResult<()> { From a85ed34c454f7074918b0f39455520a358c02416 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sun, 18 Feb 2024 22:03:43 +0000 Subject: [PATCH 013/936] add Bound API constructors from borrowed pointers (#3858) * make `Borrowed` ptr constructors public * introduce `Bound::from_borrowed_ptr` constructors * clippy `assert_eq` -> `assert` * rerrange function order and correct docstrings --- src/instance.rs | 179 +++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 155 insertions(+), 24 deletions(-) diff --git a/src/instance.rs b/src/instance.rs index 3e280c43ffe..b83b69a20d3 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -114,7 +114,7 @@ impl<'py> Bound<'py, PyAny> { Self(py, ManuallyDrop::new(Py::from_owned_ptr(py, ptr))) } - /// Constructs a new `Bound<'py, PyAny>` from a pointer. Returns `None`` if `ptr` is null. + /// Constructs a new `Bound<'py, PyAny>` from a pointer. Returns `None` if `ptr` is null. /// /// # Safety /// @@ -138,6 +138,42 @@ 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 by creating a new Python reference. + /// Panics if `ptr` is null. + /// + /// # Safety + /// + /// - `ptr` must be a valid pointer to a Python object + pub unsafe fn from_borrowed_ptr(py: Python<'py>, ptr: *mut ffi::PyObject) -> Self { + Self(py, ManuallyDrop::new(Py::from_borrowed_ptr(py, ptr))) + } + + /// Constructs a new `Bound<'py, PyAny>` from a pointer by creating a new Python reference. + /// Returns `None` if `ptr` is null. + /// + /// # Safety + /// + /// - `ptr` must be a valid pointer to a Python object, or null + pub unsafe fn from_borrowed_ptr_or_opt( + py: Python<'py>, + ptr: *mut ffi::PyObject, + ) -> Option { + Py::from_borrowed_ptr_or_opt(py, ptr).map(|obj| Self(py, ManuallyDrop::new(obj))) + } + + /// Constructs a new `Bound<'py, PyAny>` from a pointer by creating a new Python reference. + /// Returns an `Err` by calling `PyErr::fetch` if `ptr` is null. + /// + /// # Safety + /// + /// - `ptr` must be a valid pointer to a Python object, or null + pub unsafe fn from_borrowed_ptr_or_err( + py: Python<'py>, + ptr: *mut ffi::PyObject, + ) -> PyResult { + Py::from_borrowed_ptr_or_err(py, ptr).map(|obj| Self(py, ManuallyDrop::new(obj))) + } + /// This slightly strange method is used to obtain `&Bound` from a pointer in macro code /// where we need to constrain the lifetime `'a` safely. /// @@ -479,37 +515,56 @@ impl<'py, T> Borrowed<'_, 'py, T> { } impl<'a, 'py> Borrowed<'a, 'py, PyAny> { + /// Constructs a new `Borrowed<'a, 'py, PyAny>` from a pointer. Panics if `ptr` is null. + /// + /// Prefer to use [`Bound::from_borrowed_ptr`], as that avoids the major safety risk + /// of needing to precisely define the lifetime `'a` for which the borrow is valid. + /// /// # Safety - /// This is similar to `std::slice::from_raw_parts`, the lifetime `'a` is completely defined by - /// the caller and it's the caller's responsibility to ensure that the reference this is - /// derived from is valid for the lifetime `'a`. - pub(crate) unsafe fn from_ptr_or_err( - py: Python<'py>, - ptr: *mut ffi::PyObject, - ) -> PyResult { - NonNull::new(ptr).map_or_else( - || Err(PyErr::fetch(py)), - |ptr| Ok(Self(ptr, PhantomData, py)), + /// + /// - `ptr` must be a valid pointer to a Python object + /// - similar to `std::slice::from_raw_parts`, the lifetime `'a` is completely defined by + /// the caller and it is the caller's responsibility to ensure that the reference this is + /// derived from is valid for the lifetime `'a`. + pub unsafe fn from_ptr(py: Python<'py>, ptr: *mut ffi::PyObject) -> Self { + Self( + NonNull::new(ptr).unwrap_or_else(|| crate::err::panic_after_error(py)), + PhantomData, + py, ) } + /// Constructs a new `Borrowed<'a, 'py, PyAny>` from a pointer. Returns `None` if `ptr` is null. + /// + /// Prefer to use [`Bound::from_borrowed_ptr_or_opt`], as that avoids the major safety risk + /// of needing to precisely define the lifetime `'a` for which the borrow is valid. + /// /// # Safety - /// This is similar to `std::slice::from_raw_parts`, the lifetime `'a` is completely defined by - /// the caller and it's the caller's responsibility to ensure that the reference this is - /// derived from is valid for the lifetime `'a`. - pub(crate) unsafe fn from_ptr_or_opt(py: Python<'py>, ptr: *mut ffi::PyObject) -> Option { + /// + /// - `ptr` must be a valid pointer to a Python object, or null + /// - similar to `std::slice::from_raw_parts`, the lifetime `'a` is completely defined by + /// the caller and it is the caller's responsibility to ensure that the reference this is + /// derived from is valid for the lifetime `'a`. + pub unsafe fn from_ptr_or_opt(py: Python<'py>, ptr: *mut ffi::PyObject) -> Option { NonNull::new(ptr).map(|ptr| Self(ptr, PhantomData, py)) } + /// Constructs a new `Borrowed<'a, 'py, PyAny>` from a pointer. Returns an `Err` by calling `PyErr::fetch` + /// if `ptr` is null. + /// + /// Prefer to use [`Bound::from_borrowed_ptr_or_err`], as that avoids the major safety risk + /// of needing to precisely define the lifetime `'a` for which the borrow is valid. + /// /// # Safety - /// This is similar to `std::slice::from_raw_parts`, the lifetime `'a` is completely defined by - /// the caller and it's the caller's responsibility to ensure that the reference this is - /// derived from is valid for the lifetime `'a`. - pub(crate) unsafe fn from_ptr(py: Python<'py>, ptr: *mut ffi::PyObject) -> Self { - Self( - NonNull::new(ptr).unwrap_or_else(|| crate::err::panic_after_error(py)), - PhantomData, - py, + /// + /// - `ptr` must be a valid pointer to a Python object, or null + /// - similar to `std::slice::from_raw_parts`, the lifetime `'a` is completely defined by + /// the caller and it is the caller's responsibility to ensure that the reference this is + /// derived from is valid for the lifetime `'a`. + pub unsafe fn from_ptr_or_err(py: Python<'py>, ptr: *mut ffi::PyObject) -> PyResult { + NonNull::new(ptr).map_or_else( + || Err(PyErr::fetch(py)), + |ptr| Ok(Self(ptr, PhantomData, py)), ) } @@ -1798,8 +1853,9 @@ impl PyObject { #[cfg_attr(not(feature = "gil-refs"), allow(deprecated))] mod tests { use super::{Bound, Py, PyObject}; + use crate::types::PyCapsule; use crate::types::{dict::IntoPyDict, PyDict, PyString}; - use crate::{PyAny, PyNativeType, PyResult, Python, ToPyObject}; + use crate::{ffi, Borrowed, PyAny, PyNativeType, PyResult, Python, ToPyObject}; #[test] fn test_call() { @@ -1998,6 +2054,81 @@ a = A() }); } + #[test] + fn bound_from_borrowed_ptr_constructors() { + // More detailed tests of the underlying semantics in pycell.rs + Python::with_gil(|py| { + fn check_drop<'py>( + py: Python<'py>, + method: impl FnOnce(*mut ffi::PyObject) -> Bound<'py, PyAny>, + ) { + let mut dropped = false; + let capsule = PyCapsule::new_bound_with_destructor( + py, + (&mut dropped) as *mut _ as usize, + None, + |ptr, _| unsafe { std::ptr::write(ptr as *mut bool, true) }, + ) + .unwrap(); + + let bound = method(capsule.as_ptr()); + assert!(!dropped); + + // creating the bound should have increased the refcount + drop(capsule); + assert!(!dropped); + + // dropping the bound should now also decrease the refcount and free the object + drop(bound); + assert!(dropped); + } + + check_drop(py, |ptr| unsafe { Bound::from_borrowed_ptr(py, ptr) }); + check_drop(py, |ptr| unsafe { + Bound::from_borrowed_ptr_or_opt(py, ptr).unwrap() + }); + check_drop(py, |ptr| unsafe { + Bound::from_borrowed_ptr_or_err(py, ptr).unwrap() + }); + }) + } + + #[test] + fn borrowed_ptr_constructors() { + // More detailed tests of the underlying semantics in pycell.rs + Python::with_gil(|py| { + fn check_drop<'py>( + py: Python<'py>, + method: impl FnOnce(&*mut ffi::PyObject) -> Borrowed<'_, 'py, PyAny>, + ) { + let mut dropped = false; + let capsule = PyCapsule::new_bound_with_destructor( + py, + (&mut dropped) as *mut _ as usize, + None, + |ptr, _| unsafe { std::ptr::write(ptr as *mut bool, true) }, + ) + .unwrap(); + + let ptr = &capsule.as_ptr(); + let _borrowed = method(ptr); + assert!(!dropped); + + // creating the borrow should not have increased the refcount + drop(capsule); + assert!(dropped); + } + + check_drop(py, |&ptr| unsafe { Borrowed::from_ptr(py, ptr) }); + check_drop(py, |&ptr| unsafe { + Borrowed::from_ptr_or_opt(py, ptr).unwrap() + }); + check_drop(py, |&ptr| unsafe { + Borrowed::from_ptr_or_err(py, ptr).unwrap() + }); + }) + } + #[cfg(feature = "macros")] mod using_macros { use crate::PyCell; From 0bb9cab6d312cf308f55204c64381f0fac013493 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Mon, 19 Feb 2024 00:37:02 +0100 Subject: [PATCH 014/936] port `PyComplex::from_complex` to `Bound` API (#3866) * port `PyComplex::from_complex` to `Bound` API * add `PyComplexMethods` to the prelude --- src/conversions/num_complex.rs | 33 +++++++++++++++++++++++++-------- src/prelude.rs | 1 + src/types/mod.rs | 2 +- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/src/conversions/num_complex.rs b/src/conversions/num_complex.rs index ba741323611..3b4fe0fc217 100644 --- a/src/conversions/num_complex.rs +++ b/src/conversions/num_complex.rs @@ -93,21 +93,37 @@ //! result = get_eigenvalues(m11,m12,m21,m22) //! assert result == [complex(1,-1), complex(-2,0)] //! ``` -#[cfg(any(Py_LIMITED_API, PyPy))] -use crate::types::any::PyAnyMethods; use crate::{ - ffi, types::PyComplex, Bound, FromPyObject, PyAny, PyErr, PyObject, PyResult, Python, - ToPyObject, + ffi, + ffi_ptr_ext::FfiPtrExt, + types::{any::PyAnyMethods, PyComplex}, + Bound, FromPyObject, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject, }; use num_complex::Complex; use std::os::raw::c_double; impl PyComplex { - /// Creates a new Python `PyComplex` object from `num_complex`'s [`Complex`]. + /// Deprecated form of [`PyComplex::from_complex_bound`] + #[cfg_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" + ) + )] pub fn from_complex>(py: Python<'_>, complex: Complex) -> &PyComplex { + Self::from_complex_bound(py, complex).into_gil_ref() + } + + /// Creates a new Python `PyComplex` object from `num_complex`'s [`Complex`]. + pub fn from_complex_bound>( + py: Python<'_>, + complex: Complex, + ) -> Bound<'_, PyComplex> { unsafe { - let ptr = ffi::PyComplex_FromDoubles(complex.re.into(), complex.im.into()); - py.from_owned_ptr(ptr) + ffi::PyComplex_FromDoubles(complex.re.into(), complex.im.into()) + .assume_owned(py) + .downcast_into_unchecked() } } } @@ -183,13 +199,14 @@ complex_conversion!(f64); #[cfg(test)] mod tests { use super::*; + use crate::types::complex::PyComplexMethods; use crate::types::PyModule; #[test] fn from_complex() { Python::with_gil(|py| { let complex = Complex::new(3.0, 1.2); - let py_c = PyComplex::from_complex(py, complex); + let py_c = PyComplex::from_complex_bound(py, complex); assert_eq!(py_c.real(), 3.0); assert_eq!(py_c.imag(), 1.2); }); diff --git a/src/prelude.rs b/src/prelude.rs index 3ac239f49e1..5a342281eeb 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -30,6 +30,7 @@ pub use crate::types::boolobject::PyBoolMethods; pub use crate::types::bytearray::PyByteArrayMethods; pub use crate::types::bytes::PyBytesMethods; pub use crate::types::capsule::PyCapsuleMethods; +pub use crate::types::complex::PyComplexMethods; pub use crate::types::dict::PyDictMethods; pub use crate::types::float::PyFloatMethods; pub use crate::types::frozenset::PyFrozenSetMethods; diff --git a/src/types/mod.rs b/src/types/mod.rs index 46909c880bb..4ebf050b4b8 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -283,7 +283,7 @@ pub(crate) mod bytes; pub(crate) mod capsule; #[cfg(not(Py_LIMITED_API))] mod code; -mod complex; +pub(crate) mod complex; #[cfg(not(Py_LIMITED_API))] pub(crate) mod datetime; pub(crate) mod dict; From 4efc4b82a381391bdd200063e2774aed4ac3700d Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Mon, 19 Feb 2024 22:07:05 +0000 Subject: [PATCH 015/936] ci: fix redundant import warnings on nightly (#3873) --- guide/src/class/numeric.md | 1 - guide/src/python_from_rust.md | 5 ++--- pyo3-build-config/src/impl_.rs | 2 -- src/conversions/chrono.rs | 1 - src/conversions/hashbrown.rs | 1 - src/conversions/num_bigint.rs | 10 ++++------ src/conversions/rust_decimal.rs | 2 -- src/conversions/smallvec.rs | 2 +- src/conversions/std/map.rs | 1 - src/conversions/std/num.rs | 2 -- src/exceptions.rs | 2 +- src/marker.rs | 2 +- src/pyclass/create_type_object.rs | 1 - src/sync.rs | 2 +- src/types/capsule.rs | 2 +- src/types/dict.rs | 4 +--- src/types/function.rs | 2 +- src/types/list.rs | 1 - src/types/mapping.rs | 6 +----- src/types/string.rs | 3 --- src/types/traceback.rs | 4 ++-- src/types/tuple.rs | 1 - tests/test_class_conversion.rs | 1 - tests/test_exceptions.rs | 2 +- tests/test_gc.rs | 3 ++- tests/test_inheritance.rs | 2 +- tests/test_methods.rs | 1 - tests/test_proto_methods.rs | 2 +- tests/test_pyself.rs | 1 - tests/test_text_signature.rs | 2 +- tests/test_various.rs | 2 +- 31 files changed, 23 insertions(+), 50 deletions(-) diff --git a/guide/src/class/numeric.md b/guide/src/class/numeric.md index c6b6a65b711..cbd9db824a0 100644 --- a/guide/src/class/numeric.md +++ b/guide/src/class/numeric.md @@ -206,7 +206,6 @@ assert hash_djb2('l50_50') == Number(-1152549421) ```rust use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; -use std::convert::TryInto; use pyo3::exceptions::{PyValueError, PyZeroDivisionError}; use pyo3::prelude::*; diff --git a/guide/src/python_from_rust.md b/guide/src/python_from_rust.md index 0b412c871c7..4306795e5b3 100644 --- a/guide/src/python_from_rust.md +++ b/guide/src/python_from_rust.md @@ -181,7 +181,7 @@ quickly testing your Python extensions. ```rust use pyo3::prelude::*; -use pyo3::{PyCell, py_run}; +use pyo3::py_run; # fn main() { #[pyclass] @@ -228,7 +228,7 @@ to this function! ```rust use pyo3::{ prelude::*, - types::{IntoPyDict, PyModule}, + types::IntoPyDict, }; # fn main() -> PyResult<()> { @@ -437,7 +437,6 @@ Use context managers by directly invoking `__enter__` and `__exit__`. ```rust use pyo3::prelude::*; -use pyo3::types::PyModule; fn main() { Python::with_gil(|py| { diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index fd467b72e0a..698c58a18ec 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -8,7 +8,6 @@ mod import_lib; use std::{ collections::{HashMap, HashSet}, - convert::AsRef, env, ffi::{OsStr, OsString}, fmt::Display, @@ -1785,7 +1784,6 @@ fn unescape(escaped: &str) -> Vec { #[cfg(test)] mod tests { - use std::iter::FromIterator; use target_lexicon::triple; use super::*; diff --git a/src/conversions/chrono.rs b/src/conversions/chrono.rs index 6878b9d9b1e..c4fa106887a 100644 --- a/src/conversions/chrono.rs +++ b/src/conversions/chrono.rs @@ -1097,7 +1097,6 @@ mod tests { mod proptests { use super::*; use crate::tests::common::CatchWarnings; - use crate::types::any::PyAnyMethods; use crate::types::IntoPyDict; use proptest::prelude::*; diff --git a/src/conversions/hashbrown.rs b/src/conversions/hashbrown.rs index c9b6c61c6eb..8ac95d87f76 100644 --- a/src/conversions/hashbrown.rs +++ b/src/conversions/hashbrown.rs @@ -112,7 +112,6 @@ where #[cfg(test)] mod tests { use super::*; - use crate::types::any::PyAnyMethods; #[test] fn test_hashbrown_hashmap_to_python() { diff --git a/src/conversions/num_bigint.rs b/src/conversions/num_bigint.rs index 3c98a90f092..652df154258 100644 --- a/src/conversions/num_bigint.rs +++ b/src/conversions/num_bigint.rs @@ -48,11 +48,11 @@ //! ``` #[cfg(Py_LIMITED_API)] -use crate::types::bytes::PyBytesMethods; +use crate::types::{bytes::PyBytesMethods, PyBytes}; use crate::{ ffi, instance::Bound, - types::{any::PyAnyMethods, *}, + types::{any::PyAnyMethods, PyLong}, FromPyObject, IntoPy, Py, PyAny, PyObject, PyResult, Python, ToPyObject, }; @@ -85,7 +85,7 @@ macro_rules! bigint_conversion { let bytes = $to_bytes(self); let bytes_obj = PyBytes::new_bound(py, &bytes); let kwargs = if $is_signed > 0 { - let kwargs = PyDict::new_bound(py); + let kwargs = crate::types::PyDict::new_bound(py); kwargs.set_item(crate::intern!(py, "signed"), true).unwrap(); Some(kwargs) } else { @@ -224,7 +224,7 @@ fn int_to_py_bytes<'py>( use crate::intern; let py = long.py(); let kwargs = if is_signed { - let kwargs = PyDict::new_bound(py); + let kwargs = crate::types::PyDict::new_bound(py); kwargs.set_item(intern!(py, "signed"), true)?; Some(kwargs) } else { @@ -261,8 +261,6 @@ fn int_n_bits(long: &Bound<'_, PyLong>) -> PyResult { #[cfg(test)] mod tests { - use self::{any::PyAnyMethods, dict::PyDictMethods}; - use super::*; use crate::types::{PyDict, PyModule}; use indoc::indoc; diff --git a/src/conversions/rust_decimal.rs b/src/conversions/rust_decimal.rs index 1d0f64148fa..2e3151720e6 100644 --- a/src/conversions/rust_decimal.rs +++ b/src/conversions/rust_decimal.rs @@ -108,10 +108,8 @@ impl IntoPy for Decimal { mod test_rust_decimal { use super::*; use crate::err::PyErr; - use crate::types::any::PyAnyMethods; use crate::types::dict::PyDictMethods; use crate::types::PyDict; - use rust_decimal::Decimal; #[cfg(not(target_arch = "wasm32"))] use proptest::prelude::*; diff --git a/src/conversions/smallvec.rs b/src/conversions/smallvec.rs index f51da1de32a..96dbfad14b7 100644 --- a/src/conversions/smallvec.rs +++ b/src/conversions/smallvec.rs @@ -97,7 +97,7 @@ where #[cfg(test)] mod tests { use super::*; - use crate::types::{any::PyAnyMethods, PyDict, PyList}; + use crate::types::{PyDict, PyList}; #[test] fn test_smallvec_into_py() { diff --git a/src/conversions/std/map.rs b/src/conversions/std/map.rs index 8c6835d7215..c17caa8b252 100644 --- a/src/conversions/std/map.rs +++ b/src/conversions/std/map.rs @@ -111,7 +111,6 @@ where #[cfg(test)] mod tests { use super::*; - use crate::{IntoPy, PyObject, Python, ToPyObject}; use std::collections::{BTreeMap, HashMap}; #[test] diff --git a/src/conversions/std/num.rs b/src/conversions/std/num.rs index 75c2e16b52b..027982461e9 100644 --- a/src/conversions/std/num.rs +++ b/src/conversions/std/num.rs @@ -5,7 +5,6 @@ use crate::{ exceptions, ffi, Bound, FromPyObject, IntoPy, PyAny, PyErr, PyObject, PyResult, Python, ToPyObject, }; -use std::convert::TryFrom; use std::num::{ NonZeroI128, NonZeroI16, NonZeroI32, NonZeroI64, NonZeroI8, NonZeroIsize, NonZeroU128, NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU8, NonZeroUsize, @@ -371,7 +370,6 @@ nonzero_int_impl!(NonZeroUsize, usize); #[cfg(test)] mod test_128bit_integers { use super::*; - use crate::types::any::PyAnyMethods; #[cfg(not(target_arch = "wasm32"))] use crate::types::PyDict; diff --git a/src/exceptions.rs b/src/exceptions.rs index 6ae9febc695..cf471053264 100644 --- a/src/exceptions.rs +++ b/src/exceptions.rs @@ -842,7 +842,7 @@ mod tests { use super::*; use crate::types::any::PyAnyMethods; use crate::types::{IntoPyDict, PyDict}; - use crate::{PyErr, PyNativeType, Python}; + use crate::{PyErr, PyNativeType}; import_exception!(socket, gaierror); import_exception!(email.errors, MessageError); diff --git a/src/marker.rs b/src/marker.rs index 4b46d3badbd..8a52bbaf416 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -1166,7 +1166,7 @@ impl<'unbound> Python<'unbound> { #[cfg(test)] mod tests { use super::*; - use crate::types::{any::PyAnyMethods, IntoPyDict, PyDict, PyList}; + use crate::types::{IntoPyDict, PyList}; use std::sync::Arc; #[test] diff --git a/src/pyclass/create_type_object.rs b/src/pyclass/create_type_object.rs index a7196f30288..65d16e59511 100644 --- a/src/pyclass/create_type_object.rs +++ b/src/pyclass/create_type_object.rs @@ -17,7 +17,6 @@ use crate::{ use std::{ borrow::Cow, collections::HashMap, - convert::TryInto, ffi::{CStr, CString}, os::raw::{c_char, c_int, c_ulong, c_void}, ptr, diff --git a/src/sync.rs b/src/sync.rs index 8fff0e82c0f..f4ffe0409a7 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -275,7 +275,7 @@ impl Interned { mod tests { use super::*; - use crate::types::{any::PyAnyMethods, dict::PyDictMethods, PyDict}; + use crate::types::{dict::PyDictMethods, PyDict}; #[test] fn test_intern() { diff --git a/src/types/capsule.rs b/src/types/capsule.rs index 466315ad6c7..342ee9b44bc 100644 --- a/src/types/capsule.rs +++ b/src/types/capsule.rs @@ -1,8 +1,8 @@ use crate::ffi_ptr_ext::FfiPtrExt; use crate::py_result_ext::PyResultExt; use crate::{ffi, PyAny, PyNativeType}; -use crate::{pyobject_native_type_core, PyErr, PyResult}; use crate::{Bound, Python}; +use crate::{PyErr, PyResult}; use std::ffi::{CStr, CString}; use std::os::raw::{c_char, c_int, c_void}; diff --git a/src/types/dict.rs b/src/types/dict.rs index 741d974e264..39a0c45e35e 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -735,9 +735,7 @@ mod tests { use super::*; #[cfg(not(PyPy))] use crate::exceptions; - #[cfg(not(PyPy))] - use crate::types::PyList; - use crate::{types::PyTuple, Python, ToPyObject}; + use crate::types::PyTuple; use std::collections::{BTreeMap, HashMap}; #[test] diff --git a/src/types/function.rs b/src/types/function.rs index 79f91cfe081..6c7db09aed3 100644 --- a/src/types/function.rs +++ b/src/types/function.rs @@ -1,7 +1,6 @@ use crate::derive_utils::PyFunctionArguments; use crate::ffi_ptr_ext::FfiPtrExt; use crate::methods::PyMethodDefDestructor; -use crate::prelude::*; use crate::py_result_ext::PyResultExt; use crate::types::capsule::PyCapsuleMethods; use crate::{ @@ -9,6 +8,7 @@ use crate::{ impl_::pymethods::{self, PyMethodDef}, types::{PyCapsule, PyDict, PyString, PyTuple}, }; +use crate::{Bound, IntoPy, Py, PyAny, PyResult, Python}; use std::cell::UnsafeCell; use std::ffi::CStr; diff --git a/src/types/list.rs b/src/types/list.rs index 1389d25ed29..3e3942bcc13 100644 --- a/src/types/list.rs +++ b/src/types/list.rs @@ -1,4 +1,3 @@ -use std::convert::TryInto; use std::iter::FusedIterator; use crate::err::{self, PyResult}; diff --git a/src/types/mapping.rs b/src/types/mapping.rs index 86c605f8bed..afbdae688b4 100644 --- a/src/types/mapping.rs +++ b/src/types/mapping.rs @@ -286,11 +286,7 @@ impl<'v> crate::PyTryFrom<'v> for PyMapping { mod tests { use std::collections::HashMap; - use crate::{ - exceptions::PyKeyError, - types::{PyDict, PyTuple}, - Python, - }; + use crate::{exceptions::PyKeyError, types::PyTuple}; use super::*; diff --git a/src/types/string.rs b/src/types/string.rs index 2e17f909df5..bc4419944bc 100644 --- a/src/types/string.rs +++ b/src/types/string.rs @@ -514,10 +514,7 @@ impl IntoPy> for &'_ Py { #[cfg_attr(not(feature = "gil-refs"), allow(deprecated))] mod tests { use super::*; - use crate::Python; use crate::{PyObject, ToPyObject}; - #[cfg(not(Py_LIMITED_API))] - use std::borrow::Cow; #[test] fn test_to_str_utf8() { diff --git a/src/types/traceback.rs b/src/types/traceback.rs index 19f42d4b64c..33a1ec7c749 100644 --- a/src/types/traceback.rs +++ b/src/types/traceback.rs @@ -116,8 +116,8 @@ impl<'py> PyTracebackMethods<'py> for Bound<'py, PyTraceback> { #[cfg(test)] mod tests { use crate::{ - prelude::*, - types::{traceback::PyTracebackMethods, PyDict}, + types::{any::PyAnyMethods, dict::PyDictMethods, traceback::PyTracebackMethods, PyDict}, + IntoPy, PyErr, Python, }; #[test] diff --git a/src/types/tuple.rs b/src/types/tuple.rs index e41fcbf0020..caad7d9c0e9 100644 --- a/src/types/tuple.rs +++ b/src/types/tuple.rs @@ -1,4 +1,3 @@ -use std::convert::TryInto; use std::iter::FusedIterator; use crate::ffi::{self, Py_ssize_t}; diff --git a/tests/test_class_conversion.rs b/tests/test_class_conversion.rs index 217d2d59d45..29723b4ad97 100644 --- a/tests/test_class_conversion.rs +++ b/tests/test_class_conversion.rs @@ -1,7 +1,6 @@ #![cfg(feature = "macros")] use pyo3::prelude::*; -use pyo3::ToPyObject; #[macro_use] #[path = "../src/tests/common.rs"] diff --git a/tests/test_exceptions.rs b/tests/test_exceptions.rs index d6d6b46fcad..3603586033e 100644 --- a/tests/test_exceptions.rs +++ b/tests/test_exceptions.rs @@ -1,7 +1,7 @@ #![cfg(feature = "macros")] use pyo3::prelude::*; -use pyo3::{exceptions, py_run, PyErr, PyResult}; +use pyo3::{exceptions, py_run}; use std::error::Error; use std::fmt; #[cfg(not(target_os = "windows"))] diff --git a/tests/test_gc.rs b/tests/test_gc.rs index 9e236a27dbf..637596fd0c8 100644 --- a/tests/test_gc.rs +++ b/tests/test_gc.rs @@ -3,7 +3,7 @@ use pyo3::class::PyTraverseError; use pyo3::class::PyVisit; use pyo3::prelude::*; -use pyo3::{py_run, PyCell}; +use pyo3::py_run; use std::cell::Cell; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; @@ -398,6 +398,7 @@ impl HijackedTraverse { } } +#[allow(dead_code)] trait Traversable { fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError>; } diff --git a/tests/test_inheritance.rs b/tests/test_inheritance.rs index af34974c90b..64c4eeffe89 100644 --- a/tests/test_inheritance.rs +++ b/tests/test_inheritance.rs @@ -177,7 +177,7 @@ except Exception as e: mod inheriting_native_type { use super::*; use pyo3::exceptions::PyException; - use pyo3::types::{IntoPyDict, PyDict}; + use pyo3::types::PyDict; #[cfg(not(PyPy))] #[test] diff --git a/tests/test_methods.rs b/tests/test_methods.rs index 96b40174928..e3ec3c76722 100644 --- a/tests/test_methods.rs +++ b/tests/test_methods.rs @@ -3,7 +3,6 @@ use pyo3::prelude::*; use pyo3::py_run; use pyo3::types::{IntoPyDict, PyDict, PyList, PySet, PyString, PyTuple, PyType}; -use pyo3::PyCell; #[path = "../src/tests/common.rs"] mod common; diff --git a/tests/test_proto_methods.rs b/tests/test_proto_methods.rs index 9126555df72..15c8a0e019c 100644 --- a/tests/test_proto_methods.rs +++ b/tests/test_proto_methods.rs @@ -2,7 +2,7 @@ use pyo3::exceptions::{PyAttributeError, PyIndexError, PyValueError}; use pyo3::types::{PyDict, PyList, PyMapping, PySequence, PySlice, PyType}; -use pyo3::{prelude::*, py_run, PyCell}; +use pyo3::{prelude::*, py_run}; use std::{isize, iter}; #[path = "../src/tests/common.rs"] diff --git a/tests/test_pyself.rs b/tests/test_pyself.rs index e14d117a639..5019360236e 100644 --- a/tests/test_pyself.rs +++ b/tests/test_pyself.rs @@ -3,7 +3,6 @@ //! Test slf: PyRef/PyMutRef(especially, slf.into::) works use pyo3::prelude::*; use pyo3::types::{PyBytes, PyString}; -use pyo3::PyCell; use std::collections::HashMap; #[path = "../src/tests/common.rs"] diff --git a/tests/test_text_signature.rs b/tests/test_text_signature.rs index b72ad2a2efb..cb2cd85e99b 100644 --- a/tests/test_text_signature.rs +++ b/tests/test_text_signature.rs @@ -2,7 +2,7 @@ use pyo3::prelude::*; use pyo3::types::{PyDict, PyTuple}; -use pyo3::{types::PyType, wrap_pymodule, PyCell}; +use pyo3::{types::PyType, wrap_pymodule}; #[path = "../src/tests/common.rs"] mod common; diff --git a/tests/test_various.rs b/tests/test_various.rs index a2ca12fbbda..a1c53fd28db 100644 --- a/tests/test_various.rs +++ b/tests/test_various.rs @@ -1,8 +1,8 @@ #![cfg(feature = "macros")] use pyo3::prelude::*; +use pyo3::py_run; use pyo3::types::{PyDict, PyTuple}; -use pyo3::{py_run, PyCell}; use std::fmt; From ececa86a7d7742db5538e4f739dd5455212a34f1 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Mon, 19 Feb 2024 22:14:00 +0000 Subject: [PATCH 016/936] ci: cancel in-progress benches job on push (#3874) --- .github/workflows/benches.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/benches.yml b/.github/workflows/benches.yml index 01ab5f802dd..6c4d31e074a 100644 --- a/.github/workflows/benches.yml +++ b/.github/workflows/benches.yml @@ -9,6 +9,10 @@ on: # performance analysis in order to generate initial data. workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.event.pull_request.number || github.sha }}-benches + cancel-in-progress: true + jobs: benchmarks: runs-on: ubuntu-latest From 96b8c9facffbd6c4ad90353310a2ad88c561f28b Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Mon, 19 Feb 2024 22:14:26 +0000 Subject: [PATCH 017/936] migrate some final `FromPyObject` implementations to the `Bound` API (#3869) * update `Py::extract` to use `extract_bound` * update docstring of `FromPyObject` * move `Option` conversions to new module & update * move `Cell` conversions to new module & update --- src/conversion.rs | 121 ++++++---------------------------- src/conversions/std/cell.rs | 24 +++++++ src/conversions/std/mod.rs | 2 + src/conversions/std/option.rs | 73 ++++++++++++++++++++ src/instance.rs | 4 +- 5 files changed, 120 insertions(+), 104 deletions(-) create mode 100644 src/conversions/std/cell.rs create mode 100644 src/conversions/std/option.rs diff --git a/src/conversion.rs b/src/conversion.rs index d9536aa9445..d9f79ffa104 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -8,7 +8,6 @@ use crate::types::PyTuple; use crate::{ ffi, gil, Bound, Py, PyAny, PyCell, PyClass, PyNativeType, PyObject, PyRef, PyRefMut, Python, }; -use std::cell::Cell; use std::ptr::NonNull; /// Returns a borrowed pointer to a Python object. @@ -63,18 +62,6 @@ pub unsafe trait AsPyPointer { fn as_ptr(&self) -> *mut ffi::PyObject; } -/// Convert `None` into a null pointer. -unsafe impl AsPyPointer for Option -where - T: AsPyPointer, -{ - #[inline] - fn as_ptr(&self) -> *mut ffi::PyObject { - self.as_ref() - .map_or_else(std::ptr::null_mut, |t| t.as_ptr()) - } -} - /// Conversion trait that allows various objects to be converted into `PyObject`. pub trait ToPyObject { /// Converts self into a Python object. @@ -180,7 +167,7 @@ pub trait IntoPy: Sized { /// Extract a type from a Python object. /// /// -/// Normal usage is through the `extract` methods on [`Py`] and [`PyAny`], which forward to this trait. +/// Normal usage is through the `extract` methods on [`Bound`] and [`Py`], which forward to this trait. /// /// # Examples /// @@ -190,30 +177,32 @@ pub trait IntoPy: Sized { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { -/// let obj: Py = PyString::new_bound(py, "blah").into(); -/// -/// // Straight from an owned reference -/// let s: &str = obj.extract(py)?; +/// // Calling `.extract()` on a `Bound` smart pointer +/// let obj: Bound<'_, PyString> = PyString::new_bound(py, "blah"); +/// let s: &str = obj.extract()?; /// # assert_eq!(s, "blah"); /// -/// // Or from a borrowed reference -/// let obj: &PyString = obj.as_ref(py); -/// let s: &str = obj.extract()?; +/// // Calling `.extract(py)` on a `Py` smart pointer +/// let obj: Py = obj.unbind(); +/// let s: &str = obj.extract(py)?; /// # assert_eq!(s, "blah"); /// # Ok(()) /// }) /// # } /// ``` /// -/// Note: depending on the implementation, the lifetime of the extracted result may -/// depend on the lifetime of the `obj` or the `prepared` variable. -/// -/// For example, when extracting `&str` from a Python byte string, the resulting string slice will -/// point to the existing string data (lifetime: `'py`). -/// On the other hand, when extracting `&str` from a Python Unicode string, the preparation step -/// will convert the string to UTF-8, and the resulting string slice will have lifetime `'prepared`. -/// Since which case applies depends on the runtime type of the Python object, -/// both the `obj` and `prepared` variables must outlive the resulting string slice. +// /// FIXME: until `FromPyObject` can pick up a second lifetime, the below commentary is no longer +// /// true. Update and restore this documentation at that time. +// /// +// /// Note: depending on the implementation, the lifetime of the extracted result may +// /// depend on the lifetime of the `obj` or the `prepared` variable. +// /// +// /// For example, when extracting `&str` from a Python byte string, the resulting string slice will +// /// point to the existing string data (lifetime: `'py`). +// /// On the other hand, when extracting `&str` from a Python Unicode string, the preparation step +// /// will convert the string to UTF-8, and the resulting string slice will have lifetime `'prepared`. +// /// Since which case applies depends on the runtime type of the Python object, +// /// both the `obj` and `prepared` variables must outlive the resulting string slice. /// /// During the migration of PyO3 from the "GIL Refs" API to the `Bound` smart pointer, this trait /// has two methods `extract` and `extract_bound` which are defaulted to call each other. To avoid @@ -258,27 +247,6 @@ impl ToPyObject for &'_ T { } } -/// `Option::Some` is converted like `T`. -/// `Option::None` is converted to Python `None`. -impl ToPyObject for Option -where - T: ToPyObject, -{ - fn to_object(&self, py: Python<'_>) -> PyObject { - self.as_ref() - .map_or_else(|| py.None(), |val| val.to_object(py)) - } -} - -impl IntoPy for Option -where - T: IntoPy, -{ - fn into_py(self, py: Python<'_>) -> PyObject { - self.map_or_else(|| py.None(), |val| val.into_py(py)) - } -} - impl IntoPy for &'_ PyAny { #[inline] fn into_py(self, py: Python<'_>) -> PyObject { @@ -296,24 +264,6 @@ where } } -impl ToPyObject for Cell { - fn to_object(&self, py: Python<'_>) -> PyObject { - self.get().to_object(py) - } -} - -impl> IntoPy for Cell { - fn into_py(self, py: Python<'_>) -> PyObject { - self.get().into_py(py) - } -} - -impl<'py, T: FromPyObject<'py>> FromPyObject<'py> for Cell { - fn extract(ob: &'py PyAny) -> PyResult { - T::extract(ob).map(Cell::new) - } -} - impl<'py, T> FromPyObject<'py> for &'py PyCell where T: PyClass, @@ -353,19 +303,6 @@ where } } -impl<'py, T> FromPyObject<'py> for Option -where - T: FromPyObject<'py>, -{ - fn extract(obj: &'py PyAny) -> PyResult { - if obj.as_ptr() == unsafe { ffi::Py_None() } { - Ok(None) - } else { - T::extract(obj).map(Some) - } - } -} - /// Trait implemented by Python object types that allow a checked downcast. /// If `T` implements `PyTryFrom`, we can convert `&PyAny` to `&T`. /// @@ -593,8 +530,6 @@ mod test_no_clone {} #[cfg(test)] mod tests { - use crate::{PyObject, Python}; - #[allow(deprecated)] mod deprecated { use super::super::PyTryFrom; @@ -638,22 +573,4 @@ mod tests { }); } } - - #[test] - fn test_option_as_ptr() { - Python::with_gil(|py| { - use crate::AsPyPointer; - let mut option: Option = None; - assert_eq!(option.as_ptr(), std::ptr::null_mut()); - - let none = py.None(); - option = Some(none.clone()); - - let ref_cnt = none.get_refcnt(py); - assert_eq!(option.as_ptr(), none.as_ptr()); - - // Ensure ref count not changed by as_ptr call - assert_eq!(none.get_refcnt(py), ref_cnt); - }); - } } diff --git a/src/conversions/std/cell.rs b/src/conversions/std/cell.rs new file mode 100644 index 00000000000..69d990910d3 --- /dev/null +++ b/src/conversions/std/cell.rs @@ -0,0 +1,24 @@ +use std::cell::Cell; + +use crate::{ + types::any::PyAnyMethods, Bound, FromPyObject, IntoPy, PyAny, PyObject, PyResult, Python, + ToPyObject, +}; + +impl ToPyObject for Cell { + fn to_object(&self, py: Python<'_>) -> PyObject { + self.get().to_object(py) + } +} + +impl> IntoPy for Cell { + fn into_py(self, py: Python<'_>) -> PyObject { + self.get().into_py(py) + } +} + +impl<'py, T: FromPyObject<'py>> FromPyObject<'py> for Cell { + fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { + ob.extract().map(Cell::new) + } +} diff --git a/src/conversions/std/mod.rs b/src/conversions/std/mod.rs index 9b10b59fd3f..305344b1284 100644 --- a/src/conversions/std/mod.rs +++ b/src/conversions/std/mod.rs @@ -1,7 +1,9 @@ mod array; +mod cell; mod ipaddr; mod map; mod num; +mod option; mod osstr; mod path; mod set; diff --git a/src/conversions/std/option.rs b/src/conversions/std/option.rs new file mode 100644 index 00000000000..2fa082ba16a --- /dev/null +++ b/src/conversions/std/option.rs @@ -0,0 +1,73 @@ +use crate::{ + ffi, types::any::PyAnyMethods, AsPyPointer, Bound, FromPyObject, IntoPy, PyAny, PyObject, + PyResult, Python, ToPyObject, +}; + +/// `Option::Some` is converted like `T`. +/// `Option::None` is converted to Python `None`. +impl ToPyObject for Option +where + T: ToPyObject, +{ + fn to_object(&self, py: Python<'_>) -> PyObject { + self.as_ref() + .map_or_else(|| py.None(), |val| val.to_object(py)) + } +} + +impl IntoPy for Option +where + T: IntoPy, +{ + fn into_py(self, py: Python<'_>) -> PyObject { + self.map_or_else(|| py.None(), |val| val.into_py(py)) + } +} + +impl<'py, T> FromPyObject<'py> for Option +where + T: FromPyObject<'py>, +{ + fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { + if obj.is_none() { + Ok(None) + } else { + obj.extract().map(Some) + } + } +} + +/// Convert `None` into a null pointer. +unsafe impl AsPyPointer for Option +where + T: AsPyPointer, +{ + #[inline] + fn as_ptr(&self) -> *mut ffi::PyObject { + self.as_ref() + .map_or_else(std::ptr::null_mut, |t| t.as_ptr()) + } +} + +#[cfg(test)] +mod tests { + use crate::{PyObject, Python}; + + #[test] + fn test_option_as_ptr() { + Python::with_gil(|py| { + use crate::AsPyPointer; + let mut option: Option = None; + assert_eq!(option.as_ptr(), std::ptr::null_mut()); + + let none = py.None(); + option = Some(none.clone()); + + let ref_cnt = none.get_refcnt(py); + assert_eq!(option.as_ptr(), none.as_ptr()); + + // Ensure ref count not changed by as_ptr call + assert_eq!(none.get_refcnt(py), ref_cnt); + }); + } +} diff --git a/src/instance.rs b/src/instance.rs index b83b69a20d3..a280f179f05 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -1250,11 +1250,11 @@ impl Py { /// Extracts some type from the Python object. /// /// This is a wrapper function around `FromPyObject::extract()`. - pub fn extract<'py, D>(&'py self, py: Python<'py>) -> PyResult + pub fn extract<'py, D>(&self, py: Python<'py>) -> PyResult where D: FromPyObject<'py>, { - FromPyObject::extract(unsafe { py.from_borrowed_ptr(self.as_ptr()) }) + self.bind(py).as_any().extract() } /// Retrieves an attribute value. From 9a36b5078989a7c07a5e880aea3c6da205585ee3 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Mon, 19 Feb 2024 22:15:36 +0000 Subject: [PATCH 018/936] update `py.import` -> `py.import_bound` in benches (#3868) --- pyo3-benches/benches/bench_intern.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyo3-benches/benches/bench_intern.rs b/pyo3-benches/benches/bench_intern.rs index d8dd1b8fd30..806c4e95a9d 100644 --- a/pyo3-benches/benches/bench_intern.rs +++ b/pyo3-benches/benches/bench_intern.rs @@ -6,7 +6,7 @@ use pyo3::intern; fn getattr_direct(b: &mut Bencher<'_>) { Python::with_gil(|py| { - let sys = py.import("sys").unwrap(); + let sys = py.import_bound("sys").unwrap(); b.iter(|| sys.getattr("version").unwrap()); }); @@ -14,7 +14,7 @@ fn getattr_direct(b: &mut Bencher<'_>) { fn getattr_intern(b: &mut Bencher<'_>) { Python::with_gil(|py| { - let sys = py.import("sys").unwrap(); + let sys = py.import_bound("sys").unwrap(); b.iter(|| sys.getattr(intern!(py, "version")).unwrap()); }); From 76dabd4e601b6200141ac2cac92d7069c23a23f9 Mon Sep 17 00:00:00 2001 From: Lily Foote Date: Mon, 19 Feb 2024 22:39:54 +0000 Subject: [PATCH 019/936] Replace as_ref(py) with Bound APIs (#3863) --- src/err/mod.rs | 2 +- src/pyclass/create_type_object.rs | 3 ++- tests/test_methods.rs | 8 +++----- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/err/mod.rs b/src/err/mod.rs index d922baaad21..46f482a6879 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -957,7 +957,7 @@ impl PyErrArguments for PyDowncastErrorArguments { format!( "'{}' object cannot be converted to '{}'", self.from - .as_ref(py) + .bind(py) .qualname() .as_deref() .unwrap_or(""), diff --git a/src/pyclass/create_type_object.rs b/src/pyclass/create_type_object.rs index 65d16e59511..d0c271cf31a 100644 --- a/src/pyclass/create_type_object.rs +++ b/src/pyclass/create_type_object.rs @@ -11,6 +11,7 @@ use crate::{ pymethods::{get_doc, get_name, Getter, Setter}, trampoline::trampoline, }, + types::typeobject::PyTypeMethods, types::PyType, Py, PyCell, PyClass, PyGetterDef, PyMethodDefType, PyResult, PySetterDef, PyTypeInfo, Python, }; @@ -434,7 +435,7 @@ impl PyTypeBuilder { bpo_45315_workaround(py, class_name); for cleanup in std::mem::take(&mut self.cleanup) { - cleanup(&self, type_object.as_ref(py).as_type_ptr()); + cleanup(&self, type_object.bind(py).as_type_ptr()); } Ok(PyClassTypeObject { diff --git a/tests/test_methods.rs b/tests/test_methods.rs index e3ec3c76722..7ca75016aec 100644 --- a/tests/test_methods.rs +++ b/tests/test_methods.rs @@ -73,7 +73,7 @@ impl ClassMethod { #[classmethod] /// Test class method. fn method(cls: &Bound<'_, PyType>) -> PyResult { - Ok(format!("{}.method()!", cls.as_gil_ref().qualname()?)) + Ok(format!("{}.method()!", cls.qualname()?)) } #[classmethod] @@ -84,10 +84,8 @@ impl ClassMethod { #[classmethod] fn method_owned(cls: Py) -> PyResult { - Ok(format!( - "{}.method_owned()!", - Python::with_gil(|gil| cls.as_ref(gil).qualname())? - )) + let qualname = Python::with_gil(|gil| cls.bind(gil).qualname())?; + Ok(format!("{}.method_owned()!", qualname)) } } From 7a03a6fe6def65b445c5af424e1f44d8574462eb Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Mon, 19 Feb 2024 22:40:08 +0000 Subject: [PATCH 020/936] ci: disable some benchmarks volatile on codspeed (#3861) --- pyo3-benches/benches/bench_dict.rs | 2 ++ pyo3-benches/benches/bench_extract.rs | 9 +++++++++ pyo3-benches/benches/bench_frompyobject.rs | 8 ++++++++ pyo3-benches/benches/bench_list.rs | 2 ++ pyo3-benches/benches/bench_tuple.rs | 2 ++ 5 files changed, 23 insertions(+) diff --git a/pyo3-benches/benches/bench_dict.rs b/pyo3-benches/benches/bench_dict.rs index 072dd9408ce..b63e010c1ff 100644 --- a/pyo3-benches/benches/bench_dict.rs +++ b/pyo3-benches/benches/bench_dict.rs @@ -69,6 +69,7 @@ fn extract_hashbrown_map(b: &mut Bencher<'_>) { }); } +#[cfg(not(codspeed))] fn mapping_from_dict(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 100_000; @@ -87,6 +88,7 @@ fn criterion_benchmark(c: &mut Criterion) { #[cfg(feature = "hashbrown")] c.bench_function("extract_hashbrown_map", extract_hashbrown_map); + #[cfg(not(codspeed))] c.bench_function("mapping_from_dict", mapping_from_dict); } diff --git a/pyo3-benches/benches/bench_extract.rs b/pyo3-benches/benches/bench_extract.rs index 1c783c3b706..434d8eb5b33 100644 --- a/pyo3-benches/benches/bench_extract.rs +++ b/pyo3-benches/benches/bench_extract.rs @@ -70,6 +70,7 @@ fn extract_int_extract_fail(bench: &mut Bencher<'_>) { }); } +#[cfg(not(codspeed))] fn extract_int_downcast_success(bench: &mut Bencher<'_>) { Python::with_gil(|py| { let int_obj: PyObject = 123.into_py(py); @@ -83,6 +84,7 @@ fn extract_int_downcast_success(bench: &mut Bencher<'_>) { }); } +#[cfg(not(codspeed))] fn extract_int_downcast_fail(bench: &mut Bencher<'_>) { Python::with_gil(|py| { let d = PyDict::new_bound(py).into_any(); @@ -94,6 +96,7 @@ fn extract_int_downcast_fail(bench: &mut Bencher<'_>) { }); } +#[cfg(not(codspeed))] fn extract_float_extract_success(bench: &mut Bencher<'_>) { Python::with_gil(|py| { let float_obj: PyObject = 23.42.into_py(py); @@ -117,6 +120,7 @@ fn extract_float_extract_fail(bench: &mut Bencher<'_>) { }); } +#[cfg(not(codspeed))] fn extract_float_downcast_success(bench: &mut Bencher<'_>) { Python::with_gil(|py| { let float_obj: PyObject = 23.42.into_py(py); @@ -147,15 +151,20 @@ fn criterion_benchmark(c: &mut Criterion) { #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] c.bench_function("extract_str_downcast_success", extract_str_downcast_success); c.bench_function("extract_str_downcast_fail", extract_str_downcast_fail); + #[cfg(not(codspeed))] c.bench_function("extract_int_extract_success", extract_int_extract_success); c.bench_function("extract_int_extract_fail", extract_int_extract_fail); + #[cfg(not(codspeed))] c.bench_function("extract_int_downcast_success", extract_int_downcast_success); + #[cfg(not(codspeed))] c.bench_function("extract_int_downcast_fail", extract_int_downcast_fail); + #[cfg(not(codspeed))] c.bench_function( "extract_float_extract_success", extract_float_extract_success, ); c.bench_function("extract_float_extract_fail", extract_float_extract_fail); + #[cfg(not(codspeed))] c.bench_function( "extract_float_downcast_success", extract_float_downcast_success, diff --git a/pyo3-benches/benches/bench_frompyobject.rs b/pyo3-benches/benches/bench_frompyobject.rs index 8114ee5a802..e78c48fcaa1 100644 --- a/pyo3-benches/benches/bench_frompyobject.rs +++ b/pyo3-benches/benches/bench_frompyobject.rs @@ -20,6 +20,7 @@ fn enum_from_pyobject(b: &mut Bencher<'_>) { }) } +#[cfg(not(codspeed))] fn list_via_downcast(b: &mut Bencher<'_>) { Python::with_gil(|py| { let any: &Bound<'_, PyAny> = &PyList::empty_bound(py); @@ -28,6 +29,7 @@ fn list_via_downcast(b: &mut Bencher<'_>) { }) } +#[cfg(not(codspeed))] fn list_via_extract(b: &mut Bencher<'_>) { Python::with_gil(|py| { let any: &Bound<'_, PyAny> = &PyList::empty_bound(py); @@ -36,6 +38,7 @@ fn list_via_extract(b: &mut Bencher<'_>) { }) } +#[cfg(not(codspeed))] fn not_a_list_via_downcast(b: &mut Bencher<'_>) { Python::with_gil(|py| { let any: &Bound<'_, PyAny> = &PyString::new_bound(py, "foobar"); @@ -70,6 +73,7 @@ fn not_a_list_via_extract_enum(b: &mut Bencher<'_>) { }) } +#[cfg(not(codspeed))] fn f64_from_pyobject(b: &mut Bencher<'_>) { Python::with_gil(|py| { let obj = &PyFloat::new_bound(py, 1.234); @@ -79,11 +83,15 @@ fn f64_from_pyobject(b: &mut Bencher<'_>) { fn criterion_benchmark(c: &mut Criterion) { c.bench_function("enum_from_pyobject", enum_from_pyobject); + #[cfg(not(codspeed))] c.bench_function("list_via_downcast", list_via_downcast); + #[cfg(not(codspeed))] c.bench_function("list_via_extract", list_via_extract); + #[cfg(not(codspeed))] c.bench_function("not_a_list_via_downcast", not_a_list_via_downcast); c.bench_function("not_a_list_via_extract", not_a_list_via_extract); c.bench_function("not_a_list_via_extract_enum", not_a_list_via_extract_enum); + #[cfg(not(codspeed))] c.bench_function("f64_from_pyobject", f64_from_pyobject); } diff --git a/pyo3-benches/benches/bench_list.rs b/pyo3-benches/benches/bench_list.rs index e0f238c5599..dddd04d81ee 100644 --- a/pyo3-benches/benches/bench_list.rs +++ b/pyo3-benches/benches/bench_list.rs @@ -55,6 +55,7 @@ fn list_get_item_unchecked(b: &mut Bencher<'_>) { }); } +#[cfg(not(codspeed))] fn sequence_from_list(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 50_000; @@ -69,6 +70,7 @@ fn criterion_benchmark(c: &mut Criterion) { c.bench_function("list_get_item", list_get_item); #[cfg(not(Py_LIMITED_API))] c.bench_function("list_get_item_unchecked", list_get_item_unchecked); + #[cfg(not(codspeed))] c.bench_function("sequence_from_list", sequence_from_list); } diff --git a/pyo3-benches/benches/bench_tuple.rs b/pyo3-benches/benches/bench_tuple.rs index 24f32fac364..22bf7588ed1 100644 --- a/pyo3-benches/benches/bench_tuple.rs +++ b/pyo3-benches/benches/bench_tuple.rs @@ -89,6 +89,7 @@ fn tuple_get_borrowed_item_unchecked(b: &mut Bencher<'_>) { }); } +#[cfg(not(codspeed))] fn sequence_from_tuple(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 50_000; @@ -131,6 +132,7 @@ fn criterion_benchmark(c: &mut Criterion) { "tuple_get_borrowed_item_unchecked", tuple_get_borrowed_item_unchecked, ); + #[cfg(not(codspeed))] c.bench_function("sequence_from_tuple", sequence_from_tuple); c.bench_function("tuple_new_list", tuple_new_list); c.bench_function("tuple_to_list", tuple_to_list); From 8ac7834f986d087293756b7d6b471399f3099051 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Tue, 20 Feb 2024 07:08:49 +0000 Subject: [PATCH 021/936] docs: update example for storing Py in structs (#3876) --- src/instance.rs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/instance.rs b/src/instance.rs index a280f179f05..5049a66bb32 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -656,9 +656,14 @@ impl IntoPy for Borrowed<'_, '_, T> { /// These require passing in the [`Python<'py>`](crate::Python) token but are otherwise similar to the corresponding /// methods on [`PyAny`]. /// -/// # Example: Storing Python objects in structs +/// # Example: Storing Python objects in `#[pyclass]` structs +/// +/// Usually `Bound<'py, T>` is recommended for interacting with Python objects as its lifetime `'py` +/// is an association to the GIL and that enables many operations to be done as efficiently as possible. +/// +/// However, `#[pyclass]` structs cannot carry a lifetime, so `Py` is the only way to store +/// a Python object in a `#[pyclass]` struct. /// -/// As all the native Python objects only appear as references, storing them in structs doesn't work well. /// For example, this won't compile: /// /// ```compile_fail @@ -667,7 +672,7 @@ impl IntoPy for Borrowed<'_, '_, T> { /// # /// #[pyclass] /// struct Foo<'py> { -/// inner: &'py PyDict, +/// inner: Bound<'py, PyDict>, /// } /// /// impl Foo { @@ -675,9 +680,9 @@ impl IntoPy for Borrowed<'_, '_, T> { /// let foo = Python::with_gil(|py| { /// // `py` will only last for this scope. /// -/// // `&PyDict` derives its lifetime from `py` and +/// // `Bound<'py, PyDict>` inherits the GIL lifetime from `py` and /// // so won't be able to outlive this closure. -/// let dict: &PyDict = PyDict::new(py); +/// let dict: Bound<'_, PyDict> = PyDict::new_bound(py); /// /// // because `Foo` contains `dict` its lifetime /// // is now also tied to `py`. From a93900686ecd6292cecbd7d55db1a7ae691d7bcd Mon Sep 17 00:00:00 2001 From: Lily Foote Date: Tue, 20 Feb 2024 07:10:45 +0000 Subject: [PATCH 022/936] Deprecate Py::into_ref (#3867) * Migrate into_ref calls to Bound api * Mark Py::into_ref as deprecated --- guide/src/types.md | 3 +++ src/conversions/chrono.rs | 2 +- src/exceptions.rs | 5 +++-- src/impl_/pymodule.rs | 4 ++-- src/instance.rs | 8 ++++++++ src/marker.rs | 2 ++ src/types/any.rs | 20 ++++++++++---------- src/types/traceback.rs | 2 +- tests/test_proto_methods.rs | 32 +++++++++++++++++++++----------- 9 files changed, 51 insertions(+), 27 deletions(-) diff --git a/guide/src/types.md b/guide/src/types.md index 882baddb33c..1a926f43dab 100644 --- a/guide/src/types.md +++ b/guide/src/types.md @@ -92,6 +92,7 @@ For a `&PyAny` object reference `any` where the underlying object is a `#[pyclas # use pyo3::prelude::*; # #[pyclass] #[derive(Clone)] struct MyClass { } # Python::with_gil(|py| -> PyResult<()> { +# #[allow(deprecated)] let obj: &PyAny = Py::new(py, MyClass {})?.into_ref(py); // To &PyCell with PyAny::downcast @@ -182,6 +183,7 @@ let _: &PyList = list.as_ref(py); # let list_clone = list.clone(); // Because `.into_ref()` will consume `list`. // To &PyList with Py::into_ref() (moves the pointer into PyO3's object storage) +# #[allow(deprecated)] let _: &PyList = list.into_ref(py); # let list = list_clone; @@ -204,6 +206,7 @@ let _: &PyCell = my_class.as_ref(py); # let my_class_clone = my_class.clone(); // Because `.into_ref()` will consume `my_class`. // To &PyCell with Py::into_ref() (moves the pointer into PyO3's object storage) +# #[allow(deprecated)] let _: &PyCell = my_class.into_ref(py); # let my_class = my_class_clone.clone(); diff --git a/src/conversions/chrono.rs b/src/conversions/chrono.rs index c4fa106887a..3aa6dbeb4ca 100644 --- a/src/conversions/chrono.rs +++ b/src/conversions/chrono.rs @@ -636,7 +636,7 @@ mod tests { // Test that if a user tries to convert a python's timezone aware datetime into a naive // one, the conversion fails. Python::with_gil(|py| { - let none = py.None().into_ref(py); + let none = py.None().into_bound(py); assert_eq!( none.extract::().unwrap_err().to_string(), "TypeError: 'NoneType' object cannot be converted to 'PyDelta'" diff --git a/src/exceptions.rs b/src/exceptions.rs index cf471053264..70809448bff 100644 --- a/src/exceptions.rs +++ b/src/exceptions.rs @@ -1008,7 +1008,7 @@ mod tests { .run_bound("raise Exception('banana')", None, None) .expect_err("raising should have given us an error") .into_value(py) - .into_ref(py); + .into_bound(py); assert_eq!( format!("{:?}", exc), exc.repr().unwrap().extract::().unwrap() @@ -1023,7 +1023,7 @@ mod tests { .run_bound("raise Exception('banana')", None, None) .expect_err("raising should have given us an error") .into_value(py) - .into_ref(py); + .into_bound(py); assert_eq!( exc.to_string(), exc.str().unwrap().extract::().unwrap() @@ -1036,6 +1036,7 @@ mod tests { use std::error::Error; Python::with_gil(|py| { + #[allow(deprecated)] let exc = py .run_bound( "raise Exception('banana') from TypeError('peach')", diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index ff72285558a..7103f8b3938 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -137,7 +137,7 @@ impl ModuleDef { mod tests { use std::sync::atomic::{AtomicBool, Ordering}; - use crate::{types::PyModule, PyResult, Python}; + use crate::{types::any::PyAnyMethods, types::PyModule, PyResult, Python}; use super::{ModuleDef, ModuleInitializer}; @@ -154,7 +154,7 @@ mod tests { ) }; Python::with_gil(|py| { - let module = MODULE_DEF.make_module(py).unwrap().into_ref(py); + let module = MODULE_DEF.make_module(py).unwrap().into_bound(py); assert_eq!( module .getattr("__name__") diff --git a/src/instance.rs b/src/instance.rs index 5049a66bb32..3f19637e3b3 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -958,9 +958,17 @@ where /// // This reference's lifetime is determined by `py`'s lifetime. /// // Because that originates from outside this function, /// // this return value is allowed. + /// # #[allow(deprecated)] /// obj.into_ref(py) /// } /// ``` + #[cfg_attr( + not(feature = "gil-refs"), + deprecated( + since = "0.21.0", + note = "part of the deprecated GIL Ref API; to migrate use `obj.into_bound(py)` instead of `obj.into_ref(py)`" + ) + )] pub fn into_ref(self, py: Python<'_>) -> &T::AsRefTarget { unsafe { py.from_owned_ptr(self.into_ptr()) } } diff --git a/src/marker.rs b/src/marker.rs index 8a52bbaf416..29583e8e080 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -853,6 +853,7 @@ impl<'py> Python<'py> { where T: PyTypeCheck, { + #[allow(deprecated)] obj.into_ref(self).downcast() } @@ -873,6 +874,7 @@ impl<'py> Python<'py> { where T: HasPyGilRef, { + #[allow(deprecated)] obj.into_ref(self).downcast_unchecked() } diff --git a/src/types/any.rs b/src/types/any.rs index 1cb6bb779f1..4d371764d29 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -259,8 +259,8 @@ impl PyAny { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| -> PyResult<()> { - /// let a: &PyInt = 0_u8.into_py(py).into_ref(py).downcast()?; - /// let b: &PyInt = 42_u8.into_py(py).into_ref(py).downcast()?; + /// let a: Bound<'_, PyInt> = 0_u8.into_py(py).into_bound(py).downcast_into()?; + /// let b: Bound<'_, PyInt> = 42_u8.into_py(py).into_bound(py).downcast_into()?; /// assert!(a.rich_compare(b, CompareOp::Le)?.is_truthy()?); /// Ok(()) /// })?; @@ -715,11 +715,11 @@ impl PyAny { /// } /// /// Python::with_gil(|py| { - /// let class: &PyAny = Py::new(py, Class { i: 0 }).unwrap().into_ref(py); + /// let class = Py::new(py, Class { i: 0 }).unwrap().into_bound(py).into_any(); /// - /// let class_cell: &PyCell = class.downcast()?; + /// let class_bound: &Bound<'_, Class> = class.downcast()?; /// - /// class_cell.borrow_mut().i += 1; + /// class_bound.borrow_mut().i += 1; /// /// // Alternatively you can get a `PyRefMut` directly /// let class_ref: PyRefMut<'_, Class> = class.extract()?; @@ -1112,8 +1112,8 @@ pub trait PyAnyMethods<'py> { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| -> PyResult<()> { - /// let a: &PyInt = 0_u8.into_py(py).into_ref(py).downcast()?; - /// let b: &PyInt = 42_u8.into_py(py).into_ref(py).downcast()?; + /// let a: Bound<'_, PyInt> = 0_u8.into_py(py).into_bound(py).downcast_into()?; + /// let b: Bound<'_, PyInt> = 42_u8.into_py(py).into_bound(py).downcast_into()?; /// assert!(a.rich_compare(b, CompareOp::Le)?.is_truthy()?); /// Ok(()) /// })?; @@ -1543,11 +1543,11 @@ pub trait PyAnyMethods<'py> { /// } /// /// Python::with_gil(|py| { - /// let class: &PyAny = Py::new(py, Class { i: 0 }).unwrap().into_ref(py); + /// let class = Py::new(py, Class { i: 0 }).unwrap().into_bound(py).into_any(); /// - /// let class_cell: &PyCell = class.downcast()?; + /// let class_bound: &Bound<'_, Class> = class.downcast()?; /// - /// class_cell.borrow_mut().i += 1; + /// class_bound.borrow_mut().i += 1; /// /// // Alternatively you can get a `PyRefMut` directly /// let class_ref: PyRefMut<'_, Class> = class.extract()?; diff --git a/src/types/traceback.rs b/src/types/traceback.rs index 33a1ec7c749..24b935e24c2 100644 --- a/src/types/traceback.rs +++ b/src/types/traceback.rs @@ -173,7 +173,7 @@ def f(): let f = locals.get_item("f").unwrap().unwrap(); let err = f.call0().unwrap_err(); let traceback = err.traceback_bound(py).unwrap(); - let err_object = err.clone_ref(py).into_py(py).into_ref(py); + let err_object = err.clone_ref(py).into_py(py).into_bound(py); assert!(err_object.getattr("__traceback__").unwrap().is(&traceback)); }) diff --git a/tests/test_proto_methods.rs b/tests/test_proto_methods.rs index 15c8a0e019c..c57ca6a3e0c 100644 --- a/tests/test_proto_methods.rs +++ b/tests/test_proto_methods.rs @@ -64,8 +64,8 @@ impl ExampleClass { } } -fn make_example(py: Python<'_>) -> &PyCell { - Py::new( +fn make_example(py: Python<'_>) -> Bound<'_, ExampleClass> { + Bound::new( py, ExampleClass { value: 5, @@ -73,7 +73,6 @@ fn make_example(py: Python<'_>) -> &PyCell { }, ) .unwrap() - .into_ref(py) } #[test] @@ -82,6 +81,7 @@ fn test_getattr() { let example_py = make_example(py); assert_eq!( example_py + .as_any() .getattr("value") .unwrap() .extract::() @@ -90,6 +90,7 @@ fn test_getattr() { ); assert_eq!( example_py + .as_any() .getattr("special_custom_attr") .unwrap() .extract::() @@ -97,6 +98,7 @@ fn test_getattr() { 20, ); assert!(example_py + .as_any() .getattr("other_attr") .unwrap_err() .is_instance_of::(py)); @@ -107,9 +109,13 @@ fn test_getattr() { fn test_setattr() { Python::with_gil(|py| { let example_py = make_example(py); - example_py.setattr("special_custom_attr", 15).unwrap(); + example_py + .as_any() + .setattr("special_custom_attr", 15) + .unwrap(); assert_eq!( example_py + .as_any() .getattr("special_custom_attr") .unwrap() .extract::() @@ -123,8 +129,12 @@ fn test_setattr() { fn test_delattr() { Python::with_gil(|py| { let example_py = make_example(py); - example_py.delattr("special_custom_attr").unwrap(); - assert!(example_py.getattr("special_custom_attr").unwrap().is_none()); + example_py.as_any().delattr("special_custom_attr").unwrap(); + assert!(example_py + .as_any() + .getattr("special_custom_attr") + .unwrap() + .is_none()); }) } @@ -132,7 +142,7 @@ fn test_delattr() { fn test_str() { Python::with_gil(|py| { let example_py = make_example(py); - assert_eq!(example_py.str().unwrap().to_str().unwrap(), "5"); + assert_eq!(example_py.as_any().str().unwrap().to_cow().unwrap(), "5"); }) } @@ -141,7 +151,7 @@ fn test_repr() { Python::with_gil(|py| { let example_py = make_example(py); assert_eq!( - example_py.repr().unwrap().to_str().unwrap(), + example_py.as_any().repr().unwrap().to_cow().unwrap(), "ExampleClass(value=5)" ); }) @@ -151,7 +161,7 @@ fn test_repr() { fn test_hash() { Python::with_gil(|py| { let example_py = make_example(py); - assert_eq!(example_py.hash().unwrap(), 5); + assert_eq!(example_py.as_any().hash().unwrap(), 5); }) } @@ -159,9 +169,9 @@ fn test_hash() { fn test_bool() { Python::with_gil(|py| { let example_py = make_example(py); - assert!(example_py.is_truthy().unwrap()); + assert!(example_py.as_any().is_truthy().unwrap()); example_py.borrow_mut().value = 0; - assert!(!example_py.is_truthy().unwrap()); + assert!(!example_py.as_any().is_truthy().unwrap()); }) } From 61bc02d9276928708e6eacc14ed2f512fc7541f5 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Tue, 20 Feb 2024 08:45:47 +0100 Subject: [PATCH 023/936] deprecate `PyCell::new` in favor of `Py::new` or `Bound::new` (#3872) * deprecate `PyCell::new` in favor of `Py::new` or `Bound::new` * update deprecation warning Co-authored-by: David Hewitt --------- Co-authored-by: David Hewitt --- guide/src/class.md | 5 +++-- guide/src/class/object.md | 4 ++-- guide/src/class/protocols.md | 2 +- guide/src/migration.md | 4 ++-- guide/src/python_from_rust.md | 2 +- guide/src/types.md | 2 ++ src/macros.rs | 2 +- src/pycell.rs | 30 ++++++++++++++++++++++++++---- tests/test_arithmetics.rs | 22 +++++++++++----------- tests/test_class_basics.rs | 14 +++++++------- tests/test_class_conversion.rs | 18 ++++++++++-------- tests/test_class_new.rs | 12 +++--------- tests/test_gc.rs | 8 ++++---- tests/test_inheritance.rs | 8 ++++---- tests/test_methods.rs | 10 +++++----- tests/test_proto_methods.rs | 12 ++++++------ tests/test_pyself.rs | 2 +- tests/test_sequence.rs | 18 ++++++++++-------- tests/test_various.rs | 10 +++++----- 19 files changed, 104 insertions(+), 81 deletions(-) diff --git a/guide/src/class.md b/guide/src/class.md index 9a2d91c1cb1..9ebba38a820 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -216,6 +216,7 @@ struct MyClass { num: i32, } Python::with_gil(|py| { +# #[allow(deprecated)] let obj = PyCell::new(py, MyClass { num: 3 }).unwrap(); { let obj_ref = obj.borrow(); // Get PyRef @@ -397,7 +398,7 @@ impl SubSubClass { } } # Python::with_gil(|py| { -# let subsub = pyo3::PyCell::new(py, SubSubClass::new()).unwrap(); +# let subsub = pyo3::Py::new(py, SubSubClass::new()).unwrap(); # pyo3::py_run!(py, subsub, "assert subsub.method3() == 3000"); # let subsub = SubSubClass::factory_method(py, 2).unwrap(); # let subsubsub = SubSubClass::factory_method(py, 3).unwrap(); @@ -441,7 +442,7 @@ impl DictWithCounter { } } # Python::with_gil(|py| { -# let cnt = pyo3::PyCell::new(py, DictWithCounter::new()).unwrap(); +# let cnt = pyo3::Py::new(py, DictWithCounter::new()).unwrap(); # pyo3::py_run!(py, cnt, "cnt.set('abc', 10); assert cnt['abc'] == 10") # }); # } diff --git a/guide/src/class/object.md b/guide/src/class/object.md index 5d4f53a17d4..471889391e2 100644 --- a/guide/src/class/object.md +++ b/guide/src/class/object.md @@ -217,8 +217,8 @@ impl Number { # fn main() -> PyResult<()> { # Python::with_gil(|py| { -# let x = PyCell::new(py, Number(4))?; -# let y = PyCell::new(py, Number(4))?; +# let x = &Bound::new(py, Number(4))?.into_any(); +# let y = &Bound::new(py, Number(4))?.into_any(); # assert!(x.eq(y)?); # assert!(!x.ne(y)?); # Ok(()) diff --git a/guide/src/class/protocols.md b/guide/src/class/protocols.md index 411978f0567..516c051664b 100644 --- a/guide/src/class/protocols.md +++ b/guide/src/class/protocols.md @@ -207,7 +207,7 @@ impl Container { # Python::with_gil(|py| { # let container = Container { iter: vec![1, 2, 3, 4] }; -# let inst = pyo3::PyCell::new(py, container).unwrap(); +# let inst = pyo3::Py::new(py, container).unwrap(); # pyo3::py_run!(py, inst, "assert list(inst) == [1, 2, 3, 4]"); # pyo3::py_run!(py, inst, "assert list(iter(iter(inst))) == [1, 2, 3, 4]"); # }); diff --git a/guide/src/migration.md b/guide/src/migration.md index 33b465de8dc..9a958f35458 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -1317,7 +1317,7 @@ impl Names { } } # Python::with_gil(|py| { -# let names = PyCell::new(py, Names::new()).unwrap(); +# let names = Py::new(py, Names::new()).unwrap(); # pyo3::py_run!(py, names, r" # try: # names.merge(names) @@ -1352,7 +1352,7 @@ let obj_ref = PyRef::new(py, MyClass {}).unwrap(); ``` After: -```rust +```rust,ignore # use pyo3::prelude::*; # #[pyclass] # struct MyClass {} diff --git a/guide/src/python_from_rust.md b/guide/src/python_from_rust.md index 4306795e5b3..4b81e2f62fd 100644 --- a/guide/src/python_from_rust.md +++ b/guide/src/python_from_rust.md @@ -206,7 +206,7 @@ Python::with_gil(|py| { id: 34, name: "Yu".to_string(), }; - let userdata = PyCell::new(py, userdata).unwrap(); + let userdata = Py::new(py, userdata).unwrap(); let userdata_as_tuple = (34, "Yu"); py_run!(py, userdata userdata_as_tuple, r#" assert repr(userdata) == "User Yu(id: 34)" diff --git a/guide/src/types.md b/guide/src/types.md index 1a926f43dab..f768a31f019 100644 --- a/guide/src/types.md +++ b/guide/src/types.md @@ -245,6 +245,7 @@ so it also exposes all of the methods on `PyAny`. # use pyo3::prelude::*; # #[pyclass] struct MyClass { } # Python::with_gil(|py| -> PyResult<()> { +# #[allow(deprecated)] let cell: &PyCell = PyCell::new(py, MyClass {})?; // To PyRef with .borrow() or .try_borrow() @@ -265,6 +266,7 @@ let _: &mut MyClass = &mut *py_ref_mut; # use pyo3::prelude::*; # #[pyclass] struct MyClass { } # Python::with_gil(|py| -> PyResult<()> { +# #[allow(deprecated)] let cell: &PyCell = PyCell::new(py, MyClass {})?; // Use methods from PyAny on PyCell with Deref implementation diff --git a/src/macros.rs b/src/macros.rs index 0779fb98a4b..29c2033dd4b 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -45,7 +45,7 @@ /// } /// /// Python::with_gil(|py| { -/// let time = PyCell::new(py, Time {hour: 8, minute: 43, second: 16}).unwrap(); +/// let time = Py::new(py, Time {hour: 8, minute: 43, second: 16}).unwrap(); /// let time_as_tuple = (8, 43, 16); /// py_run!(py, time time_as_tuple, r#" /// assert time.hour == 8 diff --git a/src/pycell.rs b/src/pycell.rs index 8409c5cc679..2a3f73dde78 100644 --- a/src/pycell.rs +++ b/src/pycell.rs @@ -249,6 +249,7 @@ unsafe impl PyLayout for PyCellBase where U: PySizedLayout {} /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { +/// # #[allow(deprecated)] /// let n = PyCell::new(py, Number { inner: 0 })?; /// /// let n_mutable: &mut Number = &mut n.borrow_mut(); @@ -284,6 +285,13 @@ 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 = "part of the deprecated GIL Ref API; to migrate use `Bound::new(py, value)` or `Py::new(py, value)` instead of `PyCell::new(py, value)`" + ) + )] pub fn new(py: Python<'_>, value: impl Into>) -> PyResult<&Self> { unsafe { let initializer = value.into(); @@ -332,6 +340,7 @@ impl PyCell { /// struct Class {} /// /// Python::with_gil(|py| { + /// # #[allow(deprecated)] /// let c = PyCell::new(py, Class {}).unwrap(); /// { /// let m = c.borrow_mut(); @@ -371,6 +380,7 @@ impl PyCell { /// #[pyclass] /// struct Class {} /// Python::with_gil(|py| { + /// # #[allow(deprecated)] /// let c = PyCell::new(py, Class {}).unwrap(); /// { /// let m = c.borrow(); @@ -406,6 +416,7 @@ impl PyCell { /// #[pyclass] /// struct Class {} /// Python::with_gil(|py| { + /// # #[allow(deprecated)] /// let c = PyCell::new(py, Class {}).unwrap(); /// /// { @@ -448,6 +459,7 @@ impl PyCell { /// Python::with_gil(|py| { /// let counter = FrozenCounter { value: AtomicUsize::new(0) }; /// + /// # #[allow(deprecated)] /// let cell = PyCell::new(py, counter).unwrap(); /// /// cell.get().value.fetch_add(1, Ordering::Relaxed); @@ -640,7 +652,7 @@ impl fmt::Debug for PyCell { /// } /// } /// # Python::with_gil(|py| { -/// # let sub = PyCell::new(py, Child::new()).unwrap(); +/// # let sub = Py::new(py, Child::new()).unwrap(); /// # pyo3::py_run!(py, sub, "assert sub.format() == 'Caterpillar(base: Butterfly, cnt: 3)'"); /// # }); /// ``` @@ -739,7 +751,7 @@ where /// } /// } /// # Python::with_gil(|py| { - /// # let sub = PyCell::new(py, Sub::new()).unwrap(); + /// # let sub = Py::new(py, Sub::new()).unwrap(); /// # pyo3::py_run!(py, sub, "assert sub.name() == 'base1 base2 sub'") /// # }); /// ``` @@ -1069,6 +1081,7 @@ mod tests { #[test] fn pycell_replace() { Python::with_gil(|py| { + #[allow(deprecated)] let cell = PyCell::new(py, SomeClass(0)).unwrap(); assert_eq!(*cell.borrow(), SomeClass(0)); @@ -1082,6 +1095,7 @@ mod tests { #[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(); @@ -1092,6 +1106,7 @@ mod tests { #[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)); @@ -1108,6 +1123,7 @@ mod tests { #[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(); @@ -1118,7 +1134,9 @@ mod tests { #[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)); @@ -1133,7 +1151,9 @@ mod tests { #[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(); @@ -1145,7 +1165,9 @@ mod tests { #[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(); @@ -1156,7 +1178,7 @@ mod tests { #[test] fn test_as_ptr() { Python::with_gil(|py| { - let cell = PyCell::new(py, SomeClass(0)).unwrap(); + let cell = Bound::new(py, SomeClass(0)).unwrap(); let ptr = cell.as_ptr(); assert_eq!(cell.borrow().as_ptr(), ptr); @@ -1167,7 +1189,7 @@ mod tests { #[test] fn test_into_ptr() { Python::with_gil(|py| { - let cell = PyCell::new(py, SomeClass(0)).unwrap(); + let cell = Bound::new(py, SomeClass(0)).unwrap(); let ptr = cell.as_ptr(); assert_eq!(cell.borrow().into_ptr(), ptr); diff --git a/tests/test_arithmetics.rs b/tests/test_arithmetics.rs index f336140e432..88f9ca44c28 100644 --- a/tests/test_arithmetics.rs +++ b/tests/test_arithmetics.rs @@ -43,7 +43,7 @@ impl UnaryArithmetic { #[test] fn unary_arithmetic() { Python::with_gil(|py| { - let c = PyCell::new(py, UnaryArithmetic::new(2.7)).unwrap(); + let c = Py::new(py, UnaryArithmetic::new(2.7)).unwrap(); py_run!(py, c, "assert repr(-c) == 'UA(-2.7)'"); py_run!(py, c, "assert repr(+c) == 'UA(2.7)'"); py_run!(py, c, "assert repr(abs(c)) == 'UA(2.7)'"); @@ -77,7 +77,7 @@ impl Indexable { #[test] fn indexable() { Python::with_gil(|py| { - let i = PyCell::new(py, Indexable(5)).unwrap(); + let i = Py::new(py, Indexable(5)).unwrap(); py_run!(py, i, "assert int(i) == 5"); py_run!(py, i, "assert [0, 1, 2, 3, 4, 5][i] == 5"); py_run!(py, i, "assert float(i) == 5.0"); @@ -137,7 +137,7 @@ impl InPlaceOperations { fn inplace_operations() { Python::with_gil(|py| { let init = |value, code| { - let c = PyCell::new(py, InPlaceOperations { value }).unwrap(); + let c = Py::new(py, InPlaceOperations { value }).unwrap(); py_run!(py, c, code); }; @@ -210,7 +210,7 @@ impl BinaryArithmetic { #[test] fn binary_arithmetic() { Python::with_gil(|py| { - let c = PyCell::new(py, BinaryArithmetic {}).unwrap(); + let c = Py::new(py, BinaryArithmetic {}).unwrap(); py_run!(py, c, "assert c + c == 'BA + BA'"); py_run!(py, c, "assert c.__add__(c) == 'BA + BA'"); py_run!(py, c, "assert c + 1 == 'BA + 1'"); @@ -238,7 +238,7 @@ fn binary_arithmetic() { py_run!(py, c, "assert pow(c, 1, 100) == 'BA ** 1 (mod: Some(100))'"); - let c: Bound<'_, PyAny> = c.extract().unwrap(); + let c: Bound<'_, PyAny> = c.extract(py).unwrap(); assert_py_eq!(c.add(&c).unwrap(), "BA + BA"); assert_py_eq!(c.sub(&c).unwrap(), "BA - BA"); assert_py_eq!(c.mul(&c).unwrap(), "BA * BA"); @@ -297,7 +297,7 @@ impl RhsArithmetic { #[test] fn rhs_arithmetic() { Python::with_gil(|py| { - let c = PyCell::new(py, RhsArithmetic {}).unwrap(); + let c = Py::new(py, RhsArithmetic {}).unwrap(); py_run!(py, c, "assert c.__radd__(1) == '1 + RA'"); py_run!(py, c, "assert 1 + c == '1 + RA'"); py_run!(py, c, "assert c.__rsub__(1) == '1 - RA'"); @@ -426,7 +426,7 @@ impl LhsAndRhs { #[test] fn lhs_fellback_to_rhs() { Python::with_gil(|py| { - let c = PyCell::new(py, LhsAndRhs {}).unwrap(); + let c = Py::new(py, LhsAndRhs {}).unwrap(); // If the light hand value is `LhsAndRhs`, LHS is used. py_run!(py, c, "assert c + 1 == 'LR + 1'"); py_run!(py, c, "assert c - 1 == 'LR - 1'"); @@ -494,7 +494,7 @@ impl RichComparisons2 { #[test] fn rich_comparisons() { Python::with_gil(|py| { - let c = PyCell::new(py, RichComparisons {}).unwrap(); + let c = Py::new(py, RichComparisons {}).unwrap(); py_run!(py, c, "assert (c < c) == 'RC < RC'"); py_run!(py, c, "assert (c < 1) == 'RC < 1'"); py_run!(py, c, "assert (1 < c) == 'RC > 1'"); @@ -519,7 +519,7 @@ fn rich_comparisons() { #[test] fn rich_comparisons_python_3_type_error() { Python::with_gil(|py| { - let c2 = PyCell::new(py, RichComparisons2 {}).unwrap(); + let c2 = Py::new(py, RichComparisons2 {}).unwrap(); py_expect_exception!(py, c2, "c2 < c2", PyTypeError); py_expect_exception!(py, c2, "c2 < 1", PyTypeError); py_expect_exception!(py, c2, "1 < c2", PyTypeError); @@ -620,7 +620,7 @@ mod return_not_implemented { fn _test_binary_dunder(dunder: &str) { Python::with_gil(|py| { - let c2 = PyCell::new(py, RichComparisonToSelf {}).unwrap(); + let c2 = Py::new(py, RichComparisonToSelf {}).unwrap(); py_run!( py, c2, @@ -636,7 +636,7 @@ mod return_not_implemented { _test_binary_dunder(dunder); Python::with_gil(|py| { - let c2 = PyCell::new(py, RichComparisonToSelf {}).unwrap(); + let c2 = Py::new(py, RichComparisonToSelf {}).unwrap(); py_expect_exception!( py, c2, diff --git a/tests/test_class_basics.rs b/tests/test_class_basics.rs index 6d282765c31..fe80625e86b 100644 --- a/tests/test_class_basics.rs +++ b/tests/test_class_basics.rs @@ -376,7 +376,7 @@ struct DunderDictSupport { #[cfg_attr(all(Py_LIMITED_API, not(Py_3_9)), ignore)] fn dunder_dict_support() { Python::with_gil(|py| { - let inst = PyCell::new( + let inst = Py::new( py, DunderDictSupport { _pad: *b"DEADBEEFDEADBEEFDEADBEEFDEADBEEF", @@ -399,7 +399,7 @@ fn dunder_dict_support() { #[cfg_attr(all(Py_LIMITED_API, not(Py_3_10)), ignore)] fn access_dunder_dict() { Python::with_gil(|py| { - let inst = PyCell::new( + let inst = Py::new( py, DunderDictSupport { _pad: *b"DEADBEEFDEADBEEFDEADBEEFDEADBEEF", @@ -427,7 +427,7 @@ struct InheritDict { #[cfg_attr(all(Py_LIMITED_API, not(Py_3_9)), ignore)] fn inherited_dict() { Python::with_gil(|py| { - let inst = PyCell::new( + let inst = Py::new( py, ( InheritDict { _value: 0 }, @@ -458,7 +458,7 @@ struct WeakRefDunderDictSupport { #[cfg_attr(all(Py_LIMITED_API, not(Py_3_9)), ignore)] fn weakref_dunder_dict_support() { Python::with_gil(|py| { - let inst = PyCell::new( + let inst = Py::new( py, WeakRefDunderDictSupport { _pad: *b"DEADBEEFDEADBEEFDEADBEEFDEADBEEF", @@ -482,7 +482,7 @@ struct WeakRefSupport { #[cfg_attr(all(Py_LIMITED_API, not(Py_3_9)), ignore)] fn weakref_support() { Python::with_gil(|py| { - let inst = PyCell::new( + let inst = Py::new( py, WeakRefSupport { _pad: *b"DEADBEEFDEADBEEFDEADBEEFDEADBEEF", @@ -507,7 +507,7 @@ struct InheritWeakRef { #[cfg_attr(all(Py_LIMITED_API, not(Py_3_9)), ignore)] fn inherited_weakref() { Python::with_gil(|py| { - let inst = PyCell::new( + let inst = Py::new( py, ( InheritWeakRef { _value: 0 }, @@ -539,7 +539,7 @@ fn access_frozen_class_without_gil() { value: AtomicUsize::new(0), }; - let cell = PyCell::new(py, counter).unwrap(); + let cell = Bound::new(py, counter).unwrap(); cell.get().value.fetch_add(1, Ordering::Relaxed); diff --git a/tests/test_class_conversion.rs b/tests/test_class_conversion.rs index 29723b4ad97..ccb974097e1 100644 --- a/tests/test_class_conversion.rs +++ b/tests/test_class_conversion.rs @@ -63,7 +63,7 @@ struct PolymorphicContainer { #[test] fn test_polymorphic_container_stores_base_class() { Python::with_gil(|py| { - let p = PyCell::new( + let p = Py::new( py, PolymorphicContainer { inner: Py::new(py, BaseClass::default()).unwrap(), @@ -79,7 +79,7 @@ fn test_polymorphic_container_stores_base_class() { #[test] fn test_polymorphic_container_stores_sub_class() { Python::with_gil(|py| { - let p = PyCell::new( + let p = Py::new( py, PolymorphicContainer { inner: Py::new(py, BaseClass::default()).unwrap(), @@ -91,7 +91,7 @@ fn test_polymorphic_container_stores_sub_class() { p.bind(py) .setattr( "inner", - PyCell::new( + Py::new( py, PyClassInitializer::from(BaseClass::default()).add_subclass(SubClass {}), ) @@ -106,7 +106,7 @@ fn test_polymorphic_container_stores_sub_class() { #[test] fn test_polymorphic_container_does_not_accept_other_types() { Python::with_gil(|py| { - let p = PyCell::new( + let p = Py::new( py, PolymorphicContainer { inner: Py::new(py, BaseClass::default()).unwrap(), @@ -126,7 +126,7 @@ fn test_polymorphic_container_does_not_accept_other_types() { #[test] fn test_pyref_as_base() { Python::with_gil(|py| { - let cell = PyCell::new(py, (SubClass {}, BaseClass { value: 120 })).unwrap(); + let cell = Bound::new(py, (SubClass {}, BaseClass { value: 120 })).unwrap(); // First try PyRefMut let sub: PyRefMut<'_, SubClass> = cell.borrow_mut(); @@ -146,12 +146,14 @@ fn test_pyref_as_base() { #[test] fn test_pycell_deref() { Python::with_gil(|py| { - let cell = PyCell::new(py, (SubClass {}, BaseClass { value: 120 })).unwrap(); + let cell = Bound::new(py, (SubClass {}, BaseClass { value: 120 })).unwrap(); // Should be able to deref as PyAny + // FIXME: This deref does _not_ work assert_eq!( - cell.call_method0("foo") - .and_then(PyAny::extract::<&str>) + cell.as_any() + .call_method0("foo") + .and_then(|e| e.extract::<&str>()) .unwrap(), "SubClass" ); diff --git a/tests/test_class_new.rs b/tests/test_class_new.rs index b1a0e594799..9037590f678 100644 --- a/tests/test_class_new.rs +++ b/tests/test_class_new.rs @@ -222,12 +222,8 @@ impl NewExisting { static PRE_BUILT: GILOnceCell<[pyo3::Py; 2]> = GILOnceCell::new(); let existing = PRE_BUILT.get_or_init(py, || { [ - pyo3::PyCell::new(py, NewExisting { num: 0 }) - .unwrap() - .into(), - pyo3::PyCell::new(py, NewExisting { num: 1 }) - .unwrap() - .into(), + pyo3::Py::new(py, NewExisting { num: 0 }).unwrap(), + pyo3::Py::new(py, NewExisting { num: 1 }).unwrap(), ] }); @@ -235,9 +231,7 @@ impl NewExisting { return existing[val].clone_ref(py); } - pyo3::PyCell::new(py, NewExisting { num: val }) - .unwrap() - .into() + pyo3::Py::new(py, NewExisting { num: val }).unwrap() } } diff --git a/tests/test_gc.rs b/tests/test_gc.rs index 637596fd0c8..7a8bc9ded2d 100644 --- a/tests/test_gc.rs +++ b/tests/test_gc.rs @@ -99,7 +99,7 @@ fn gc_integration() { let drop_called = Arc::new(AtomicBool::new(false)); Python::with_gil(|py| { - let inst = PyCell::new( + let inst = Bound::new( py, GcIntegration { self_ref: py.None(), @@ -260,7 +260,7 @@ fn gc_during_borrow() { // create an object and check that traversing it works normally // when it's not borrowed - let cell = PyCell::new(py, TraversableClass::new()).unwrap(); + let cell = Bound::new(py, TraversableClass::new()).unwrap(); let obj = cell.to_object(py); assert!(!cell.borrow().traversed.load(Ordering::Relaxed)); traverse(obj.as_ptr(), novisit, std::ptr::null_mut()); @@ -268,7 +268,7 @@ fn gc_during_borrow() { // create an object and check that it is not traversed if the GC // is invoked while it is already borrowed mutably - let cell2 = PyCell::new(py, TraversableClass::new()).unwrap(); + let cell2 = Bound::new(py, TraversableClass::new()).unwrap(); let obj2 = cell2.to_object(py); let guard = cell2.borrow_mut(); assert!(!guard.traversed.load(Ordering::Relaxed)); @@ -417,7 +417,7 @@ fn traverse_cannot_be_hijacked() { let ty = py.get_type_bound::(); let traverse = get_type_traverse(ty.as_type_ptr()).unwrap(); - let cell = PyCell::new(py, HijackedTraverse::new()).unwrap(); + let cell = Bound::new(py, HijackedTraverse::new()).unwrap(); let obj = cell.to_object(py); assert_eq!(cell.borrow().traversed_and_hijacked(), (false, false)); traverse(obj.as_ptr(), novisit, std::ptr::null_mut()); diff --git a/tests/test_inheritance.rs b/tests/test_inheritance.rs index 64c4eeffe89..9b29fefdbda 100644 --- a/tests/test_inheritance.rs +++ b/tests/test_inheritance.rs @@ -81,7 +81,7 @@ fn inheritance_with_new_methods() { #[test] fn call_base_and_sub_methods() { Python::with_gil(|py| { - let obj = PyCell::new(py, SubClass::new()).unwrap(); + let obj = Py::new(py, SubClass::new()).unwrap(); py_run!( py, obj, @@ -96,7 +96,7 @@ fn call_base_and_sub_methods() { #[test] fn mutation_fails() { Python::with_gil(|py| { - let obj = PyCell::new(py, SubClass::new()).unwrap(); + let obj = Py::new(py, SubClass::new()).unwrap(); let global = [("obj", obj)].into_py_dict_bound(py); let e = py .run_bound( @@ -202,7 +202,7 @@ mod inheriting_native_type { } Python::with_gil(|py| { - let set_sub = pyo3::PyCell::new(py, SetWithName::new()).unwrap(); + let set_sub = pyo3::Py::new(py, SetWithName::new()).unwrap(); py_run!( py, set_sub, @@ -229,7 +229,7 @@ mod inheriting_native_type { #[test] fn inherit_dict() { Python::with_gil(|py| { - let dict_sub = pyo3::PyCell::new(py, DictWithName::new()).unwrap(); + let dict_sub = pyo3::Py::new(py, DictWithName::new()).unwrap(); py_run!( py, dict_sub, diff --git a/tests/test_methods.rs b/tests/test_methods.rs index 7ca75016aec..68a004bd4bc 100644 --- a/tests/test_methods.rs +++ b/tests/test_methods.rs @@ -28,7 +28,7 @@ impl InstanceMethod { #[test] fn instance_method() { Python::with_gil(|py| { - let obj = PyCell::new(py, InstanceMethod { member: 42 }).unwrap(); + let obj = Bound::new(py, InstanceMethod { member: 42 }).unwrap(); let obj_ref = obj.borrow(); assert_eq!(obj_ref.method(), 42); py_assert!(py, obj, "obj.method() == 42"); @@ -52,7 +52,7 @@ impl InstanceMethodWithArgs { #[test] fn instance_method_with_args() { Python::with_gil(|py| { - let obj = PyCell::new(py, InstanceMethodWithArgs { member: 7 }).unwrap(); + let obj = Bound::new(py, InstanceMethodWithArgs { member: 7 }).unwrap(); let obj_ref = obj.borrow(); assert_eq!(obj_ref.method(6), 42); py_assert!(py, obj, "obj.method(3) == 21"); @@ -710,7 +710,7 @@ impl MethodWithLifeTime { #[test] fn method_with_lifetime() { Python::with_gil(|py| { - let obj = PyCell::new(py, MethodWithLifeTime {}).unwrap(); + let obj = Py::new(py, MethodWithLifeTime {}).unwrap(); py_run!( py, obj, @@ -758,8 +758,8 @@ impl MethodWithPyClassArg { #[test] fn method_with_pyclassarg() { Python::with_gil(|py| { - let obj1 = PyCell::new(py, MethodWithPyClassArg { value: 10 }).unwrap(); - let obj2 = PyCell::new(py, MethodWithPyClassArg { value: 10 }).unwrap(); + let obj1 = Py::new(py, MethodWithPyClassArg { value: 10 }).unwrap(); + let obj2 = Py::new(py, MethodWithPyClassArg { value: 10 }).unwrap(); let d = [("obj1", obj1), ("obj2", obj2)].into_py_dict_bound(py); py_run!(py, *d, "obj = obj1.add(obj2); assert obj.value == 20"); py_run!(py, *d, "obj = obj1.add_pyref(obj2); assert obj.value == 20"); diff --git a/tests/test_proto_methods.rs b/tests/test_proto_methods.rs index c57ca6a3e0c..fb813b6e52d 100644 --- a/tests/test_proto_methods.rs +++ b/tests/test_proto_methods.rs @@ -447,7 +447,7 @@ impl SetItem { #[test] fn setitem() { Python::with_gil(|py| { - let c = PyCell::new(py, SetItem { key: 0, val: 0 }).unwrap(); + let c = Bound::new(py, SetItem { key: 0, val: 0 }).unwrap(); py_run!(py, c, "c[1] = 2"); { let c = c.borrow(); @@ -473,7 +473,7 @@ impl DelItem { #[test] fn delitem() { Python::with_gil(|py| { - let c = PyCell::new(py, DelItem { key: 0 }).unwrap(); + let c = Bound::new(py, DelItem { key: 0 }).unwrap(); py_run!(py, c, "del c[1]"); { let c = c.borrow(); @@ -502,7 +502,7 @@ impl SetDelItem { #[test] fn setdelitem() { Python::with_gil(|py| { - let c = PyCell::new(py, SetDelItem { val: None }).unwrap(); + let c = Bound::new(py, SetDelItem { val: None }).unwrap(); py_run!(py, c, "c[1] = 2"); { let c = c.borrow(); @@ -580,7 +580,7 @@ impl ClassWithGetAttr { #[test] fn getattr_doesnt_override_member() { Python::with_gil(|py| { - let inst = PyCell::new(py, ClassWithGetAttr { data: 4 }).unwrap(); + let inst = Py::new(py, ClassWithGetAttr { data: 4 }).unwrap(); py_assert!(py, inst, "inst.data == 4"); py_assert!(py, inst, "inst.a == 8"); }); @@ -602,7 +602,7 @@ impl ClassWithGetAttribute { #[test] fn getattribute_overrides_member() { Python::with_gil(|py| { - let inst = PyCell::new(py, ClassWithGetAttribute { data: 4 }).unwrap(); + let inst = Py::new(py, ClassWithGetAttribute { data: 4 }).unwrap(); py_assert!(py, inst, "inst.data == 8"); py_assert!(py, inst, "inst.y == 8"); }); @@ -635,7 +635,7 @@ impl ClassWithGetAttrAndGetAttribute { #[test] fn getattr_and_getattribute() { Python::with_gil(|py| { - let inst = PyCell::new(py, ClassWithGetAttrAndGetAttribute).unwrap(); + let inst = Py::new(py, ClassWithGetAttrAndGetAttribute).unwrap(); py_assert!(py, inst, "inst.exists == 42"); py_assert!(py, inst, "inst.lucky == 57"); py_expect_exception!(py, inst, "inst.error", PyValueError); diff --git a/tests/test_pyself.rs b/tests/test_pyself.rs index 5019360236e..fa736f68455 100644 --- a/tests/test_pyself.rs +++ b/tests/test_pyself.rs @@ -111,7 +111,7 @@ fn test_clone_ref() { #[test] fn test_nested_iter_reset() { Python::with_gil(|py| { - let reader = PyCell::new(py, reader()).unwrap(); + let reader = Bound::new(py, reader()).unwrap(); py_assert!( py, reader, diff --git a/tests/test_sequence.rs b/tests/test_sequence.rs index d73d2d110b4..363c27f5eb5 100644 --- a/tests/test_sequence.rs +++ b/tests/test_sequence.rs @@ -268,7 +268,7 @@ fn test_generic_list_get() { #[test] fn test_generic_list_set() { Python::with_gil(|py| { - let list = PyCell::new(py, GenericList { items: vec![] }).unwrap(); + let list = Bound::new(py, GenericList { items: vec![] }).unwrap(); py_run!(py, list, "list.items = [1, 2, 3]"); assert!(list @@ -304,7 +304,7 @@ impl OptionList { fn test_option_list_get() { // Regression test for #798 Python::with_gil(|py| { - let list = PyCell::new( + let list = Py::new( py, OptionList { items: vec![Some(1), None], @@ -321,31 +321,33 @@ fn test_option_list_get() { #[test] fn sequence_is_not_mapping() { Python::with_gil(|py| { - let list = PyCell::new( + let list = Bound::new( py, OptionList { items: vec![Some(1), None], }, ) - .unwrap(); + .unwrap() + .into_any(); PySequence::register::(py).unwrap(); - assert!(list.as_ref().downcast::().is_err()); - assert!(list.as_ref().downcast::().is_ok()); + assert!(list.downcast::().is_err()); + assert!(list.downcast::().is_ok()); }) } #[test] fn sequence_length() { Python::with_gil(|py| { - let list = PyCell::new( + let list = Bound::new( py, OptionList { items: vec![Some(1), None], }, ) - .unwrap(); + .unwrap() + .into_any(); assert_eq!(list.len().unwrap(), 2); assert_eq!(unsafe { ffi::PySequence_Length(list.as_ptr()) }, 2); diff --git a/tests/test_various.rs b/tests/test_various.rs index a1c53fd28db..9ed1e1cf546 100644 --- a/tests/test_various.rs +++ b/tests/test_various.rs @@ -79,8 +79,8 @@ struct SimplePyClass {} fn intopytuple_pyclass() { Python::with_gil(|py| { let tup = ( - PyCell::new(py, SimplePyClass {}).unwrap(), - PyCell::new(py, SimplePyClass {}).unwrap(), + Py::new(py, SimplePyClass {}).unwrap(), + Py::new(py, SimplePyClass {}).unwrap(), ); py_assert!(py, tup, "type(tup[0]).__name__ == 'SimplePyClass'"); py_assert!(py, tup, "type(tup[0]).__name__ == type(tup[1]).__name__"); @@ -102,8 +102,8 @@ fn pytuple_pyclass_iter() { let tup = PyTuple::new_bound( py, [ - PyCell::new(py, SimplePyClass {}).unwrap(), - PyCell::new(py, SimplePyClass {}).unwrap(), + Py::new(py, SimplePyClass {}).unwrap(), + Py::new(py, SimplePyClass {}).unwrap(), ] .iter(), ); @@ -150,7 +150,7 @@ fn test_pickle() { let module = PyModule::new(py, "test_module").unwrap(); module.add_class::().unwrap(); add_module(py, module).unwrap(); - let inst = PyCell::new(py, PickleSupport {}).unwrap(); + let inst = Py::new(py, PickleSupport {}).unwrap(); py_run!( py, inst, From 885883bf68aacd6d5703283a12e899636939a06a Mon Sep 17 00:00:00 2001 From: Juniper Tyree <50025784+juntyr@users.noreply.github.com> Date: Thu, 22 Feb 2024 00:56:03 +0200 Subject: [PATCH 024/936] Add `Py::drop_ref` method (#3871) * add Py::drop_ref method * add changelog entry * fix ffi import * integrate review feedback * Add a test * Fix some build errors * Fix some more build errors --- newsfragments/3871.added.md | 1 + src/instance.rs | 50 +++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 newsfragments/3871.added.md diff --git a/newsfragments/3871.added.md b/newsfragments/3871.added.md new file mode 100644 index 00000000000..f90e92fdfff --- /dev/null +++ b/newsfragments/3871.added.md @@ -0,0 +1 @@ +Add `Py::drop_ref` to explicitly drop a `Py`` and immediately decrease the Python reference count if the GIL is already held. diff --git a/src/instance.rs b/src/instance.rs index 3f19637e3b3..8d57bb9ea81 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -822,6 +822,10 @@ impl IntoPy for Borrowed<'_, '_, T> { /// Otherwise, the reference count will be decreased the next time the GIL is /// reacquired. /// +/// If you happen to be already holding the GIL, [`Py::drop_ref`] will decrease +/// the Python reference count immediately and will execute slightly faster than +/// relying on implicit [`Drop`]s. +/// /// # A note on `Send` and `Sync` /// /// Accessing this object is threadsafe, since any access to its API requires a [`Python<'py>`](crate::Python) token. @@ -1228,6 +1232,35 @@ impl Py { unsafe { Py::from_borrowed_ptr(py, self.0.as_ptr()) } } + /// Drops `self` and immediately decreases its reference count. + /// + /// This method is a micro-optimisation over [`Drop`] if you happen to be holding the GIL + /// already. + /// + /// Note that if you are using [`Bound`], you do not need to use [`Self::drop_ref`] since + /// [`Bound`] guarantees that the GIL is held. + /// + /// # Examples + /// + /// ```rust + /// use pyo3::prelude::*; + /// use pyo3::types::PyDict; + /// + /// # fn main() { + /// Python::with_gil(|py| { + /// let object: Py = PyDict::new_bound(py).unbind(); + /// + /// // some usage of object + /// + /// object.drop_ref(py); + /// }); + /// # } + /// ``` + #[inline] + pub fn drop_ref(self, py: Python<'_>) { + let _ = self.into_bound(py); + } + /// Returns whether the object is considered to be None. /// /// This is equivalent to the Python expression `self is None`. @@ -2142,6 +2175,23 @@ a = A() }) } + #[test] + fn explicit_drop_ref() { + Python::with_gil(|py| { + let object: Py = PyDict::new_bound(py).unbind(); + let object2 = object.clone_ref(py); + + assert_eq!(object.as_ptr(), object2.as_ptr()); + assert_eq!(object.get_refcnt(py), 2); + + object.drop_ref(py); + + assert_eq!(object2.get_refcnt(py), 1); + + object2.drop_ref(py); + }); + } + #[cfg(feature = "macros")] mod using_macros { use crate::PyCell; From 5ddcd46980834339e8809869496ba0a97278f0a3 Mon Sep 17 00:00:00 2001 From: Lily Foote Date: Thu, 22 Feb 2024 00:05:08 +0000 Subject: [PATCH 025/936] Deprecate `py.from_owned_ptr` methods (#3875) * Deprecate `py.from_owned_ptr` methods * Refactor PyString.to_str Co-authored-by: David Hewitt --------- Co-authored-by: David Hewitt --- src/conversion.rs | 31 +++++++++++++++++++++++++++++++ src/exceptions.rs | 1 + src/ffi/tests.rs | 10 +++++----- src/instance.rs | 10 ++++++++-- src/marker.rs | 24 ++++++++++++++++++++++++ src/pycell.rs | 1 + src/types/bytearray.rs | 5 +---- src/types/complex.rs | 6 +----- src/types/memoryview.rs | 5 +---- src/types/module.rs | 11 +++++++++-- src/types/slice.rs | 5 +---- src/types/string.rs | 5 +---- 12 files changed, 84 insertions(+), 30 deletions(-) diff --git a/src/conversion.rs b/src/conversion.rs index d9f79ffa104..00b6ceae389 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -434,13 +434,28 @@ 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 = "part of the deprecated GIL Ref API; to migrate 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). + #[cfg_attr( + not(feature = "gil-refs"), + deprecated( + since = "0.21.0", + note = "part of the deprecated GIL Ref API; to migrate 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. @@ -448,7 +463,15 @@ 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 = "part of the deprecated GIL Ref API; to migrate 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`. @@ -456,7 +479,15 @@ 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 = "part of the deprecated GIL Ref API; to migrate 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`. diff --git a/src/exceptions.rs b/src/exceptions.rs index 70809448bff..da48475e0d6 100644 --- a/src/exceptions.rs +++ b/src/exceptions.rs @@ -43,6 +43,7 @@ macro_rules! impl_exception_boilerplate { 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()))?; diff --git a/src/ffi/tests.rs b/src/ffi/tests.rs index 610edb1c92c..d67bd8485d8 100644 --- a/src/ffi/tests.rs +++ b/src/ffi/tests.rs @@ -5,7 +5,7 @@ use crate::Python; #[cfg(not(Py_LIMITED_API))] use crate::{ types::{PyDict, PyString}, - IntoPy, Py, PyAny, + Bound, IntoPy, Py, PyAny, }; #[cfg(not(any(Py_3_12, Py_LIMITED_API)))] use libc::wchar_t; @@ -16,9 +16,9 @@ use libc::wchar_t; fn test_datetime_fromtimestamp() { Python::with_gil(|py| { let args: Py = (100,).into_py(py); - let dt: &PyAny = unsafe { + let dt = unsafe { PyDateTime_IMPORT(); - py.from_owned_ptr(PyDateTime_FromTimestamp(args.as_ptr())) + Bound::from_owned_ptr(py, PyDateTime_FromTimestamp(args.as_ptr())) }; let locals = PyDict::new_bound(py); locals.set_item("dt", dt).unwrap(); @@ -37,9 +37,9 @@ fn test_datetime_fromtimestamp() { fn test_date_fromtimestamp() { Python::with_gil(|py| { let args: Py = (100,).into_py(py); - let dt: &PyAny = unsafe { + let dt = unsafe { PyDateTime_IMPORT(); - py.from_owned_ptr(PyDate_FromTimestamp(args.as_ptr())) + Bound::from_owned_ptr(py, PyDate_FromTimestamp(args.as_ptr())) }; let locals = PyDict::new_bound(py); locals.set_item("dt", dt).unwrap(); diff --git a/src/instance.rs b/src/instance.rs index 8d57bb9ea81..84b4cb62d9d 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -488,7 +488,10 @@ impl<'py, T> Bound<'py, T> { where T: HasPyGilRef, { - unsafe { self.py().from_owned_ptr(self.into_ptr()) } + #[allow(deprecated)] + unsafe { + self.py().from_owned_ptr(self.into_ptr()) + } } } @@ -974,7 +977,10 @@ where ) )] pub fn into_ref(self, py: Python<'_>) -> &T::AsRefTarget { - unsafe { py.from_owned_ptr(self.into_ptr()) } + #[allow(deprecated)] + unsafe { + py.from_owned_ptr(self.into_ptr()) + } } } diff --git a/src/marker.rs b/src/marker.rs index 29583e8e080..059a5662b5d 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -885,10 +885,18 @@ impl<'py> Python<'py> { /// /// Callers must ensure that ensure that the cast is valid. #[allow(clippy::wrong_self_convention)] + #[cfg_attr( + not(feature = "gil-refs"), + deprecated( + since = "0.21.0", + note = "part of the deprecated GIL Ref API; to migrate 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>, { + #[allow(deprecated)] FromPyPointer::from_owned_ptr(self, ptr) } @@ -901,10 +909,18 @@ impl<'py> Python<'py> { /// /// Callers must ensure that ensure that the cast is valid. #[allow(clippy::wrong_self_convention)] + #[cfg_attr( + not(feature = "gil-refs"), + deprecated( + since = "0.21.0", + note = "part of the deprecated GIL Ref API; to migrate 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>, { + #[allow(deprecated)] FromPyPointer::from_owned_ptr_or_err(self, ptr) } @@ -917,10 +933,18 @@ impl<'py> Python<'py> { /// /// Callers must ensure that ensure that the cast is valid. #[allow(clippy::wrong_self_convention)] + #[cfg_attr( + not(feature = "gil-refs"), + deprecated( + since = "0.21.0", + note = "part of the deprecated GIL Ref API; to migrate 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>, { + #[allow(deprecated)] FromPyPointer::from_owned_ptr_or_opt(self, ptr) } diff --git a/src/pycell.rs b/src/pycell.rs index 2a3f73dde78..5a42dfa514a 100644 --- a/src/pycell.rs +++ b/src/pycell.rs @@ -296,6 +296,7 @@ impl PyCell { unsafe { let initializer = value.into(); let self_ = initializer.create_cell(py)?; + #[allow(deprecated)] FromPyPointer::from_owned_ptr_or_err(py, self_ as _) } } diff --git a/src/types/bytearray.rs b/src/types/bytearray.rs index 7f0fdf9ebbe..2a21509d87f 100644 --- a/src/types/bytearray.rs +++ b/src/types/bytearray.rs @@ -112,10 +112,7 @@ impl PyByteArray { ) )] pub fn from(src: &PyAny) -> PyResult<&PyByteArray> { - unsafe { - src.py() - .from_owned_ptr_or_err(ffi::PyByteArray_FromObject(src.as_ptr())) - } + PyByteArray::from_bound(&src.as_borrowed()).map(Bound::into_gil_ref) } /// Creates a new Python `bytearray` object from another Python object that diff --git a/src/types/complex.rs b/src/types/complex.rs index dafda97dbd5..6d4bc7a2244 100644 --- a/src/types/complex.rs +++ b/src/types/complex.rs @@ -141,11 +141,7 @@ mod not_limited_impls { impl<'py> Neg for &'py PyComplex { type Output = &'py PyComplex; fn neg(self) -> &'py PyComplex { - unsafe { - let val = (*self.as_ptr().cast::()).cval; - self.py() - .from_owned_ptr(ffi::PyComplex_FromCComplex(ffi::_Py_c_neg(val))) - } + (-self.as_borrowed()).into_gil_ref() } } diff --git a/src/types/memoryview.rs b/src/types/memoryview.rs index 414bfc69cfa..c04a98e7886 100644 --- a/src/types/memoryview.rs +++ b/src/types/memoryview.rs @@ -19,10 +19,7 @@ impl PyMemoryView { ) )] pub fn from(src: &PyAny) -> PyResult<&PyMemoryView> { - unsafe { - src.py() - .from_owned_ptr_or_err(ffi::PyMemoryView_FromObject(src.as_ptr())) - } + PyMemoryView::from_bound(&src.as_borrowed()).map(Bound::into_gil_ref) } /// Creates a new Python `memoryview` object from another Python object that diff --git a/src/types/module.rs b/src/types/module.rs index 8824dfcf030..137b1b37e3a 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -41,7 +41,10 @@ impl PyModule { pub fn new<'p>(py: Python<'p>, name: &str) -> PyResult<&'p PyModule> { // Could use PyModule_NewObject, but it doesn't exist on PyPy. let name = CString::new(name)?; - unsafe { py.from_owned_ptr_or_err(ffi::PyModule_New(name.as_ptr())) } + #[allow(deprecated)] + unsafe { + py.from_owned_ptr_or_err(ffi::PyModule_New(name.as_ptr())) + } } /// Imports the Python module with the specified name. @@ -67,7 +70,10 @@ impl PyModule { N: IntoPy>, { let name: Py = name.into_py(py); - unsafe { py.from_owned_ptr_or_err(ffi::PyImport_Import(name.as_ptr())) } + #[allow(deprecated)] + unsafe { + py.from_owned_ptr_or_err(ffi::PyImport_Import(name.as_ptr())) + } } /// Creates and loads a module named `module_name`, @@ -146,6 +152,7 @@ impl PyModule { return Err(PyErr::fetch(py)); } + #[allow(deprecated)] <&PyModule as FromPyObject>::extract(py.from_owned_ptr_or_err(mptr)?) } } diff --git a/src/types/slice.rs b/src/types/slice.rs index 8e86ff7ceee..b4b6731d695 100644 --- a/src/types/slice.rs +++ b/src/types/slice.rs @@ -78,10 +78,7 @@ impl PySlice { ) )] pub fn full(py: Python<'_>) -> &PySlice { - unsafe { - let ptr = ffi::PySlice_New(ffi::Py_None(), ffi::Py_None(), ffi::Py_None()); - py.from_owned_ptr(ptr) - } + PySlice::full_bound(py).into_gil_ref() } /// Constructs a new full slice that is equivalent to `::`. diff --git a/src/types/string.rs b/src/types/string.rs index bc4419944bc..0a7847d2959 100644 --- a/src/types/string.rs +++ b/src/types/string.rs @@ -241,10 +241,7 @@ impl PyString { #[cfg(not(any(Py_3_10, not(Py_LIMITED_API))))] { - let bytes = unsafe { - self.py() - .from_owned_ptr_or_err::(ffi::PyUnicode_AsUTF8String(self.as_ptr())) - }?; + let bytes = self.as_borrowed().encode_utf8()?.into_gil_ref(); Ok(unsafe { std::str::from_utf8_unchecked(bytes.as_bytes()) }) } } From c4f66657c58401023b54a858ebf04400827baa7c Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Thu, 22 Feb 2024 08:05:37 +0000 Subject: [PATCH 026/936] fix `either` feature conditional compilation, again (#3834) * fix `either` feature conditional compilation, again * test feature powerset in CI * install `rust-src` for feature powerset tests * review: adamreichold feedback * Fix one more case of redundant imports. * just check feature powerset for now --------- Co-authored-by: Adam Reichold --- .github/workflows/ci.yml | 17 ++++++++ Cargo.toml | 17 ++++---- newsfragments/3834.fixed.md | 1 + noxfile.py | 82 +++++++++++++++++++++++++++++++++++-- pytests/src/comparisons.rs | 1 - src/conversions/either.rs | 10 ++++- 6 files changed, 113 insertions(+), 15 deletions(-) create mode 100644 newsfragments/3834.fixed.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 22281185caa..6331a79c645 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -480,6 +480,22 @@ jobs: - run: python3 -m pip install --upgrade pip && pip install nox - run: python3 -m nox -s test-version-limits + check-feature-powerset: + needs: [fmt] + if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || (github.event_name != 'pull_request' && github.ref != 'refs/heads/main') }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + - uses: Swatinem/rust-cache@v2 + continue-on-error: true + - uses: dtolnay/rust-toolchain@stable + with: + components: rust-src + - uses: taiki-e/install-action@cargo-hack + - run: python3 -m pip install --upgrade pip && pip install nox + - run: python3 -m nox -s check-feature-powerset + conclusion: needs: - fmt @@ -494,6 +510,7 @@ jobs: - emscripten - test-debug - test-version-limits + - check-feature-powerset if: always() runs-on: ubuntu-latest steps: diff --git a/Cargo.toml b/Cargo.toml index 5386b76f573..4d1899cbdb9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -107,20 +107,21 @@ nightly = [] # This is mostly intended for testing purposes - activating *all* of these isn't particularly useful. full = [ "macros", + "gil-refs", # "multiple-pymethods", # TODO re-add this when MSRV is greater than 1.62 + "anyhow", "chrono", "chrono-tz", - "num-bigint", - "num-complex", - "hashbrown", - "smallvec", - "serde", - "indexmap", "either", - "eyre", - "anyhow", "experimental-inspect", + "eyre", + "hashbrown", + "indexmap", + "num-bigint", + "num-complex", "rust_decimal", + "serde", + "smallvec", ] [workspace] diff --git a/newsfragments/3834.fixed.md b/newsfragments/3834.fixed.md new file mode 100644 index 00000000000..fa77e84f36e --- /dev/null +++ b/newsfragments/3834.fixed.md @@ -0,0 +1 @@ +Fix compilation failure with `either` feature enabled without `experimental-inspect` enabled. diff --git a/noxfile.py b/noxfile.py index 3981e62e100..00288273aec 100644 --- a/noxfile.py +++ b/noxfile.py @@ -13,6 +13,14 @@ import nox import nox.command +try: + import tomllib as toml +except ImportError: + try: + import toml + except ImportError: + toml = None + nox.options.sessions = ["test", "clippy", "rustfmt", "ruff", "docs"] @@ -479,10 +487,8 @@ def check_changelog(session: nox.Session): def set_minimal_package_versions(session: nox.Session): from collections import defaultdict - try: - import tomllib as toml - except ImportError: - import toml + if toml is None: + session.error("requires Python 3.11 or `toml` to be installed") projects = ( None, @@ -593,6 +599,74 @@ def test_version_limits(session: nox.Session): _run_cargo(session, "check", env=env, expect_error=True) +@nox.session(name="check-feature-powerset", venv_backend="none") +def check_feature_powerset(session: nox.Session): + if toml is None: + session.error("requires Python 3.11 or `toml` to be installed") + + with (PYO3_DIR / "Cargo.toml").open("rb") as cargo_toml_file: + cargo_toml = toml.load(cargo_toml_file) + + EXCLUDED_FROM_FULL = { + "nightly", + "extension-module", + "full", + "default", + "auto-initialize", + "generate-import-lib", + "multiple-pymethods", # TODO add this after MSRV 1.62 + } + + features = cargo_toml["features"] + + full_feature = set(features["full"]) + abi3_features = {feature for feature in features if feature.startswith("abi3")} + abi3_version_features = abi3_features - {"abi3"} + + expected_full_feature = features.keys() - EXCLUDED_FROM_FULL - abi3_features + + uncovered_features = expected_full_feature - full_feature + if uncovered_features: + session.error( + f"some features missing from `full` meta feature: {uncovered_features}" + ) + + experimental_features = { + feature for feature in features if feature.startswith("experimental-") + } + full_without_experimental = full_feature - experimental_features + + if len(experimental_features) >= 2: + # justification: we always assume that feature within these groups are + # mutually exclusive to simplify CI + features_to_group = [ + full_without_experimental, + experimental_features, + ] + elif len(experimental_features) == 1: + # no need to make an experimental features group + features_to_group = [full_without_experimental] + else: + session.error("no experimental features exist; please simplify the noxfile") + + features_to_skip = [ + *EXCLUDED_FROM_FULL, + *abi3_version_features, + ] + + comma_join = ",".join + _run_cargo( + session, + "hack", + "--feature-powerset", + '--optional-deps=""', + f'--skip="{comma_join(features_to_skip)}"', + *(f"--group-features={comma_join(group)}" for group in features_to_group), + "check", + "--all-targets", + ) + + 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") diff --git a/pytests/src/comparisons.rs b/pytests/src/comparisons.rs index d8c2f5a6a52..b3ba293186a 100644 --- a/pytests/src/comparisons.rs +++ b/pytests/src/comparisons.rs @@ -1,5 +1,4 @@ use pyo3::prelude::*; -use pyo3::{types::PyModule, Python}; #[pyclass] struct Eq(i64); diff --git a/src/conversions/either.rs b/src/conversions/either.rs index c763cdf95e5..16d5ad491eb 100644 --- a/src/conversions/either.rs +++ b/src/conversions/either.rs @@ -94,7 +94,13 @@ where } else if let Ok(r) = obj.extract::() { Ok(Either::Right(r)) } else { - let err_msg = format!("failed to convert the value to '{}'", Self::type_input()); + // TODO: it might be nice to use the `type_input()` name here once `type_input` + // is not experimental, rather than the Rust type names. + let err_msg = format!( + "failed to convert the value to 'Union[{}, {}]'", + std::any::type_name::(), + std::any::type_name::() + ); Err(PyTypeError::new_err(err_msg)) } } @@ -134,7 +140,7 @@ mod tests { assert!(err.is_instance_of::(py)); assert_eq!( err.to_string(), - "TypeError: failed to convert the value to 'Union[int, float]'" + "TypeError: failed to convert the value to 'Union[i32, f32]'" ); let obj_i = 42.to_object(py); From 9e74c858c272b6fab4193ae1fd7b7dc1ee6b8a2a Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Thu, 22 Feb 2024 09:35:47 +0000 Subject: [PATCH 027/936] add `PyModule::new_bound` and `PyModule::import_bound` (#3775) * add `PyModule::new` and `PyModule::import_bound` * review: Icxolu feedback --- examples/maturin-starter/src/lib.rs | 4 +- examples/plugin/src/main.rs | 2 +- examples/setuptools-rust-starter/src/lib.rs | 4 +- guide/src/class.md | 8 +- guide/src/class/numeric.md | 2 +- guide/src/conversions/traits.md | 8 +- guide/src/function/signature.md | 8 +- guide/src/module.md | 6 +- guide/src/python_from_rust.md | 26 ++-- pyo3-benches/benches/bench_call.rs | 13 +- pyo3-benches/benches/bench_intern.rs | 10 +- pytests/src/lib.rs | 4 +- src/conversions/anyhow.rs | 2 +- src/conversions/chrono.rs | 2 +- src/conversions/chrono_tz.rs | 2 + src/conversions/eyre.rs | 2 +- src/conversions/num_bigint.rs | 9 +- src/conversions/num_complex.rs | 17 ++- src/instance.rs | 17 +-- src/marker.rs | 8 +- src/pyclass_init.rs | 2 +- src/types/any.rs | 67 +++++----- src/types/capsule.rs | 6 +- src/types/module.rs | 131 ++++++++++++++------ src/types/traceback.rs | 5 +- tests/test_class_basics.rs | 2 +- tests/test_class_new.rs | 5 +- tests/test_coroutine.rs | 2 +- tests/test_inheritance.rs | 2 +- tests/test_module.rs | 20 +-- tests/test_proto_methods.rs | 15 +-- tests/test_various.rs | 8 +- 32 files changed, 229 insertions(+), 190 deletions(-) diff --git a/examples/maturin-starter/src/lib.rs b/examples/maturin-starter/src/lib.rs index 96ace0f97d5..8b0748f8311 100644 --- a/examples/maturin-starter/src/lib.rs +++ b/examples/maturin-starter/src/lib.rs @@ -27,8 +27,8 @@ fn maturin_starter(py: Python<'_>, m: &PyModule) -> PyResult<()> { // Inserting to sys.modules allows importing submodules nicely from Python // e.g. from maturin_starter.submodule import SubmoduleClass - let sys = PyModule::import(py, "sys")?; - let sys_modules: &PyDict = sys.getattr("modules")?.downcast()?; + let sys = PyModule::import_bound(py, "sys")?; + let sys_modules: Bound<'_, PyDict> = sys.getattr("modules")?.downcast_into()?; sys_modules.set_item("maturin_starter.submodule", m.getattr("submodule")?)?; Ok(()) diff --git a/examples/plugin/src/main.rs b/examples/plugin/src/main.rs index b50b54548e5..5a54a1837cb 100644 --- a/examples/plugin/src/main.rs +++ b/examples/plugin/src/main.rs @@ -19,7 +19,7 @@ fn main() -> Result<(), Box> { // Now we can load our python_plugin/gadget_init_plugin.py file. // It can in turn import other stuff as it deems appropriate - let plugin = PyModule::import(py, "gadget_init_plugin")?; + let plugin = PyModule::import_bound(py, "gadget_init_plugin")?; // and call start function there, which will return a python reference to Gadget. // Gadget here is a "pyclass" object reference let gadget = plugin.getattr("start")?.call0()?; diff --git a/examples/setuptools-rust-starter/src/lib.rs b/examples/setuptools-rust-starter/src/lib.rs index fbfeccc1555..2bcd411c238 100644 --- a/examples/setuptools-rust-starter/src/lib.rs +++ b/examples/setuptools-rust-starter/src/lib.rs @@ -27,8 +27,8 @@ fn _setuptools_rust_starter(py: Python<'_>, m: &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(py, "sys")?; - let sys_modules: &PyDict = sys.getattr("modules")?.downcast()?; + let sys = PyModule::import_bound(py, "sys")?; + let sys_modules: Bound<'_, PyDict> = sys.getattr("modules")?.downcast_into()?; sys_modules.set_item("setuptools_rust_starter.submodule", m.getattr("submodule")?)?; Ok(()) diff --git a/guide/src/class.md b/guide/src/class.md index 9ebba38a820..9662e8f6e09 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -941,8 +941,8 @@ impl MyClass { # # fn main() -> PyResult<()> { # Python::with_gil(|py| { -# let inspect = PyModule::import(py, "inspect")?.getattr("signature")?; -# let module = PyModule::new(py, "my_module")?; +# let inspect = PyModule::import_bound(py, "inspect")?.getattr("signature")?; +# let module = PyModule::new_bound(py, "my_module")?; # module.add_class::()?; # let class = module.getattr("MyClass")?; # @@ -951,7 +951,7 @@ impl MyClass { # assert_eq!(doc, ""); # # let sig: String = inspect -# .call1((class,))? +# .call1((&class,))? # .call_method0("__str__")? # .extract()?; # assert_eq!(sig, "(c, d)"); @@ -959,7 +959,7 @@ impl MyClass { # let doc: String = class.getattr("__doc__")?.extract()?; # assert_eq!(doc, ""); # -# inspect.call1((class,)).expect_err("`text_signature` on classes is not compatible with compilation in `abi3` mode until Python 3.10 or greater"); +# inspect.call1((&class,)).expect_err("`text_signature` on classes is not compatible with compilation in `abi3` mode until Python 3.10 or greater"); # } # # { diff --git a/guide/src/class/numeric.md b/guide/src/class/numeric.md index cbd9db824a0..2ffb0a543ef 100644 --- a/guide/src/class/numeric.md +++ b/guide/src/class/numeric.md @@ -386,7 +386,7 @@ fn my_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { # # fn main() -> PyResult<()> { # Python::with_gil(|py| -> PyResult<()> { -# let globals = PyModule::import(py, "__main__")?.dict().as_borrowed(); +# let globals = PyModule::import_bound(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 b46e5c02f4c..5cdf2c590b9 100644 --- a/guide/src/conversions/traits.md +++ b/guide/src/conversions/traits.md @@ -54,7 +54,7 @@ struct RustyStruct { # # fn main() -> PyResult<()> { # Python::with_gil(|py| -> PyResult<()> { -# let module = PyModule::from_code( +# let module = PyModule::from_code_bound( # py, # "class Foo: # def __init__(self): @@ -111,7 +111,7 @@ struct RustyStruct { # # fn main() -> PyResult<()> { # Python::with_gil(|py| -> PyResult<()> { -# let module = PyModule::from_code( +# let module = PyModule::from_code_bound( # py, # "class Foo(dict): # def __init__(self): @@ -339,7 +339,7 @@ enum RustyEnum<'a> { # ); # } # { -# let module = PyModule::from_code( +# let module = PyModule::from_code_bound( # py, # "class Foo(dict): # def __init__(self): @@ -364,7 +364,7 @@ enum RustyEnum<'a> { # } # # { -# let module = PyModule::from_code( +# let module = PyModule::from_code_bound( # py, # "class Foo(dict): # def __init__(self): diff --git a/guide/src/function/signature.md b/guide/src/function/signature.md index 8341f51c38c..d92767e7bde 100644 --- a/guide/src/function/signature.md +++ b/guide/src/function/signature.md @@ -138,7 +138,7 @@ fn increment(x: u64, amount: Option) -> u64 { # Python::with_gil(|py| { # let fun = pyo3::wrap_pyfunction!(increment, py)?; # -# let inspect = PyModule::import(py, "inspect")?.getattr("signature")?; +# let inspect = PyModule::import_bound(py, "inspect")?.getattr("signature")?; # let sig: String = inspect # .call1((fun,))? # .call_method0("__str__")? @@ -166,7 +166,7 @@ fn increment(x: u64, amount: Option) -> u64 { # Python::with_gil(|py| { # let fun = pyo3::wrap_pyfunction!(increment, py)?; # -# let inspect = PyModule::import(py, "inspect")?.getattr("signature")?; +# let inspect = PyModule::import_bound(py, "inspect")?.getattr("signature")?; # let sig: String = inspect # .call1((fun,))? # .call_method0("__str__")? @@ -209,7 +209,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(py, "inspect")?.getattr("signature")?; +# let inspect = PyModule::import_bound(py, "inspect")?.getattr("signature")?; # let sig: String = inspect # .call1((fun,))? # .call_method0("__str__")? @@ -257,7 +257,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(py, "inspect")?.getattr("signature")?; +# let inspect = PyModule::import_bound(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 3d984f60d39..789c3d91ccb 100644 --- a/guide/src/module.md +++ b/guide/src/module.md @@ -78,9 +78,9 @@ fn parent_module(py: Python<'_>, m: &PyModule) -> PyResult<()> { } fn register_child_module(py: Python<'_>, parent_module: &PyModule) -> PyResult<()> { - let child_module = PyModule::new(py, "child_module")?; - child_module.add_function(wrap_pyfunction!(func, child_module)?)?; - parent_module.add_submodule(child_module)?; + let child_module = PyModule::new_bound(py, "child_module")?; + child_module.add_function(&wrap_pyfunction!(func, child_module.as_gil_ref())?.as_borrowed())?; + parent_module.add_submodule(child_module.as_gil_ref())?; Ok(()) } diff --git a/guide/src/python_from_rust.md b/guide/src/python_from_rust.md index 4b81e2f62fd..973f997cbf8 100644 --- a/guide/src/python_from_rust.md +++ b/guide/src/python_from_rust.md @@ -32,7 +32,7 @@ fn main() -> PyResult<()> { let arg3 = "arg3"; Python::with_gil(|py| { - let fun: Py = PyModule::from_code( + let fun: Py = PyModule::from_code_bound( py, "def example(*args, **kwargs): if args != (): @@ -78,7 +78,7 @@ fn main() -> PyResult<()> { let val2 = 2; Python::with_gil(|py| { - let fun: Py = PyModule::from_code( + let fun: Py = PyModule::from_code_bound( py, "def example(*args, **kwargs): if args != (): @@ -134,7 +134,7 @@ use pyo3::prelude::*; fn main() -> PyResult<()> { Python::with_gil(|py| { - let builtins = PyModule::import(py, "builtins")?; + let builtins = PyModule::import_bound(py, "builtins")?; let total: i32 = builtins .getattr("sum")? .call1((vec![1, 2, 3],))? @@ -233,7 +233,7 @@ use pyo3::{ # fn main() -> PyResult<()> { Python::with_gil(|py| { - let activators = PyModule::from_code( + let activators = PyModule::from_code_bound( py, r#" def relu(x): @@ -253,7 +253,7 @@ def leaky_relu(x, slope=0.01): let kwargs = [("slope", 0.2)].into_py_dict_bound(py); let lrelu_result: f64 = activators .getattr("leaky_relu")? - .call((-1.0,), Some(kwargs.as_gil_ref()))? + .call((-1.0,), Some(&kwargs))? .extract()?; assert_eq!(lrelu_result, -0.2); # Ok(()) @@ -310,12 +310,12 @@ pub fn add_one(x: i64) -> i64 { fn main() -> PyResult<()> { Python::with_gil(|py| { // Create new module - let foo_module = PyModule::new(py, "foo")?; - foo_module.add_function(wrap_pyfunction!(add_one, foo_module)?)?; + let foo_module = PyModule::new_bound(py, "foo")?; + foo_module.add_function(&wrap_pyfunction!(add_one, foo_module.as_gil_ref())?.as_borrowed())?; // Import and get sys.modules - let sys = PyModule::import(py, "sys")?; - let py_modules: &PyDict = sys.getattr("modules")?.downcast()?; + let sys = PyModule::import_bound(py, "sys")?; + let py_modules: Bound<'_, PyDict> = sys.getattr("modules")?.downcast_into()?; // Insert foo into sys.modules py_modules.set_item("foo", foo_module)?; @@ -381,8 +381,8 @@ fn main() -> PyResult<()> { )); let py_app = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/python_app/app.py")); let from_python = Python::with_gil(|py| -> PyResult> { - PyModule::from_code(py, py_foo, "utils.foo", "utils.foo")?; - let app: Py = PyModule::from_code(py, py_app, "", "")? + PyModule::from_code_bound(py, py_foo, "utils.foo", "utils.foo")?; + let app: Py = PyModule::from_code_bound(py, py_app, "", "")? .getattr("run")? .into(); app.call0(py) @@ -416,7 +416,7 @@ fn main() -> PyResult<()> { let from_python = Python::with_gil(|py| -> PyResult> { let syspath = py.import_bound("sys")?.getattr("path")?.downcast_into::()?; syspath.insert(0, &path)?; - let app: Py = PyModule::from_code(py, &py_app, "", "")? + let app: Py = PyModule::from_code_bound(py, &py_app, "", "")? .getattr("run")? .into(); app.call0(py) @@ -440,7 +440,7 @@ use pyo3::prelude::*; fn main() { Python::with_gil(|py| { - let custom_manager = PyModule::from_code( + let custom_manager = PyModule::from_code_bound( py, r#" class House(object): diff --git a/pyo3-benches/benches/bench_call.rs b/pyo3-benches/benches/bench_call.rs index 50772097961..8470c8768d3 100644 --- a/pyo3-benches/benches/bench_call.rs +++ b/pyo3-benches/benches/bench_call.rs @@ -1,10 +1,13 @@ +use std::hint::black_box; + use codspeed_criterion_compat::{criterion_group, criterion_main, Bencher, Criterion}; use pyo3::prelude::*; macro_rules! test_module { ($py:ident, $code:literal) => { - PyModule::from_code($py, $code, file!(), "test_module").expect("module creation failed") + PyModule::from_code_bound($py, $code, file!(), "test_module") + .expect("module creation failed") }; } @@ -12,11 +15,11 @@ fn bench_call_0(b: &mut Bencher<'_>) { Python::with_gil(|py| { let module = test_module!(py, "def foo(): pass"); - let foo_module = module.getattr("foo").unwrap(); + let foo_module = &module.getattr("foo").unwrap(); b.iter(|| { for _ in 0..1000 { - foo_module.call0().unwrap(); + black_box(foo_module).call0().unwrap(); } }); }) @@ -33,11 +36,11 @@ class Foo: " ); - let foo_module = module.getattr("Foo").unwrap().call0().unwrap(); + let foo_module = &module.getattr("Foo").unwrap().call0().unwrap(); b.iter(|| { for _ in 0..1000 { - foo_module.call_method0("foo").unwrap(); + black_box(foo_module).call_method0("foo").unwrap(); } }); }) diff --git a/pyo3-benches/benches/bench_intern.rs b/pyo3-benches/benches/bench_intern.rs index 806c4e95a9d..f9f9162a5ee 100644 --- a/pyo3-benches/benches/bench_intern.rs +++ b/pyo3-benches/benches/bench_intern.rs @@ -1,3 +1,5 @@ +use std::hint::black_box; + use codspeed_criterion_compat::{criterion_group, criterion_main, Bencher, Criterion}; use pyo3::prelude::*; @@ -6,17 +8,17 @@ use pyo3::intern; fn getattr_direct(b: &mut Bencher<'_>) { Python::with_gil(|py| { - let sys = py.import_bound("sys").unwrap(); + let sys = &py.import_bound("sys").unwrap(); - b.iter(|| sys.getattr("version").unwrap()); + b.iter(|| black_box(sys).getattr("version").unwrap()); }); } fn getattr_intern(b: &mut Bencher<'_>) { Python::with_gil(|py| { - let sys = py.import_bound("sys").unwrap(); + let sys = &py.import_bound("sys").unwrap(); - b.iter(|| sys.getattr(intern!(py, "version")).unwrap()); + b.iter(|| black_box(sys).getattr(intern!(py, "version")).unwrap()); }); } diff --git a/pytests/src/lib.rs b/pytests/src/lib.rs index e65385bf679..bfd80edb719 100644 --- a/pytests/src/lib.rs +++ b/pytests/src/lib.rs @@ -39,8 +39,8 @@ fn pyo3_pytests(py: Python<'_>, m: &PyModule) -> PyResult<()> { // Inserting to sys.modules allows importing submodules nicely from Python // e.g. import pyo3_pytests.buf_and_str as bas - let sys = PyModule::import(py, "sys")?; - let sys_modules: &PyDict = sys.getattr("modules")?.downcast()?; + let sys = PyModule::import_bound(py, "sys")?; + let sys_modules = sys.getattr("modules")?.downcast_into::()?; sys_modules.set_item("pyo3_pytests.awaitable", m.getattr("awaitable")?)?; sys_modules.set_item("pyo3_pytests.buf_and_str", m.getattr("buf_and_str")?)?; sys_modules.set_item("pyo3_pytests.comparisons", m.getattr("comparisons")?)?; diff --git a/src/conversions/anyhow.rs b/src/conversions/anyhow.rs index a490bd4ce31..fba8816d23a 100644 --- a/src/conversions/anyhow.rs +++ b/src/conversions/anyhow.rs @@ -75,7 +75,7 @@ //! // could call inside an application... //! // This might return a `PyErr`. //! let res = Python::with_gil(|py| { -//! let zlib = PyModule::import(py, "zlib")?; +//! let zlib = PyModule::import_bound(py, "zlib")?; //! let decompress = zlib.getattr("decompress")?; //! let bytes = PyBytes::new_bound(py, bytes); //! let value = decompress.call1((bytes,))?; diff --git a/src/conversions/chrono.rs b/src/conversions/chrono.rs index 3aa6dbeb4ca..0c96ef9c705 100644 --- a/src/conversions/chrono.rs +++ b/src/conversions/chrono.rs @@ -564,7 +564,7 @@ fn timezone_utc_bound(py: Python<'_>) -> Bound<'_, PyAny> { #[cfg(test)] mod tests { use super::*; - use crate::{types::PyTuple, Py}; + use crate::{types::PyTuple, Bound, Py}; use std::{cmp::Ordering, panic}; #[test] diff --git a/src/conversions/chrono_tz.rs b/src/conversions/chrono_tz.rs index b4d0e7edf8c..986bb8313d2 100644 --- a/src/conversions/chrono_tz.rs +++ b/src/conversions/chrono_tz.rs @@ -69,6 +69,8 @@ impl FromPyObject<'_> for Tz { #[cfg(all(test, not(windows)))] // Troubles loading timezones on Windows mod tests { + use crate::{types::any::PyAnyMethods, Bound}; + use super::*; #[test] diff --git a/src/conversions/eyre.rs b/src/conversions/eyre.rs index 64ed4a0320f..236e2c8bc92 100644 --- a/src/conversions/eyre.rs +++ b/src/conversions/eyre.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(py, "zlib")?; +//! let zlib = PyModule::import_bound(py, "zlib")?; //! let decompress = zlib.getattr("decompress")?; //! let bytes = PyBytes::new_bound(py, bytes); //! let value = decompress.call1((bytes,))?; diff --git a/src/conversions/num_bigint.rs b/src/conversions/num_bigint.rs index 652df154258..48a4ee60d73 100644 --- a/src/conversions/num_bigint.rs +++ b/src/conversions/num_bigint.rs @@ -262,7 +262,10 @@ fn int_n_bits(long: &Bound<'_, PyLong>) -> PyResult { #[cfg(test)] mod tests { use super::*; - use crate::types::{PyDict, PyModule}; + use crate::{ + types::{PyDict, PyModule}, + Bound, + }; use indoc::indoc; fn rust_fib() -> impl Iterator @@ -323,7 +326,7 @@ mod tests { }); } - fn python_index_class(py: Python<'_>) -> &PyModule { + fn python_index_class(py: Python<'_>) -> Bound<'_, PyModule> { let index_code = indoc!( r#" class C: @@ -333,7 +336,7 @@ mod tests { return self.x "# ); - PyModule::from_code(py, index_code, "index.py", "index").unwrap() + PyModule::from_code_bound(py, index_code, "index.py", "index").unwrap() } #[test] diff --git a/src/conversions/num_complex.rs b/src/conversions/num_complex.rs index 3b4fe0fc217..c736a7baa77 100644 --- a/src/conversions/num_complex.rs +++ b/src/conversions/num_complex.rs @@ -27,7 +27,7 @@ //! ```ignore //! # // not tested because nalgebra isn't supported on msrv //! # // please file an issue if it breaks! -//! use nalgebra::base::{dimension::Const, storage::Storage, Matrix}; +//! use nalgebra::base::{dimension::Const, Matrix}; //! use num_complex::Complex; //! use pyo3::prelude::*; //! @@ -55,9 +55,9 @@ //! # //! # fn main() -> PyResult<()> { //! # Python::with_gil(|py| -> PyResult<()> { -//! # let module = PyModule::new(py, "my_module")?; +//! # let module = PyModule::new_bound(py, "my_module")?; //! # -//! # module.add_function(wrap_pyfunction!(get_eigenvalues, module)?)?; +//! # module.add_function(&wrap_pyfunction!(get_eigenvalues, module.as_gil_ref())?.as_borrowed())?; //! # //! # let m11 = PyComplex::from_doubles_bound(py, 0_f64, -1_f64); //! # let m12 = PyComplex::from_doubles_bound(py, 1_f64, 0_f64); @@ -199,8 +199,7 @@ complex_conversion!(f64); #[cfg(test)] mod tests { use super::*; - use crate::types::complex::PyComplexMethods; - use crate::types::PyModule; + use crate::types::{any::PyAnyMethods, complex::PyComplexMethods, PyModule}; #[test] fn from_complex() { @@ -229,7 +228,7 @@ mod tests { #[test] fn from_python_magic() { Python::with_gil(|py| { - let module = PyModule::from_code( + let module = PyModule::from_code_bound( py, r#" class A: @@ -267,7 +266,7 @@ class C: #[test] fn from_python_inherited_magic() { Python::with_gil(|py| { - let module = PyModule::from_code( + let module = PyModule::from_code_bound( py, r#" class First: pass @@ -311,7 +310,7 @@ class C(First, IndexMixin): pass // `type(inst).attr(inst)` equivalent to `inst.attr()` for methods, but this isn't the only // way the descriptor protocol might be implemented. Python::with_gil(|py| { - let module = PyModule::from_code( + let module = PyModule::from_code_bound( py, r#" class A: @@ -334,7 +333,7 @@ class A: fn from_python_nondescriptor_magic() { // Magic methods don't need to implement the descriptor protocol, if they're callable. Python::with_gil(|py| { - let module = PyModule::from_code( + let module = PyModule::from_code_bound( py, r#" class MyComplex: diff --git a/src/instance.rs b/src/instance.rs index 84b4cb62d9d..f35ab4bbb3a 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -722,12 +722,12 @@ impl IntoPy for Borrowed<'_, '_, T> { /// # /// # fn main() -> PyResult<()> { /// # Python::with_gil(|py| { -/// # let m = pyo3::types::PyModule::new(py, "test")?; +/// # let m = pyo3::types::PyModule::new_bound(py, "test")?; /// # m.add_class::()?; /// # -/// # let foo: &PyCell = m.getattr("Foo")?.call0()?.downcast()?; +/// # let foo: Bound<'_, Foo> = m.getattr("Foo")?.call0()?.downcast_into()?; /// # let dict = &foo.borrow().inner; -/// # let dict: &PyDict = dict.as_ref(py); +/// # let dict: &Bound<'_, PyDict> = dict.bind(py); /// # /// # Ok(()) /// # }) @@ -759,10 +759,10 @@ impl IntoPy for Borrowed<'_, '_, T> { /// # /// # fn main() -> PyResult<()> { /// # Python::with_gil(|py| { -/// # let m = pyo3::types::PyModule::new(py, "test")?; +/// # let m = pyo3::types::PyModule::new_bound(py, "test")?; /// # m.add_class::()?; /// # -/// # let foo: &PyCell = m.getattr("Foo")?.call0()?.downcast()?; +/// # let foo: Bound<'_, Foo> = m.getattr("Foo")?.call0()?.downcast_into()?; /// # let bar = &foo.borrow().inner; /// # let bar: &Bar = &*bar.borrow(py); /// # @@ -1357,7 +1357,7 @@ impl Py { /// } /// # /// # Python::with_gil(|py| { - /// # let ob = PyModule::new(py, "empty").unwrap().into_py(py); + /// # let ob = PyModule::new_bound(py, "empty").unwrap().into_py(py); /// # set_answer(ob, py).unwrap(); /// # }); /// ``` @@ -1905,6 +1905,7 @@ impl PyObject { #[cfg_attr(not(feature = "gil-refs"), allow(deprecated))] mod tests { use super::{Bound, Py, PyObject}; + use crate::types::any::PyAnyMethods; use crate::types::PyCapsule; use crate::types::{dict::IntoPyDict, PyDict, PyString}; use crate::{ffi, Borrowed, PyAny, PyNativeType, PyResult, Python, ToPyObject}; @@ -1978,7 +1979,7 @@ class A: pass a = A() "#; - let module = PyModule::from_code(py, CODE, "", "")?; + let module = PyModule::from_code_bound(py, CODE, "", "")?; let instance: Py = module.getattr("a")?.into(); instance.getattr(py, "foo").unwrap_err(); @@ -2005,7 +2006,7 @@ class A: pass a = A() "#; - let module = PyModule::from_code(py, CODE, "", "")?; + let module = PyModule::from_code_bound(py, CODE, "", "")?; let instance: Py = module.getattr("a")?.into(); let foo = crate::intern!(py, "foo"); diff --git a/src/marker.rs b/src/marker.rs index 059a5662b5d..449ea2bde6b 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -764,7 +764,7 @@ impl<'py> Python<'py> { where N: IntoPy>, { - PyModule::import(self, name) + Self::import_bound(self, name).map(Bound::into_gil_ref) } /// Imports the Python module with the specified name. @@ -772,11 +772,7 @@ impl<'py> Python<'py> { where N: IntoPy>, { - // FIXME: This should be replaced by `PyModule::import_bound` once thats - // implemented. - PyModule::import(self, name) - .map(PyNativeType::as_borrowed) - .map(crate::Borrowed::to_owned) + PyModule::import_bound(self, name) } /// Gets the Python builtin value `None`. diff --git a/src/pyclass_init.rs b/src/pyclass_init.rs index ac3aa3ef852..94d377a732c 100644 --- a/src/pyclass_init.rs +++ b/src/pyclass_init.rs @@ -184,7 +184,7 @@ impl PyClassInitializer { /// /// fn main() -> PyResult<()> { /// Python::with_gil(|py| { - /// let m = PyModule::new(py, "example")?; + /// let m = PyModule::new_bound(py, "example")?; /// m.add_class::()?; /// m.add_class::()?; /// diff --git a/src/types/any.rs b/src/types/any.rs index 4d371764d29..15f2fd3135c 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -146,16 +146,16 @@ impl PyAny { /// # Example: `intern!`ing the attribute name /// /// ``` - /// # use pyo3::{intern, pyfunction, types::PyModule, PyAny, Python, PyResult}; + /// # use pyo3::{prelude::*, intern}; /// # /// #[pyfunction] - /// fn set_answer(ob: &PyAny) -> PyResult<()> { + /// fn set_answer(ob: &Bound<'_, PyAny>) -> PyResult<()> { /// ob.setattr(intern!(ob.py(), "answer"), 42) /// } /// # /// # Python::with_gil(|py| { - /// # let ob = PyModule::new(py, "empty").unwrap(); - /// # set_answer(ob).unwrap(); + /// # let ob = PyModule::new_bound(py, "empty").unwrap(); + /// # set_answer(&ob).unwrap(); /// # }); /// ``` pub fn setattr(&self, attr_name: N, value: V) -> PyResult<()> @@ -346,7 +346,7 @@ impl PyAny { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| -> PyResult<()> { - /// let builtins = PyModule::import(py, "builtins")?; + /// let builtins = PyModule::import_bound(py, "builtins")?; /// let print = builtins.getattr("print")?; /// assert!(print.is_callable()); /// Ok(()) @@ -385,12 +385,12 @@ impl PyAny { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { - /// let module = PyModule::from_code(py, CODE, "", "")?; + /// let module = PyModule::from_code_bound(py, CODE, "", "")?; /// let fun = module.getattr("function")?; /// let args = ("hello",); /// let kwargs = PyDict::new_bound(py); /// kwargs.set_item("cruel", "world")?; - /// let result = fun.call(args, Some(kwargs.as_gil_ref()))?; + /// let result = fun.call(args, Some(&kwargs))?; /// assert_eq!(result.extract::<&str>()?, "called with args and kwargs"); /// Ok(()) /// }) @@ -417,7 +417,7 @@ impl PyAny { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| -> PyResult<()> { - /// let module = PyModule::import(py, "builtins")?; + /// let module = PyModule::import_bound(py, "builtins")?; /// let help = module.getattr("help")?; /// help.call0()?; /// Ok(()) @@ -448,7 +448,7 @@ impl PyAny { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { - /// let module = PyModule::from_code(py, CODE, "", "")?; + /// let module = PyModule::from_code_bound(py, CODE, "", "")?; /// let fun = module.getattr("function")?; /// let args = ("hello",); /// let result = fun.call1(args)?; @@ -485,12 +485,12 @@ impl PyAny { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { - /// let module = PyModule::from_code(py, CODE, "", "")?; + /// let module = PyModule::from_code_bound(py, CODE, "", "")?; /// let instance = module.getattr("a")?; /// let args = ("hello",); /// let kwargs = PyDict::new_bound(py); /// kwargs.set_item("cruel", "world")?; - /// let result = instance.call_method("method", args, Some(kwargs.as_gil_ref()))?; + /// let result = instance.call_method("method", args, Some(&kwargs))?; /// assert_eq!(result.extract::<&str>()?, "called with args and kwargs"); /// Ok(()) /// }) @@ -529,7 +529,7 @@ impl PyAny { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { - /// let module = PyModule::from_code(py, CODE, "", "")?; + /// let module = PyModule::from_code_bound(py, CODE, "", "")?; /// let instance = module.getattr("a")?; /// let result = instance.call_method0("method")?; /// assert_eq!(result.extract::<&str>()?, "called with no arguments"); @@ -569,7 +569,7 @@ impl PyAny { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { - /// let module = PyModule::from_code(py, CODE, "", "")?; + /// let module = PyModule::from_code_bound(py, CODE, "", "")?; /// let instance = module.getattr("a")?; /// let args = ("hello",); /// let result = instance.call_method1("method", args)?; @@ -1008,16 +1008,16 @@ pub trait PyAnyMethods<'py> { /// # Example: `intern!`ing the attribute name /// /// ``` - /// # use pyo3::{intern, pyfunction, types::PyModule, PyAny, Python, PyResult}; + /// # use pyo3::{prelude::*, intern}; /// # /// #[pyfunction] - /// fn set_answer(ob: &PyAny) -> PyResult<()> { + /// fn set_answer(ob: &Bound<'_, PyAny>) -> PyResult<()> { /// ob.setattr(intern!(ob.py(), "answer"), 42) /// } /// # /// # Python::with_gil(|py| { - /// # let ob = PyModule::new(py, "empty").unwrap(); - /// # set_answer(ob).unwrap(); + /// # let ob = PyModule::new_bound(py, "empty").unwrap(); + /// # set_answer(&ob).unwrap(); /// # }); /// ``` fn setattr(&self, attr_name: N, value: V) -> PyResult<()> @@ -1228,7 +1228,7 @@ pub trait PyAnyMethods<'py> { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| -> PyResult<()> { - /// let builtins = PyModule::import(py, "builtins")?; + /// let builtins = PyModule::import_bound(py, "builtins")?; /// let print = builtins.getattr("print")?; /// assert!(print.is_callable()); /// Ok(()) @@ -1265,12 +1265,12 @@ pub trait PyAnyMethods<'py> { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { - /// let module = PyModule::from_code(py, CODE, "", "")?; + /// let module = PyModule::from_code_bound(py, CODE, "", "")?; /// let fun = module.getattr("function")?; /// let args = ("hello",); /// let kwargs = PyDict::new_bound(py); /// kwargs.set_item("cruel", "world")?; - /// let result = fun.call(args, Some(kwargs.as_gil_ref()))?; + /// let result = fun.call(args, Some(&kwargs))?; /// assert_eq!(result.extract::<&str>()?, "called with args and kwargs"); /// Ok(()) /// }) @@ -1293,7 +1293,7 @@ pub trait PyAnyMethods<'py> { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| -> PyResult<()> { - /// let module = PyModule::import(py, "builtins")?; + /// let module = PyModule::import_bound(py, "builtins")?; /// let help = module.getattr("help")?; /// help.call0()?; /// Ok(()) @@ -1322,7 +1322,7 @@ pub trait PyAnyMethods<'py> { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { - /// let module = PyModule::from_code(py, CODE, "", "")?; + /// let module = PyModule::from_code_bound(py, CODE, "", "")?; /// let fun = module.getattr("function")?; /// let args = ("hello",); /// let result = fun.call1(args)?; @@ -1357,12 +1357,12 @@ pub trait PyAnyMethods<'py> { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { - /// let module = PyModule::from_code(py, CODE, "", "")?; + /// let module = PyModule::from_code_bound(py, CODE, "", "")?; /// let instance = module.getattr("a")?; /// let args = ("hello",); /// let kwargs = PyDict::new_bound(py); /// kwargs.set_item("cruel", "world")?; - /// let result = instance.call_method("method", args, Some(kwargs.as_gil_ref()))?; + /// let result = instance.call_method("method", args, Some(&kwargs))?; /// assert_eq!(result.extract::<&str>()?, "called with args and kwargs"); /// Ok(()) /// }) @@ -1401,7 +1401,7 @@ pub trait PyAnyMethods<'py> { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { - /// let module = PyModule::from_code(py, CODE, "", "")?; + /// let module = PyModule::from_code_bound(py, CODE, "", "")?; /// let instance = module.getattr("a")?; /// let result = instance.call_method0("method")?; /// assert_eq!(result.extract::<&str>()?, "called with no arguments"); @@ -1436,7 +1436,7 @@ pub trait PyAnyMethods<'py> { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { - /// let module = PyModule::from_code(py, CODE, "", "")?; + /// let module = PyModule::from_code_bound(py, CODE, "", "")?; /// let instance = module.getattr("a")?; /// let args = ("hello",); /// let result = instance.call_method1("method", args)?; @@ -2326,13 +2326,13 @@ mod tests { use crate::{ basic::CompareOp, types::{any::PyAnyMethods, IntoPyDict, PyAny, PyBool, PyList, PyLong, PyModule}, - PyNativeType, PyTypeInfo, Python, ToPyObject, + Bound, PyNativeType, PyTypeInfo, Python, ToPyObject, }; #[test] fn test_lookup_special() { Python::with_gil(|py| { - let module = PyModule::from_code( + let module = PyModule::from_code_bound( py, r#" class CustomCallable: @@ -2371,13 +2371,8 @@ class NonHeapNonDescriptorInt: .unwrap(); let int = crate::intern!(py, "__int__"); - let eval_int = |obj: &PyAny| { - obj.as_borrowed() - .lookup_special(int)? - .unwrap() - .call0()? - .extract::() - }; + let eval_int = + |obj: Bound<'_, PyAny>| obj.lookup_special(int)?.unwrap().call0()?.extract::(); let simple = module.getattr("SimpleInt").unwrap().call0().unwrap(); assert_eq!(eval_int(simple).unwrap(), 1); @@ -2430,7 +2425,7 @@ class NonHeapNonDescriptorInt: #[test] fn test_call_method0() { Python::with_gil(|py| { - let module = PyModule::from_code( + let module = PyModule::from_code_bound( py, r#" class SimpleClass: diff --git a/src/types/capsule.rs b/src/types/capsule.rs index 342ee9b44bc..aa1a910d21b 100644 --- a/src/types/capsule.rs +++ b/src/types/capsule.rs @@ -31,7 +31,7 @@ use std::os::raw::{c_char, c_int, c_void}; /// /// let capsule = PyCapsule::new_bound(py, foo, Some(name.clone()))?; /// -/// let module = PyModule::import(py, "builtins")?; +/// let module = PyModule::import_bound(py, "builtins")?; /// module.add("capsule", capsule)?; /// /// let cap: &Foo = unsafe { PyCapsule::import(py, name.as_ref())? }; @@ -441,11 +441,13 @@ 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; use crate::prelude::PyModule; use crate::types::capsule::PyCapsuleMethods; + use crate::types::module::PyModuleMethods; use crate::{types::PyCapsule, Py, PyResult, Python}; use std::ffi::CString; use std::sync::mpsc::{channel, Sender}; @@ -528,7 +530,7 @@ mod tests { let capsule = PyCapsule::new_bound(py, foo, Some(name.clone()))?; - let module = PyModule::import(py, "builtins")?; + let module = PyModule::import_bound(py, "builtins")?; module.add("capsule", capsule)?; // check error when wrong named passed for capsule. diff --git a/src/types/module.rs b/src/types/module.rs index 137b1b37e3a..245d38d9e08 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -1,11 +1,12 @@ use crate::callback::IntoPyCallbackOutput; use crate::err::{PyErr, PyResult}; use crate::ffi_ptr_ext::FfiPtrExt; +use crate::py_result_ext::PyResultExt; use crate::pyclass::PyClass; use crate::types::{ any::PyAnyMethods, list::PyListMethods, PyAny, PyCFunction, PyDict, PyList, PyString, }; -use crate::{exceptions, ffi, Bound, FromPyObject, IntoPy, Py, PyNativeType, PyObject, Python}; +use crate::{exceptions, ffi, Bound, IntoPy, Py, PyNativeType, PyObject, Python}; use std::ffi::CString; use std::str; @@ -22,6 +23,19 @@ 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_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" + ) + )] + 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 @@ -31,22 +45,39 @@ impl PyModule { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| -> PyResult<()> { - /// let module = PyModule::new(py, "my_module")?; + /// let module = PyModule::new_bound(py, "my_module")?; /// - /// assert_eq!(module.name()?, "my_module"); + /// assert_eq!(module.name()?.to_cow()?, "my_module"); /// Ok(()) /// })?; /// # Ok(())} /// ``` - pub fn new<'p>(py: Python<'p>, name: &str) -> PyResult<&'p PyModule> { + pub fn new_bound<'py>(py: Python<'py>, name: &str) -> PyResult> { // Could use PyModule_NewObject, but it doesn't exist on PyPy. let name = CString::new(name)?; - #[allow(deprecated)] unsafe { - py.from_owned_ptr_or_err(ffi::PyModule_New(name.as_ptr())) + ffi::PyModule_New(name.as_ptr()) + .assume_owned_or_err(py) + .downcast_into_unchecked() } } + /// 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" + ) + )] + 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 @@ -56,7 +87,7 @@ impl PyModule { /// use pyo3::prelude::*; /// /// Python::with_gil(|py| { - /// let module = PyModule::import(py, "antigravity").expect("No flying for you."); + /// let module = PyModule::import_bound(py, "antigravity").expect("No flying for you."); /// }); /// # } /// ``` @@ -65,17 +96,36 @@ impl PyModule { /// ```python /// import antigravity /// ``` - pub fn import(py: Python<'_>, name: N) -> PyResult<&PyModule> + pub fn import_bound(py: Python<'_>, name: N) -> PyResult> where N: IntoPy>, { let name: Py = name.into_py(py); - #[allow(deprecated)] unsafe { - py.from_owned_ptr_or_err(ffi::PyImport_Import(name.as_ptr())) + ffi::PyImport_Import(name.as_ptr()) + .assume_owned_or_err(py) + .downcast_into_unchecked() } } + /// 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" + ) + )] + 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`. @@ -105,7 +155,7 @@ impl PyModule { /// let code = include_str!("../../assets/script.py"); /// /// Python::with_gil(|py| -> PyResult<()> { - /// PyModule::from_code(py, code, "example.py", "example")?; + /// PyModule::from_code_bound(py, code, "example.py", "example")?; /// Ok(()) /// })?; /// # Ok(()) @@ -124,36 +174,29 @@ impl PyModule { /// let code = std::fs::read_to_string("assets/script.py")?; /// /// Python::with_gil(|py| -> PyResult<()> { - /// PyModule::from_code(py, &code, "example.py", "example")?; + /// PyModule::from_code_bound(py, &code, "example.py", "example")?; /// Ok(()) /// })?; /// Ok(()) /// # } /// ``` - pub fn from_code<'p>( - py: Python<'p>, + pub fn from_code_bound<'py>( + py: Python<'py>, code: &str, file_name: &str, module_name: &str, - ) -> PyResult<&'p PyModule> { + ) -> PyResult> { let data = CString::new(code)?; let filename = CString::new(file_name)?; let module = CString::new(module_name)?; unsafe { - let cptr = ffi::Py_CompileString(data.as_ptr(), filename.as_ptr(), ffi::Py_file_input); - if cptr.is_null() { - return Err(PyErr::fetch(py)); - } - - let mptr = ffi::PyImport_ExecCodeModuleEx(module.as_ptr(), cptr, filename.as_ptr()); - ffi::Py_DECREF(cptr); - if mptr.is_null() { - return Err(PyErr::fetch(py)); - } + let code = ffi::Py_CompileString(data.as_ptr(), filename.as_ptr(), ffi::Py_file_input) + .assume_owned_or_err(py)?; - #[allow(deprecated)] - <&PyModule as FromPyObject>::extract(py.from_owned_ptr_or_err(mptr)?) + ffi::PyImport_ExecCodeModuleEx(module.as_ptr(), code.as_ptr(), filename.as_ptr()) + .assume_owned_or_err(py) + .downcast_into() } } @@ -293,10 +336,10 @@ impl PyModule { /// /// #[pymodule] /// fn my_module(py: Python<'_>, module: &PyModule) -> PyResult<()> { - /// let submodule = PyModule::new(py, "submodule")?; + /// let submodule = PyModule::new_bound(py, "submodule")?; /// submodule.add("super_useful_constant", "important")?; /// - /// module.add_submodule(submodule)?; + /// module.add_submodule(submodule.as_gil_ref())?; /// Ok(()) /// } /// ``` @@ -487,10 +530,10 @@ pub trait PyModuleMethods<'py> { /// /// #[pymodule] /// fn my_module(py: Python<'_>, module: &PyModule) -> PyResult<()> { - /// let submodule = PyModule::new(py, "submodule")?; + /// let submodule = PyModule::new_bound(py, "submodule")?; /// submodule.add("super_useful_constant", "important")?; /// - /// module.add_submodule(submodule)?; + /// module.add_submodule(submodule.as_gil_ref())?; /// Ok(()) /// } /// ``` @@ -580,8 +623,6 @@ impl<'py> PyModuleMethods<'py> for Bound<'py, PyModule> { fn name(&self) -> PyResult> { #[cfg(not(PyPy))] { - use crate::py_result_ext::PyResultExt; - unsafe { ffi::PyModule_GetNameObject(self.as_ptr()) .assume_owned_or_err(self.py()) @@ -601,8 +642,6 @@ impl<'py> PyModuleMethods<'py> for Bound<'py, PyModule> { #[cfg(not(PyPy))] fn filename(&self) -> PyResult> { - use crate::py_result_ext::PyResultExt; - unsafe { ffi::PyModule_GetFilenameObject(self.as_ptr()) .assume_owned_or_err(self.py()) @@ -676,14 +715,21 @@ fn __name__(py: Python<'_>) -> &Bound<'_, PyString> { } #[cfg(test)] +#[cfg_attr(not(feature = "gil-refs"), allow(deprecated))] mod tests { - use crate::{types::PyModule, Python}; + use crate::{ + types::{module::PyModuleMethods, string::PyStringMethods, PyModule}, + Python, + }; #[test] fn module_import_and_name() { Python::with_gil(|py| { - let builtins = PyModule::import(py, "builtins").unwrap(); - assert_eq!(builtins.name().unwrap(), "builtins"); + let builtins = PyModule::import_bound(py, "builtins").unwrap(); + assert_eq!( + builtins.name().unwrap().to_cow().unwrap().as_ref(), + "builtins" + ); }) } @@ -691,8 +737,13 @@ mod tests { #[cfg(not(PyPy))] fn module_filename() { Python::with_gil(|py| { - let site = PyModule::import(py, "site").unwrap(); - assert!(site.filename().unwrap().ends_with("site.py")); + let site = PyModule::import_bound(py, "site").unwrap(); + assert!(site + .filename() + .unwrap() + .to_cow() + .unwrap() + .ends_with("site.py")); }) } } diff --git a/src/types/traceback.rs b/src/types/traceback.rs index 24b935e24c2..b8782463367 100644 --- a/src/types/traceback.rs +++ b/src/types/traceback.rs @@ -1,11 +1,8 @@ use crate::err::{error_on_minusone, PyResult}; -use crate::types::PyString; +use crate::types::{any::PyAnyMethods, string::PyStringMethods, PyString}; use crate::{ffi, Bound}; use crate::{PyAny, PyNativeType}; -use super::any::PyAnyMethods; -use super::string::PyStringMethods; - /// Represents a Python traceback. #[repr(transparent)] pub struct PyTraceback(PyAny); diff --git a/tests/test_class_basics.rs b/tests/test_class_basics.rs index fe80625e86b..21ccc4a6bb6 100644 --- a/tests/test_class_basics.rs +++ b/tests/test_class_basics.rs @@ -154,7 +154,7 @@ struct EmptyClassInModule {} #[ignore] fn empty_class_in_module() { Python::with_gil(|py| { - let module = PyModule::new(py, "test_module.nested").unwrap(); + let module = PyModule::new_bound(py, "test_module.nested").unwrap(); module.add_class::().unwrap(); let ty = module.getattr("EmptyClassInModule").unwrap(); diff --git a/tests/test_class_new.rs b/tests/test_class_new.rs index 9037590f678..9e16631bb83 100644 --- a/tests/test_class_new.rs +++ b/tests/test_class_new.rs @@ -169,10 +169,7 @@ c = Class() assert c.from_rust is False "# ); - let globals = PyModule::import(py, "__main__") - .unwrap() - .dict() - .as_borrowed(); + let globals = PyModule::import_bound(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_coroutine.rs b/tests/test_coroutine.rs index b3b8ba7be1d..e04aafda24d 100644 --- a/tests/test_coroutine.rs +++ b/tests/test_coroutine.rs @@ -262,7 +262,7 @@ fn test_async_method_receiver() { Python::with_gil(|gil| { let test = r#" import asyncio - + obj = Counter() coro1 = obj.get() coro2 = obj.get() diff --git a/tests/test_inheritance.rs b/tests/test_inheritance.rs index 9b29fefdbda..7a465f1a995 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(py, "test_module").unwrap(); + let m = PyModule::new_bound(py, "test_module").unwrap(); let err = m.add_class::().unwrap_err(); assert_eq!( diff --git a/tests/test_module.rs b/tests/test_module.rs index 9a59770e047..9d14f243d50 100644 --- a/tests/test_module.rs +++ b/tests/test_module.rs @@ -135,9 +135,9 @@ fn test_module_renaming() { } #[test] -fn test_module_from_code() { +fn test_module_from_code_bound() { Python::with_gil(|py| { - let adder_mod = PyModule::from_code( + let adder_mod = PyModule::from_code_bound( py, "def add(a,b):\n\treturn a+b", "adder_mod.py", @@ -239,8 +239,8 @@ fn subfunction() -> String { "Subfunction".to_string() } -fn submodule(module: &PyModule) -> PyResult<()> { - module.add_function(wrap_pyfunction!(subfunction, module)?)?; +fn submodule(module: &Bound<'_, PyModule>) -> PyResult<()> { + module.add_function(&wrap_pyfunction!(subfunction, module.as_gil_ref())?.as_borrowed())?; Ok(()) } @@ -258,12 +258,12 @@ fn superfunction() -> String { #[pymodule] fn supermodule(py: Python<'_>, module: &PyModule) -> PyResult<()> { module.add_function(wrap_pyfunction!(superfunction, module)?)?; - let module_to_add = PyModule::new(py, "submodule")?; - submodule(module_to_add)?; - module.add_submodule(module_to_add)?; - let module_to_add = PyModule::new(py, "submodule_with_init_fn")?; - submodule_with_init_fn(py, module_to_add)?; - module.add_submodule(module_to_add)?; + let module_to_add = PyModule::new_bound(py, "submodule")?; + submodule(&module_to_add)?; + module.add_submodule(module_to_add.as_gil_ref())?; + let module_to_add = PyModule::new_bound(py, "submodule_with_init_fn")?; + submodule_with_init_fn(py, module_to_add.as_gil_ref())?; + module.add_submodule(module_to_add.as_gil_ref())?; Ok(()) } diff --git a/tests/test_proto_methods.rs b/tests/test_proto_methods.rs index fb813b6e52d..61b912431e8 100644 --- a/tests/test_proto_methods.rs +++ b/tests/test_proto_methods.rs @@ -697,10 +697,7 @@ if sys.platform == "win32" and sys.version_info >= (3, 8, 0): asyncio.run(main()) "#; - let globals = PyModule::import(py, "__main__") - .unwrap() - .dict() - .as_borrowed(); + let globals = PyModule::import_bound(py, "__main__").unwrap().dict(); globals.set_item("Once", once).unwrap(); py.run_bound(source, Some(&globals), None) .map_err(|e| e.display(py)) @@ -754,10 +751,7 @@ if sys.platform == "win32" and sys.version_info >= (3, 8, 0): asyncio.run(main()) "#; - let globals = PyModule::import(py, "__main__") - .unwrap() - .dict() - .as_borrowed(); + let globals = PyModule::import_bound(py, "__main__").unwrap().dict(); globals.set_item("Once", once).unwrap(); globals .set_item("AsyncIterator", py.get_type_bound::()) @@ -829,10 +823,7 @@ del c.counter assert c.counter.count == 1 "# ); - let globals = PyModule::import(py, "__main__") - .unwrap() - .dict() - .as_borrowed(); + let globals = PyModule::import_bound(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 9ed1e1cf546..6f1bfd908a7 100644 --- a/tests/test_various.rs +++ b/tests/test_various.rs @@ -133,8 +133,8 @@ impl PickleSupport { } } -fn add_module(py: Python<'_>, module: &PyModule) -> PyResult<()> { - py.import_bound("sys")? +fn add_module(module: Bound<'_, PyModule>) -> PyResult<()> { + PyModule::import_bound(module.py(), "sys")? .dict() .get_item("modules") .unwrap() @@ -147,9 +147,9 @@ fn add_module(py: Python<'_>, module: &PyModule) -> PyResult<()> { #[cfg_attr(all(Py_LIMITED_API, not(Py_3_10)), ignore)] fn test_pickle() { Python::with_gil(|py| { - let module = PyModule::new(py, "test_module").unwrap(); + let module = PyModule::new_bound(py, "test_module").unwrap(); module.add_class::().unwrap(); - add_module(py, module).unwrap(); + add_module(module).unwrap(); let inst = Py::new(py, PickleSupport {}).unwrap(); py_run!( py, From 4f8ee968812977c1ecc9909143ecea75b114870d Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Thu, 22 Feb 2024 23:38:42 +0100 Subject: [PATCH 028/936] fix `AsRef` and `Deref` impls on `Bound` (#3879) * fix `AsRef` and `Deref` of `Bound` to `Bound` * cleanup unnessesary `.as_any()` calls * remove trait bound on `AsRef` impl * add comment for `Deref` trait bound * rename marker trait --- guide/src/class/object.md | 4 ++-- pyo3-macros-backend/src/pyclass.rs | 2 ++ src/instance.rs | 12 +++++----- src/types/boolobject.rs | 4 ++-- src/types/mod.rs | 27 ++++++++++++++++++++++ tests/test_buffer.rs | 12 +++++----- tests/test_class_conversion.rs | 6 ++--- tests/test_field_cfg.rs | 10 ++------ tests/test_inheritance.rs | 2 +- tests/test_mapping.rs | 4 ++-- tests/test_proto_methods.rs | 37 +++++++++++------------------- 11 files changed, 65 insertions(+), 55 deletions(-) diff --git a/guide/src/class/object.md b/guide/src/class/object.md index 471889391e2..10867976df0 100644 --- a/guide/src/class/object.md +++ b/guide/src/class/object.md @@ -217,8 +217,8 @@ impl Number { # fn main() -> PyResult<()> { # Python::with_gil(|py| { -# let x = &Bound::new(py, Number(4))?.into_any(); -# let y = &Bound::new(py, Number(4))?.into_any(); +# let x = &Bound::new(py, Number(4))?; +# let y = &Bound::new(py, Number(4))?; # assert!(x.eq(y)?); # assert!(!x.ne(y)?); # Ok(()) diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 806df88eee5..ce39cb01196 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -365,6 +365,8 @@ fn impl_class( const _: () = { use #krate as _pyo3; + impl _pyo3::types::DerefToPyAny for #cls {} + #pytypeinfo_impl #py_class_impl diff --git a/src/instance.rs b/src/instance.rs index f35ab4bbb3a..ecf128112a2 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -4,7 +4,7 @@ use crate::pycell::{PyBorrowError, PyBorrowMutError, PyCell}; use crate::pyclass::boolean_struct::{False, True}; use crate::type_object::HasPyGilRef; use crate::types::{any::PyAnyMethods, string::PyStringMethods, typeobject::PyTypeMethods}; -use crate::types::{PyDict, PyString, PyTuple}; +use crate::types::{DerefToPyAny, PyDict, PyString, PyTuple}; use crate::{ ffi, AsPyPointer, DowncastError, FromPyObject, IntoPy, PyAny, PyClass, PyClassInitializer, PyRef, PyRefMut, PyTypeInfo, Python, ToPyObject, @@ -366,9 +366,12 @@ fn python_format( } } +// The trait bound is needed to avoid running into the auto-deref recursion +// limit (error[E0055]), because `Bound` would deref into itself. See: +// https://github.com/rust-lang/rust/issues/19509 impl<'py, T> Deref for Bound<'py, T> where - T: AsRef, + T: DerefToPyAny, { type Target = Bound<'py, PyAny>; @@ -378,10 +381,7 @@ where } } -impl<'py, T> AsRef> for Bound<'py, T> -where - T: AsRef, -{ +impl<'py, T> AsRef> for Bound<'py, T> { #[inline] fn as_ref(&self) -> &Bound<'py, PyAny> { self.as_any() diff --git a/src/types/boolobject.rs b/src/types/boolobject.rs index 01bc14b4431..906a967069a 100644 --- a/src/types/boolobject.rs +++ b/src/types/boolobject.rs @@ -168,7 +168,7 @@ mod tests { Python::with_gil(|py| { assert!(PyBool::new_bound(py, true).is_true()); let t = PyBool::new_bound(py, true); - assert!(t.as_any().extract::().unwrap()); + assert!(t.extract::().unwrap()); assert!(true.to_object(py).is(&*PyBool::new_bound(py, true))); }); } @@ -178,7 +178,7 @@ mod tests { Python::with_gil(|py| { assert!(!PyBool::new_bound(py, false).is_true()); let t = PyBool::new_bound(py, false); - assert!(!t.as_any().extract::().unwrap()); + assert!(!t.extract::().unwrap()); assert!(false.to_object(py).is(&*PyBool::new_bound(py, false))); }); } diff --git a/src/types/mod.rs b/src/types/mod.rs index 4ebf050b4b8..217e240129f 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -86,6 +86,31 @@ pub mod iter { pub use super::tuple::{BorrowedTupleIterator, BoundTupleIterator, PyTupleIterator}; } +/// Python objects that have a base type. +/// +/// This marks types that can be upcast into a [`PyAny`] and used in its place. +/// This essentially includes every Python object except [`PyAny`] itself. +/// +/// This is used to provide the [`Deref>`](std::ops::Deref) +/// implementations for [`Bound<'_, T>`](crate::Bound). +/// +/// Users should not need to implement this trait directly. It's implementation +/// is provided by the [`#[pyclass]`](macro@crate::pyclass) attribute. +/// +/// ## Note +/// This is needed because the compiler currently tries to figure out all the +/// types in a deref-chain before starting to look for applicable method calls. +/// So we need to prevent [`Bound<'_, PyAny`](crate::Bound) dereferencing to +/// itself in order to avoid running into the recursion limit. This trait is +/// used to exclude this from our blanket implementation. See [this Rust +/// issue][1] for more details. If the compiler limitation gets resolved, this +/// trait will be removed. +/// +/// [1]: https://github.com/rust-lang/rust/issues/19509 +pub trait DerefToPyAny { + // Empty. +} + // Implementations core to all native types #[doc(hidden)] #[macro_export] @@ -183,6 +208,8 @@ macro_rules! pyobject_native_type_named ( unsafe{&*(ob as *const $name as *const $crate::PyAny)} } } + + impl $crate::types::DerefToPyAny for $name {} }; ); diff --git a/tests/test_buffer.rs b/tests/test_buffer.rs index 72f94b3bcc8..0b3da881884 100644 --- a/tests/test_buffer.rs +++ b/tests/test_buffer.rs @@ -96,11 +96,11 @@ fn test_get_buffer_errors() { ) .unwrap(); - assert!(PyBuffer::::get_bound(instance.bind(py).as_any()).is_ok()); + assert!(PyBuffer::::get_bound(instance.bind(py)).is_ok()); instance.borrow_mut(py).error = Some(TestGetBufferError::NullShape); assert_eq!( - PyBuffer::::get_bound(instance.bind(py).as_any()) + PyBuffer::::get_bound(instance.bind(py)) .unwrap_err() .to_string(), "BufferError: shape is null" @@ -108,7 +108,7 @@ fn test_get_buffer_errors() { instance.borrow_mut(py).error = Some(TestGetBufferError::NullStrides); assert_eq!( - PyBuffer::::get_bound(instance.bind(py).as_any()) + PyBuffer::::get_bound(instance.bind(py)) .unwrap_err() .to_string(), "BufferError: strides is null" @@ -116,7 +116,7 @@ fn test_get_buffer_errors() { instance.borrow_mut(py).error = Some(TestGetBufferError::IncorrectItemSize); assert_eq!( - PyBuffer::::get_bound(instance.bind(py).as_any()) + PyBuffer::::get_bound(instance.bind(py)) .unwrap_err() .to_string(), "BufferError: buffer contents are not compatible with u32" @@ -124,7 +124,7 @@ fn test_get_buffer_errors() { instance.borrow_mut(py).error = Some(TestGetBufferError::IncorrectFormat); assert_eq!( - PyBuffer::::get_bound(instance.bind(py).as_any()) + PyBuffer::::get_bound(instance.bind(py)) .unwrap_err() .to_string(), "BufferError: buffer contents are not compatible with u32" @@ -132,7 +132,7 @@ fn test_get_buffer_errors() { instance.borrow_mut(py).error = Some(TestGetBufferError::IncorrectAlignment); assert_eq!( - PyBuffer::::get_bound(instance.bind(py).as_any()) + PyBuffer::::get_bound(instance.bind(py)) .unwrap_err() .to_string(), "BufferError: buffer contents are insufficiently aligned for u32" diff --git a/tests/test_class_conversion.rs b/tests/test_class_conversion.rs index ccb974097e1..a09195278b0 100644 --- a/tests/test_class_conversion.rs +++ b/tests/test_class_conversion.rs @@ -146,13 +146,11 @@ fn test_pyref_as_base() { #[test] fn test_pycell_deref() { Python::with_gil(|py| { - let cell = Bound::new(py, (SubClass {}, BaseClass { value: 120 })).unwrap(); + let obj = Bound::new(py, (SubClass {}, BaseClass { value: 120 })).unwrap(); // Should be able to deref as PyAny - // FIXME: This deref does _not_ work assert_eq!( - cell.as_any() - .call_method0("foo") + obj.call_method0("foo") .and_then(|e| e.extract::<&str>()) .unwrap(), "SubClass" diff --git a/tests/test_field_cfg.rs b/tests/test_field_cfg.rs index be400f2d749..c5fc4958dbf 100644 --- a/tests/test_field_cfg.rs +++ b/tests/test_field_cfg.rs @@ -22,14 +22,8 @@ fn test_cfg() { Python::with_gil(|py| { let cfg = CfgClass { b: 3 }; let py_cfg = Py::new(py, cfg).unwrap(); - assert!(py_cfg.bind(py).as_any().getattr("a").is_err()); - let b: u32 = py_cfg - .bind(py) - .as_any() - .getattr("b") - .unwrap() - .extract() - .unwrap(); + assert!(py_cfg.bind(py).getattr("a").is_err()); + let b: u32 = py_cfg.bind(py).getattr("b").unwrap().extract().unwrap(); assert_eq!(b, 3); }); } diff --git a/tests/test_inheritance.rs b/tests/test_inheritance.rs index 7a465f1a995..1209713e487 100644 --- a/tests/test_inheritance.rs +++ b/tests/test_inheritance.rs @@ -247,7 +247,7 @@ mod inheriting_native_type { let item = &py.eval_bound("object()", None, None).unwrap(); assert_eq!(item.get_refcnt(), 1); - dict_sub.bind(py).as_any().set_item("foo", item).unwrap(); + dict_sub.bind(py).set_item("foo", item).unwrap(); assert_eq!(item.get_refcnt(), 2); drop(dict_sub); diff --git a/tests/test_mapping.rs b/tests/test_mapping.rs index 06928e74183..4e24db97793 100644 --- a/tests/test_mapping.rs +++ b/tests/test_mapping.rs @@ -123,7 +123,7 @@ fn mapping_is_not_sequence() { PyMapping::register::(py).unwrap(); - assert!(m.bind(py).as_any().downcast::().is_ok()); - assert!(m.bind(py).as_any().downcast::().is_err()); + assert!(m.bind(py).downcast::().is_ok()); + assert!(m.bind(py).downcast::().is_err()); }); } diff --git a/tests/test_proto_methods.rs b/tests/test_proto_methods.rs index 61b912431e8..a1cfde2badf 100644 --- a/tests/test_proto_methods.rs +++ b/tests/test_proto_methods.rs @@ -81,7 +81,6 @@ fn test_getattr() { let example_py = make_example(py); assert_eq!( example_py - .as_any() .getattr("value") .unwrap() .extract::() @@ -90,7 +89,6 @@ fn test_getattr() { ); assert_eq!( example_py - .as_any() .getattr("special_custom_attr") .unwrap() .extract::() @@ -98,7 +96,6 @@ fn test_getattr() { 20, ); assert!(example_py - .as_any() .getattr("other_attr") .unwrap_err() .is_instance_of::(py)); @@ -109,13 +106,9 @@ fn test_getattr() { fn test_setattr() { Python::with_gil(|py| { let example_py = make_example(py); - example_py - .as_any() - .setattr("special_custom_attr", 15) - .unwrap(); + example_py.setattr("special_custom_attr", 15).unwrap(); assert_eq!( example_py - .as_any() .getattr("special_custom_attr") .unwrap() .extract::() @@ -129,12 +122,8 @@ fn test_setattr() { fn test_delattr() { Python::with_gil(|py| { let example_py = make_example(py); - example_py.as_any().delattr("special_custom_attr").unwrap(); - assert!(example_py - .as_any() - .getattr("special_custom_attr") - .unwrap() - .is_none()); + example_py.delattr("special_custom_attr").unwrap(); + assert!(example_py.getattr("special_custom_attr").unwrap().is_none()); }) } @@ -142,7 +131,7 @@ fn test_delattr() { fn test_str() { Python::with_gil(|py| { let example_py = make_example(py); - assert_eq!(example_py.as_any().str().unwrap().to_cow().unwrap(), "5"); + assert_eq!(example_py.str().unwrap().to_cow().unwrap(), "5"); }) } @@ -151,7 +140,7 @@ fn test_repr() { Python::with_gil(|py| { let example_py = make_example(py); assert_eq!( - example_py.as_any().repr().unwrap().to_cow().unwrap(), + example_py.repr().unwrap().to_cow().unwrap(), "ExampleClass(value=5)" ); }) @@ -161,7 +150,7 @@ fn test_repr() { fn test_hash() { Python::with_gil(|py| { let example_py = make_example(py); - assert_eq!(example_py.as_any().hash().unwrap(), 5); + assert_eq!(example_py.hash().unwrap(), 5); }) } @@ -169,9 +158,9 @@ fn test_hash() { fn test_bool() { Python::with_gil(|py| { let example_py = make_example(py); - assert!(example_py.as_any().is_truthy().unwrap()); + assert!(example_py.is_truthy().unwrap()); example_py.borrow_mut().value = 0; - assert!(!example_py.as_any().is_truthy().unwrap()); + assert!(!example_py.is_truthy().unwrap()); }) } @@ -231,7 +220,7 @@ fn mapping() { ) .unwrap(); - let mapping: &Bound<'_, PyMapping> = inst.bind(py).as_any().downcast().unwrap(); + let mapping: &Bound<'_, PyMapping> = inst.bind(py).downcast().unwrap(); py_assert!(py, inst, "len(inst) == 0"); @@ -333,7 +322,7 @@ fn sequence() { let inst = Py::new(py, Sequence { values: vec![] }).unwrap(); - let sequence: &Bound<'_, PySequence> = inst.bind(py).as_any().downcast().unwrap(); + let sequence: &Bound<'_, PySequence> = inst.bind(py).downcast().unwrap(); py_assert!(py, inst, "len(inst) == 0"); @@ -360,16 +349,16 @@ fn sequence() { // indices. assert!(sequence.len().is_err()); // however regular python len() works thanks to mp_len slot - assert_eq!(inst.bind(py).as_any().len().unwrap(), 0); + assert_eq!(inst.bind(py).len().unwrap(), 0); py_run!(py, inst, "inst.append(0)"); sequence.set_item(0, 5).unwrap(); - assert_eq!(inst.bind(py).as_any().len().unwrap(), 1); + assert_eq!(inst.bind(py).len().unwrap(), 1); assert_eq!(sequence.get_item(0).unwrap().extract::().unwrap(), 5); sequence.del_item(0).unwrap(); - assert_eq!(inst.bind(py).as_any().len().unwrap(), 0); + assert_eq!(inst.bind(py).len().unwrap(), 0); }); } From 8bd82da93923c42acac860bc182398c8ad9c617a Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Thu, 22 Feb 2024 23:52:25 +0100 Subject: [PATCH 029/936] add missing deprecation for `PyDict::from_sequence` (#3884) --- src/types/dict.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/types/dict.rs b/src/types/dict.rs index 39a0c45e35e..2d215c1f8b9 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -75,6 +75,13 @@ impl PyDict { } /// Deprecated form of [`from_sequence_bound`][PyDict::from_sequence_bound]. + #[cfg_attr( + all(not(PyPy), 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" + ) + )] #[inline] #[cfg(not(PyPy))] pub fn from_sequence(seq: &PyAny) -> PyResult<&PyDict> { From 22a23ffb3150037fc62a5b74e3a0a8cec9cbdf6f Mon Sep 17 00:00:00 2001 From: Lily Foote Date: Thu, 22 Feb 2024 23:06:55 +0000 Subject: [PATCH 030/936] Tidy some usage of `py.from_borrowed_ptr` and `py.from_borrowed_ptr_or_opt` (#3877) * Tidy some usage of py.from_borrowed_ptr * Add BoundRef::ref_from_ptr_or_opt --- pyo3-macros-backend/src/pymethod.rs | 6 +++--- src/ffi/tests.rs | 14 ++++++-------- src/impl_/pyclass.rs | 8 +++++--- src/impl_/pymethods.rs | 7 +++++++ src/instance.rs | 12 ++++++++++++ 5 files changed, 33 insertions(+), 14 deletions(-) diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index d45d2e12f26..74072f2a745 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -572,12 +572,12 @@ pub fn impl_py_setter_def( _slf: *mut _pyo3::ffi::PyObject, _value: *mut _pyo3::ffi::PyObject, ) -> _pyo3::PyResult<::std::os::raw::c_int> { - let _value = py - .from_borrowed_ptr_or_opt(_value) + use ::std::convert::Into; + let _value = _pyo3::impl_::pymethods::BoundRef::ref_from_ptr_or_opt(py, &_value) .ok_or_else(|| { _pyo3::exceptions::PyAttributeError::new_err("can't delete attribute") })?; - let _val = _pyo3::FromPyObject::extract(_value)?; + let _val = _pyo3::FromPyObject::extract_bound(_value.into())?; #( #holders )* _pyo3::callback::convert(py, #setter_impl) } diff --git a/src/ffi/tests.rs b/src/ffi/tests.rs index d67bd8485d8..3532172c933 100644 --- a/src/ffi/tests.rs +++ b/src/ffi/tests.rs @@ -57,9 +57,9 @@ fn test_date_fromtimestamp() { #[test] fn test_utc_timezone() { Python::with_gil(|py| { - let utc_timezone: &PyAny = unsafe { + let utc_timezone: Bound<'_, PyAny> = unsafe { PyDateTime_IMPORT(); - py.from_borrowed_ptr(PyDateTime_TimeZone_UTC()) + Bound::from_borrowed_ptr(py, PyDateTime_TimeZone_UTC()) }; let locals = PyDict::new_bound(py); locals.set_item("utc_timezone", utc_timezone).unwrap(); @@ -254,35 +254,33 @@ fn test_get_tzinfo() { crate::Python::with_gil(|py| { use crate::types::{PyDateTime, PyTime}; - use crate::PyAny; let utc = &timezone_utc_bound(py); let dt = PyDateTime::new_bound(py, 2018, 1, 1, 0, 0, 0, 0, Some(utc)).unwrap(); assert!( - unsafe { py.from_borrowed_ptr::(PyDateTime_DATE_GET_TZINFO(dt.as_ptr())) } + unsafe { Bound::from_borrowed_ptr(py, PyDateTime_DATE_GET_TZINFO(dt.as_ptr())) } .is(utc) ); let dt = PyDateTime::new_bound(py, 2018, 1, 1, 0, 0, 0, 0, None).unwrap(); assert!( - unsafe { py.from_borrowed_ptr::(PyDateTime_DATE_GET_TZINFO(dt.as_ptr())) } + unsafe { Bound::from_borrowed_ptr(py, PyDateTime_DATE_GET_TZINFO(dt.as_ptr())) } .is_none() ); let t = PyTime::new_bound(py, 0, 0, 0, 0, Some(utc)).unwrap(); assert!( - unsafe { py.from_borrowed_ptr::(PyDateTime_TIME_GET_TZINFO(t.as_ptr())) } - .is(utc) + unsafe { Bound::from_borrowed_ptr(py, PyDateTime_TIME_GET_TZINFO(t.as_ptr())) }.is(utc) ); let t = PyTime::new_bound(py, 0, 0, 0, 0, None).unwrap(); assert!( - unsafe { py.from_borrowed_ptr::(PyDateTime_TIME_GET_TZINFO(t.as_ptr())) } + unsafe { Bound::from_borrowed_ptr(py, PyDateTime_TIME_GET_TZINFO(t.as_ptr())) } .is_none() ); }) diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index 2bd39dda1a0..e2204fabb02 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -6,8 +6,10 @@ use crate::{ internal_tricks::extract_c_string, pycell::PyCellLayout, pyclass_init::PyObjectInit, + types::any::PyAnyMethods, types::PyBool, - Py, PyAny, PyCell, PyClass, PyErr, PyMethodDefType, PyNativeType, PyResult, PyTypeInfo, Python, + Borrowed, Py, PyAny, PyCell, PyClass, PyErr, PyMethodDefType, PyNativeType, PyResult, + PyTypeInfo, Python, }; use std::{ borrow::Cow, @@ -811,8 +813,8 @@ slot_fragment_trait! { other: *mut ffi::PyObject, ) -> PyResult<*mut ffi::PyObject> { // By default `__ne__` will try `__eq__` and invert the result - let slf: &PyAny = py.from_borrowed_ptr(slf); - let other: &PyAny = py.from_borrowed_ptr(other); + 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()) } } diff --git a/src/impl_/pymethods.rs b/src/impl_/pymethods.rs index eef3b569d08..196766d0034 100644 --- a/src/impl_/pymethods.rs +++ b/src/impl_/pymethods.rs @@ -483,6 +483,13 @@ impl<'a, 'py> BoundRef<'a, 'py, PyAny> { BoundRef(Bound::ref_from_ptr(py, ptr)) } + pub unsafe fn ref_from_ptr_or_opt( + py: Python<'py>, + ptr: &'a *mut ffi::PyObject, + ) -> Option { + Bound::ref_from_ptr_or_opt(py, ptr).as_ref().map(BoundRef) + } + pub unsafe fn downcast_unchecked(self) -> BoundRef<'a, 'py, T> { BoundRef(self.0.downcast_unchecked::()) } diff --git a/src/instance.rs b/src/instance.rs index ecf128112a2..307b341fac7 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -191,6 +191,18 @@ impl<'py> Bound<'py, PyAny> { ) -> &'a Self { &*(ptr as *const *mut ffi::PyObject).cast::>() } + + /// Variant of the above which returns `None` for null pointers. + /// + /// # Safety + /// - `ptr` must be a valid pointer to a Python object for the lifetime `'a, or null. + #[inline] + pub(crate) unsafe fn ref_from_ptr_or_opt<'a>( + _py: Python<'py>, + ptr: &'a *mut ffi::PyObject, + ) -> &'a Option { + &*(ptr as *const *mut ffi::PyObject).cast::>>() + } } impl<'py, T> Bound<'py, T> From 5ca810236dcd3dac183d60b33a7115b29226bf65 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 23 Feb 2024 00:51:50 +0000 Subject: [PATCH 031/936] ci: rework GitHub caching strategy (#3886) * ci: rework GitHub caching strategy * clean up some more redundant imports flagged by nightly --- .github/workflows/benches.yml | 11 +++---- .github/workflows/build.yml | 3 +- .github/workflows/cache-cleanup.yml | 29 ++++++++++++++++++ .github/workflows/ci.yml | 46 ++++++++++++----------------- src/conversions/chrono.rs | 2 +- src/conversions/chrono_tz.rs | 2 -- src/conversions/num_bigint.rs | 5 +--- src/conversions/num_complex.rs | 2 +- 8 files changed, 56 insertions(+), 44 deletions(-) create mode 100644 .github/workflows/cache-cleanup.yml diff --git a/.github/workflows/benches.yml b/.github/workflows/benches.yml index 6c4d31e074a..572f77efd76 100644 --- a/.github/workflows/benches.yml +++ b/.github/workflows/benches.yml @@ -23,14 +23,11 @@ jobs: with: components: rust-src - - uses: actions/cache@v4 + - uses: Swatinem/rust-cache@v2 with: - path: | - ~/.cargo/registry - ~/.cargo/git - pyo3-benches/target - target - key: cargo-${{ runner.os }}-bench-${{ hashFiles('**/Cargo.toml') }} + workspaces: | + . + pyo3-benches continue-on-error: true - name: Install cargo-codspeed diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index af05eb20376..0f3e707bfba 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -47,8 +47,7 @@ jobs: - uses: Swatinem/rust-cache@v2 with: - key: cargo-${{ inputs.python-architecture }} - continue-on-error: true + save-if: ${{ github.event_name != 'merge_group' }} - if: inputs.os == 'ubuntu-latest' name: Prepare LD_LIBRARY_PATH (Ubuntu only) diff --git a/.github/workflows/cache-cleanup.yml b/.github/workflows/cache-cleanup.yml new file mode 100644 index 00000000000..2833ed093ee --- /dev/null +++ b/.github/workflows/cache-cleanup.yml @@ -0,0 +1,29 @@ +name: CI Cache Cleanup +on: + pull_request: + types: + - closed + +jobs: + cleanup: + runs-on: ubuntu-latest + steps: + - name: Cleanup + run: | + gh extension install actions/gh-actions-cache + + echo "Fetching list of cache key" + cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH -L 100 | cut -f 1 ) + + ## Setting this to not fail the workflow while deleting cache keys. + set +e + echo "Deleting caches..." + for cacheKey in $cacheKeysForPR + do + gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm + done + echo "Done" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + BRANCH: refs/pull/${{ github.event.pull_request.number }}/merge diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6331a79c645..a61da751394 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,7 +44,6 @@ jobs: check-msrv: needs: [fmt] runs-on: ubuntu-latest - if: github.ref != 'refs/heads/main' steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@master @@ -57,8 +56,7 @@ jobs: architecture: "x64" - uses: Swatinem/rust-cache@v2 with: - key: check-msrv-1.56.0 - continue-on-error: true + save-if: ${{ github.event_name != 'merge_group' }} - run: python -m pip install --upgrade pip && pip install nox - name: Prepare minimal package versions run: nox -s set-minimal-package-versions @@ -70,7 +68,6 @@ jobs: clippy: needs: [fmt] runs-on: ${{ matrix.platform.os }} - if: github.ref != 'refs/heads/main' strategy: # If one platform fails, allow the rest to keep testing if `CI-no-fail-fast` label is present fail-fast: ${{ !contains(github.event.pull_request.labels.*.name, 'CI-no-fail-fast') }} @@ -136,8 +133,7 @@ jobs: architecture: ${{ matrix.platform.python-architecture }} - uses: Swatinem/rust-cache@v2 with: - key: clippy-${{ matrix.platform.rust-target }}-${{ matrix.platform.os }}-${{ matrix.rust }} - continue-on-error: true + save-if: ${{ github.event_name != 'merge_group' }} - run: python -m pip install --upgrade pip && pip install nox - run: nox -s clippy-all env: @@ -199,7 +195,7 @@ jobs: } extra-features: "nightly multiple-pymethods" build-full: - if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || (github.event_name != 'pull_request' && github.ref != 'refs/heads/main') }} + if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || github.event_name != 'pull_request' }} name: python${{ matrix.python-version }}-${{ matrix.platform.python-architecture }} ${{ matrix.platform.os }} rust-${{ matrix.rust }} needs: [fmt] uses: ./.github/workflows/build.yml @@ -293,7 +289,7 @@ jobs: extra-features: "multiple-pymethods" valgrind: - if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || (github.event_name != 'pull_request' && github.ref != 'refs/heads/main') }} + if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || github.event_name != 'pull_request' }} needs: [fmt] runs-on: ubuntu-latest steps: @@ -301,8 +297,7 @@ jobs: - uses: actions/setup-python@v5 - uses: Swatinem/rust-cache@v2 with: - key: cargo-valgrind - continue-on-error: true + save-if: ${{ github.event_name != 'merge_group' }} - uses: dtolnay/rust-toolchain@stable - uses: taiki-e/install-action@valgrind - run: python -m pip install --upgrade pip && pip install nox @@ -313,7 +308,7 @@ jobs: TRYBUILD: overwrite careful: - if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || (github.event_name != 'pull_request' && github.ref != 'refs/heads/main') }} + if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || github.event_name != 'pull_request' }} needs: [fmt] runs-on: ubuntu-latest steps: @@ -321,8 +316,7 @@ jobs: - uses: actions/setup-python@v5 - uses: Swatinem/rust-cache@v2 with: - key: cargo-careful - continue-on-error: true + save-if: ${{ github.event_name != 'merge_group' }} - uses: dtolnay/rust-toolchain@nightly with: components: rust-src @@ -334,7 +328,7 @@ jobs: TRYBUILD: overwrite docsrs: - if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || (github.event_name != 'pull_request' && github.ref != 'refs/heads/main') }} + if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || github.event_name != 'pull_request' }} needs: [fmt] runs-on: ubuntu-latest steps: @@ -342,8 +336,7 @@ jobs: - uses: actions/setup-python@v5 - uses: Swatinem/rust-cache@v2 with: - key: cargo-careful - continue-on-error: true + save-if: ${{ github.event_name != 'merge_group' }} - uses: dtolnay/rust-toolchain@nightly with: components: rust-src @@ -368,8 +361,7 @@ jobs: - uses: Swatinem/rust-cache@v2 if: steps.should-skip.outputs.skip != 'true' with: - key: coverage-cargo-${{ matrix.os }} - continue-on-error: true + save-if: ${{ github.event_name != 'merge_group' }} - uses: dtolnay/rust-toolchain@stable if: steps.should-skip.outputs.skip != 'true' with: @@ -390,7 +382,7 @@ jobs: emscripten: name: emscripten - if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || (github.event_name != 'pull_request' && github.ref != 'refs/heads/main') }} + if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || github.event_name != 'pull_request' }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -416,7 +408,7 @@ jobs: key: ${{ hashFiles('emscripten/*') }} - ${{ hashFiles('noxfile.py') }} - ${{ steps.setup-python.outputs.python-path }} - uses: Swatinem/rust-cache@v2 with: - key: cargo-emscripten-wasm32 + save-if: ${{ github.event_name != 'merge_group' }} - name: Build if: steps.cache.outputs.cache-hit != 'true' run: nox -s build-emscripten @@ -425,14 +417,12 @@ jobs: test-debug: needs: [fmt] - if: github.ref != 'refs/heads/main' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: Swatinem/rust-cache@v2 with: - key: cargo-test-debug - continue-on-error: true + save-if: ${{ github.event_name != 'merge_group' }} - uses: dtolnay/rust-toolchain@stable with: components: rust-src @@ -470,25 +460,27 @@ jobs: test-version-limits: needs: [fmt] - if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || (github.event_name != 'pull_request' && github.ref != 'refs/heads/main') }} + if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || github.event_name != 'pull_request' }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: Swatinem/rust-cache@v2 - continue-on-error: true + with: + save-if: ${{ github.event_name != 'merge_group' }} - uses: dtolnay/rust-toolchain@stable - run: python3 -m pip install --upgrade pip && pip install nox - run: python3 -m nox -s test-version-limits check-feature-powerset: needs: [fmt] - if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || (github.event_name != 'pull_request' && github.ref != 'refs/heads/main') }} + if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || github.event_name != 'pull_request' }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 - uses: Swatinem/rust-cache@v2 - continue-on-error: true + with: + save-if: ${{ github.event_name != 'merge_group' }} - uses: dtolnay/rust-toolchain@stable with: components: rust-src diff --git a/src/conversions/chrono.rs b/src/conversions/chrono.rs index 0c96ef9c705..3aa6dbeb4ca 100644 --- a/src/conversions/chrono.rs +++ b/src/conversions/chrono.rs @@ -564,7 +564,7 @@ fn timezone_utc_bound(py: Python<'_>) -> Bound<'_, PyAny> { #[cfg(test)] mod tests { use super::*; - use crate::{types::PyTuple, Bound, Py}; + use crate::{types::PyTuple, Py}; use std::{cmp::Ordering, panic}; #[test] diff --git a/src/conversions/chrono_tz.rs b/src/conversions/chrono_tz.rs index 986bb8313d2..b4d0e7edf8c 100644 --- a/src/conversions/chrono_tz.rs +++ b/src/conversions/chrono_tz.rs @@ -69,8 +69,6 @@ impl FromPyObject<'_> for Tz { #[cfg(all(test, not(windows)))] // Troubles loading timezones on Windows mod tests { - use crate::{types::any::PyAnyMethods, Bound}; - use super::*; #[test] diff --git a/src/conversions/num_bigint.rs b/src/conversions/num_bigint.rs index 48a4ee60d73..e6f345fd200 100644 --- a/src/conversions/num_bigint.rs +++ b/src/conversions/num_bigint.rs @@ -262,10 +262,7 @@ fn int_n_bits(long: &Bound<'_, PyLong>) -> PyResult { #[cfg(test)] mod tests { use super::*; - use crate::{ - types::{PyDict, PyModule}, - Bound, - }; + use crate::types::{PyDict, PyModule}; use indoc::indoc; fn rust_fib() -> impl Iterator diff --git a/src/conversions/num_complex.rs b/src/conversions/num_complex.rs index c736a7baa77..369888af85f 100644 --- a/src/conversions/num_complex.rs +++ b/src/conversions/num_complex.rs @@ -199,7 +199,7 @@ complex_conversion!(f64); #[cfg(test)] mod tests { use super::*; - use crate::types::{any::PyAnyMethods, complex::PyComplexMethods, PyModule}; + use crate::types::{complex::PyComplexMethods, PyModule}; #[test] fn from_complex() { From 6a815875a0d010aea40b65d7348764998e33ec23 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Fri, 23 Feb 2024 07:31:51 +0100 Subject: [PATCH 032/936] port `PyErr::from_type` to `Bound` API (#3885) --- src/err/mod.rs | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/err/mod.rs b/src/err/mod.rs index 46f482a6879..ab39e8cd46f 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -182,6 +182,21 @@ 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" + ) + )] + 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 @@ -192,11 +207,11 @@ impl PyErr { /// If `ty` does not inherit from `BaseException`, then a `TypeError` will be returned. /// /// If calling `ty` with `args` raises an exception, that exception will be returned. - pub fn from_type(ty: &PyType, args: A) -> PyErr + pub fn from_type_bound(ty: Bound<'_, PyType>, args: A) -> PyErr where A: PyErrArguments + Send + Sync + 'static, { - PyErr::from_state(PyErrState::lazy(ty.into(), args)) + PyErr::from_state(PyErrState::lazy(ty.unbind().into_any(), args)) } /// Deprecated form of [`PyErr::from_value_bound`]. @@ -1231,10 +1246,8 @@ 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( - crate::types::PyString::type_object_bound(py).as_gil_ref(), - "foo", - ); + let err: PyErr = + PyErr::from_type_bound(crate::types::PyString::type_object_bound(py), "foo"); assert!(err.matches(py, PyTypeError::type_object_bound(py))); }) } From 0f92b670b2a474acc2a8faf44b1f1926ddfb741a Mon Sep 17 00:00:00 2001 From: Alexander Hill Date: Fri, 23 Feb 2024 01:49:44 -0500 Subject: [PATCH 033/936] docs: Add lcov / codecov options for nox coverage and update docs (#3825) * docs: Clarify use of llmv-cov plugin * Mention first-run * Update Contributing.md * Add options * fix --- .gitignore | 1 + Contributing.md | 9 +++++++-- noxfile.py | 12 ++++++++++-- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 4240d326f71..d27dfa6f9a5 100644 --- a/.gitignore +++ b/.gitignore @@ -22,5 +22,6 @@ pip-wheel-metadata valgrind-python.supp *.pyd lcov.info +coverage.json netlify_build/ .nox/ diff --git a/Contributing.md b/Contributing.md index dad543856cc..f87fffc0906 100644 --- a/Contributing.md +++ b/Contributing.md @@ -177,9 +177,14 @@ 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, generate a `lcov.info` file with +- First, ensure the llmv-cov cargo plugin is installed. You may need to run the plugin through cargo once before using it with `nox`. ```shell -nox -s coverage +cargo install cargo-llvm-cov +cargo llvm-cov +``` +- Then, generate an `lcov.info` file with +```shell +nox -s coverage -- lcov ``` You can install an IDE plugin to view the coverage. For example, if you use VSCode: - Add the [coverage-gutters](https://marketplace.visualstudio.com/items?itemName=ryanluker.vscode-coverage-gutters) plugin. diff --git a/noxfile.py b/noxfile.py index 00288273aec..add737e5f15 100644 --- a/noxfile.py +++ b/noxfile.py @@ -61,6 +61,14 @@ def coverage(session: nox.Session) -> None: session.env.update(_get_coverage_env()) _run_cargo(session, "llvm-cov", "clean", "--workspace") test(session) + + cov_format = "codecov" + output_file = "coverage.json" + + if "lcov" in session.posargs: + cov_format = "lcov" + output_file = "lcov.info" + _run_cargo( session, "llvm-cov", @@ -70,9 +78,9 @@ def coverage(session: nox.Session) -> None: "--package=pyo3-macros", "--package=pyo3-ffi", "report", - "--codecov", + f"--{cov_format}", "--output-path", - "coverage.json", + output_file, ) From 9186da38924cd9dbbf8b98282e16e4382beccc7f Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 23 Feb 2024 08:16:37 +0000 Subject: [PATCH 034/936] ci: run fmt unconditionally (#3888) --- .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 a61da751394..d22dc2236b1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,6 @@ env: jobs: fmt: - if: github.ref != 'refs/heads/main' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -383,6 +382,7 @@ jobs: emscripten: name: emscripten if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || github.event_name != 'pull_request' }} + needs: [fmt] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 From 11d143d0c9cb33cc67e4933f54d0a0d8680899cb Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 23 Feb 2024 12:30:38 +0000 Subject: [PATCH 035/936] release: 0.20.3 (#3890) --- CHANGELOG.md | 15 ++++++++++++++- 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/3619.fixed.md | 1 - newsfragments/3821.packaging.md | 1 - newsfragments/3834.fixed.md | 1 - 10 files changed, 21 insertions(+), 11 deletions(-) delete mode 100644 newsfragments/3619.fixed.md delete mode 100644 newsfragments/3821.packaging.md delete mode 100644 newsfragments/3834.fixed.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 9104db2921f..295a035370a 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.20.3] - 2024-02-23 + +### Packaging + +- Add `portable-atomic` dependency. [#3619](https://github.com/PyO3/pyo3/pull/3619) +- Check maximum version of Python at build time and for versions not yet supported require opt-in to the `abi3` stable ABI by the environment variable `PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1`. [#3821](https://github.com/PyO3/pyo3/pull/3821) + +### Fixed + +- Use `portable-atomic` to support platforms without 64-bit atomics. [#3619](https://github.com/PyO3/pyo3/pull/3619) +- Fix compilation failure with `either` feature enabled without `experimental-inspect` enabled. [#3834](https://github.com/PyO3/pyo3/pull/3834) + ## [0.20.2] - 2024-01-04 ### Packaging @@ -1628,7 +1640,8 @@ Yanked - Initial release -[Unreleased]: https://github.com/pyo3/pyo3/compare/v0.20.2...HEAD +[Unreleased]: https://github.com/pyo3/pyo3/compare/v0.20.3...HEAD +[0.20.3]: https://github.com/pyo3/pyo3/compare/v0.20.2...v0.20.3 [0.20.2]: https://github.com/pyo3/pyo3/compare/v0.20.1...v0.20.2 [0.20.1]: https://github.com/pyo3/pyo3/compare/v0.20.0...v0.20.1 [0.20.0]: https://github.com/pyo3/pyo3/compare/v0.19.2...v0.20.0 diff --git a/README.md b/README.md index 21e09157d8d..6769025e9fd 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ name = "string_sum" crate-type = ["cdylib"] [dependencies] -pyo3 = { version = "0.20.2", features = ["extension-module"] } +pyo3 = { version = "0.20.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.20.2" +version = "0.20.3" features = ["auto-initialize"] ``` diff --git a/examples/decorator/.template/pre-script.rhai b/examples/decorator/.template/pre-script.rhai index 12b203c3bb4..f059903cc18 100644 --- a/examples/decorator/.template/pre-script.rhai +++ b/examples/decorator/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.20.2"); +variable::set("PYO3_VERSION", "0.20.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 12b203c3bb4..f059903cc18 100644 --- a/examples/maturin-starter/.template/pre-script.rhai +++ b/examples/maturin-starter/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.20.2"); +variable::set("PYO3_VERSION", "0.20.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 72cfe2be91d..90989a891ef 100644 --- a/examples/plugin/.template/pre-script.rhai +++ b/examples/plugin/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.20.2"); +variable::set("PYO3_VERSION", "0.20.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 78a656558f8..d5520ae1d28 100644 --- a/examples/setuptools-rust-starter/.template/pre-script.rhai +++ b/examples/setuptools-rust-starter/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.20.2"); +variable::set("PYO3_VERSION", "0.20.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 12b203c3bb4..f059903cc18 100644 --- a/examples/word-count/.template/pre-script.rhai +++ b/examples/word-count/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.20.2"); +variable::set("PYO3_VERSION", "0.20.3"); file::rename(".template/Cargo.toml", "Cargo.toml"); file::rename(".template/pyproject.toml", "pyproject.toml"); file::delete(".template"); diff --git a/newsfragments/3619.fixed.md b/newsfragments/3619.fixed.md deleted file mode 100644 index 690542409f4..00000000000 --- a/newsfragments/3619.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Use portable-atomic to support platforms without 64-bit atomics diff --git a/newsfragments/3821.packaging.md b/newsfragments/3821.packaging.md deleted file mode 100644 index 4bd89355086..00000000000 --- a/newsfragments/3821.packaging.md +++ /dev/null @@ -1 +0,0 @@ -Check maximum version of Python at build time and for versions not yet supported require opt-in to the `abi3` stable ABI by the environment variable `PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1`. diff --git a/newsfragments/3834.fixed.md b/newsfragments/3834.fixed.md deleted file mode 100644 index fa77e84f36e..00000000000 --- a/newsfragments/3834.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fix compilation failure with `either` feature enabled without `experimental-inspect` enabled. From fbf2e919147009f46efee3ceb1b4feea764349f5 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 23 Feb 2024 12:30:46 +0000 Subject: [PATCH 036/936] macros: exact dependency on `pyo3-build-config` (#3889) --- pyo3-macros-backend/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyo3-macros-backend/Cargo.toml b/pyo3-macros-backend/Cargo.toml index 458b280f881..0e83cc29fd4 100644 --- a/pyo3-macros-backend/Cargo.toml +++ b/pyo3-macros-backend/Cargo.toml @@ -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.0-dev", features = ["resolve-config"] } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.21.0-dev", features = ["resolve-config"] } quote = { version = "1", default-features = false } [dependencies.syn] From e145ae851adca553f62b0f9a0466605cd55fe34a Mon Sep 17 00:00:00 2001 From: Lily Foote Date: Fri, 23 Feb 2024 14:07:54 +0000 Subject: [PATCH 037/936] Update new_closure_bound closure signature (#3883) * Update new_closure_bound closure signature Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> * Use anonymous lifetimes in closure bounds Co-authored-by: David Hewitt * Take &Bound in PyCFunction closures --------- Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> Co-authored-by: David Hewitt --- src/types/function.rs | 28 ++++++++++++++++++---------- tests/test_pyfunction.rs | 17 ++++++++++------- tests/ui/invalid_closure.rs | 9 +++++---- tests/ui/invalid_closure.stderr | 4 ++-- 4 files changed, 35 insertions(+), 23 deletions(-) diff --git a/src/types/function.rs b/src/types/function.rs index 6c7db09aed3..7cbb05e2a48 100644 --- a/src/types/function.rs +++ b/src/types/function.rs @@ -101,7 +101,10 @@ impl PyCFunction { F: Fn(&PyTuple, Option<&PyDict>) -> R + Send + 'static, R: crate::callback::IntoPyCallbackOutput<*mut ffi::PyObject>, { - Self::new_closure_bound(py, name, doc, closure).map(Bound::into_gil_ref) + 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. @@ -113,7 +116,7 @@ impl PyCFunction { /// # use pyo3::{py_run, types::{PyCFunction, PyDict, PyTuple}}; /// /// Python::with_gil(|py| { - /// let add_one = |args: &PyTuple, _kwargs: Option<&PyDict>| -> PyResult<_> { + /// let add_one = |args: &Bound<'_, PyTuple>, _kwargs: Option<&Bound<'_, PyDict>>| -> PyResult<_> { /// let i = args.extract::<(i64,)>()?.0; /// Ok(i+1) /// }; @@ -121,14 +124,14 @@ impl PyCFunction { /// py_run!(py, add_one, "assert add_one(42) == 43"); /// }); /// ``` - pub fn new_closure_bound<'a, F, R>( - py: Python<'a>, + pub fn new_closure_bound<'py, F, R>( + py: Python<'py>, name: Option<&'static str>, doc: Option<&'static str>, closure: F, - ) -> PyResult> + ) -> PyResult> where - F: Fn(&PyTuple, Option<&PyDict>) -> R + Send + 'static, + F: Fn(&Bound<'_, PyTuple>, Option<&Bound<'_, PyDict>>) -> R + Send + 'static, R: crate::callback::IntoPyCallbackOutput<*mut ffi::PyObject>, { let method_def = pymethods::PyMethodDef::cfunction_with_keywords( @@ -199,9 +202,11 @@ unsafe extern "C" fn run_closure( kwargs: *mut ffi::PyObject, ) -> *mut ffi::PyObject where - F: Fn(&PyTuple, Option<&PyDict>) -> R + Send + 'static, + F: Fn(&Bound<'_, PyTuple>, Option<&Bound<'_, PyDict>>) -> R + Send + 'static, R: crate::callback::IntoPyCallbackOutput<*mut ffi::PyObject>, { + use crate::types::any::PyAnyMethods; + crate::impl_::trampoline::cfunction_with_keywords( capsule_ptr, args, @@ -210,9 +215,12 @@ where let boxed_fn: &ClosureDestructor = &*(ffi::PyCapsule_GetPointer(capsule_ptr, closure_capsule_name().as_ptr()) as *mut ClosureDestructor); - let args = py.from_borrowed_ptr::(args); - let kwargs = py.from_borrowed_ptr_or_opt::(kwargs); - crate::callback::convert(py, (boxed_fn.closure)(args, kwargs)) + let args = Bound::ref_from_ptr(py, &args).downcast_unchecked::(); + let kwargs = Bound::ref_from_ptr_or_opt(py, &kwargs) + .as_ref() + .map(|b| b.downcast_unchecked::()); + let result = (boxed_fn.closure)(args, kwargs); + crate::callback::convert(py, result) }, ) } diff --git a/tests/test_pyfunction.rs b/tests/test_pyfunction.rs index e06f7c2fb9c..14748418687 100644 --- a/tests/test_pyfunction.rs +++ b/tests/test_pyfunction.rs @@ -401,7 +401,9 @@ fn test_pycfunction_new_with_keywords() { #[test] fn test_closure() { Python::with_gil(|py| { - let f = |args: &types::PyTuple, _kwargs: Option<&types::PyDict>| -> PyResult<_> { + let f = |args: &Bound<'_, types::PyTuple>, + _kwargs: Option<&Bound<'_, types::PyDict>>| + -> PyResult<_> { Python::with_gil(|py| { let res: Vec<_> = args .iter() @@ -439,12 +441,13 @@ fn test_closure() { fn test_closure_counter() { Python::with_gil(|py| { let counter = std::cell::RefCell::new(0); - let counter_fn = - move |_args: &types::PyTuple, _kwargs: Option<&types::PyDict>| -> PyResult { - let mut counter = counter.borrow_mut(); - *counter += 1; - Ok(*counter) - }; + let counter_fn = move |_args: &Bound<'_, types::PyTuple>, + _kwargs: Option<&Bound<'_, types::PyDict>>| + -> PyResult { + let mut counter = counter.borrow_mut(); + *counter += 1; + Ok(*counter) + }; let counter_py = PyCFunction::new_closure_bound(py, None, None, counter_fn).unwrap(); py_assert!(py, counter_py, "counter_py() == 1"); diff --git a/tests/ui/invalid_closure.rs b/tests/ui/invalid_closure.rs index 0613b027665..eca988f1e57 100644 --- a/tests/ui/invalid_closure.rs +++ b/tests/ui/invalid_closure.rs @@ -6,10 +6,11 @@ fn main() { let local_data = vec![0, 1, 2, 3, 4]; let ref_: &[u8] = &local_data; - let closure_fn = |_args: &PyTuple, _kwargs: Option<&PyDict>| -> PyResult<()> { - println!("This is five: {:?}", ref_.len()); - Ok(()) - }; + let closure_fn = + |_args: &Bound<'_, PyTuple>, _kwargs: Option<&Bound<'_, PyDict>>| -> PyResult<()> { + println!("This is five: {:?}", ref_.len()); + Ok(()) + }; PyCFunction::new_closure_bound(py, None, None, closure_fn) .unwrap() .into() diff --git a/tests/ui/invalid_closure.stderr b/tests/ui/invalid_closure.stderr index 7fed8908583..890d7640502 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 ... -13 | PyCFunction::new_closure_bound(py, None, None, closure_fn) +14 | PyCFunction::new_closure_bound(py, None, None, closure_fn) | ---------------------------------------------------------- argument requires that `local_data` is borrowed for `'static` ... -16 | }); +17 | }); | - `local_data` dropped here while still borrowed From 0f29feca8fe0a6fc969136f60064814611585a35 Mon Sep 17 00:00:00 2001 From: David Matos Date: Sat, 24 Feb 2024 14:25:06 +0100 Subject: [PATCH 038/936] Tidy up deprecation message on bound api (#3893) --- src/conversion.rs | 8 ++++---- src/instance.rs | 2 +- src/marker.rs | 10 +++++----- src/pycell.rs | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/conversion.rs b/src/conversion.rs index 00b6ceae389..415f11089df 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -438,7 +438,7 @@ pub unsafe trait FromPyPointer<'p>: Sized { not(feature = "gil-refs"), deprecated( since = "0.21.0", - note = "part of the deprecated GIL Ref API; to migrate use `Py::from_owned_ptr_or_opt(py, ptr)` or `Bound::from_owned_ptr_or_opt(py, ptr)` instead" + 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>; @@ -451,7 +451,7 @@ pub unsafe trait FromPyPointer<'p>: Sized { not(feature = "gil-refs"), deprecated( since = "0.21.0", - note = "part of the deprecated GIL Ref API; to migrate use `Py::from_owned_ptr(py, ptr)` or `Bound::from_owned_ptr(py, ptr)` instead" + 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 { @@ -467,7 +467,7 @@ pub unsafe trait FromPyPointer<'p>: Sized { not(feature = "gil-refs"), deprecated( since = "0.21.0", - note = "part of the deprecated GIL Ref API; to migrate use `Py::from_owned_ptr(py, ptr)` or `Bound::from_owned_ptr(py, ptr)` instead" + 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 { @@ -483,7 +483,7 @@ pub unsafe trait FromPyPointer<'p>: Sized { not(feature = "gil-refs"), deprecated( since = "0.21.0", - note = "part of the deprecated GIL Ref API; to migrate use `Py::from_owned_ptr_or_err(py, ptr)` or `Bound::from_owned_ptr_or_err(py, ptr)` instead" + 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> { diff --git a/src/instance.rs b/src/instance.rs index 307b341fac7..3efdf58f597 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -985,7 +985,7 @@ where not(feature = "gil-refs"), deprecated( since = "0.21.0", - note = "part of the deprecated GIL Ref API; to migrate use `obj.into_bound(py)` instead of `obj.into_ref(py)`" + note = "use `obj.into_bound(py)` instead of `obj.into_ref(py)`" ) )] pub fn into_ref(self, py: Python<'_>) -> &T::AsRefTarget { diff --git a/src/marker.rs b/src/marker.rs index 449ea2bde6b..5609601f440 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -842,7 +842,7 @@ impl<'py> Python<'py> { not(feature = "gil-refs"), deprecated( since = "0.21.0", - note = "part of the deprecated GIL Ref API; to migrate use `obj.downcast_bound::(py)` instead of `py.checked_cast_as::(obj)`" + 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>> @@ -863,7 +863,7 @@ impl<'py> Python<'py> { not(feature = "gil-refs"), deprecated( since = "0.21.0", - note = "part of the deprecated GIL Ref API; to migrate use `obj.downcast_bound_unchecked::(py)` instead of `py.cast_as::(obj)`" + note = "use `obj.downcast_bound_unchecked::(py)` instead of `py.cast_as::(obj)`" ) )] pub unsafe fn cast_as(self, obj: PyObject) -> &'py T @@ -885,7 +885,7 @@ impl<'py> Python<'py> { not(feature = "gil-refs"), deprecated( since = "0.21.0", - note = "part of the deprecated GIL Ref API; to migrate use `Py::from_owned_ptr(py, ptr)` or `Bound::from_owned_ptr(py, ptr)` instead" + 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 @@ -909,7 +909,7 @@ impl<'py> Python<'py> { not(feature = "gil-refs"), deprecated( since = "0.21.0", - note = "part of the deprecated GIL Ref API; to migrate use `Py::from_owned_ptr_or_err(py, ptr)` or `Bound::from_owned_ptr_or_err(py, ptr)` instead" + 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> @@ -933,7 +933,7 @@ impl<'py> Python<'py> { not(feature = "gil-refs"), deprecated( since = "0.21.0", - note = "part of the deprecated GIL Ref API; to migrate use `Py::from_owned_ptr_or_opt(py, ptr)` or `Bound::from_owned_ptr_or_opt(py, ptr)` instead" + 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> diff --git a/src/pycell.rs b/src/pycell.rs index 5a42dfa514a..989d039f9d2 100644 --- a/src/pycell.rs +++ b/src/pycell.rs @@ -289,7 +289,7 @@ impl PyCell { not(feature = "gil-refs"), deprecated( since = "0.21.0", - note = "part of the deprecated GIL Ref API; to migrate use `Bound::new(py, value)` or `Py::new(py, value)` instead of `PyCell::new(py, value)`" + 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> { From c06bb8f1f1188dad78ec96baa3fa687731aba9b4 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sat, 24 Feb 2024 14:46:59 +0100 Subject: [PATCH 039/936] reexport `PyAnyMethods` and friends from `pyo3::types` (#3895) * reexport `PyAnyMethods` and friends from `pyo3::types` * remove duplicated imports --- src/conversions/indexmap.rs | 4 ---- src/prelude.rs | 1 + src/types/mod.rs | 40 ++++++++++++++++++------------------- 3 files changed, 21 insertions(+), 24 deletions(-) diff --git a/src/conversions/indexmap.rs b/src/conversions/indexmap.rs index 5a8cd3d6951..e908cddb621 100644 --- a/src/conversions/indexmap.rs +++ b/src/conversions/indexmap.rs @@ -87,8 +87,6 @@ //! # if another hash table was used, the order could be random //! ``` -use crate::types::any::PyAnyMethods; -use crate::types::dict::PyDictMethods; use crate::types::*; use crate::{Bound, FromPyObject, IntoPy, PyErr, PyObject, Python, ToPyObject}; use std::{cmp, hash}; @@ -137,8 +135,6 @@ where #[cfg(test)] mod test_indexmap { - use crate::types::any::PyAnyMethods; - use crate::types::dict::PyDictMethods; use crate::types::*; use crate::{IntoPy, PyObject, Python, ToPyObject}; diff --git a/src/prelude.rs b/src/prelude.rs index 5a342281eeb..1de7c3acd2d 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -39,6 +39,7 @@ pub use crate::types::mapping::PyMappingMethods; pub use crate::types::module::PyModuleMethods; pub use crate::types::sequence::PySequenceMethods; pub use crate::types::set::PySetMethods; +pub use crate::types::slice::PySliceMethods; pub use crate::types::string::PyStringMethods; pub use crate::types::traceback::PyTracebackMethods; pub use crate::types::tuple::PyTupleMethods; diff --git a/src/types/mod.rs b/src/types/mod.rs index 217e240129f..4baa086ab79 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -1,13 +1,13 @@ //! Various types defined by the Python interpreter such as `int`, `str` and `tuple`. -pub use self::any::PyAny; -pub use self::boolobject::PyBool; -pub use self::bytearray::PyByteArray; -pub use self::bytes::PyBytes; -pub use self::capsule::PyCapsule; +pub use self::any::{PyAny, PyAnyMethods}; +pub use self::boolobject::{PyBool, PyBoolMethods}; +pub use self::bytearray::{PyByteArray, PyByteArrayMethods}; +pub use self::bytes::{PyBytes, PyBytesMethods}; +pub use self::capsule::{PyCapsule, PyCapsuleMethods}; #[cfg(not(Py_LIMITED_API))] pub use self::code::PyCode; -pub use self::complex::PyComplex; +pub use self::complex::{PyComplex, PyComplexMethods}; #[allow(deprecated)] #[cfg(not(Py_LIMITED_API))] pub use self::datetime::timezone_utc; @@ -16,37 +16,37 @@ pub use self::datetime::{ timezone_utc_bound, PyDate, PyDateAccess, PyDateTime, PyDelta, PyDeltaAccess, PyTime, PyTimeAccess, PyTzInfo, PyTzInfoAccess, }; -pub use self::dict::{IntoPyDict, PyDict}; +pub use self::dict::{IntoPyDict, PyDict, PyDictMethods}; #[cfg(not(PyPy))] pub use self::dict::{PyDictItems, PyDictKeys, PyDictValues}; pub use self::ellipsis::PyEllipsis; -pub use self::float::PyFloat; +pub use self::float::{PyFloat, PyFloatMethods}; #[cfg(all(not(Py_LIMITED_API), not(PyPy)))] pub use self::frame::PyFrame; -pub use self::frozenset::{PyFrozenSet, PyFrozenSetBuilder}; +pub use self::frozenset::{PyFrozenSet, PyFrozenSetBuilder, PyFrozenSetMethods}; pub use self::function::PyCFunction; #[cfg(all(not(Py_LIMITED_API), not(PyPy)))] pub use self::function::PyFunction; pub use self::iterator::PyIterator; -pub use self::list::PyList; -pub use self::mapping::PyMapping; +pub use self::list::{PyList, PyListMethods}; +pub use self::mapping::{PyMapping, PyMappingMethods}; pub use self::memoryview::PyMemoryView; -pub use self::module::PyModule; +pub use self::module::{PyModule, PyModuleMethods}; pub use self::none::PyNone; pub use self::notimplemented::PyNotImplemented; pub use self::num::PyLong; pub use self::num::PyLong as PyInt; #[cfg(not(PyPy))] pub use self::pysuper::PySuper; -pub use self::sequence::PySequence; -pub use self::set::PySet; -pub use self::slice::{PySlice, PySliceIndices}; +pub use self::sequence::{PySequence, PySequenceMethods}; +pub use self::set::{PySet, PySetMethods}; +pub use self::slice::{PySlice, PySliceIndices, PySliceMethods}; #[cfg(not(Py_LIMITED_API))] pub use self::string::PyStringData; -pub use self::string::{PyString, PyString as PyUnicode}; -pub use self::traceback::PyTraceback; -pub use self::tuple::PyTuple; -pub use self::typeobject::PyType; +pub use self::string::{PyString, PyString as PyUnicode, PyStringMethods}; +pub use self::traceback::{PyTraceback, PyTracebackMethods}; +pub use self::tuple::{PyTuple, PyTupleMethods}; +pub use self::typeobject::{PyType, PyTypeMethods}; /// Iteration over Python collections. /// @@ -332,7 +332,7 @@ mod num; mod pysuper; pub(crate) mod sequence; pub(crate) mod set; -mod slice; +pub(crate) mod slice; pub(crate) mod string; pub(crate) mod traceback; pub(crate) mod tuple; From e0e3981e17c7b0f9b221ac1566e5a2eadf1a4f6b Mon Sep 17 00:00:00 2001 From: Thomas Tanon Date: Sat, 24 Feb 2024 14:50:18 +0100 Subject: [PATCH 040/936] #[pymodule] mod some_module { ... } v3 (#3815) * #[pymodule] mod some_module { ... } v3 Based on #2367 and #3294 Allows to export classes, native classes, functions and submodules and provide an init function See test/test_module.rs for an example Future work: - update examples, README and guide - investigate having #[pyclass] and #[pyfunction] directly in the #[pymodule] Co-authored-by: David Hewitt Co-authored-by: Georg Brandl * tests: group exported imports * Consolidate pymodule macro code to avoid duplicates * Makes pymodule_init take Bound<'_, PyModule> * Renames #[pyo3] to #[pymodule_export] * Gates #[pymodule] mod behind the experimental-declarative-modules feature * Properly fails on functions inside of declarative modules --------- Co-authored-by: David Hewitt Co-authored-by: Georg Brandl --- Cargo.toml | 4 + newsfragments/3815.added.md | 2 + pyo3-macros-backend/src/lib.rs | 2 +- pyo3-macros-backend/src/module.rs | 219 +++++++++++++++--- pyo3-macros-backend/src/pyfunction.rs | 6 + pyo3-macros/Cargo.toml | 1 + pyo3-macros/src/lib.rs | 40 ++-- src/impl_/pymodule.rs | 32 ++- src/macros.rs | 4 +- tests/test_append_to_inittab.rs | 37 ++- tests/test_compile_error.rs | 8 + tests/test_declarative_module.rs | 101 ++++++++ tests/ui/invalid_pymodule_glob.rs | 14 ++ tests/ui/invalid_pymodule_glob.stderr | 5 + tests/ui/invalid_pymodule_in_root.rs | 6 + tests/ui/invalid_pymodule_in_root.stderr | 13 ++ tests/ui/invalid_pymodule_trait.rs | 9 + tests/ui/invalid_pymodule_trait.stderr | 5 + .../ui/invalid_pymodule_two_pymodule_init.rs | 16 ++ .../invalid_pymodule_two_pymodule_init.stderr | 5 + 20 files changed, 458 insertions(+), 71 deletions(-) create mode 100644 newsfragments/3815.added.md create mode 100644 tests/test_declarative_module.rs create mode 100644 tests/ui/invalid_pymodule_glob.rs create mode 100644 tests/ui/invalid_pymodule_glob.stderr create mode 100644 tests/ui/invalid_pymodule_in_root.rs create mode 100644 tests/ui/invalid_pymodule_in_root.stderr create mode 100644 tests/ui/invalid_pymodule_trait.rs create mode 100644 tests/ui/invalid_pymodule_trait.stderr create mode 100644 tests/ui/invalid_pymodule_two_pymodule_init.rs create mode 100644 tests/ui/invalid_pymodule_two_pymodule_init.stderr diff --git a/Cargo.toml b/Cargo.toml index 4d1899cbdb9..e7364a7c9f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,6 +69,9 @@ default = ["macros"] # 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"] @@ -114,6 +117,7 @@ full = [ "chrono-tz", "either", "experimental-inspect", + "experimental-declarative-modules", "eyre", "hashbrown", "indexmap", diff --git a/newsfragments/3815.added.md b/newsfragments/3815.added.md new file mode 100644 index 00000000000..e4fd3e9315a --- /dev/null +++ b/newsfragments/3815.added.md @@ -0,0 +1,2 @@ +The ability to create Python modules with a Rust `mod` block +behind the `experimental-declarative-modules` feature. \ No newline at end of file diff --git a/pyo3-macros-backend/src/lib.rs b/pyo3-macros-backend/src/lib.rs index 745a8471c2b..a9d75a2a6fe 100644 --- a/pyo3-macros-backend/src/lib.rs +++ b/pyo3-macros-backend/src/lib.rs @@ -22,7 +22,7 @@ mod pymethod; mod quotes; pub use frompyobject::build_derive_from_pyobject; -pub use module::{process_functions_in_module, pymodule_impl, PyModuleOptions}; +pub use module::{pymodule_function_impl, pymodule_module_impl, PyModuleOptions}; pub use pyclass::{build_py_class, build_py_enum, PyClassArgs}; pub use pyfunction::{build_py_function, PyFunctionOptions}; pub use pyimpl::{build_py_methods, PyClassMethodsType}; diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index ccd84bb363a..6907e484f71 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -2,8 +2,9 @@ use crate::{ attributes::{self, take_attributes, take_pyo3_options, CrateAttribute, NameAttribute}, + get_doc, pyfunction::{impl_wrap_pyfunction, PyFunctionOptions}, - utils::{get_pyo3_crate, PythonDoc}, + utils::get_pyo3_crate, }; use proc_macro2::TokenStream; use quote::quote; @@ -12,7 +13,7 @@ use syn::{ parse::{Parse, ParseStream}, spanned::Spanned, token::Comma, - Ident, Path, Result, Visibility, + Item, Path, Result, }; #[derive(Default)] @@ -56,33 +57,154 @@ impl PyModuleOptions { } } +pub fn pymodule_module_impl(mut module: syn::ItemMod) -> Result { + let syn::ItemMod { + attrs, + vis, + unsafety: _, + ident, + mod_token: _, + content, + semi: _, + } = &mut module; + let items = if let Some((_, items)) = content { + items + } else { + bail_spanned!(module.span() => "`#[pymodule]` can only be used on inline modules") + }; + let options = PyModuleOptions::from_attrs(attrs)?; + let krate = get_pyo3_crate(&options.krate); + let doc = get_doc(attrs, None); + + let mut module_items = Vec::new(); + let mut module_items_cfg_attrs = Vec::new(); + + fn extract_use_items( + source: &syn::UseTree, + cfg_attrs: &[syn::Attribute], + target_items: &mut Vec, + target_cfg_attrs: &mut Vec>, + ) -> Result<()> { + match source { + syn::UseTree::Name(name) => { + target_items.push(name.ident.clone()); + target_cfg_attrs.push(cfg_attrs.to_vec()); + } + syn::UseTree::Path(path) => { + extract_use_items(&path.tree, cfg_attrs, target_items, target_cfg_attrs)? + } + syn::UseTree::Group(group) => { + for tree in &group.items { + extract_use_items(tree, cfg_attrs, target_items, target_cfg_attrs)? + } + } + syn::UseTree::Glob(glob) => { + bail_spanned!(glob.span() => "#[pymodule] cannot import glob statements") + } + syn::UseTree::Rename(rename) => { + target_items.push(rename.rename.clone()); + target_cfg_attrs.push(cfg_attrs.to_vec()); + } + } + Ok(()) + } + + let mut pymodule_init = None; + + for item in &mut *items { + match item { + Item::Use(item_use) => { + let mut is_pyo3 = false; + item_use.attrs.retain(|attr| { + let found = attr.path().is_ident("pymodule_export"); + is_pyo3 |= found; + !found + }); + if is_pyo3 { + let cfg_attrs = item_use + .attrs + .iter() + .filter(|attr| attr.path().is_ident("cfg")) + .cloned() + .collect::>(); + extract_use_items( + &item_use.tree, + &cfg_attrs, + &mut module_items, + &mut module_items_cfg_attrs, + )?; + } + } + Item::Fn(item_fn) => { + let mut is_module_init = false; + item_fn.attrs.retain(|attr| { + let found = attr.path().is_ident("pymodule_init"); + is_module_init |= found; + !found + }); + if is_module_init { + ensure_spanned!(pymodule_init.is_none(), item_fn.span() => "only one pymodule_init may be specified"); + let ident = &item_fn.sig.ident; + pymodule_init = Some(quote! { #ident(module)?; }); + } else { + bail_spanned!(item.span() => "only 'use' statements and and pymodule_init functions are allowed in #[pymodule]") + } + } + item => { + bail_spanned!(item.span() => "only 'use' statements and and pymodule_init functions are allowed in #[pymodule]") + } + } + } + + let initialization = module_initialization(options, ident); + Ok(quote!( + #vis mod #ident { + #(#items)* + + #initialization + + impl MakeDef { + const fn make_def() -> #krate::impl_::pymodule::ModuleDef { + use #krate::impl_::pymodule as impl_; + const INITIALIZER: impl_::ModuleInitializer = impl_::ModuleInitializer(__pyo3_pymodule); + unsafe { + impl_::ModuleDef::new( + __PYO3_NAME, + #doc, + INITIALIZER + ) + } + } + } + + fn __pyo3_pymodule(module: &#krate::Bound<'_, #krate::types::PyModule>) -> #krate::PyResult<()> { + use #krate::impl_::pymodule::PyAddToModule; + #( + #(#module_items_cfg_attrs)* + #module_items::add_to_module(module)?; + )* + #pymodule_init + Ok(()) + } + } + )) +} + /// Generates the function that is called by the python interpreter to initialize the native /// module -pub fn pymodule_impl( - fnname: &Ident, - options: PyModuleOptions, - doc: PythonDoc, - visibility: &Visibility, -) -> TokenStream { - let name = options.name.unwrap_or_else(|| fnname.unraw()); +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 krate = get_pyo3_crate(&options.krate); - let pyinit_symbol = format!("PyInit_{}", name); + let ident = &function.sig.ident; + let vis = &function.vis; + let doc = get_doc(&function.attrs, None); - quote! { - // Create a module with the same name as the `#[pymodule]` - this way `use ` - // will actually bring both the module and the function into scope. - #[doc(hidden)] - #visibility mod #fnname { - pub(crate) struct MakeDef; - pub static DEF: #krate::impl_::pymodule::ModuleDef = MakeDef::make_def(); - pub const NAME: &'static str = concat!(stringify!(#name), "\0"); - - /// This autogenerated function is called by the python interpreter when importing - /// the module. - #[export_name = #pyinit_symbol] - pub unsafe extern "C" fn init() -> *mut #krate::ffi::PyObject { - #krate::impl_::trampoline::module_init(|py| DEF.make_module(py)) - } + let initialization = module_initialization(options, ident); + Ok(quote! { + #function + #vis mod #ident { + #initialization } // Generate the definition inside an anonymous function in the same scope as the original function - @@ -91,28 +213,59 @@ pub fn pymodule_impl( // inside a function body) const _: () = { use #krate::impl_::pymodule as impl_; - impl #fnname::MakeDef { + + fn __pyo3_pymodule(module: &#krate::Bound<'_, #krate::types::PyModule>) -> #krate::PyResult<()> { + #ident(module.py(), module.as_gil_ref()) + } + + impl #ident::MakeDef { const fn make_def() -> impl_::ModuleDef { - const INITIALIZER: impl_::ModuleInitializer = impl_::ModuleInitializer(#fnname); unsafe { - impl_::ModuleDef::new(#fnname::NAME, #doc, INITIALIZER) + const INITIALIZER: impl_::ModuleInitializer = impl_::ModuleInitializer(__pyo3_pymodule); + impl_::ModuleDef::new( + #ident::__PYO3_NAME, + #doc, + INITIALIZER + ) } } } }; + }) +} + +fn module_initialization(options: PyModuleOptions, ident: &syn::Ident) -> TokenStream { + let name = options.name.unwrap_or_else(|| ident.unraw()); + let krate = get_pyo3_crate(&options.krate); + let pyinit_symbol = format!("PyInit_{}", name); + + quote! { + pub const __PYO3_NAME: &'static str = concat!(stringify!(#name), "\0"); + + pub(super) struct MakeDef; + pub static DEF: #krate::impl_::pymodule::ModuleDef = MakeDef::make_def(); + + pub fn add_to_module(module: &#krate::Bound<'_, #krate::types::PyModule>) -> #krate::PyResult<()> { + use #krate::prelude::PyModuleMethods; + module.add_submodule(DEF.make_module(module.py())?.bind(module.py())) + } + + /// This autogenerated function is called by the python interpreter when importing + /// the module. + #[export_name = #pyinit_symbol] + pub unsafe extern "C" fn __pyo3_init() -> *mut #krate::ffi::PyObject { + #krate::impl_::trampoline::module_init(|py| DEF.make_module(py)) + } } } /// Finds and takes care of the #[pyfn(...)] in `#[pymodule]` -pub fn process_functions_in_module( - options: &PyModuleOptions, - func: &mut syn::ItemFn, -) -> syn::Result<()> { +fn process_functions_in_module(options: &PyModuleOptions, func: &mut syn::ItemFn) -> Result<()> { let mut stmts: Vec = Vec::new(); let krate = get_pyo3_crate(&options.krate); for mut stmt in func.block.stmts.drain(..) { - if let syn::Stmt::Item(syn::Item::Fn(func)) = &mut stmt { + if let syn::Stmt::Item(Item::Fn(func)) = &mut stmt { if let Some(pyfn_args) = get_pyfn_attr(&mut func.attrs)? { let module_name = pyfn_args.modname; let wrapped_function = impl_wrap_pyfunction(func, pyfn_args.options)?; diff --git a/pyo3-macros-backend/src/pyfunction.rs b/pyo3-macros-backend/src/pyfunction.rs index b265a34d39f..7b48585cddc 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -269,6 +269,12 @@ pub fn impl_wrap_pyfunction( #vis mod #name { pub(crate) struct MakeDef; pub const DEF: #krate::impl_::pyfunction::PyMethodDef = MakeDef::DEF; + + pub fn add_to_module(module: &#krate::Bound<'_, #krate::types::PyModule>) -> #krate::PyResult<()> { + use #krate::prelude::PyModuleMethods; + use ::std::convert::Into; + module.add_function(&#krate::types::PyCFunction::internal_new(&DEF, module.as_gil_ref().into())?) + } } // Generate the definition inside an anonymous function in the same scope as the original function - diff --git a/pyo3-macros/Cargo.toml b/pyo3-macros/Cargo.toml index 576c94a2bc1..a0368a5f364 100644 --- a/pyo3-macros/Cargo.toml +++ b/pyo3-macros/Cargo.toml @@ -15,6 +15,7 @@ proc-macro = true [features] multiple-pymethods = [] +experimental-declarative-modules = [] [dependencies] proc-macro2 = { version = "1", default-features = false } diff --git a/pyo3-macros/src/lib.rs b/pyo3-macros/src/lib.rs index d00ede89143..64756a1c73b 100644 --- a/pyo3-macros/src/lib.rs +++ b/pyo3-macros/src/lib.rs @@ -6,11 +6,11 @@ use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; use pyo3_macros_backend::{ build_derive_from_pyobject, build_py_class, build_py_enum, build_py_function, build_py_methods, - get_doc, process_functions_in_module, pymodule_impl, PyClassArgs, PyClassMethodsType, - PyFunctionOptions, PyModuleOptions, + pymodule_function_impl, pymodule_module_impl, PyClassArgs, PyClassMethodsType, + PyFunctionOptions, }; use quote::quote; -use syn::{parse::Nothing, parse_macro_input}; +use syn::{parse::Nothing, parse_macro_input, Item}; /// A proc macro used to implement Python modules. /// @@ -36,31 +36,27 @@ use syn::{parse::Nothing, parse_macro_input}; #[proc_macro_attribute] pub fn pymodule(args: TokenStream, input: TokenStream) -> TokenStream { parse_macro_input!(args as Nothing); - - let mut ast = parse_macro_input!(input as syn::ItemFn); - let options = match PyModuleOptions::from_attrs(&mut ast.attrs) { - Ok(options) => options, - Err(e) => return e.into_compile_error().into(), - }; - - if let Err(err) = process_functions_in_module(&options, &mut ast) { - return err.into_compile_error().into(); + match parse_macro_input!(input as Item) { + Item::Mod(module) => if cfg!(feature = "experimental-declarative-modules") { + pymodule_module_impl(module) + } else { + Err(syn::Error::new_spanned( + module, + "#[pymodule] requires the 'experimental-declarative-modules' feature to be used on Rust modules.", + )) + }, + Item::Fn(function) => pymodule_function_impl(function), + unsupported => Err(syn::Error::new_spanned( + unsupported, + "#[pymodule] only supports modules and functions.", + )), } - - let doc = get_doc(&ast.attrs, None); - - let expanded = pymodule_impl(&ast.sig.ident, options, doc, &ast.vis); - - quote!( - #ast - #expanded - ) + .unwrap_or_compile_error() .into() } #[proc_macro_attribute] pub fn pyclass(attr: TokenStream, input: TokenStream) -> TokenStream { - use syn::Item; let item = parse_macro_input!(input as Item); match item { Item::Struct(struct_) => pyclass_impl(attr, struct_, methods_type()), diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index 7103f8b3938..9fff799c37b 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -7,7 +7,8 @@ use portable_atomic::{AtomicI64, Ordering}; #[cfg(not(PyPy))] use crate::exceptions::PyImportError; -use crate::{ffi, sync::GILOnceCell, types::PyModule, Py, PyResult, Python}; +use crate::types::module::PyModuleMethods; +use crate::{ffi, sync::GILOnceCell, types::PyModule, Bound, Py, PyResult, PyTypeInfo, Python}; /// `Sync` wrapper of `ffi::PyModuleDef`. pub struct ModuleDef { @@ -22,7 +23,7 @@ pub struct ModuleDef { } /// Wrapper to enable initializer to be used in const fns. -pub struct ModuleInitializer(pub for<'py> fn(Python<'py>, &PyModule) -> PyResult<()>); +pub struct ModuleInitializer(pub for<'py> fn(&Bound<'py, PyModule>) -> PyResult<()>); unsafe impl Sync for ModuleDef {} @@ -126,18 +127,34 @@ impl ModuleDef { ffi::PyModule_Create(self.ffi_def.get()), )? }; - (self.initializer.0)(py, module.as_ref(py))?; + self.initializer.0(module.bind(py))?; Ok(module) }) .map(|py_module| py_module.clone_ref(py)) } } +/// Trait to add an element (class, function...) to a module. +/// +/// Currently only implemented for classes. +pub trait PyAddToModule { + fn add_to_module(module: &Bound<'_, PyModule>) -> PyResult<()>; +} + +impl PyAddToModule for T { + fn add_to_module(module: &Bound<'_, PyModule>) -> PyResult<()> { + module.add(Self::NAME, Self::type_object_bound(module.py())) + } +} + #[cfg(test)] mod tests { use std::sync::atomic::{AtomicBool, Ordering}; - use crate::{types::any::PyAnyMethods, types::PyModule, PyResult, Python}; + use crate::{ + types::{any::PyAnyMethods, module::PyModuleMethods, PyModule}, + Bound, PyResult, Python, + }; use super::{ModuleDef, ModuleInitializer}; @@ -147,7 +164,7 @@ mod tests { ModuleDef::new( "test_module\0", "some doc\0", - ModuleInitializer(|_, m| { + ModuleInitializer(|m| { m.add("SOME_CONSTANT", 42)?; Ok(()) }), @@ -192,7 +209,7 @@ mod tests { static INIT_CALLED: AtomicBool = AtomicBool::new(false); #[allow(clippy::unnecessary_wraps)] - fn init(_: Python<'_>, _: &PyModule) -> PyResult<()> { + fn init(_: &Bound<'_, PyModule>) -> PyResult<()> { INIT_CALLED.store(true, Ordering::SeqCst); Ok(()) } @@ -203,8 +220,7 @@ mod tests { assert_eq!((*module_def.ffi_def.get()).m_doc, DOC.as_ptr() as _); Python::with_gil(|py| { - module_def.initializer.0(py, py.import_bound("builtins").unwrap().into_gil_ref()) - .unwrap(); + module_def.initializer.0(&py.import_bound("builtins").unwrap()).unwrap(); assert!(INIT_CALLED.load(Ordering::SeqCst)); }) } diff --git a/src/macros.rs b/src/macros.rs index 29c2033dd4b..9b0d2816882 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -169,8 +169,8 @@ macro_rules! append_to_inittab { ); } $crate::ffi::PyImport_AppendInittab( - $module::NAME.as_ptr() as *const ::std::os::raw::c_char, - ::std::option::Option::Some($module::init), + $module::__PYO3_NAME.as_ptr() as *const ::std::os::raw::c_char, + ::std::option::Option::Some($module::__pyo3_init), ); } }; diff --git a/tests/test_append_to_inittab.rs b/tests/test_append_to_inittab.rs index 00cccdbb49e..59ecaf42909 100644 --- a/tests/test_append_to_inittab.rs +++ b/tests/test_append_to_inittab.rs @@ -1,4 +1,5 @@ #![cfg(all(feature = "macros", not(PyPy)))] + use pyo3::prelude::*; #[pyfunction] @@ -7,26 +8,52 @@ fn foo() -> usize { } #[pymodule] -fn module_with_functions(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn module_fn_with_functions(_py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_function(wrap_pyfunction!(foo, m)?).unwrap(); Ok(()) } +#[cfg(feature = "experimental-declarative-modules")] +#[pymodule] +mod module_mod_with_functions { + #[pymodule_export] + use super::foo; +} + #[cfg(not(PyPy))] #[test] fn test_module_append_to_inittab() { use pyo3::append_to_inittab; - append_to_inittab!(module_with_functions); + + append_to_inittab!(module_fn_with_functions); + + #[cfg(feature = "experimental-declarative-modules")] + append_to_inittab!(module_mod_with_functions); + + Python::with_gil(|py| { + py.run_bound( + r#" +import module_fn_with_functions +assert module_fn_with_functions.foo() == 123 +"#, + None, + None, + ) + .map_err(|e| e.display(py)) + .unwrap(); + }); + + #[cfg(feature = "experimental-declarative-modules")] Python::with_gil(|py| { py.run_bound( r#" -import module_with_functions -assert module_with_functions.foo() == 123 +import module_mod_with_functions +assert module_mod_with_functions.foo() == 123 "#, None, None, ) .map_err(|e| e.display(py)) .unwrap(); - }) + }); } diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index adcef887f5c..5f2d25db92f 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -40,4 +40,12 @@ 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"); } diff --git a/tests/test_declarative_module.rs b/tests/test_declarative_module.rs new file mode 100644 index 00000000000..86913d9b800 --- /dev/null +++ b/tests/test_declarative_module.rs @@ -0,0 +1,101 @@ +#![cfg(feature = "experimental-declarative-modules")] + +use pyo3::create_exception; +use pyo3::exceptions::PyException; +use pyo3::prelude::*; + +#[path = "../src/tests/common.rs"] +mod common; + +#[pyclass] +struct ValueClass { + value: usize, +} + +#[pymethods] +impl ValueClass { + #[new] + fn new(value: usize) -> ValueClass { + ValueClass { value } + } +} + +#[pyclass(module = "module")] +struct LocatedClass {} + +#[pyfunction] +fn double(x: usize) -> usize { + x * 2 +} + +create_exception!( + declarative_module, + MyError, + PyException, + "Some description." +); + +/// A module written using declarative syntax. +#[pymodule] +mod declarative_module { + #[pymodule_export] + use super::declarative_submodule; + #[pymodule_export] + // This is not a real constraint but to test cfg attribute support + #[cfg(not(Py_LIMITED_API))] + use super::LocatedClass; + use super::*; + #[pymodule_export] + use super::{declarative_module2, double, MyError, ValueClass as Value}; + + #[pymodule_init] + fn init(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add("double2", m.getattr("double")?) + } +} + +#[pyfunction] +fn double_value(v: &ValueClass) -> usize { + v.value * 2 +} + +#[pymodule] +mod declarative_submodule { + #[pymodule_export] + use super::{double, double_value}; +} + +/// A module written using declarative syntax. +#[pymodule] +#[pyo3(name = "declarative_module_renamed")] +mod declarative_module2 { + #[pymodule_export] + use super::double; +} + +#[test] +fn test_declarative_module() { + Python::with_gil(|py| { + let m = pyo3::wrap_pymodule!(declarative_module)(py).into_bound(py); + py_assert!( + py, + m, + "m.__doc__ == 'A module written using declarative syntax.'" + ); + + py_assert!(py, m, "m.double(2) == 4"); + py_assert!(py, m, "m.double2(3) == 6"); + py_assert!(py, m, "m.declarative_submodule.double(4) == 8"); + py_assert!( + py, + m, + "m.declarative_submodule.double_value(m.ValueClass(1)) == 2" + ); + py_assert!(py, m, "str(m.MyError('foo')) == 'foo'"); + py_assert!(py, m, "m.declarative_module_renamed.double(2) == 4"); + #[cfg(Py_LIMITED_API)] + py_assert!(py, m, "not hasattr(m, 'LocatedClass')"); + #[cfg(not(Py_LIMITED_API))] + py_assert!(py, m, "hasattr(m, 'LocatedClass')"); + }) +} diff --git a/tests/ui/invalid_pymodule_glob.rs b/tests/ui/invalid_pymodule_glob.rs new file mode 100644 index 00000000000..107cdf9382a --- /dev/null +++ b/tests/ui/invalid_pymodule_glob.rs @@ -0,0 +1,14 @@ +use pyo3::prelude::*; + +#[pyfunction] +fn foo() -> usize { + 0 +} + +#[pymodule] +mod module { + #[pymodule_export] + use super::*; +} + +fn main() {} diff --git a/tests/ui/invalid_pymodule_glob.stderr b/tests/ui/invalid_pymodule_glob.stderr new file mode 100644 index 00000000000..237e02037aa --- /dev/null +++ b/tests/ui/invalid_pymodule_glob.stderr @@ -0,0 +1,5 @@ +error: #[pymodule] cannot import glob statements + --> tests/ui/invalid_pymodule_glob.rs:11:16 + | +11 | use super::*; + | ^ diff --git a/tests/ui/invalid_pymodule_in_root.rs b/tests/ui/invalid_pymodule_in_root.rs new file mode 100644 index 00000000000..47af4205f71 --- /dev/null +++ b/tests/ui/invalid_pymodule_in_root.rs @@ -0,0 +1,6 @@ +use pyo3::prelude::*; + +#[pymodule] +mod invalid_pymodule_in_root_module; + +fn main() {} diff --git a/tests/ui/invalid_pymodule_in_root.stderr b/tests/ui/invalid_pymodule_in_root.stderr new file mode 100644 index 00000000000..91783be0e97 --- /dev/null +++ b/tests/ui/invalid_pymodule_in_root.stderr @@ -0,0 +1,13 @@ +error[E0658]: non-inline modules in proc macro input are unstable + --> tests/ui/invalid_pymodule_in_root.rs:4:1 + | +4 | mod invalid_pymodule_in_root_module; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: see issue #54727 for more information + +error: `#[pymodule]` can only be used on inline modules + --> tests/ui/invalid_pymodule_in_root.rs:4:1 + | +4 | mod invalid_pymodule_in_root_module; + | ^^^ diff --git a/tests/ui/invalid_pymodule_trait.rs b/tests/ui/invalid_pymodule_trait.rs new file mode 100644 index 00000000000..6649a3547a0 --- /dev/null +++ b/tests/ui/invalid_pymodule_trait.rs @@ -0,0 +1,9 @@ +use pyo3::prelude::*; + +#[pymodule] +mod module { + #[pymodule_export] + trait Foo {} +} + +fn main() {} diff --git a/tests/ui/invalid_pymodule_trait.stderr b/tests/ui/invalid_pymodule_trait.stderr new file mode 100644 index 00000000000..3ed128617f5 --- /dev/null +++ b/tests/ui/invalid_pymodule_trait.stderr @@ -0,0 +1,5 @@ +error: only 'use' statements and and pymodule_init functions are allowed in #[pymodule] + --> tests/ui/invalid_pymodule_trait.rs:5:5 + | +5 | #[pymodule_export] + | ^ diff --git a/tests/ui/invalid_pymodule_two_pymodule_init.rs b/tests/ui/invalid_pymodule_two_pymodule_init.rs new file mode 100644 index 00000000000..d676b0fa277 --- /dev/null +++ b/tests/ui/invalid_pymodule_two_pymodule_init.rs @@ -0,0 +1,16 @@ +use pyo3::prelude::*; + +#[pymodule] +mod module { + #[pymodule_init] + fn init(m: &PyModule) -> PyResult<()> { + Ok(()) + } + + #[pymodule_init] + fn init2(m: &PyModule) -> PyResult<()> { + Ok(()) + } +} + +fn main() {} diff --git a/tests/ui/invalid_pymodule_two_pymodule_init.stderr b/tests/ui/invalid_pymodule_two_pymodule_init.stderr new file mode 100644 index 00000000000..9f0900f9348 --- /dev/null +++ b/tests/ui/invalid_pymodule_two_pymodule_init.stderr @@ -0,0 +1,5 @@ +error: only one pymodule_init may be specified + --> tests/ui/invalid_pymodule_two_pymodule_init.rs:11:5 + | +11 | fn init2(m: &PyModule) -> PyResult<()> { + | ^^ From 8f1b99e1e955a333ba9382d3c46d74bd82b1eab9 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sat, 24 Feb 2024 22:35:01 +0000 Subject: [PATCH 041/936] move chat discussions to Discord (#3892) * move chat discussions to Discord * guide: add some more signposting to the PyO3 Discord --- .github/ISSUE_TEMPLATE/config.yml | 6 +++--- Contributing.md | 4 ++-- README.md | 4 ++-- guide/src/faq.md | 4 +++- guide/src/getting_started.md | 2 ++ 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 4d31afe411d..ca4239b6fa7 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -5,7 +5,7 @@ contact_links: about: Ask and answer questions about PyO3 on Discussions - name: 🔧 Troubleshooting url: https://github.com/PyO3/pyo3/discussions - about: For troubleshooting help, see the Discussions + about: For troubleshooting help, see the Discussions - name: 👋 Chat - url: https://gitter.im/PyO3/Lobby - about: Engage with PyO3's users and developers on Gitter \ No newline at end of file + url: https://discord.gg/c4BwayXQ + about: Engage with PyO3's users and developers on Discord diff --git a/Contributing.md b/Contributing.md index f87fffc0906..9392c0fc2de 100644 --- a/Contributing.md +++ b/Contributing.md @@ -29,7 +29,7 @@ To work and develop PyO3, you need Python & Rust installed on your system. ### Help users identify bugs -The [PyO3 Gitter channel](https://gitter.im/PyO3/Lobby) is very active with users who are new to PyO3, and often completely new to Rust. Helping them debug is a great way to get experience with the PyO3 codebase. +The [PyO3 Discord server](https://discord.gg/c4BwayXQ) is very active with users who are new to PyO3, and often completely new to Rust. Helping them debug is a great way to get experience with the PyO3 codebase. Helping others often reveals bugs, documentation weaknesses, and missing APIs. It's a good idea to open GitHub issues for these immediately so the resolution can be designed and implemented! @@ -203,7 +203,7 @@ You can install an IDE plugin to view the coverage. For example, if you use VSCo ## Sponsor this project -At the moment there is no official organisation that accepts sponsorship on PyO3's behalf. If you're seeking to provide significant funding to the PyO3 ecosystem, please reach out to us on [GitHub](https://github.com/PyO3/pyo3/issues/new) or [Gitter](https://gitter.im/PyO3/Lobby) and we can discuss. +At the moment there is no official organisation that accepts sponsorship on PyO3's behalf. If you're seeking to provide significant funding to the PyO3 ecosystem, please reach out to us on [GitHub](https://github.com/PyO3/pyo3/issues/new) or [Discord](https://discord.gg/c4BwayXQ) and we can discuss. In the meanwhile, some of our maintainers have personal GitHub sponsorship pages and would be grateful for your support: diff --git a/README.md b/README.md index 6769025e9fd..c139f13693e 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![codecov](https://img.shields.io/codecov/c/gh/PyO3/pyo3?logo=codecov)](https://codecov.io/gh/PyO3/pyo3) [![crates.io](https://img.shields.io/crates/v/pyo3?logo=rust)](https://crates.io/crates/pyo3) [![minimum rustc 1.56](https://img.shields.io/badge/rustc-1.56+-blue?logo=rust)](https://rust-lang.github.io/rfcs/2495-min-rust-version.html) -[![dev chat](https://img.shields.io/gitter/room/PyO3/Lobby?logo=gitter)](https://gitter.im/PyO3/Lobby) +[![discord server](https://img.shields.io/discord/1209263839632424990?logo=discord)](https://discord.gg/c4BwayXQ) [![contributing notes](https://img.shields.io/badge/contribute-on%20github-Green?logo=github)](https://github.com/PyO3/pyo3/blob/main/Contributing.md) [Rust](https://www.rust-lang.org/) bindings for [Python](https://www.python.org/), including tools for creating native Python extension modules. Running and interacting with Python code from a Rust binary is also supported. @@ -237,7 +237,7 @@ about this topic. Everyone is welcomed to contribute to PyO3! There are many ways to support the project, such as: -- help PyO3 users with issues on GitHub and Gitter +- help PyO3 users with issues on GitHub and [Discord](https://discord.gg/c4BwayXQ) - improve documentation - write features and bugfixes - publish blogs and examples of how to use PyO3 diff --git a/guide/src/faq.md b/guide/src/faq.md index 8308acc64de..6f5188c73be 100644 --- a/guide/src/faq.md +++ b/guide/src/faq.md @@ -1,5 +1,7 @@ # Frequently Asked Questions and troubleshooting +Sorry that you're having trouble using PyO3. If you can't find the answer to your problem in the list below, you can also reach out for help on [GitHub Discussions](https://github.com/PyO3/pyo3/discussions) and on [Discord](https://discord.gg/c4BwayXQ). + ## I'm experiencing deadlocks using PyO3 with lazy_static or once_cell! `lazy_static` and `once_cell::sync` both use locks to ensure that initialization is performed only by a single thread. Because the Python GIL is an additional lock this can lead to deadlocks in the following way: @@ -183,7 +185,7 @@ struct MyClass; ## I'm trying to call Python from Rust but I get `STATUS_DLL_NOT_FOUND` or `STATUS_ENTRYPOINT_NOT_FOUND`! -This happens on Windows when linking to the python DLL fails or the wrong one is linked. The Python DLL on Windows will usually be called something like: +This happens on Windows when linking to the python DLL fails or the wrong one is linked. The Python DLL on Windows will usually be called something like: - `python3X.dll` for Python 3.X, e.g. `python310.dll` for Python 3.10 - `python3.dll` when using PyO3's `abi3` feature diff --git a/guide/src/getting_started.md b/guide/src/getting_started.md index 22bce336d80..235272dab20 100644 --- a/guide/src/getting_started.md +++ b/guide/src/getting_started.md @@ -2,6 +2,8 @@ To get started using PyO3 you will need three things: a Rust toolchain, a Python environment, and a way to build. We'll cover each of these below. +> If you'd like to chat to the PyO3 maintainers and other PyO3 users, consider joining the [PyO3 Discord server](https://discord.gg/c4BwayXQ). We're keen to hear about your experience getting started, so we can make PyO3 as accessible as possible for everyone! + ## Rust First, make sure you have Rust installed on your system. If you haven't already done so, try following the instructions [here](https://www.rust-lang.org/tools/install). PyO3 runs on both the `stable` and `nightly` versions so you can choose whichever one fits you best. The minimum required Rust version is 1.56. From 7c10ff4327b613b6239b0c270e2aaf490b0d6b45 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sun, 25 Feb 2024 08:13:36 +0100 Subject: [PATCH 042/936] allow `Bound<'_, T>` in #[pymethods] `self` position (#3896) * allow `Bound<'_, T>` in #[pymethods] `self` position * rename `TryFromPyCell` -> `TryFromBoundRef` * remove unneccessary unsafe --- pyo3-macros-backend/src/method.rs | 11 +++---- pyo3-macros-backend/src/pyclass.rs | 2 +- src/impl_/pymethods.rs | 40 +++++++++++++++++++++-- src/pycell.rs | 2 +- tests/ui/invalid_pymethod_receiver.stderr | 8 ++--- 5 files changed, 49 insertions(+), 14 deletions(-) diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index f492a330c92..2d21c265683 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -151,7 +151,7 @@ impl FnType { #[derive(Clone, Debug)] pub enum SelfType { Receiver { mutable: bool, span: Span }, - TryFromPyCell(Span), + TryFromBoundRef(Span), } #[derive(Clone, Copy)] @@ -204,15 +204,14 @@ impl SelfType { ) }) } - SelfType::TryFromPyCell(span) => { + SelfType::TryFromBoundRef(span) => { error_mode.handle_error( quote_spanned! { *span => - #py.from_borrowed_ptr::<_pyo3::PyAny>(#slf).downcast::<_pyo3::PyCell<#cls>>() + _pyo3::impl_::pymethods::BoundRef::ref_from_ptr(#py, &#slf).downcast::<#cls>() .map_err(::std::convert::Into::<_pyo3::PyErr>::into) .and_then( - #[allow(clippy::useless_conversion)] // In case slf is PyCell #[allow(unknown_lints, clippy::unnecessary_fallible_conversions)] // In case slf is Py (unknown_lints can be removed when MSRV is 1.75+) - |cell| ::std::convert::TryFrom::try_from(cell).map_err(::std::convert::Into::into) + |bound| ::std::convert::TryFrom::try_from(bound).map_err(::std::convert::Into::into) ) } @@ -291,7 +290,7 @@ pub fn parse_method_receiver(arg: &syn::FnArg) -> Result { if let syn::Type::ImplTrait(_) = &**ty { bail_spanned!(ty.span() => IMPL_TRAIT_ERR); } - Ok(SelfType::TryFromPyCell(ty.span())) + Ok(SelfType::TryFromBoundRef(ty.span())) } } } diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index ce39cb01196..784c39f71aa 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -1188,7 +1188,7 @@ fn complex_enum_variant_field_getter<'a>( ) -> Result { let signature = crate::pyfunction::FunctionSignature::from_arguments(vec![])?; - let self_type = crate::method::SelfType::TryFromPyCell(field_span); + let self_type = crate::method::SelfType::TryFromBoundRef(field_span); let spec = FnSpec { tp: crate::method::FnType::Getter(self_type.clone()), diff --git a/src/impl_/pymethods.rs b/src/impl_/pymethods.rs index 196766d0034..9afb6699269 100644 --- a/src/impl_/pymethods.rs +++ b/src/impl_/pymethods.rs @@ -3,10 +3,12 @@ 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, PyModule, PyType}; use crate::{ - ffi, Bound, Py, PyAny, PyCell, PyClass, PyErr, PyObject, PyResult, PyTraverseError, PyVisit, - Python, + ffi, Bound, DowncastError, Py, PyAny, PyCell, PyClass, PyErr, PyObject, PyRef, PyRefMut, + PyResult, PyTraverseError, PyTypeCheck, PyVisit, Python, }; use std::borrow::Cow; use std::ffi::CStr; @@ -490,6 +492,10 @@ impl<'a, 'py> BoundRef<'a, 'py, PyAny> { Bound::ref_from_ptr_or_opt(py, ptr).as_ref().map(BoundRef) } + pub fn downcast(self) -> Result, DowncastError<'a, 'py>> { + self.0.downcast::().map(BoundRef) + } + pub unsafe fn downcast_unchecked(self) -> BoundRef<'a, 'py, T> { BoundRef(self.0.downcast_unchecked::()) } @@ -511,6 +517,36 @@ impl<'a> From> for &'a PyModule { } } +impl<'a, 'py, T: PyClass> From> for &'a PyCell { + #[inline] + fn from(bound: BoundRef<'a, 'py, T>) -> Self { + bound.0.as_gil_ref() + } +} + +impl<'a, 'py, T: PyClass> TryFrom> for PyRef<'py, T> { + type Error = PyBorrowError; + #[inline] + fn try_from(value: BoundRef<'a, 'py, T>) -> Result { + value.0.clone().into_gil_ref().try_into() + } +} + +impl<'a, 'py, T: PyClass> TryFrom> for PyRefMut<'py, T> { + type Error = PyBorrowMutError; + #[inline] + fn try_from(value: BoundRef<'a, 'py, T>) -> Result { + value.0.clone().into_gil_ref().try_into() + } +} + +impl<'a, 'py, T> From> for Bound<'py, T> { + #[inline] + fn from(bound: BoundRef<'a, 'py, T>) -> Self { + bound.0.clone() + } +} + impl<'a, 'py, T> From> for &'a Bound<'py, T> { #[inline] fn from(bound: BoundRef<'a, 'py, T>) -> Self { diff --git a/src/pycell.rs b/src/pycell.rs index 989d039f9d2..71f2f7cc655 100644 --- a/src/pycell.rs +++ b/src/pycell.rs @@ -654,7 +654,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: 3)'"); +/// # pyo3::py_run!(py, sub, "assert sub.format() == 'Caterpillar(base: Butterfly, cnt: 4)'"); /// # }); /// ``` /// diff --git a/tests/ui/invalid_pymethod_receiver.stderr b/tests/ui/invalid_pymethod_receiver.stderr index b7a7880dbde..b6dd44bd9bf 100644 --- a/tests/ui/invalid_pymethod_receiver.stderr +++ b/tests/ui/invalid_pymethod_receiver.stderr @@ -1,8 +1,8 @@ -error[E0277]: the trait bound `i32: From<&PyCell>` is not satisfied +error[E0277]: the trait bound `i32: From>` is not satisfied --> tests/ui/invalid_pymethod_receiver.rs:8:43 | 8 | fn method_with_invalid_self_type(slf: i32, py: Python<'_>, index: u32) {} - | ^^^ the trait `From<&PyCell>` is not implemented for `i32` + | ^^^ the trait `From>` is not implemented for `i32` | = help: the following other types implement trait `From`: > @@ -11,5 +11,5 @@ error[E0277]: the trait bound `i32: From<&PyCell>` is not satisfied > > > - = note: required for `&PyCell` to implement `Into` - = note: required for `i32` to implement `TryFrom<&PyCell>` + = note: required for `BoundRef<'_, '_, MyClass>` to implement `Into` + = note: required for `i32` to implement `TryFrom>` From 404161c96972c6d6aa5a2b08096472a92b9600cb Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sun, 25 Feb 2024 09:49:22 +0000 Subject: [PATCH 043/936] ci: apply correct permissions for cache cleanup job (#3898) --- .github/workflows/cache-cleanup.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/cache-cleanup.yml b/.github/workflows/cache-cleanup.yml index 2833ed093ee..2b81a69ce88 100644 --- a/.github/workflows/cache-cleanup.yml +++ b/.github/workflows/cache-cleanup.yml @@ -7,6 +7,8 @@ on: jobs: cleanup: runs-on: ubuntu-latest + permissions: + actions: write steps: - name: Cleanup run: | From 8e2219b0d9685244b615bfaa1acab8d56498a866 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Mon, 26 Feb 2024 20:28:04 +0000 Subject: [PATCH 044/936] silence non-local-definitions nightly lint (#3901) * silence non-local-definitions nightly lint * add newsfragment * add FIXMEs for `non_local_definitions` * also allow `non_local_definitions` in doctests --- newsfragments/3901.fixed.md | 1 + pyo3-macros-backend/src/frompyobject.rs | 2 ++ pyo3-macros-backend/src/module.rs | 2 ++ pyo3-macros-backend/src/pyclass.rs | 6 ++++++ pyo3-macros-backend/src/pyfunction.rs | 3 +++ pyo3-macros-backend/src/pyimpl.rs | 2 ++ pyo3-macros-backend/src/pymethod.rs | 23 ++++++++++++----------- src/exceptions.rs | 2 ++ src/lib.rs | 9 ++++++++- src/types/mod.rs | 8 ++++++++ 10 files changed, 46 insertions(+), 12 deletions(-) create mode 100644 newsfragments/3901.fixed.md diff --git a/newsfragments/3901.fixed.md b/newsfragments/3901.fixed.md new file mode 100644 index 00000000000..0845c2bbcf5 --- /dev/null +++ b/newsfragments/3901.fixed.md @@ -0,0 +1 @@ +Fix `non_local_definitions` lint warning triggered by many PyO3 macros. diff --git a/pyo3-macros-backend/src/frompyobject.rs b/pyo3-macros-backend/src/frompyobject.rs index 5e193bf4a24..c1410180d05 100644 --- a/pyo3-macros-backend/src/frompyobject.rs +++ b/pyo3-macros-backend/src/frompyobject.rs @@ -604,6 +604,8 @@ pub fn build_derive_from_pyobject(tokens: &DeriveInput) -> Result { let ident = &tokens.ident; Ok(quote!( + // FIXME https://github.com/PyO3/pyo3/issues/3903 + #[allow(unknown_lints, non_local_definitions)] const _: () = { use #krate as _pyo3; use _pyo3::prelude::PyAnyMethods; diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index 6907e484f71..148cea6f8dd 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -211,6 +211,8 @@ 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) + // FIXME https://github.com/PyO3/pyo3/issues/3903 + #[allow(unknown_lints, non_local_definitions)] const _: () = { use #krate::impl_::pymodule as impl_; diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 784c39f71aa..1547e78b4c2 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -362,6 +362,8 @@ fn impl_class( .impl_all()?; Ok(quote! { + // FIXME https://github.com/PyO3/pyo3/issues/3903 + #[allow(unknown_lints, non_local_definitions)] const _: () = { use #krate as _pyo3; @@ -783,6 +785,8 @@ fn impl_simple_enum( .impl_all()?; Ok(quote! { + // FIXME https://github.com/PyO3/pyo3/issues/3903 + #[allow(unknown_lints, non_local_definitions)] const _: () = { use #krate as _pyo3; @@ -917,6 +921,8 @@ fn impl_complex_enum( } Ok(quote! { + // FIXME https://github.com/PyO3/pyo3/issues/3903 + #[allow(unknown_lints, non_local_definitions)] const _: () = { use #krate as _pyo3; diff --git a/pyo3-macros-backend/src/pyfunction.rs b/pyo3-macros-backend/src/pyfunction.rs index 7b48585cddc..38362d08a00 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -281,8 +281,11 @@ 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) + // FIXME https://github.com/PyO3/pyo3/issues/3903 + #[allow(unknown_lints, non_local_definitions)] const _: () = { use #krate as _pyo3; + impl #name::MakeDef { const DEF: #krate::impl_::pyfunction::PyMethodDef = #methoddef; } diff --git a/pyo3-macros-backend/src/pyimpl.rs b/pyo3-macros-backend/src/pyimpl.rs index f5ae111bf48..5802638e340 100644 --- a/pyo3-macros-backend/src/pyimpl.rs +++ b/pyo3-macros-backend/src/pyimpl.rs @@ -168,6 +168,8 @@ pub fn impl_methods( }; Ok(quote! { + // FIXME https://github.com/PyO3/pyo3/issues/3903 + #[allow(unknown_lints, non_local_definitions)] const _: () = { use #krate as _pyo3; diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index 74072f2a745..358d327ed0b 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -1241,6 +1241,18 @@ impl SlotFragmentDef { )?; let ret_ty = ret_ty.ffi_type(); Ok(quote! { + impl #cls { + unsafe fn #wrapper_ident( + py: _pyo3::Python, + _raw_slf: *mut _pyo3::ffi::PyObject, + #(#arg_idents: #arg_types),* + ) -> _pyo3::PyResult<#ret_ty> { + let _slf = _raw_slf; + #( #holders )* + #body + } + } + impl _pyo3::impl_::pyclass::#fragment_trait<#cls> for _pyo3::impl_::pyclass::PyClassImplCollector<#cls> { #[inline] @@ -1250,17 +1262,6 @@ impl SlotFragmentDef { _raw_slf: *mut _pyo3::ffi::PyObject, #(#arg_idents: #arg_types),* ) -> _pyo3::PyResult<#ret_ty> { - impl #cls { - unsafe fn #wrapper_ident( - py: _pyo3::Python, - _raw_slf: *mut _pyo3::ffi::PyObject, - #(#arg_idents: #arg_types),* - ) -> _pyo3::PyResult<#ret_ty> { - let _slf = _raw_slf; - #( #holders )* - #body - } - } #cls::#wrapper_ident(py, _raw_slf, #(#arg_idents),*) } } diff --git a/src/exceptions.rs b/src/exceptions.rs index da48475e0d6..071470160ae 100644 --- a/src/exceptions.rs +++ b/src/exceptions.rs @@ -19,6 +19,8 @@ use std::os::raw::c_char; #[macro_export] macro_rules! impl_exception_boilerplate { ($name: ident) => { + // FIXME https://github.com/PyO3/pyo3/issues/3903 + #[allow(unknown_lints, non_local_definitions)] impl ::std::convert::From<&$name> for $crate::PyErr { #[inline] fn from(err: &$name) -> $crate::PyErr { diff --git a/src/lib.rs b/src/lib.rs index 0a0b4eae684..6b39b534794 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,7 +10,14 @@ rust_2021_prelude_collisions, warnings ), - allow(unused_variables, unused_assignments, unused_extern_crates) + allow( + unused_variables, + unused_assignments, + unused_extern_crates, + // FIXME https://github.com/rust-lang/rust/issues/121621#issuecomment-1965156376 + unknown_lints, + non_local_definitions, + ) )))] //! Rust bindings to the Python interpreter. diff --git a/src/types/mod.rs b/src/types/mod.rs index 4baa086ab79..938716f78f4 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -188,6 +188,8 @@ macro_rules! pyobject_native_type_named ( } } + // FIXME https://github.com/PyO3/pyo3/issues/3903 + #[allow(unknown_lints, non_local_definitions)] impl<$($generics,)*> $crate::IntoPy<$crate::Py<$name>> for &'_ $name { #[inline] fn into_py(self, py: $crate::Python<'_>) -> $crate::Py<$name> { @@ -195,6 +197,8 @@ macro_rules! pyobject_native_type_named ( } } + // FIXME https://github.com/PyO3/pyo3/issues/3903 + #[allow(unknown_lints, non_local_definitions)] impl<$($generics,)*> ::std::convert::From<&'_ $name> for $crate::Py<$name> { #[inline] fn from(other: &$name) -> Self { @@ -203,6 +207,8 @@ macro_rules! pyobject_native_type_named ( } } + // FIXME https://github.com/PyO3/pyo3/issues/3903 + #[allow(unknown_lints, non_local_definitions)] 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)} @@ -252,6 +258,8 @@ macro_rules! pyobject_native_type_info( #[macro_export] macro_rules! pyobject_native_type_extract { ($name:ty $(;$generics:ident)*) => { + // FIXME https://github.com/PyO3/pyo3/issues/3903 + #[allow(unknown_lints, non_local_definitions)] impl<'py, $($generics,)*> $crate::FromPyObject<'py> for &'py $name { #[inline] fn extract_bound(obj: &$crate::Bound<'py, $crate::PyAny>) -> $crate::PyResult { From cd1c0dbf39c636321e6679d66b82294dca36969b Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Mon, 26 Feb 2024 21:47:35 +0000 Subject: [PATCH 045/936] ci: move cross compile tests to their own jobs (#3887) * ci: move cross compile tests to their own jobs * don't run cross-compile jobs on regular PRs --- .github/workflows/build.yml | 54 ----------------------- .github/workflows/ci.yml | 86 +++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 54 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0f3e707bfba..25c8014f762 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -138,60 +138,6 @@ jobs: if: ${{ endsWith(inputs.python-version, '-dev') || (steps.ffi-changes.outputs.changed == 'true' && inputs.rust == 'stable' && inputs.python-version != 'pypy3.7' && inputs.python-version != 'pypy3.8' && !(inputs.python-version == 'pypy3.9' && contains(inputs.os, 'windows'))) }} run: nox -s ffi-check - - - name: Test cross compilation - if: ${{ inputs.os == 'ubuntu-latest' && inputs.python-version == '3.9' }} - uses: PyO3/maturin-action@v1 - env: - PYO3_CROSS_LIB_DIR: /opt/python/cp39-cp39/lib - with: - target: aarch64-unknown-linux-gnu - manylinux: auto - args: --release -i python3.9 -m examples/maturin-starter/Cargo.toml - - - run: sudo rm -rf examples/maturin-starter/target - if: ${{ inputs.os == 'ubuntu-latest' && inputs.python-version == '3.9' }} - - name: Test cross compile to same architecture - if: ${{ inputs.os == 'ubuntu-latest' && inputs.python-version == '3.9' }} - uses: PyO3/maturin-action@v1 - env: - PYO3_CROSS_LIB_DIR: /opt/python/cp39-cp39/lib - with: - target: x86_64-unknown-linux-gnu - manylinux: auto - args: --release -i python3.9 -m examples/maturin-starter/Cargo.toml - - - name: Test cross compilation - if: ${{ inputs.os == 'macos-latest' && inputs.python-version == '3.9' }} - uses: PyO3/maturin-action@v1 - with: - target: aarch64-apple-darwin - args: --release -i python3.9 -m examples/maturin-starter/Cargo.toml - - - name: Test cross compile to Windows - if: ${{ inputs.os == 'ubuntu-latest' && inputs.python-version == '3.8' }} - env: - XWIN_ARCH: x86_64 - run: | - set -ex - sudo apt-get install -y mingw-w64 llvm - rustup target add x86_64-pc-windows-gnu x86_64-pc-windows-msvc - pip install cargo-xwin - # abi3 - cargo build --manifest-path examples/maturin-starter/Cargo.toml --features abi3 --target x86_64-pc-windows-gnu - cargo xwin build --manifest-path examples/maturin-starter/Cargo.toml --features abi3 --target x86_64-pc-windows-msvc - # non-abi3 - export PYO3_CROSS_PYTHON_VERSION=3.9 - cargo build --manifest-path examples/maturin-starter/Cargo.toml --features generate-import-lib --target x86_64-pc-windows-gnu - cargo xwin build --manifest-path examples/maturin-starter/Cargo.toml --features generate-import-lib --target x86_64-pc-windows-msvc - - - name: Test cross compile to Windows with maturin - if: ${{ inputs.os == 'ubuntu-latest' && inputs.python-version == '3.8' }} - uses: PyO3/maturin-action@v1 - with: - target: x86_64-pc-windows-gnu - args: -i python3.8 -m examples/maturin-starter/Cargo.toml --features abi3 - env: CARGO_TERM_VERBOSE: true CARGO_BUILD_TARGET: ${{ inputs.rust-target }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d22dc2236b1..5f4347aa6f1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -488,6 +488,90 @@ jobs: - run: python3 -m pip install --upgrade pip && pip install nox - run: python3 -m nox -s check-feature-powerset + test-cross-compilation: + needs: [fmt] + if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || github.event_name != 'pull_request' }} + runs-on: ${{ matrix.os }} + name: test-cross-compilation ${{ matrix.os }} -> ${{ matrix.target }} + strategy: + # If one platform fails, allow the rest to keep testing if `CI-no-fail-fast` label is present + fail-fast: ${{ !contains(github.event.pull_request.labels.*.name, 'CI-no-fail-fast') }} + matrix: + include: + # ubuntu "cross compile" to itself + - os: "ubuntu-latest" + target: "x86_64-unknown-linux-gnu" + flags: "-i python3.12" + manylinux: auto + # ubuntu x86_64 -> aarch64 + - os: "ubuntu-latest" + target: "aarch64-unknown-linux-gnu" + flags: "-i python3.12" + manylinux: auto + # ubuntu x86_64 -> windows x86_64 + - os: "ubuntu-latest" + target: "x86_64-pc-windows-gnu" + flags: "-i python3.12 --features abi3 --features generate-import-lib" + manylinux: off + # macos x86_64 -> aarch64 + - os: "macos-13" # last x86_64 macos runners + target: "aarch64-apple-darwin" + # macos aarch64 -> x86_64 + - os: "macos-14" # aarch64 macos runners + target: "x86_64-apple-darwin" + steps: + - uses: actions/checkout@v4 + - uses: Swatinem/rust-cache@v2 + with: + workspaces: + examples/maturin-starter + save-if: ${{ github.event_name != 'merge_group' }} + - name: Setup cross-compiler + if: ${{ matrix.target == 'x86_64-pc-windows-gnu' }} + run: sudo apt-get install -y mingw-w64 llvm + - uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.target }} + manylinux: ${{ matrix.manylinux }} + args: --release -m examples/maturin-starter/Cargo.toml ${{ matrix.flags }} + + test-cross-compilation-windows: + needs: [fmt] + if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || github.event_name != 'pull_request' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + - uses: Swatinem/rust-cache@v2 + with: + workspaces: + examples/maturin-starter + save-if: ${{ github.event_name != 'merge_group' }} + - uses: actions/cache/restore@v4 + with: + # https://github.com/PyO3/maturin/discussions/1953 + path: ~/.cache/cargo-xwin + key: cargo-xwin-cache + - name: Test cross compile to Windows + env: + XWIN_ARCH: x86_64 + run: | + set -ex + sudo apt-get install -y mingw-w64 llvm + rustup target add x86_64-pc-windows-gnu x86_64-pc-windows-msvc + pip install cargo-xwin + # abi3 + cargo build --manifest-path examples/maturin-starter/Cargo.toml --features abi3 --target x86_64-pc-windows-gnu + cargo xwin build --manifest-path examples/maturin-starter/Cargo.toml --features abi3 --target x86_64-pc-windows-msvc + # non-abi3 + export PYO3_CROSS_PYTHON_VERSION=3.12 + cargo build --manifest-path examples/maturin-starter/Cargo.toml --features generate-import-lib --target x86_64-pc-windows-gnu + cargo xwin build --manifest-path examples/maturin-starter/Cargo.toml --features generate-import-lib --target x86_64-pc-windows-msvc + - if: ${{ github.ref == 'refs/heads/main' }} + uses: actions/cache/save@v4 + with: + path: ~/.cache/cargo-xwin + key: cargo-xwin-cache conclusion: needs: - fmt @@ -503,6 +587,8 @@ jobs: - test-debug - test-version-limits - check-feature-powerset + - test-cross-compilation + - test-cross-compilation-windows if: always() runs-on: ubuntu-latest steps: From 5c41ea0aded350d7c70c15cfacc3862bc5dce8b2 Mon Sep 17 00:00:00 2001 From: Lily Foote Date: Mon, 26 Feb 2024 23:14:41 +0000 Subject: [PATCH 046/936] Implement `From>` for PyErr (#3881) * Implement `From>` for PyErr * Replace PyErr::from_value_bound calls with .into * Fix From expected error message * Add a trait bound to From> for PyErr --- src/err/mod.rs | 18 ++++++++++ src/exceptions.rs | 4 ++- src/lib.rs | 2 +- src/types/string.rs | 40 ++++++++++------------- tests/ui/invalid_result_conversion.stderr | 2 +- 5 files changed, 40 insertions(+), 26 deletions(-) diff --git a/src/err/mod.rs b/src/err/mod.rs index ab39e8cd46f..5e054449bc9 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -982,6 +982,24 @@ impl PyErrArguments for PyDowncastErrorArguments { } } +/// Python exceptions that can be converted to [`PyErr`]. +/// +/// This is used to implement [`From> for PyErr`]. +/// +/// Users should not need to implement this trait directly. It is implemented automatically in the +/// [`crate::import_exception!`] and [`crate::create_exception!`] macros. +pub trait ToPyErr {} + +impl<'py, T> std::convert::From> for PyErr +where + T: ToPyErr, +{ + #[inline] + fn from(err: Bound<'py, T>) -> PyErr { + PyErr::from_value_bound(err.into_any()) + } +} + /// Convert `PyDowncastError` to Python `TypeError`. impl<'a> std::convert::From> for PyErr { fn from(err: PyDowncastError<'_>) -> PyErr { diff --git a/src/exceptions.rs b/src/exceptions.rs index 071470160ae..1c952393241 100644 --- a/src/exceptions.rs +++ b/src/exceptions.rs @@ -54,6 +54,8 @@ macro_rules! impl_exception_boilerplate { } } } + + impl $crate::ToPyErr for $name {} }; } @@ -1074,7 +1076,7 @@ mod tests { ); // Restoring should preserve the same error - let e = PyErr::from_value_bound(decode_err.into_any()); + let e: PyErr = decode_err.into(); e.restore(py); assert_eq!( diff --git a/src/lib.rs b/src/lib.rs index 6b39b534794..d0c53c4a0c9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -308,7 +308,7 @@ pub use crate::conversion::{AsPyPointer, FromPyObject, FromPyPointer, IntoPy, To #[allow(deprecated)] pub use crate::conversion::{PyTryFrom, PyTryInto}; pub use crate::err::{ - DowncastError, DowncastIntoError, PyDowncastError, PyErr, PyErrArguments, PyResult, + DowncastError, DowncastIntoError, PyDowncastError, PyErr, PyErrArguments, PyResult, ToPyErr, }; pub use crate::gil::GILPool; #[cfg(not(PyPy))] diff --git a/src/types/string.rs b/src/types/string.rs index 0a7847d2959..4a33236b47b 100644 --- a/src/types/string.rs +++ b/src/types/string.rs @@ -73,9 +73,7 @@ impl<'a> PyStringData<'a> { match self { Self::Ucs1(data) => match str::from_utf8(data) { Ok(s) => Ok(Cow::Borrowed(s)), - Err(e) => Err(crate::PyErr::from_value_bound( - PyUnicodeDecodeError::new_utf8_bound(py, data, e)?.into_any(), - )), + Err(e) => Err(PyUnicodeDecodeError::new_utf8_bound(py, data, e)?.into()), }, Self::Ucs2(data) => match String::from_utf16(data) { Ok(s) => Ok(Cow::Owned(s)), @@ -83,30 +81,26 @@ impl<'a> PyStringData<'a> { let mut message = e.to_string().as_bytes().to_vec(); message.push(0); - Err(crate::PyErr::from_value_bound( - PyUnicodeDecodeError::new_bound( - py, - CStr::from_bytes_with_nul(b"utf-16\0").unwrap(), - self.as_bytes(), - 0..self.as_bytes().len(), - CStr::from_bytes_with_nul(&message).unwrap(), - )? - .into_any(), - )) - } - }, - Self::Ucs4(data) => match data.iter().map(|&c| std::char::from_u32(c)).collect() { - Some(s) => Ok(Cow::Owned(s)), - None => Err(crate::PyErr::from_value_bound( - PyUnicodeDecodeError::new_bound( + Err(PyUnicodeDecodeError::new_bound( py, - CStr::from_bytes_with_nul(b"utf-32\0").unwrap(), + CStr::from_bytes_with_nul(b"utf-16\0").unwrap(), self.as_bytes(), 0..self.as_bytes().len(), - CStr::from_bytes_with_nul(b"error converting utf-32\0").unwrap(), + CStr::from_bytes_with_nul(&message).unwrap(), )? - .into_any(), - )), + .into()) + } + }, + 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( + py, + CStr::from_bytes_with_nul(b"utf-32\0").unwrap(), + self.as_bytes(), + 0..self.as_bytes().len(), + CStr::from_bytes_with_nul(b"error converting utf-32\0").unwrap(), + )? + .into()), }, } } diff --git a/tests/ui/invalid_result_conversion.stderr b/tests/ui/invalid_result_conversion.stderr index b3e65517e36..73ed2cba1aa 100644 --- a/tests/ui/invalid_result_conversion.stderr +++ b/tests/ui/invalid_result_conversion.stderr @@ -5,6 +5,7 @@ error[E0277]: the trait bound `PyErr: From` is not satisfied | ^^^^^^^^^^^^^ the trait `From` is not implemented for `PyErr` | = help: the following other types implement trait `From`: + >> > > > @@ -12,7 +13,6 @@ 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 93704047a58335cc9de584325826aeac7abf84f7 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Tue, 27 Feb 2024 18:56:22 +0000 Subject: [PATCH 047/936] store `Bound` inside `PyRef` and `PyRefMut` (#3860) * store `Bound` inside `PyRef` and `PyRefMut` * update `FromPyObject` for `PyRef` to use `extract_bound` * review: Icxolu feedback --- src/conversion.rs | 11 ++-- src/impl_/pymethods.rs | 8 +-- src/instance.rs | 18 +++---- src/pycell.rs | 114 ++++++++++++++++++++++++++--------------- 4 files changed, 91 insertions(+), 60 deletions(-) diff --git a/src/conversion.rs b/src/conversion.rs index 415f11089df..fc407fa5bc8 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -4,6 +4,7 @@ use crate::err::{self, PyDowncastError, PyResult}; use crate::inspect::types::TypeInfo; use crate::pyclass::boolean_struct::False; use crate::type_object::PyTypeInfo; +use crate::types::any::PyAnyMethods; use crate::types::PyTuple; use crate::{ ffi, gil, Bound, Py, PyAny, PyCell, PyClass, PyNativeType, PyObject, PyRef, PyRefMut, Python, @@ -287,9 +288,8 @@ impl<'py, T> FromPyObject<'py> for PyRef<'py, T> where T: PyClass, { - fn extract(obj: &'py PyAny) -> PyResult { - let cell: &PyCell = obj.downcast()?; - cell.try_borrow().map_err(Into::into) + fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { + obj.downcast::()?.try_borrow().map_err(Into::into) } } @@ -297,9 +297,8 @@ impl<'py, T> FromPyObject<'py> for PyRefMut<'py, T> where T: PyClass, { - fn extract(obj: &'py PyAny) -> PyResult { - let cell: &PyCell = obj.downcast()?; - cell.try_borrow_mut().map_err(Into::into) + fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { + obj.downcast::()?.try_borrow_mut().map_err(Into::into) } } diff --git a/src/impl_/pymethods.rs b/src/impl_/pymethods.rs index 9afb6699269..0b73ce9bc73 100644 --- a/src/impl_/pymethods.rs +++ b/src/impl_/pymethods.rs @@ -7,8 +7,8 @@ use crate::pycell::{PyBorrowError, PyBorrowMutError}; use crate::pyclass::boolean_struct::False; use crate::types::{any::PyAnyMethods, PyModule, PyType}; use crate::{ - ffi, Bound, DowncastError, Py, PyAny, PyCell, PyClass, PyErr, PyObject, PyRef, PyRefMut, - PyResult, PyTraverseError, PyTypeCheck, PyVisit, Python, + ffi, Borrowed, Bound, DowncastError, Py, PyAny, PyCell, PyClass, PyErr, PyObject, PyRef, + PyRefMut, PyResult, PyTraverseError, PyTypeCheck, PyVisit, Python, }; use std::borrow::Cow; use std::ffi::CStr; @@ -272,8 +272,8 @@ where let trap = PanicTrap::new("uncaught panic inside __traverse__ handler"); let py = Python::assume_gil_acquired(); - let slf = py.from_borrowed_ptr::>(slf); - let borrow = slf.try_borrow_threadsafe(); + let slf = Borrowed::from_ptr_unchecked(py, slf).downcast_unchecked::(); + let borrow = PyRef::try_borrow_threadsafe(&slf); let visit = PyVisit::from_raw(visit, arg, py); let retval = if let Ok(borrow) = borrow { diff --git a/src/instance.rs b/src/instance.rs index 3efdf58f597..cb33ed32c75 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -242,8 +242,8 @@ where /// /// Panics if the value is currently mutably borrowed. For a non-panicking variant, use /// [`try_borrow`](#method.try_borrow). - pub fn borrow(&'py self) -> PyRef<'py, T> { - self.get_cell().borrow() + pub fn borrow(&self) -> PyRef<'py, T> { + PyRef::borrow(self) } /// Mutably borrows the value `T`. @@ -275,11 +275,11 @@ where /// # 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(&'py self) -> PyRefMut<'py, T> + pub fn borrow_mut(&self) -> PyRefMut<'py, T> where T: PyClass, { - self.get_cell().borrow_mut() + PyRefMut::borrow(self) } /// Attempts to immutably borrow the value `T`, returning an error if the value is currently mutably borrowed. @@ -289,8 +289,8 @@ where /// This is the non-panicking variant of [`borrow`](#method.borrow). /// /// For frozen classes, the simpler [`get`][Self::get] is available. - pub fn try_borrow(&'py self) -> Result, PyBorrowError> { - self.get_cell().try_borrow() + pub fn try_borrow(&self) -> Result, PyBorrowError> { + PyRef::try_borrow(self) } /// Attempts to mutably borrow the value `T`, returning an error if the value is currently borrowed. @@ -298,11 +298,11 @@ where /// The borrow lasts while the returned [`PyRefMut`] exists. /// /// This is the non-panicking variant of [`borrow_mut`](#method.borrow_mut). - pub fn try_borrow_mut(&'py self) -> Result, PyBorrowMutError> + pub fn try_borrow_mut(&self) -> Result, PyBorrowMutError> where T: PyClass, { - self.get_cell().try_borrow_mut() + PyRefMut::try_borrow(self) } /// Provide an immutable borrow of the value `T` without acquiring the GIL. @@ -337,7 +337,7 @@ where unsafe { &*cell.get_ptr() } } - fn get_cell(&'py self) -> &'py PyCell { + pub(crate) fn get_cell(&'py self) -> &'py PyCell { let cell = self.as_ptr().cast::>(); // SAFETY: Bound is known to contain an object which is laid out in memory as a // PyCell. diff --git a/src/pycell.rs b/src/pycell.rs index 71f2f7cc655..29fd1b37886 100644 --- a/src/pycell.rs +++ b/src/pycell.rs @@ -192,6 +192,7 @@ //! [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::exceptions::PyRuntimeError; +use crate::ffi_ptr_ext::FfiPtrExt; use crate::impl_::pyclass::{ PyClassBaseType, PyClassDict, PyClassImpl, PyClassThreadChecker, PyClassWeakRef, }; @@ -201,6 +202,7 @@ use crate::pyclass::{ }; use crate::pyclass_init::PyClassInitializer; use crate::type_object::{PyLayout, PySizedLayout}; +use crate::types::any::PyAnyMethods; use crate::types::PyAny; use crate::{ conversion::{AsPyPointer, FromPyPointer, ToPyObject}, @@ -310,7 +312,7 @@ impl PyCell { /// Panics if the value is currently mutably borrowed. For a non-panicking variant, use /// [`try_borrow`](#method.try_borrow). pub fn borrow(&self) -> PyRef<'_, T> { - self.try_borrow().expect("Already mutably borrowed") + PyRef::borrow(&self.as_borrowed()) } /// Mutably borrows the value `T`. This borrow lasts as long as the returned `PyRefMut` exists. @@ -323,7 +325,7 @@ impl PyCell { where T: PyClass, { - self.try_borrow_mut().expect("Already borrowed") + PyRefMut::borrow(&self.as_borrowed()) } /// Immutably borrows the value `T`, returning an error if the value is currently @@ -355,18 +357,7 @@ impl PyCell { /// }); /// ``` pub fn try_borrow(&self) -> Result, PyBorrowError> { - self.ensure_threadsafe(); - self.borrow_checker() - .try_borrow() - .map(|_| PyRef { inner: self }) - } - - /// Variant of [`try_borrow`][Self::try_borrow] which fails instead of panicking if called from the wrong thread - pub(crate) fn try_borrow_threadsafe(&self) -> Result, PyBorrowError> { - self.check_threadsafe()?; - self.borrow_checker() - .try_borrow() - .map(|_| PyRef { inner: self }) + PyRef::try_borrow(&self.as_borrowed()) } /// Mutably borrows the value `T`, returning an error if the value is currently borrowed. @@ -395,10 +386,7 @@ impl PyCell { where T: PyClass, { - self.ensure_threadsafe(); - self.borrow_checker() - .try_borrow_mut() - .map(|_| PyRefMut { inner: self }) + PyRefMut::try_borrow(&self.as_borrowed()) } /// Immutably borrows the value `T`, returning an error if the value is @@ -654,13 +642,15 @@ 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: 4)'"); +/// # pyo3::py_run!(py, sub, "assert sub.format() == 'Caterpillar(base: Butterfly, cnt: 5)', sub.format()"); /// # }); /// ``` /// /// See the [module-level documentation](self) for more information. pub struct PyRef<'p, T: PyClass> { - inner: &'p PyCell, + // TODO: once the GIL Ref API is removed, consider adding a lifetime parameter to `PyRef` to + // store `Borrowed` here instead, avoiding reference counting overhead. + inner: Bound<'p, T>, } impl<'p, T: PyClass> PyRef<'p, T> { @@ -676,11 +666,11 @@ where U: PyClass, { fn as_ref(&self) -> &T::BaseType { - unsafe { &*self.inner.ob_base.get_ptr() } + unsafe { &*self.inner.get_cell().ob_base.get_ptr() } } } -impl<'p, T: PyClass> PyRef<'p, T> { +impl<'py, T: PyClass> PyRef<'py, T> { /// Returns the raw FFI pointer represented by self. /// /// # Safety @@ -702,7 +692,27 @@ impl<'p, T: PyClass> PyRef<'p, T> { /// of the pointer or decrease the reference count (e.g. with [`pyo3::ffi::Py_DecRef`](crate::ffi::Py_DecRef)). #[inline] pub fn into_ptr(self) -> *mut ffi::PyObject { - self.inner.into_ptr() + self.inner.clone().into_ptr() + } + + pub(crate) fn borrow(obj: &Bound<'py, T>) -> Self { + Self::try_borrow(obj).expect("Already mutably borrowed") + } + + pub(crate) fn try_borrow(obj: &Bound<'py, T>) -> Result { + let cell = obj.get_cell(); + cell.ensure_threadsafe(); + cell.borrow_checker() + .try_borrow() + .map(|_| Self { inner: obj.clone() }) + } + + pub(crate) fn try_borrow_threadsafe(obj: &Bound<'py, T>) -> Result { + let cell = obj.get_cell(); + cell.check_threadsafe()?; + cell.borrow_checker() + .try_borrow() + .map(|_| Self { inner: obj.clone() }) } } @@ -757,10 +767,14 @@ where /// # }); /// ``` pub fn into_super(self) -> PyRef<'p, U> { - let PyRef { inner } = self; - std::mem::forget(self); + let py = self.py(); PyRef { - inner: &inner.ob_base, + inner: unsafe { + ManuallyDrop::new(self) + .as_ptr() + .assume_owned(py) + .downcast_into_unchecked() + }, } } } @@ -770,13 +784,13 @@ impl<'p, T: PyClass> Deref for PyRef<'p, T> { #[inline] fn deref(&self) -> &T { - unsafe { &*self.inner.get_ptr() } + unsafe { &*self.inner.get_cell().get_ptr() } } } impl<'p, T: PyClass> Drop for PyRef<'p, T> { fn drop(&mut self) { - self.inner.borrow_checker().release_borrow() + self.inner.get_cell().borrow_checker().release_borrow() } } @@ -788,7 +802,7 @@ impl IntoPy for PyRef<'_, T> { impl IntoPy for &'_ PyRef<'_, T> { fn into_py(self, py: Python<'_>) -> PyObject { - self.inner.into_py(py) + unsafe { PyObject::from_borrowed_ptr(py, self.inner.as_ptr()) } } } @@ -815,7 +829,9 @@ impl fmt::Debug for PyRef<'_, T> { /// /// See the [module-level documentation](self) for more information. pub struct PyRefMut<'p, T: PyClass> { - inner: &'p PyCell, + // TODO: once the GIL Ref API is removed, consider adding a lifetime parameter to `PyRef` to + // store `Borrowed` here instead, avoiding reference counting overhead. + inner: Bound<'p, T>, } impl<'p, T: PyClass> PyRefMut<'p, T> { @@ -831,7 +847,7 @@ where U: PyClass, { fn as_ref(&self) -> &T::BaseType { - unsafe { &*self.inner.ob_base.get_ptr() } + unsafe { &*self.inner.get_cell().ob_base.get_ptr() } } } @@ -841,11 +857,11 @@ where U: PyClass, { fn as_mut(&mut self) -> &mut T::BaseType { - unsafe { &mut *self.inner.ob_base.get_ptr() } + unsafe { &mut *self.inner.get_cell().ob_base.get_ptr() } } } -impl<'p, T: PyClass> PyRefMut<'p, T> { +impl<'py, T: PyClass> PyRefMut<'py, T> { /// Returns the raw FFI pointer represented by self. /// /// # Safety @@ -867,7 +883,19 @@ impl<'p, T: PyClass> PyRefMut<'p, T> { /// of the pointer or decrease the reference count (e.g. with [`pyo3::ffi::Py_DecRef`](crate::ffi::Py_DecRef)). #[inline] pub fn into_ptr(self) -> *mut ffi::PyObject { - self.inner.into_ptr() + self.inner.clone().into_ptr() + } + + pub(crate) fn borrow(obj: &Bound<'py, T>) -> Self { + Self::try_borrow(obj).expect("Already borrowed") + } + + pub(crate) fn try_borrow(obj: &Bound<'py, T>) -> Result { + let cell = obj.get_cell(); + cell.ensure_threadsafe(); + cell.borrow_checker() + .try_borrow_mut() + .map(|_| Self { inner: obj.clone() }) } } @@ -880,10 +908,14 @@ where /// /// See [`PyRef::into_super`] for more. pub fn into_super(self) -> PyRefMut<'p, U> { - let PyRefMut { inner } = self; - std::mem::forget(self); + let py = self.py(); PyRefMut { - inner: &inner.ob_base, + inner: unsafe { + ManuallyDrop::new(self) + .as_ptr() + .assume_owned(py) + .downcast_into_unchecked() + }, } } } @@ -893,20 +925,20 @@ impl<'p, T: PyClass> Deref for PyRefMut<'p, T> { #[inline] fn deref(&self) -> &T { - unsafe { &*self.inner.get_ptr() } + unsafe { &*self.inner.get_cell().get_ptr() } } } impl<'p, T: PyClass> DerefMut for PyRefMut<'p, T> { #[inline] fn deref_mut(&mut self) -> &mut T { - unsafe { &mut *self.inner.get_ptr() } + unsafe { &mut *self.inner.get_cell().get_ptr() } } } impl<'p, T: PyClass> Drop for PyRefMut<'p, T> { fn drop(&mut self) { - self.inner.borrow_checker().release_borrow_mut() + self.inner.get_cell().borrow_checker().release_borrow_mut() } } @@ -918,7 +950,7 @@ impl> IntoPy for PyRefMut<'_, T> { impl> IntoPy for &'_ PyRefMut<'_, T> { fn into_py(self, py: Python<'_>) -> PyObject { - self.inner.into_py(py) + self.inner.clone().into_py(py) } } From a3ad28b70ca847459b3f5d8b8644beb826268cc2 Mon Sep 17 00:00:00 2001 From: Lily Foote Date: Tue, 27 Feb 2024 19:19:52 +0000 Subject: [PATCH 048/936] Pymodule bound (#3897) * Support Bound in pymodule and pyfunction macros Co-authored-by: David Hewitt * Remove spurious $ character Co-authored-by: Matthew Neeley * Rework PyCFunction::new_bound signatures This allows us to remove the awkward `PyFunctionArgumentsBound` enum. * Use BoundRef instead of BoundModule * support argument deduction for `wrap_pyfunction_bound!` * support `wrap_pyfunction!` with `Bound` input/output * Fix docs link to `wrap_pyfunction_bound!` * Revert back to wrap_pyfunction! --------- Co-authored-by: David Hewitt Co-authored-by: Matthew Neeley --- guide/src/module.md | 2 +- guide/src/python_from_rust.md | 2 +- pyo3-macros-backend/src/module.rs | 13 +++-- pyo3-macros-backend/src/pyfunction.rs | 6 +-- src/impl_/pyfunction.rs | 70 +++++++++++++++++++++--- src/macros.rs | 45 ++++++++++++++-- src/prelude.rs | 2 +- src/types/function.rs | 72 ++++++++++++++++++++----- src/types/module.rs | 7 +-- tests/test_module.rs | 12 ++--- tests/test_pyfunction.rs | 6 ++- tests/test_wrap_pyfunction_deduction.rs | 9 ++++ 12 files changed, 203 insertions(+), 43 deletions(-) diff --git a/guide/src/module.md b/guide/src/module.md index 789c3d91ccb..53c390bec06 100644 --- a/guide/src/module.md +++ b/guide/src/module.md @@ -79,7 +79,7 @@ fn parent_module(py: Python<'_>, m: &PyModule) -> PyResult<()> { fn register_child_module(py: Python<'_>, parent_module: &PyModule) -> PyResult<()> { let child_module = PyModule::new_bound(py, "child_module")?; - child_module.add_function(&wrap_pyfunction!(func, child_module.as_gil_ref())?.as_borrowed())?; + child_module.add_function(wrap_pyfunction!(func, &child_module)?)?; parent_module.add_submodule(child_module.as_gil_ref())?; Ok(()) } diff --git a/guide/src/python_from_rust.md b/guide/src/python_from_rust.md index 973f997cbf8..b51bbe592eb 100644 --- a/guide/src/python_from_rust.md +++ b/guide/src/python_from_rust.md @@ -311,7 +311,7 @@ fn main() -> PyResult<()> { Python::with_gil(|py| { // Create new module let foo_module = PyModule::new_bound(py, "foo")?; - foo_module.add_function(&wrap_pyfunction!(add_one, foo_module.as_gil_ref())?.as_borrowed())?; + foo_module.add_function(wrap_pyfunction!(add_one, &foo_module)?)?; // Import and get sys.modules let sys = PyModule::import_bound(py, "sys")?; diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index 148cea6f8dd..40cf34f22fd 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -215,15 +215,16 @@ pub fn pymodule_function_impl(mut function: syn::ItemFn) -> Result #[allow(unknown_lints, non_local_definitions)] const _: () = { use #krate::impl_::pymodule as impl_; + use #krate::impl_::pymethods::BoundRef; fn __pyo3_pymodule(module: &#krate::Bound<'_, #krate::types::PyModule>) -> #krate::PyResult<()> { - #ident(module.py(), module.as_gil_ref()) + #ident(module.py(), ::std::convert::Into::into(BoundRef(module))) } impl #ident::MakeDef { const fn make_def() -> impl_::ModuleDef { + const INITIALIZER: impl_::ModuleInitializer = impl_::ModuleInitializer(__pyo3_pymodule); unsafe { - const INITIALIZER: impl_::ModuleInitializer = impl_::ModuleInitializer(__pyo3_pymodule); impl_::ModuleDef::new( #ident::__PYO3_NAME, #doc, @@ -263,9 +264,13 @@ fn module_initialization(options: PyModuleOptions, ident: &syn::Ident) -> TokenS /// Finds and takes care of the #[pyfn(...)] in `#[pymodule]` fn process_functions_in_module(options: &PyModuleOptions, func: &mut syn::ItemFn) -> Result<()> { - let mut stmts: Vec = Vec::new(); let krate = get_pyo3_crate(&options.krate); + let mut stmts: Vec = vec![syn::parse_quote!( + #[allow(unknown_lints, unused_imports, redundant_imports)] + use #krate::{PyNativeType, 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)? { @@ -274,7 +279,7 @@ 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.add_function(#krate::impl_::pyfunction::_wrap_pyfunction(&#name::DEF, #module_name)?)?; + #module_name.as_borrowed().add_function(#krate::wrap_pyfunction!(#name, #module_name.as_borrowed())?)?; }; stmts.extend(statements); } diff --git a/pyo3-macros-backend/src/pyfunction.rs b/pyo3-macros-backend/src/pyfunction.rs index 38362d08a00..265a4824109 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -268,12 +268,12 @@ pub fn impl_wrap_pyfunction( #[doc(hidden)] #vis mod #name { pub(crate) struct MakeDef; - pub const DEF: #krate::impl_::pyfunction::PyMethodDef = MakeDef::DEF; + pub const DEF: #krate::impl_::pymethods::PyMethodDef = MakeDef::DEF; pub fn add_to_module(module: &#krate::Bound<'_, #krate::types::PyModule>) -> #krate::PyResult<()> { use #krate::prelude::PyModuleMethods; use ::std::convert::Into; - module.add_function(&#krate::types::PyCFunction::internal_new(&DEF, module.as_gil_ref().into())?) + module.add_function(#krate::types::PyCFunction::internal_new(&DEF, module.as_gil_ref().into())?) } } @@ -287,7 +287,7 @@ pub fn impl_wrap_pyfunction( use #krate as _pyo3; impl #name::MakeDef { - const DEF: #krate::impl_::pyfunction::PyMethodDef = #methoddef; + const DEF: #krate::impl_::pymethods::PyMethodDef = #methoddef; } #[allow(non_snake_case)] diff --git a/src/impl_/pyfunction.rs b/src/impl_/pyfunction.rs index 464beeb8ce5..531e0c93192 100644 --- a/src/impl_/pyfunction.rs +++ b/src/impl_/pyfunction.rs @@ -1,10 +1,68 @@ -use crate::{derive_utils::PyFunctionArguments, types::PyCFunction, PyResult}; +use crate::{ + types::{PyCFunction, PyModule}, + Borrowed, Bound, PyResult, Python, +}; pub use crate::impl_::pymethods::PyMethodDef; -pub fn _wrap_pyfunction<'a>( - method_def: &PyMethodDef, - py_or_module: impl Into>, -) -> PyResult<&'a PyCFunction> { - PyCFunction::internal_new(method_def, py_or_module.into()).map(|x| x.into_gil_ref()) +/// Trait to enable the use of `wrap_pyfunction` with both `Python` and `PyModule`, +/// and also to infer the return type of either `&'py PyCFunction` or `Bound<'py, PyCFunction>`. +pub trait WrapPyFunctionArg<'py, T> { + fn wrap_pyfunction(self, method_def: &PyMethodDef) -> PyResult; +} + +impl<'py> WrapPyFunctionArg<'py, Bound<'py, PyCFunction>> for Bound<'py, PyModule> { + fn wrap_pyfunction(self, method_def: &PyMethodDef) -> PyResult> { + PyCFunction::internal_new_bound(self.py(), method_def, Some(&self)) + } +} + +impl<'py> WrapPyFunctionArg<'py, Bound<'py, PyCFunction>> for &'_ Bound<'py, PyModule> { + fn wrap_pyfunction(self, method_def: &PyMethodDef) -> PyResult> { + PyCFunction::internal_new_bound(self.py(), method_def, Some(self)) + } +} + +impl<'py> WrapPyFunctionArg<'py, Bound<'py, PyCFunction>> for Borrowed<'_, 'py, PyModule> { + fn wrap_pyfunction(self, method_def: &PyMethodDef) -> PyResult> { + PyCFunction::internal_new_bound(self.py(), method_def, Some(&self)) + } +} + +impl<'py> WrapPyFunctionArg<'py, Bound<'py, PyCFunction>> for &'_ Borrowed<'_, 'py, PyModule> { + fn wrap_pyfunction(self, method_def: &PyMethodDef) -> PyResult> { + PyCFunction::internal_new_bound(self.py(), method_def, Some(self)) + } +} + +// For Python<'py>, only the GIL Ref form exists to avoid causing type inference to kick in. +// The `wrap_pyfunction_bound!` macro is needed for the Bound form. +impl<'py> WrapPyFunctionArg<'py, &'py PyCFunction> for Python<'py> { + fn wrap_pyfunction(self, method_def: &PyMethodDef) -> PyResult<&'py PyCFunction> { + PyCFunction::internal_new(method_def, self.into()).map(Bound::into_gil_ref) + } +} + +impl<'py> WrapPyFunctionArg<'py, &'py PyCFunction> for &'py PyModule { + fn wrap_pyfunction(self, method_def: &PyMethodDef) -> PyResult<&'py PyCFunction> { + PyCFunction::internal_new(method_def, self.into()).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) + } +} + +impl<'py> WrapPyFunctionArg<'py, Bound<'py, PyCFunction>> for OnlyBound> { + fn wrap_pyfunction(self, method_def: &PyMethodDef) -> PyResult> { + PyCFunction::internal_new_bound(self.0, method_def, None) + } } diff --git a/src/macros.rs b/src/macros.rs index 9b0d2816882..33f378e7b83 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -121,18 +121,57 @@ macro_rules! py_run_impl { /// Wraps a Rust function annotated with [`#[pyfunction]`](macro@crate::pyfunction). /// /// This can be used with [`PyModule::add_function`](crate::types::PyModule::add_function) to add free -/// functions to a [`PyModule`](crate::types::PyModule) - see its documentation for more information. +/// functions to a [`PyModule`](crate::types::PyModule) - see its documentation for more +/// information. +/// +/// During the migration from the GIL Ref API to the Bound API, the return type of this macro will +/// be either the `&'py PyModule` GIL Ref or `Bound<'py, PyModule>` according to the second +/// argument. +/// +/// For backwards compatibility, if the second argument is `Python<'py>` then the return type will +/// be `&'py PyModule` GIL Ref. To get `Bound<'py, PyModule>`, use the [`crate::wrap_pyfunction_bound!`] +/// macro instead. #[macro_export] macro_rules! wrap_pyfunction { ($function:path) => { &|py_or_module| { use $function as wrapped_pyfunction; - $crate::impl_::pyfunction::_wrap_pyfunction(&wrapped_pyfunction::DEF, py_or_module) + $crate::impl_::pyfunction::WrapPyFunctionArg::wrap_pyfunction( + py_or_module, + &wrapped_pyfunction::DEF, + ) + } + }; + ($function:path, $py_or_module:expr) => {{ + use $function as wrapped_pyfunction; + $crate::impl_::pyfunction::WrapPyFunctionArg::wrap_pyfunction( + $py_or_module, + &wrapped_pyfunction::DEF, + ) + }}; +} + +/// 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. +#[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::DEF, + ) } }; ($function:path, $py_or_module:expr) => {{ use $function as wrapped_pyfunction; - $crate::impl_::pyfunction::_wrap_pyfunction(&wrapped_pyfunction::DEF, $py_or_module) + $crate::impl_::pyfunction::WrapPyFunctionArg::wrap_pyfunction( + $crate::impl_::pyfunction::OnlyBound($py_or_module), + &wrapped_pyfunction::DEF, + ) }}; } diff --git a/src/prelude.rs b/src/prelude.rs index 1de7c3acd2d..dcf4fe71cdd 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -23,7 +23,7 @@ pub use crate::PyNativeType; pub use pyo3_macros::{pyclass, pyfunction, pymethods, pymodule, FromPyObject}; #[cfg(feature = "macros")] -pub use crate::wrap_pyfunction; +pub use crate::{wrap_pyfunction, wrap_pyfunction_bound}; pub use crate::types::any::PyAnyMethods; pub use crate::types::boolobject::PyBoolMethods; diff --git a/src/types/function.rs b/src/types/function.rs index 7cbb05e2a48..75173d2aadb 100644 --- a/src/types/function.rs +++ b/src/types/function.rs @@ -3,10 +3,11 @@ use crate::ffi_ptr_ext::FfiPtrExt; use crate::methods::PyMethodDefDestructor; use crate::py_result_ext::PyResultExt; use crate::types::capsule::PyCapsuleMethods; +use crate::types::module::PyModuleMethods; use crate::{ ffi, impl_::pymethods::{self, PyMethodDef}, - types::{PyCapsule, PyDict, PyString, PyTuple}, + types::{PyCapsule, PyDict, PyModule, PyString, PyTuple}, }; use crate::{Bound, IntoPy, Py, PyAny, PyResult, Python}; use std::cell::UnsafeCell; @@ -33,23 +34,33 @@ impl PyCFunction { doc: &'static str, py_or_module: PyFunctionArguments<'a>, ) -> PyResult<&'a Self> { - Self::new_with_keywords_bound(fun, name, doc, py_or_module).map(Bound::into_gil_ref) + Self::internal_new( + &PyMethodDef::cfunction_with_keywords( + name, + pymethods::PyCFunctionWithKeywords(fun), + doc, + ), + py_or_module, + ) + .map(Bound::into_gil_ref) } /// Create a new built-in function with keywords (*args and/or **kwargs). - pub fn new_with_keywords_bound<'a>( + pub fn new_with_keywords_bound<'py>( + py: Python<'py>, fun: ffi::PyCFunctionWithKeywords, name: &'static str, doc: &'static str, - py_or_module: PyFunctionArguments<'a>, - ) -> PyResult> { - Self::internal_new( + module: Option<&Bound<'py, PyModule>>, + ) -> PyResult> { + Self::internal_new_bound( + py, &PyMethodDef::cfunction_with_keywords( name, pymethods::PyCFunctionWithKeywords(fun), doc, ), - py_or_module, + module, ) } @@ -67,19 +78,25 @@ impl PyCFunction { doc: &'static str, py_or_module: PyFunctionArguments<'a>, ) -> PyResult<&'a Self> { - Self::new_bound(fun, name, doc, py_or_module).map(Bound::into_gil_ref) + Self::internal_new( + &PyMethodDef::noargs(name, pymethods::PyCFunction(fun), doc), + py_or_module, + ) + .map(Bound::into_gil_ref) } /// Create a new built-in function which takes no arguments. - pub fn new_bound<'a>( + pub fn new_bound<'py>( + py: Python<'py>, fun: ffi::PyCFunction, name: &'static str, doc: &'static str, - py_or_module: PyFunctionArguments<'a>, - ) -> PyResult> { - Self::internal_new( + module: Option<&Bound<'py, PyModule>>, + ) -> PyResult> { + Self::internal_new_bound( + py, &PyMethodDef::noargs(name, pymethods::PyCFunction(fun), doc), - py_or_module, + module, ) } @@ -189,6 +206,35 @@ impl PyCFunction { .downcast_into_unchecked() } } + + #[doc(hidden)] + pub(crate) fn internal_new_bound<'py>( + py: Python<'py>, + method_def: &PyMethodDef, + module: Option<&Bound<'py, PyModule>>, + ) -> PyResult> { + let (mod_ptr, module_name): (_, Option>) = if let Some(m) = module { + let mod_ptr = m.as_ptr(); + (mod_ptr, Some(m.name()?.into_py(py))) + } else { + (std::ptr::null_mut(), None) + }; + let (def, destructor) = method_def.as_method_def()?; + + // FIXME: stop leaking the def and destructor + let def = Box::into_raw(Box::new(def)); + std::mem::forget(destructor); + + let module_name_ptr = module_name + .as_ref() + .map_or(std::ptr::null_mut(), Py::as_ptr); + + unsafe { + ffi::PyCFunction_NewEx(def, mod_ptr, module_name_ptr) + .assume_owned_or_err(py) + .downcast_into_unchecked() + } + } } fn closure_capsule_name() -> &'static CStr { diff --git a/src/types/module.rs b/src/types/module.rs index 245d38d9e08..75fc6625846 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -399,7 +399,8 @@ impl PyModule { /// [1]: crate::prelude::pyfunction /// [2]: crate::wrap_pyfunction pub fn add_function<'a>(&'a self, fun: &'a PyCFunction) -> PyResult<()> { - self.as_borrowed().add_function(&fun.as_borrowed()) + let name = fun.getattr(__name__(self.py()))?.extract()?; + self.add(name, fun) } } @@ -590,7 +591,7 @@ pub trait PyModuleMethods<'py> { /// /// [1]: crate::prelude::pyfunction /// [2]: crate::wrap_pyfunction - fn add_function(&self, fun: &Bound<'_, PyCFunction>) -> PyResult<()>; + fn add_function(&self, fun: Bound<'_, PyCFunction>) -> PyResult<()>; } impl<'py> PyModuleMethods<'py> for Bound<'py, PyModule> { @@ -700,7 +701,7 @@ impl<'py> PyModuleMethods<'py> for Bound<'py, PyModule> { self.add(name, module) } - fn add_function(&self, fun: &Bound<'_, PyCFunction>) -> PyResult<()> { + fn add_function(&self, fun: Bound<'_, PyCFunction>) -> PyResult<()> { let name = fun.getattr(__name__(self.py()))?; self.add(name.downcast_into::()?, fun) } diff --git a/tests/test_module.rs b/tests/test_module.rs index 9d14f243d50..5763043e57d 100644 --- a/tests/test_module.rs +++ b/tests/test_module.rs @@ -240,7 +240,7 @@ fn subfunction() -> String { } fn submodule(module: &Bound<'_, PyModule>) -> PyResult<()> { - module.add_function(&wrap_pyfunction!(subfunction, module.as_gil_ref())?.as_borrowed())?; + module.add_function(wrap_pyfunction!(subfunction, module)?)?; Ok(()) } @@ -295,14 +295,14 @@ fn test_module_nesting() { // Test that argument parsing specification works for pyfunctions #[pyfunction(signature = (a=5, *args))] -fn ext_vararg_fn(py: Python<'_>, a: i32, args: &PyTuple) -> PyObject { - [a.to_object(py), args.into()].to_object(py) +fn ext_vararg_fn(py: Python<'_>, a: i32, args: &Bound<'_, PyTuple>) -> PyObject { + [a.to_object(py), args.into_py(py)].to_object(py) } #[pymodule] -fn vararg_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn vararg_module(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { #[pyfn(m, signature = (a=5, *args))] - fn int_vararg_fn(py: Python<'_>, a: i32, args: &PyTuple) -> PyObject { + fn int_vararg_fn(py: Python<'_>, a: i32, args: &Bound<'_, PyTuple>) -> PyObject { ext_vararg_fn(py, a, args) } @@ -410,7 +410,7 @@ fn pyfunction_with_pass_module_in_attribute(module: &PyModule) -> PyResult<&str> } #[pymodule] -fn module_with_functions_with_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn module_with_functions_with_module(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(pyfunction_with_module, m)?)?; m.add_function(wrap_pyfunction!(pyfunction_with_module_gil_ref, m)?)?; m.add_function(wrap_pyfunction!(pyfunction_with_module_owned, m)?)?; diff --git a/tests/test_pyfunction.rs b/tests/test_pyfunction.rs index 14748418687..838f7353737 100644 --- a/tests/test_pyfunction.rs +++ b/tests/test_pyfunction.rs @@ -324,10 +324,11 @@ fn test_pycfunction_new() { } let py_fn = PyCFunction::new_bound( + py, c_fn, "py_fn", "py_fn for test (this is the docstring)", - py.into(), + None, ) .unwrap(); @@ -381,10 +382,11 @@ 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)", - py.into(), + None, ) .unwrap(); diff --git a/tests/test_wrap_pyfunction_deduction.rs b/tests/test_wrap_pyfunction_deduction.rs index 8723ad43fbd..52c9adcb6d7 100644 --- a/tests/test_wrap_pyfunction_deduction.rs +++ b/tests/test_wrap_pyfunction_deduction.rs @@ -13,3 +13,12 @@ pub fn add_wrapped(wrapper: &impl Fn(Python<'_>) -> PyResult<&PyCFunction>) { fn wrap_pyfunction_deduction() { add_wrapped(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)); +} From 6f03a5464f53a7f76e406eb7d09610324cd8667c Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Tue, 27 Feb 2024 23:15:35 +0100 Subject: [PATCH 049/936] cleans up `PyCFunction::internal_new` (#3910) This deduplicates some code around `PyCFunction::internal_new` --- pyo3-macros-backend/src/pyfunction.rs | 2 +- src/impl_/pyfunction.rs | 17 ++++++----- src/types/function.rs | 43 ++++++--------------------- 3 files changed, 19 insertions(+), 43 deletions(-) diff --git a/pyo3-macros-backend/src/pyfunction.rs b/pyo3-macros-backend/src/pyfunction.rs index 265a4824109..45de94e4a10 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -273,7 +273,7 @@ pub fn impl_wrap_pyfunction( pub fn add_to_module(module: &#krate::Bound<'_, #krate::types::PyModule>) -> #krate::PyResult<()> { use #krate::prelude::PyModuleMethods; use ::std::convert::Into; - module.add_function(#krate::types::PyCFunction::internal_new(&DEF, module.as_gil_ref().into())?) + module.add_function(#krate::types::PyCFunction::internal_new(module.py(), &DEF, module.into())?) } } diff --git a/src/impl_/pyfunction.rs b/src/impl_/pyfunction.rs index 531e0c93192..cb838fea6c2 100644 --- a/src/impl_/pyfunction.rs +++ b/src/impl_/pyfunction.rs @@ -1,6 +1,6 @@ use crate::{ types::{PyCFunction, PyModule}, - Borrowed, Bound, PyResult, Python, + Borrowed, Bound, PyNativeType, PyResult, Python, }; pub use crate::impl_::pymethods::PyMethodDef; @@ -13,25 +13,25 @@ pub trait WrapPyFunctionArg<'py, T> { impl<'py> WrapPyFunctionArg<'py, Bound<'py, PyCFunction>> for Bound<'py, PyModule> { fn wrap_pyfunction(self, method_def: &PyMethodDef) -> PyResult> { - PyCFunction::internal_new_bound(self.py(), method_def, Some(&self)) + PyCFunction::internal_new(self.py(), method_def, Some(&self)) } } impl<'py> WrapPyFunctionArg<'py, Bound<'py, PyCFunction>> for &'_ Bound<'py, PyModule> { fn wrap_pyfunction(self, method_def: &PyMethodDef) -> PyResult> { - PyCFunction::internal_new_bound(self.py(), method_def, Some(self)) + PyCFunction::internal_new(self.py(), method_def, Some(self)) } } impl<'py> WrapPyFunctionArg<'py, Bound<'py, PyCFunction>> for Borrowed<'_, 'py, PyModule> { fn wrap_pyfunction(self, method_def: &PyMethodDef) -> PyResult> { - PyCFunction::internal_new_bound(self.py(), method_def, Some(&self)) + PyCFunction::internal_new(self.py(), method_def, Some(&self)) } } impl<'py> WrapPyFunctionArg<'py, Bound<'py, PyCFunction>> for &'_ Borrowed<'_, 'py, PyModule> { fn wrap_pyfunction(self, method_def: &PyMethodDef) -> PyResult> { - PyCFunction::internal_new_bound(self.py(), method_def, Some(self)) + PyCFunction::internal_new(self.py(), method_def, Some(self)) } } @@ -39,13 +39,14 @@ impl<'py> WrapPyFunctionArg<'py, Bound<'py, PyCFunction>> for &'_ Borrowed<'_, ' // The `wrap_pyfunction_bound!` macro is needed for the Bound form. impl<'py> WrapPyFunctionArg<'py, &'py PyCFunction> for Python<'py> { fn wrap_pyfunction(self, method_def: &PyMethodDef) -> PyResult<&'py PyCFunction> { - PyCFunction::internal_new(method_def, self.into()).map(Bound::into_gil_ref) + PyCFunction::internal_new(self, method_def, None).map(Bound::into_gil_ref) } } impl<'py> WrapPyFunctionArg<'py, &'py PyCFunction> for &'py PyModule { fn wrap_pyfunction(self, method_def: &PyMethodDef) -> PyResult<&'py PyCFunction> { - PyCFunction::internal_new(method_def, self.into()).map(Bound::into_gil_ref) + PyCFunction::internal_new(self.py(), method_def, Some(&self.as_borrowed())) + .map(Bound::into_gil_ref) } } @@ -63,6 +64,6 @@ where impl<'py> WrapPyFunctionArg<'py, Bound<'py, PyCFunction>> for OnlyBound> { fn wrap_pyfunction(self, method_def: &PyMethodDef) -> PyResult> { - PyCFunction::internal_new_bound(self.0, method_def, None) + PyCFunction::internal_new(self.0, method_def, None) } } diff --git a/src/types/function.rs b/src/types/function.rs index 75173d2aadb..ea8201fb131 100644 --- a/src/types/function.rs +++ b/src/types/function.rs @@ -9,7 +9,7 @@ use crate::{ impl_::pymethods::{self, PyMethodDef}, types::{PyCapsule, PyDict, PyModule, PyString, PyTuple}, }; -use crate::{Bound, IntoPy, Py, PyAny, PyResult, Python}; +use crate::{Bound, IntoPy, Py, PyAny, PyNativeType, PyResult, Python}; use std::cell::UnsafeCell; use std::ffi::CStr; @@ -34,13 +34,15 @@ impl PyCFunction { doc: &'static str, py_or_module: PyFunctionArguments<'a>, ) -> PyResult<&'a Self> { + let (py, module) = py_or_module.into_py_and_maybe_module(); Self::internal_new( + py, &PyMethodDef::cfunction_with_keywords( name, pymethods::PyCFunctionWithKeywords(fun), doc, ), - py_or_module, + module.map(PyNativeType::as_borrowed).as_deref(), ) .map(Bound::into_gil_ref) } @@ -53,7 +55,7 @@ impl PyCFunction { doc: &'static str, module: Option<&Bound<'py, PyModule>>, ) -> PyResult> { - Self::internal_new_bound( + Self::internal_new( py, &PyMethodDef::cfunction_with_keywords( name, @@ -78,9 +80,11 @@ impl PyCFunction { doc: &'static str, py_or_module: PyFunctionArguments<'a>, ) -> PyResult<&'a Self> { + let (py, module) = py_or_module.into_py_and_maybe_module(); Self::internal_new( + py, &PyMethodDef::noargs(name, pymethods::PyCFunction(fun), doc), - py_or_module, + module.map(PyNativeType::as_borrowed).as_deref(), ) .map(Bound::into_gil_ref) } @@ -93,7 +97,7 @@ impl PyCFunction { doc: &'static str, module: Option<&Bound<'py, PyModule>>, ) -> PyResult> { - Self::internal_new_bound( + Self::internal_new( py, &PyMethodDef::noargs(name, pymethods::PyCFunction(fun), doc), module, @@ -180,35 +184,6 @@ impl PyCFunction { #[doc(hidden)] pub fn internal_new<'py>( - method_def: &PyMethodDef, - py_or_module: PyFunctionArguments<'py>, - ) -> PyResult> { - let (py, module) = py_or_module.into_py_and_maybe_module(); - 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))) - } else { - (std::ptr::null_mut(), None) - }; - let (def, destructor) = method_def.as_method_def()?; - - // FIXME: stop leaking the def and destructor - let def = Box::into_raw(Box::new(def)); - std::mem::forget(destructor); - - let module_name_ptr = module_name - .as_ref() - .map_or(std::ptr::null_mut(), Py::as_ptr); - - unsafe { - ffi::PyCFunction_NewEx(def, mod_ptr, module_name_ptr) - .assume_owned_or_err(py) - .downcast_into_unchecked() - } - } - - #[doc(hidden)] - pub(crate) fn internal_new_bound<'py>( py: Python<'py>, method_def: &PyMethodDef, module: Option<&Bound<'py, PyModule>>, From a15e4b1a116cb57edaf2f18c3a6bbd75dfb2c003 Mon Sep 17 00:00:00 2001 From: Matthew Neeley Date: Tue, 27 Feb 2024 14:24:14 -0800 Subject: [PATCH 050/936] Allow pymodule functions to take a single Bound<'_, PyModule> arg (#3905) --- newsfragments/3905.changed.md | 1 + pyo3-macros-backend/src/module.rs | 10 +++++++++- src/tests/hygiene/pyfunction.rs | 8 ++++++++ src/tests/hygiene/pymodule.rs | 15 +++++++++++++++ tests/test_no_imports.rs | 12 ++++++++++++ 5 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 newsfragments/3905.changed.md diff --git a/newsfragments/3905.changed.md b/newsfragments/3905.changed.md new file mode 100644 index 00000000000..917584eb72a --- /dev/null +++ b/newsfragments/3905.changed.md @@ -0,0 +1 @@ +The `#[pymodule]` macro now supports module functions that take a single argument as a `&Bound<'_, PyModule>`. diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index 40cf34f22fd..0bdb1cbc50c 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -201,6 +201,14 @@ pub fn pymodule_function_impl(mut function: syn::ItemFn) -> Result let doc = get_doc(&function.attrs, None); let initialization = module_initialization(options, ident); + + // Module function called with optional Python<'_> marker as first arg, followed by the module. + let mut module_args = Vec::new(); + if function.sig.inputs.len() == 2 { + module_args.push(quote!(module.py())); + } + module_args.push(quote!(::std::convert::Into::into(BoundRef(module)))); + Ok(quote! { #function #vis mod #ident { @@ -218,7 +226,7 @@ pub fn pymodule_function_impl(mut function: syn::ItemFn) -> Result use #krate::impl_::pymethods::BoundRef; fn __pyo3_pymodule(module: &#krate::Bound<'_, #krate::types::PyModule>) -> #krate::PyResult<()> { - #ident(module.py(), ::std::convert::Into::into(BoundRef(module))) + #ident(#(#module_args),*) } impl #ident::MakeDef { diff --git a/src/tests/hygiene/pyfunction.rs b/src/tests/hygiene/pyfunction.rs index 19fe2739407..9cfad0db6c6 100644 --- a/src/tests/hygiene/pyfunction.rs +++ b/src/tests/hygiene/pyfunction.rs @@ -14,3 +14,11 @@ fn invoke_wrap_pyfunction() { crate::py_run!(py, func, r#"func(5)"#); }); } + +#[test] +fn invoke_wrap_pyfunction_bound() { + crate::Python::with_gil(|py| { + let func = crate::wrap_pyfunction_bound!(do_something, py).unwrap(); + crate::py_run!(py, func, r#"func(5)"#); + }); +} diff --git a/src/tests/hygiene/pymodule.rs b/src/tests/hygiene/pymodule.rs index 0b37c440325..bb49d3823c1 100644 --- a/src/tests/hygiene/pymodule.rs +++ b/src/tests/hygiene/pymodule.rs @@ -21,3 +21,18 @@ fn my_module(_py: crate::Python<'_>, m: &crate::types::PyModule) -> crate::PyRes ::std::result::Result::Ok(()) } + +#[crate::pymodule] +#[pyo3(crate = "crate")] +fn my_module_bound(m: &crate::Bound<'_, crate::types::PyModule>) -> crate::PyResult<()> { + as crate::types::PyModuleMethods>::add_function( + m, + crate::wrap_pyfunction_bound!(do_something, m)?, + )?; + as crate::types::PyModuleMethods>::add_wrapped( + m, + crate::wrap_pymodule!(foo), + )?; + + ::std::result::Result::Ok(()) +} diff --git a/tests/test_no_imports.rs b/tests/test_no_imports.rs index 88932ed282a..69f4b6e4651 100644 --- a/tests/test_no_imports.rs +++ b/tests/test_no_imports.rs @@ -22,6 +22,18 @@ fn basic_module(_py: pyo3::Python<'_>, m: &pyo3::types::PyModule) -> pyo3::PyRes Ok(()) } +#[pyo3::pymodule] +fn basic_module_bound(m: &pyo3::Bound<'_, pyo3::types::PyModule>) -> pyo3::PyResult<()> { + #[pyfn(m)] + fn answer() -> usize { + 42 + } + + m.add_function(pyo3::wrap_pyfunction_bound!(basic_function, m)?)?; + + Ok(()) +} + #[pyo3::pyclass] struct BasicClass { #[pyo3(get)] From 8a12970c9694f8340318235173e53b80ee9d4ff7 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Wed, 28 Feb 2024 19:36:20 +0000 Subject: [PATCH 051/936] update `extract_argument` to use Bound APIs (#3708) * update `extract_argument` to use `Bound` APIs * tidy up borrow in macros expression * update `trybuild` output * more concise form for `DowncastError::new` Co-authored-by: Lily Foote * use `Borrowed` instead of newtype * use `Borrowed::from_ptr` methods in extract_argument * update UI tests * avoid double-negative `#[cfg]` clauses Co-authored-by: Lily Foote * review: LilyFoote, Icxolu feedback --------- Co-authored-by: Lily Foote --- guide/src/class.md | 4 +- pyo3-benches/benches/bench_comparisons.rs | 8 +- pyo3-macros-backend/src/method.rs | 12 +- pyo3-macros-backend/src/params.rs | 18 +-- pyo3-macros-backend/src/pyclass.rs | 6 +- pyo3-macros-backend/src/pymethod.rs | 31 ++-- pytests/src/pyfunctions.rs | 74 +++++----- src/impl_/extract_argument.rs | 172 ++++++++++++++-------- src/impl_/pymethods.rs | 17 ++- src/types/dict.rs | 68 +++++++++ src/types/string.rs | 2 +- src/types/tuple.rs | 6 +- 12 files changed, 274 insertions(+), 144 deletions(-) diff --git a/guide/src/class.md b/guide/src/class.md index 9662e8f6e09..8ce896f5a61 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -1254,7 +1254,7 @@ impl<'a, 'py> pyo3::impl_::extract_argument::PyFunctionArgument<'a, 'py> for &'a type Holder = ::std::option::Option>; #[inline] - fn extract(obj: &'py pyo3::PyAny, holder: &'a mut Self::Holder) -> pyo3::PyResult { + fn extract(obj: &'a pyo3::Bound<'py, PyAny>, holder: &'a mut Self::Holder) -> pyo3::PyResult { pyo3::impl_::extract_argument::extract_pyclass_ref(obj, holder) } } @@ -1264,7 +1264,7 @@ impl<'a, 'py> pyo3::impl_::extract_argument::PyFunctionArgument<'a, 'py> for &'a type Holder = ::std::option::Option>; #[inline] - fn extract(obj: &'py pyo3::PyAny, holder: &'a mut Self::Holder) -> pyo3::PyResult { + fn extract(obj: &'a pyo3::Bound<'py, PyAny>, holder: &'a mut Self::Holder) -> pyo3::PyResult { pyo3::impl_::extract_argument::extract_pyclass_ref_mut(obj, holder) } } diff --git a/pyo3-benches/benches/bench_comparisons.rs b/pyo3-benches/benches/bench_comparisons.rs index ffd4c1a452f..fbd473f06cf 100644 --- a/pyo3-benches/benches/bench_comparisons.rs +++ b/pyo3-benches/benches/bench_comparisons.rs @@ -45,8 +45,8 @@ impl OrderedRichcmp { fn bench_ordered_dunder_methods(b: &mut Bencher<'_>) { Python::with_gil(|py| { - let obj1 = Py::new(py, OrderedDunderMethods(0)).unwrap().into_ref(py); - let obj2 = Py::new(py, OrderedDunderMethods(1)).unwrap().into_ref(py); + let obj1 = &Bound::new(py, OrderedDunderMethods(0)).unwrap().into_any(); + let obj2 = &Bound::new(py, OrderedDunderMethods(1)).unwrap().into_any(); b.iter(|| obj2.gt(obj1).unwrap()); }); @@ -54,8 +54,8 @@ fn bench_ordered_dunder_methods(b: &mut Bencher<'_>) { fn bench_ordered_richcmp(b: &mut Bencher<'_>) { Python::with_gil(|py| { - let obj1 = Py::new(py, OrderedRichcmp(0)).unwrap().into_ref(py); - let obj2 = Py::new(py, OrderedRichcmp(1)).unwrap().into_ref(py); + let obj1 = &Bound::new(py, OrderedRichcmp(0)).unwrap().into_any(); + let obj2 = &Bound::new(py, OrderedRichcmp(1)).unwrap().into_any(); b.iter(|| obj2.gt(obj1).unwrap()); }); diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index 2d21c265683..6ee87fb99a6 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -196,10 +196,11 @@ impl SelfType { holders.push(quote_spanned! { *span => #[allow(clippy::let_unit_value)] let mut #holder = _pyo3::impl_::extract_argument::FunctionArgumentHolder::INIT; + let mut #slf = _pyo3::impl_::pymethods::BoundRef::ref_from_ptr(#py, &#slf); }); error_mode.handle_error(quote_spanned! { *span => _pyo3::impl_::extract_argument::#method::<#cls>( - #py.from_borrowed_ptr::<_pyo3::PyAny>(#slf), + &#slf, &mut #holder, ) }) @@ -582,7 +583,8 @@ impl<'a> FnSpec<'a> { ) -> _pyo3::PyResult<*mut _pyo3::ffi::PyObject> { let function = #rust_name; // Shadow the function name to avoid #3017 #( #holders )* - #call + let result = #call; + result } } } @@ -601,7 +603,8 @@ impl<'a> FnSpec<'a> { let function = #rust_name; // Shadow the function name to avoid #3017 #arg_convert #( #holders )* - #call + let result = #call; + result } } } @@ -619,7 +622,8 @@ impl<'a> FnSpec<'a> { let function = #rust_name; // Shadow the function name to avoid #3017 #arg_convert #( #holders )* - #call + let result = #call; + result } } } diff --git a/pyo3-macros-backend/src/params.rs b/pyo3-macros-backend/src/params.rs index 3781b41b765..679d3e4260a 100644 --- a/pyo3-macros-backend/src/params.rs +++ b/pyo3-macros-backend/src/params.rs @@ -44,8 +44,8 @@ pub fn impl_arg_params( .collect::>()?; return Ok(( quote! { - let _args = py.from_borrowed_ptr::<_pyo3::types::PyTuple>(_args); - let _kwargs: ::std::option::Option<&_pyo3::types::PyDict> = py.from_borrowed_ptr_or_opt(_kwargs); + let _args = _pyo3::impl_::pymethods::BoundRef::ref_from_ptr(py, &_args); + let _kwargs = _pyo3::impl_::pymethods::BoundRef::ref_from_ptr_or_opt(py, &_kwargs); }, arg_convert, )); @@ -180,7 +180,7 @@ fn impl_arg_param( let holder = push_holder(); return Ok(quote_arg_span! { _pyo3::impl_::extract_argument::extract_argument( - _args, + &_args, &mut #holder, #name_str )? @@ -193,7 +193,7 @@ fn impl_arg_param( let holder = push_holder(); return Ok(quote_arg_span! { _pyo3::impl_::extract_argument::extract_optional_argument( - _kwargs.map(::std::convert::AsRef::as_ref), + _kwargs.as_deref(), &mut #holder, #name_str, || ::std::option::Option::None @@ -217,7 +217,7 @@ fn impl_arg_param( quote_arg_span! { #[allow(clippy::redundant_closure)] _pyo3::impl_::extract_argument::from_py_with_with_default( - #arg_value.map(_pyo3::PyNativeType::as_borrowed).as_deref(), + #arg_value.as_deref(), #name_str, #expr_path as fn(_) -> _, || #default @@ -226,7 +226,7 @@ fn impl_arg_param( } else { quote_arg_span! { _pyo3::impl_::extract_argument::from_py_with( - &_pyo3::impl_::extract_argument::unwrap_required_argument(#arg_value).as_borrowed(), + &_pyo3::impl_::extract_argument::unwrap_required_argument(#arg_value), #name_str, #expr_path as fn(_) -> _, )? @@ -237,7 +237,7 @@ fn impl_arg_param( quote_arg_span! { #[allow(clippy::redundant_closure)] _pyo3::impl_::extract_argument::extract_optional_argument( - #arg_value, + #arg_value.as_deref(), &mut #holder, #name_str, || #default @@ -248,7 +248,7 @@ fn impl_arg_param( quote_arg_span! { #[allow(clippy::redundant_closure)] _pyo3::impl_::extract_argument::extract_argument_with_default( - #arg_value, + #arg_value.as_deref(), &mut #holder, #name_str, || #default @@ -258,7 +258,7 @@ fn impl_arg_param( let holder = push_holder(); quote_arg_span! { _pyo3::impl_::extract_argument::extract_argument( - _pyo3::impl_::extract_argument::unwrap_required_argument(#arg_value), + &_pyo3::impl_::extract_argument::unwrap_required_argument(#arg_value), &mut #holder, #name_str )? diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 1547e78b4c2..5ec6ac172e6 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -1378,7 +1378,7 @@ impl<'a> PyClassImplsBuilder<'a> { type Holder = ::std::option::Option<_pyo3::PyRef<'py, #cls>>; #[inline] - fn extract(obj: &'py _pyo3::PyAny, holder: &'a mut Self::Holder) -> _pyo3::PyResult { + fn extract(obj: &'a _pyo3::Bound<'py, _pyo3::PyAny>, holder: &'a mut Self::Holder) -> _pyo3::PyResult { _pyo3::impl_::extract_argument::extract_pyclass_ref(obj, holder) } } @@ -1390,7 +1390,7 @@ impl<'a> PyClassImplsBuilder<'a> { type Holder = ::std::option::Option<_pyo3::PyRef<'py, #cls>>; #[inline] - fn extract(obj: &'py _pyo3::PyAny, holder: &'a mut Self::Holder) -> _pyo3::PyResult { + fn extract(obj: &'a _pyo3::Bound<'py, _pyo3::PyAny>, holder: &'a mut Self::Holder) -> _pyo3::PyResult { _pyo3::impl_::extract_argument::extract_pyclass_ref(obj, holder) } } @@ -1400,7 +1400,7 @@ impl<'a> PyClassImplsBuilder<'a> { type Holder = ::std::option::Option<_pyo3::PyRefMut<'py, #cls>>; #[inline] - fn extract(obj: &'py _pyo3::PyAny, holder: &'a mut Self::Holder) -> _pyo3::PyResult { + fn extract(obj: &'a _pyo3::Bound<'py, _pyo3::PyAny>, holder: &'a mut Self::Holder) -> _pyo3::PyResult { _pyo3::impl_::extract_argument::extract_pyclass_ref_mut(obj, holder) } } diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index 358d327ed0b..4cb07a9ad2a 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -698,7 +698,8 @@ pub fn impl_py_getter_def( _slf: *mut _pyo3::ffi::PyObject ) -> _pyo3::PyResult<*mut _pyo3::ffi::PyObject> { #( #holders )* - #body + let result = #body; + result } }; @@ -930,39 +931,31 @@ impl Ty { extract_error_mode, holders, &name_str, - quote! { - py.from_borrowed_ptr::<_pyo3::PyAny>(#ident) - }, + quote! { #ident }, ), Ty::MaybeNullObject => extract_object( extract_error_mode, holders, &name_str, quote! { - py.from_borrowed_ptr::<_pyo3::PyAny>( - if #ident.is_null() { - _pyo3::ffi::Py_None() - } else { - #ident - } - ) + if #ident.is_null() { + _pyo3::ffi::Py_None() + } else { + #ident + } }, ), Ty::NonNullObject => extract_object( extract_error_mode, holders, &name_str, - quote! { - py.from_borrowed_ptr::<_pyo3::PyAny>(#ident.as_ptr()) - }, + quote! { #ident.as_ptr() }, ), Ty::IPowModulo => extract_object( extract_error_mode, holders, &name_str, - quote! { - #ident.to_borrowed_any(py) - }, + quote! { #ident.as_ptr() }, ), Ty::CompareOp => extract_error_mode.handle_error( quote! { @@ -988,7 +981,7 @@ fn extract_object( extract_error_mode: ExtractErrorMode, holders: &mut Vec, name: &str, - source: TokenStream, + source_ptr: TokenStream, ) -> TokenStream { let holder = syn::Ident::new(&format!("holder_{}", holders.len()), Span::call_site()); holders.push(quote! { @@ -997,7 +990,7 @@ fn extract_object( }); extract_error_mode.handle_error(quote! { _pyo3::impl_::extract_argument::extract_argument( - #source, + &_pyo3::impl_::pymethods::BoundRef::ref_from_ptr(py, &#source_ptr), &mut #holder, #name ) diff --git a/pytests/src/pyfunctions.rs b/pytests/src/pyfunctions.rs index 1eef970430e..a92733c2898 100644 --- a/pytests/src/pyfunctions.rs +++ b/pytests/src/pyfunctions.rs @@ -4,62 +4,66 @@ use pyo3::types::{PyDict, PyTuple}; #[pyfunction(signature = ())] fn none() {} +type Any<'py> = Bound<'py, PyAny>; +type Dict<'py> = Bound<'py, PyDict>; +type Tuple<'py> = Bound<'py, PyTuple>; + #[pyfunction(signature = (a, b = None, *, c = None))] -fn simple<'a>( - a: &'a PyAny, - b: Option<&'a PyAny>, - c: Option<&'a PyAny>, -) -> (&'a PyAny, Option<&'a PyAny>, Option<&'a PyAny>) { +fn simple<'py>( + a: Any<'py>, + b: Option>, + c: Option>, +) -> (Any<'py>, Option>, Option>) { (a, b, c) } #[pyfunction(signature = (a, b = None, *args, c = None))] -fn simple_args<'a>( - a: &'a PyAny, - b: Option<&'a PyAny>, - args: &'a PyTuple, - c: Option<&'a PyAny>, -) -> (&'a PyAny, Option<&'a PyAny>, &'a PyTuple, Option<&'a PyAny>) { +fn simple_args<'py>( + a: Any<'py>, + b: Option>, + args: Tuple<'py>, + c: Option>, +) -> (Any<'py>, Option>, Tuple<'py>, Option>) { (a, b, args, c) } #[pyfunction(signature = (a, b = None, c = None, **kwargs))] -fn simple_kwargs<'a>( - a: &'a PyAny, - b: Option<&'a PyAny>, - c: Option<&'a PyAny>, - kwargs: Option<&'a PyDict>, +fn simple_kwargs<'py>( + a: Any<'py>, + b: Option>, + c: Option>, + kwargs: Option>, ) -> ( - &'a PyAny, - Option<&'a PyAny>, - Option<&'a PyAny>, - Option<&'a PyDict>, + Any<'py>, + Option>, + Option>, + Option>, ) { (a, b, c, kwargs) } #[pyfunction(signature = (a, b = None, *args, c = None, **kwargs))] -fn simple_args_kwargs<'a>( - a: &'a PyAny, - b: Option<&'a PyAny>, - args: &'a PyTuple, - c: Option<&'a PyAny>, - kwargs: Option<&'a PyDict>, +fn simple_args_kwargs<'py>( + a: Any<'py>, + b: Option>, + args: Tuple<'py>, + c: Option>, + kwargs: Option>, ) -> ( - &'a PyAny, - Option<&'a PyAny>, - &'a PyTuple, - Option<&'a PyAny>, - Option<&'a PyDict>, + Any<'py>, + Option>, + Tuple<'py>, + Option>, + Option>, ) { (a, b, args, c, kwargs) } #[pyfunction(signature = (*args, **kwargs))] -fn args_kwargs<'a>( - args: &'a PyTuple, - kwargs: Option<&'a PyDict>, -) -> (&'a PyTuple, Option<&'a PyDict>) { +fn args_kwargs<'py>( + args: Tuple<'py>, + kwargs: Option>, +) -> (Tuple<'py>, Option>) { (args, kwargs) } diff --git a/src/impl_/extract_argument.rs b/src/impl_/extract_argument.rs index 27728c6d46f..d2e2b18cf05 100644 --- a/src/impl_/extract_argument.rs +++ b/src/impl_/extract_argument.rs @@ -2,10 +2,16 @@ use crate::{ exceptions::PyTypeError, ffi, pyclass::boolean_struct::False, - types::{PyDict, PyString, PyTuple}, - Bound, FromPyObject, PyAny, PyClass, PyErr, PyRef, PyRefMut, PyResult, PyTypeCheck, Python, + types::{any::PyAnyMethods, dict::PyDictMethods, tuple::PyTupleMethods, PyDict, PyTuple}, + Borrowed, Bound, FromPyObject, PyAny, PyClass, PyErr, PyRef, PyRefMut, PyResult, PyTypeCheck, + Python, }; +/// Helper type used to keep implementation more concise. +/// +/// (Function argument extraction borrows input arguments.) +type PyArg<'py> = Borrowed<'py, 'py, PyAny>; + /// A trait which is used to help PyO3 macros extract function arguments. /// /// `#[pyclass]` structs need to extract as `PyRef` and `PyRefMut` @@ -16,7 +22,7 @@ use crate::{ /// There exists a trivial blanket implementation for `T: FromPyObject` with `Holder = ()`. pub trait PyFunctionArgument<'a, 'py>: Sized + 'a { type Holder: FunctionArgumentHolder; - fn extract(obj: &'py PyAny, holder: &'a mut Self::Holder) -> PyResult; + fn extract(obj: &'a Bound<'py, PyAny>, holder: &'a mut Self::Holder) -> PyResult; } impl<'a, 'py, T> PyFunctionArgument<'a, 'py> for T @@ -26,20 +32,23 @@ where type Holder = (); #[inline] - fn extract(obj: &'py PyAny, _: &'a mut ()) -> PyResult { + fn extract(obj: &'a Bound<'py, PyAny>, _: &'a mut ()) -> PyResult { obj.extract() } } -impl<'a, 'py, T> PyFunctionArgument<'a, 'py> for &'a Bound<'py, T> +impl<'a, 'py, T: 'py> PyFunctionArgument<'a, 'py> for &'a Bound<'py, T> where T: PyTypeCheck, { - type Holder = Option>; + type Holder = Option<&'a Bound<'py, T>>; #[inline] - fn extract(obj: &'py PyAny, holder: &'a mut Option>) -> PyResult { - Ok(&*holder.insert(obj.extract()?)) + fn extract( + obj: &'a Bound<'py, PyAny>, + holder: &'a mut Option<&'a Bound<'py, T>>, + ) -> PyResult { + Ok(holder.insert(obj.downcast()?)) } } @@ -59,7 +68,7 @@ impl FunctionArgumentHolder for Option { #[inline] pub fn extract_pyclass_ref<'a, 'py: 'a, T: PyClass>( - obj: &'py PyAny, + obj: &'a Bound<'py, PyAny>, holder: &'a mut Option>, ) -> PyResult<&'a T> { Ok(&*holder.insert(obj.extract()?)) @@ -67,7 +76,7 @@ pub fn extract_pyclass_ref<'a, 'py: 'a, T: PyClass>( #[inline] pub fn extract_pyclass_ref_mut<'a, 'py: 'a, T: PyClass>( - obj: &'py PyAny, + obj: &'a Bound<'py, PyAny>, holder: &'a mut Option>, ) -> PyResult<&'a mut T> { Ok(&mut *holder.insert(obj.extract()?)) @@ -76,7 +85,7 @@ pub fn extract_pyclass_ref_mut<'a, 'py: 'a, T: PyClass>( /// The standard implementation of how PyO3 extracts a `#[pyfunction]` or `#[pymethod]` function argument. #[doc(hidden)] pub fn extract_argument<'a, 'py, T>( - obj: &'py PyAny, + obj: &'a Bound<'py, PyAny>, holder: &'a mut T::Holder, arg_name: &str, ) -> PyResult @@ -93,7 +102,7 @@ where /// does not implement `PyFunctionArgument` for `T: PyClass`. #[doc(hidden)] pub fn extract_optional_argument<'a, 'py, T>( - obj: Option<&'py PyAny>, + obj: Option<&'a Bound<'py, PyAny>>, holder: &'a mut T::Holder, arg_name: &str, default: fn() -> Option, @@ -117,7 +126,7 @@ where /// Alternative to [`extract_argument`] used when the argument has a default value provided by an annotation. #[doc(hidden)] pub fn extract_argument_with_default<'a, 'py, T>( - obj: Option<&'py PyAny>, + obj: Option<&'a Bound<'py, PyAny>>, holder: &'a mut T::Holder, arg_name: &str, default: fn() -> T, @@ -165,7 +174,6 @@ 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 { - use crate::types::any::PyAnyMethods; if error .get_type_bound(py) .is(&py.get_type_bound::()) @@ -189,7 +197,7 @@ pub fn argument_extraction_error(py: Python<'_>, arg_name: &str, error: PyErr) - /// `argument` must not be `None` #[doc(hidden)] #[inline] -pub unsafe fn unwrap_required_argument(argument: Option<&PyAny>) -> &PyAny { +pub unsafe fn unwrap_required_argument(argument: Option>) -> PyArg<'_> { match argument { Some(value) => value, #[cfg(debug_assertions)] @@ -236,7 +244,7 @@ impl FunctionDescription { args: *const *mut ffi::PyObject, nargs: ffi::Py_ssize_t, kwnames: *mut ffi::PyObject, - output: &mut [Option<&'py PyAny>], + output: &mut [Option>], ) -> PyResult<(V::Varargs, K::Varkeywords)> where V: VarargsHandler<'py>, @@ -253,8 +261,10 @@ impl FunctionDescription { ); // Handle positional arguments - // Safety: Option<&PyAny> has the same memory layout as `*mut ffi::PyObject` - let args: *const Option<&PyAny> = args.cast(); + // Safety: + // - Option has the same memory layout as `*mut ffi::PyObject` + // - we both have the GIL and can borrow these input references for the `'py` lifetime. + let args: *const Option> = args.cast(); let positional_args_provided = nargs as usize; let remaining_positional_args = if args.is_null() { debug_assert_eq!(positional_args_provided, 0); @@ -274,13 +284,20 @@ impl FunctionDescription { // Handle keyword arguments let mut varkeywords = K::Varkeywords::default(); - if let Some(kwnames) = py.from_borrowed_ptr_or_opt::(kwnames) { - // Safety: &PyAny has the same memory layout as `*mut ffi::PyObject` - let kwargs = - ::std::slice::from_raw_parts((args as *const &PyAny).offset(nargs), kwnames.len()); + + // Safety: kwnames is known to be a pointer to a tuple, or null + // - we both have the GIL and can borrow this input reference for the `'py` lifetime. + let kwnames: Option> = + Borrowed::from_ptr_or_opt(py, kwnames).map(|kwnames| kwnames.downcast_unchecked()); + if let Some(kwnames) = kwnames { + // Safety: PyArg has the same memory layout as `*mut ffi::PyObject` + let kwargs = ::std::slice::from_raw_parts( + (args as *const PyArg<'py>).offset(nargs), + kwnames.len(), + ); self.handle_kwargs::( - kwnames.iter().zip(kwargs.iter().copied()), + kwnames.iter_borrowed().zip(kwargs.iter().copied()), &mut varkeywords, num_positional_parameters, output, @@ -312,14 +329,20 @@ impl FunctionDescription { py: Python<'py>, args: *mut ffi::PyObject, kwargs: *mut ffi::PyObject, - output: &mut [Option<&'py PyAny>], + output: &mut [Option>], ) -> PyResult<(V::Varargs, K::Varkeywords)> where V: VarargsHandler<'py>, K: VarkeywordsHandler<'py>, { - let args = py.from_borrowed_ptr::(args); - let kwargs: ::std::option::Option<&PyDict> = py.from_borrowed_ptr_or_opt(kwargs); + // Safety: + // - `args` is known to be a tuple + // - `kwargs` is known to be a dict or null + // - we both have the GIL and can borrow these input references for the `'py` lifetime. + let args: Borrowed<'py, 'py, PyTuple> = + Borrowed::from_ptr(py, args).downcast_unchecked::(); + let kwargs: Option> = + Borrowed::from_ptr_or_opt(py, kwargs).map(|kwargs| kwargs.downcast_unchecked()); let num_positional_parameters = self.positional_parameter_names.len(); @@ -331,17 +354,26 @@ impl FunctionDescription { ); // Copy positional arguments into output - for (i, arg) in args.iter().take(num_positional_parameters).enumerate() { + for (i, arg) in args + .iter_borrowed() + .take(num_positional_parameters) + .enumerate() + { output[i] = Some(arg); } // If any arguments remain, push them to varargs (if possible) or error - let varargs = V::handle_varargs_tuple(args, self)?; + let varargs = V::handle_varargs_tuple(&args, self)?; // Handle keyword arguments let mut varkeywords = K::Varkeywords::default(); if let Some(kwargs) = kwargs { - self.handle_kwargs::(kwargs, &mut varkeywords, num_positional_parameters, output)? + self.handle_kwargs::( + kwargs.iter_borrowed(), + &mut varkeywords, + num_positional_parameters, + output, + )? } // Once all inputs have been processed, check that all required arguments have been provided. @@ -358,11 +390,11 @@ impl FunctionDescription { kwargs: I, varkeywords: &mut K::Varkeywords, num_positional_parameters: usize, - output: &mut [Option<&'py PyAny>], + output: &mut [Option>], ) -> PyResult<()> where K: VarkeywordsHandler<'py>, - I: IntoIterator, + I: IntoIterator, PyArg<'py>)>, { debug_assert_eq!( num_positional_parameters, @@ -374,11 +406,21 @@ impl FunctionDescription { ); let mut positional_only_keyword_arguments = Vec::new(); for (kwarg_name_py, value) in kwargs { - // All keyword arguments should be UTF-8 strings, but we'll check, just in case. - // If it isn't, then it will be handled below as a varkeyword (which may raise an - // error if this function doesn't accept **kwargs). Rust source is always UTF-8 - // and so all argument names in `#[pyfunction]` signature must be UTF-8. - if let Ok(kwarg_name) = kwarg_name_py.downcast::()?.to_str() { + // Safety: All keyword arguments should be UTF-8 strings, but if it's not, `.to_str()` + // will return an error anyway. + #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] + let kwarg_name = + unsafe { kwarg_name_py.downcast_unchecked::() }.to_str(); + + #[cfg(all(not(Py_3_10), Py_LIMITED_API))] + let kwarg_name = kwarg_name_py.extract::(); + + if let Ok(kwarg_name_owned) = kwarg_name { + #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] + let kwarg_name = kwarg_name_owned; + #[cfg(all(not(Py_3_10), Py_LIMITED_API))] + let kwarg_name: &str = &kwarg_name_owned; + // Try to place parameter in keyword only parameters if let Some(i) = self.find_keyword_parameter_in_keyword_only(kwarg_name) { if output[i + num_positional_parameters] @@ -397,7 +439,7 @@ impl FunctionDescription { // kwarg to conflict with a postional-only argument - the value // will go into **kwargs anyway. if K::handle_varkeyword(varkeywords, kwarg_name_py, value, self).is_err() { - positional_only_keyword_arguments.push(kwarg_name); + positional_only_keyword_arguments.push(kwarg_name_owned); } } else if output[i].replace(value).is_some() { return Err(self.multiple_values_for_argument(kwarg_name)); @@ -410,6 +452,11 @@ impl FunctionDescription { } if !positional_only_keyword_arguments.is_empty() { + #[cfg(all(not(Py_3_10), Py_LIMITED_API))] + let positional_only_keyword_arguments: Vec<_> = positional_only_keyword_arguments + .iter() + .map(std::ops::Deref::deref) + .collect(); return Err(self.positional_only_keyword_arguments(&positional_only_keyword_arguments)); } @@ -436,7 +483,7 @@ impl FunctionDescription { #[inline] fn ensure_no_missing_required_positional_arguments( &self, - output: &[Option<&PyAny>], + output: &[Option>], positional_args_provided: usize, ) -> PyResult<()> { if positional_args_provided < self.required_positional_parameters { @@ -452,7 +499,7 @@ impl FunctionDescription { #[inline] fn ensure_no_missing_required_keyword_arguments( &self, - output: &[Option<&PyAny>], + output: &[Option>], ) -> PyResult<()> { let keyword_output = &output[self.positional_parameter_names.len()..]; for (param, out) in self.keyword_only_parameters.iter().zip(keyword_output) { @@ -497,11 +544,11 @@ impl FunctionDescription { } #[cold] - fn unexpected_keyword_argument(&self, argument: &PyAny) -> PyErr { + fn unexpected_keyword_argument(&self, argument: PyArg<'_>) -> PyErr { PyTypeError::new_err(format!( "{} got an unexpected keyword argument '{}'", self.full_name(), - argument + argument.as_any() )) } @@ -534,7 +581,7 @@ impl FunctionDescription { } #[cold] - fn missing_required_keyword_arguments(&self, keyword_outputs: &[Option<&PyAny>]) -> PyErr { + fn missing_required_keyword_arguments(&self, keyword_outputs: &[Option>]) -> PyErr { debug_assert_eq!(self.keyword_only_parameters.len(), keyword_outputs.len()); let missing_keyword_only_arguments: Vec<_> = self @@ -555,7 +602,7 @@ impl FunctionDescription { } #[cold] - fn missing_required_positional_arguments(&self, output: &[Option<&PyAny>]) -> PyErr { + fn missing_required_positional_arguments(&self, output: &[Option>]) -> PyErr { let missing_positional_arguments: Vec<_> = self .positional_parameter_names .iter() @@ -575,14 +622,14 @@ pub trait VarargsHandler<'py> { /// Called by `FunctionDescription::extract_arguments_fastcall` with any additional arguments. fn handle_varargs_fastcall( py: Python<'py>, - varargs: &[Option<&PyAny>], + varargs: &[Option>], function_description: &FunctionDescription, ) -> PyResult; /// Called by `FunctionDescription::extract_arguments_tuple_dict` with the original tuple. /// /// Additional arguments are those in the tuple slice starting from `function_description.positional_parameter_names.len()`. fn handle_varargs_tuple( - args: &'py PyTuple, + args: &Bound<'py, PyTuple>, function_description: &FunctionDescription, ) -> PyResult; } @@ -596,7 +643,7 @@ impl<'py> VarargsHandler<'py> for NoVarargs { #[inline] fn handle_varargs_fastcall( _py: Python<'py>, - varargs: &[Option<&PyAny>], + varargs: &[Option>], function_description: &FunctionDescription, ) -> PyResult { let extra_arguments = varargs.len(); @@ -610,7 +657,7 @@ impl<'py> VarargsHandler<'py> for NoVarargs { #[inline] fn handle_varargs_tuple( - args: &'py PyTuple, + args: &Bound<'py, PyTuple>, function_description: &FunctionDescription, ) -> PyResult { let positional_parameter_count = function_description.positional_parameter_names.len(); @@ -627,19 +674,19 @@ impl<'py> VarargsHandler<'py> for NoVarargs { pub struct TupleVarargs; impl<'py> VarargsHandler<'py> for TupleVarargs { - type Varargs = &'py PyTuple; + type Varargs = Bound<'py, PyTuple>; #[inline] fn handle_varargs_fastcall( py: Python<'py>, - varargs: &[Option<&PyAny>], + varargs: &[Option>], _function_description: &FunctionDescription, ) -> PyResult { - Ok(PyTuple::new_bound(py, varargs).into_gil_ref()) + Ok(PyTuple::new_bound(py, varargs)) } #[inline] fn handle_varargs_tuple( - args: &'py PyTuple, + args: &Bound<'py, PyTuple>, function_description: &FunctionDescription, ) -> PyResult { let positional_parameters = function_description.positional_parameter_names.len(); @@ -652,8 +699,8 @@ pub trait VarkeywordsHandler<'py> { type Varkeywords: Default; fn handle_varkeyword( varkeywords: &mut Self::Varkeywords, - name: &'py PyAny, - value: &'py PyAny, + name: PyArg<'py>, + value: PyArg<'py>, function_description: &FunctionDescription, ) -> PyResult<()>; } @@ -666,8 +713,8 @@ impl<'py> VarkeywordsHandler<'py> for NoVarkeywords { #[inline] fn handle_varkeyword( _varkeywords: &mut Self::Varkeywords, - name: &'py PyAny, - _value: &'py PyAny, + name: PyArg<'py>, + _value: PyArg<'py>, function_description: &FunctionDescription, ) -> PyResult<()> { Err(function_description.unexpected_keyword_argument(name)) @@ -678,28 +725,29 @@ impl<'py> VarkeywordsHandler<'py> for NoVarkeywords { pub struct DictVarkeywords; impl<'py> VarkeywordsHandler<'py> for DictVarkeywords { - type Varkeywords = Option<&'py PyDict>; + type Varkeywords = Option>; #[inline] fn handle_varkeyword( varkeywords: &mut Self::Varkeywords, - name: &'py PyAny, - value: &'py PyAny, + name: PyArg<'py>, + value: PyArg<'py>, _function_description: &FunctionDescription, ) -> PyResult<()> { varkeywords - .get_or_insert_with(|| PyDict::new_bound(name.py()).into_gil_ref()) + .get_or_insert_with(|| PyDict::new_bound(name.py())) .set_item(name, value) } } fn push_parameter_list(msg: &mut String, parameter_names: &[&str]) { + let len = parameter_names.len(); for (i, parameter) in parameter_names.iter().enumerate() { if i != 0 { - if parameter_names.len() > 2 { + if len > 2 { msg.push(','); } - if i == parameter_names.len() - 1 { + if i == len - 1 { msg.push_str(" and ") } else { msg.push(' ') @@ -778,7 +826,7 @@ mod tests { }; assert_eq!( err.to_string(), - "TypeError: 'int' object cannot be converted to 'PyString'" + "TypeError: example() got an unexpected keyword argument '1'" ); }) } diff --git a/src/impl_/pymethods.rs b/src/impl_/pymethods.rs index 0b73ce9bc73..70c95ca0883 100644 --- a/src/impl_/pymethods.rs +++ b/src/impl_/pymethods.rs @@ -38,14 +38,15 @@ pub type ipowfunc = unsafe extern "C" fn( impl IPowModulo { #[cfg(Py_3_8)] #[inline] - pub fn to_borrowed_any(self, py: Python<'_>) -> &PyAny { - unsafe { py.from_borrowed_ptr::(self.0) } + pub fn as_ptr(self) -> *mut ffi::PyObject { + self.0 } #[cfg(not(Py_3_8))] #[inline] - pub fn to_borrowed_any(self, py: Python<'_>) -> &PyAny { - unsafe { py.from_borrowed_ptr::(ffi::Py_None()) } + pub fn as_ptr(self) -> *mut ffi::PyObject { + // Safety: returning a borrowed pointer to Python `None` singleton + unsafe { ffi::Py_None() } } } @@ -560,3 +561,11 @@ impl From> for Py { bound.0.clone().unbind() } } + +impl<'py, T> std::ops::Deref for BoundRef<'_, 'py, T> { + type Target = Bound<'py, T>; + #[inline] + fn deref(&self) -> &Self::Target { + self.0 + } +} diff --git a/src/types/dict.rs b/src/types/dict.rs index 2d215c1f8b9..daae753c6cc 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -524,6 +524,16 @@ impl<'py> PyDictMethods<'py> for Bound<'py, PyDict> { } } +impl<'a, 'py> Borrowed<'a, 'py, PyDict> { + /// Iterates over the contents of this dictionary without incrementing reference counts. + /// + /// # Safety + /// It must be known that this dictionary will not be modified during iteration. + pub(crate) unsafe fn iter_borrowed(self) -> BorrowedDictIter<'a, 'py> { + BorrowedDictIter::new(self) + } +} + fn dict_len(dict: &Bound<'_, PyDict>) -> Py_ssize_t { #[cfg(any(not(Py_3_8), PyPy, Py_LIMITED_API))] unsafe { @@ -662,6 +672,64 @@ impl<'py> IntoIterator for Bound<'py, PyDict> { } } +mod borrowed_iter { + use super::*; + + /// Variant of the above which is used to iterate the items of the dictionary + /// without incrementing reference counts. This is only safe if it's known + /// that the dictionary will not be modified during iteration. + pub struct BorrowedDictIter<'a, 'py> { + dict: Borrowed<'a, 'py, PyDict>, + ppos: ffi::Py_ssize_t, + len: ffi::Py_ssize_t, + } + + impl<'a, 'py> Iterator for BorrowedDictIter<'a, 'py> { + type Item = (Borrowed<'a, 'py, PyAny>, Borrowed<'a, 'py, PyAny>); + + #[inline] + fn next(&mut self) -> Option { + let mut key: *mut ffi::PyObject = std::ptr::null_mut(); + let mut value: *mut ffi::PyObject = std::ptr::null_mut(); + + // Safety: self.dict lives sufficiently long that the pointer is not dangling + if unsafe { ffi::PyDict_Next(self.dict.as_ptr(), &mut self.ppos, &mut key, &mut value) } + != 0 + { + let py = self.dict.py(); + self.len -= 1; + // Safety: + // - PyDict_Next returns borrowed values + // - we have already checked that `PyDict_Next` succeeded, so we can assume these to be non-null + Some(unsafe { (key.assume_borrowed(py), value.assume_borrowed(py)) }) + } else { + None + } + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + let len = self.len(); + (len, Some(len)) + } + } + + impl ExactSizeIterator for BorrowedDictIter<'_, '_> { + fn len(&self) -> usize { + self.len as usize + } + } + + impl<'a, 'py> BorrowedDictIter<'a, 'py> { + pub(super) fn new(dict: Borrowed<'a, 'py, PyDict>) -> Self { + let len = dict_len(&dict); + BorrowedDictIter { dict, ppos: 0, len } + } + } +} + +pub(crate) use borrowed_iter::BorrowedDictIter; + /// Conversion trait that allows a sequence of tuples to be converted into `PyDict` /// Primary use case for this trait is `call` and `call_method` methods as keywords argument. pub trait IntoPyDict: Sized { diff --git a/src/types/string.rs b/src/types/string.rs index 4a33236b47b..88ebb30b2d1 100644 --- a/src/types/string.rs +++ b/src/types/string.rs @@ -354,7 +354,7 @@ impl<'py> PyStringMethods<'py> for Bound<'py, PyString> { impl<'a> Borrowed<'a, '_, PyString> { #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] #[allow(clippy::wrong_self_convention)] - fn to_str(self) -> PyResult<&'a str> { + pub(crate) fn to_str(self) -> PyResult<&'a str> { // PyUnicode_AsUTF8AndSize only available on limited API starting with 3.10. let mut size: ffi::Py_ssize_t = 0; let data: *const u8 = diff --git a/src/types/tuple.rs b/src/types/tuple.rs index caad7d9c0e9..51d33f93460 100644 --- a/src/types/tuple.rs +++ b/src/types/tuple.rs @@ -411,7 +411,7 @@ impl<'py> PyTupleMethods<'py> for Bound<'py, PyTuple> { } fn iter_borrowed<'a>(&'a self) -> BorrowedTupleIterator<'a, 'py> { - BorrowedTupleIterator::new(self.as_borrowed()) + self.as_borrowed().iter_borrowed() } fn to_list(&self) -> Bound<'py, PyList> { @@ -433,6 +433,10 @@ impl<'a, 'py> Borrowed<'a, 'py, PyTuple> { unsafe fn get_borrowed_item_unchecked(self, index: usize) -> Borrowed<'a, 'py, PyAny> { ffi::PyTuple_GET_ITEM(self.as_ptr(), index as Py_ssize_t).assume_borrowed(self.py()) } + + pub(crate) fn iter_borrowed(self) -> BorrowedTupleIterator<'a, 'py> { + BorrowedTupleIterator::new(self) + } } /// Used by `PyTuple::iter()`. From 55833365b54dec47c2a4cd4ccbda218be1141d16 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Wed, 28 Feb 2024 20:36:55 +0100 Subject: [PATCH 052/936] seals `PyAnyMethods` and friends (#3909) * seals `PyAnyMethods` and friends This seals these new traits, preventing downstream crates from implementing them on their types. These traits are mainly a workaround for arbitrary self receiver types, so this gives us more flexibility if these need to be changed in the future. * move `PyResultExt` seal --- src/ffi_ptr_ext.rs | 11 +---------- src/lib.rs | 1 + src/py_result_ext.rs | 12 +----------- src/sealed.rs | 34 ++++++++++++++++++++++++++++++++++ src/types/any.rs | 2 +- src/types/boolobject.rs | 2 +- src/types/bytearray.rs | 2 +- src/types/bytes.rs | 2 +- src/types/capsule.rs | 2 +- src/types/complex.rs | 2 +- src/types/dict.rs | 2 +- src/types/float.rs | 2 +- src/types/frozenset.rs | 2 +- src/types/list.rs | 2 +- src/types/mapping.rs | 2 +- src/types/module.rs | 2 +- src/types/sequence.rs | 2 +- src/types/set.rs | 2 +- src/types/slice.rs | 2 +- src/types/string.rs | 2 +- src/types/traceback.rs | 2 +- src/types/tuple.rs | 2 +- src/types/typeobject.rs | 2 +- 23 files changed, 56 insertions(+), 40 deletions(-) create mode 100644 src/sealed.rs diff --git a/src/ffi_ptr_ext.rs b/src/ffi_ptr_ext.rs index 8a45f5d70c0..3ca8671f1f6 100644 --- a/src/ffi_ptr_ext.rs +++ b/src/ffi_ptr_ext.rs @@ -1,19 +1,10 @@ +use crate::sealed::Sealed; use crate::{ ffi, instance::{Borrowed, Bound}, PyAny, PyResult, Python, }; -mod sealed { - use super::*; - - pub trait Sealed {} - - impl Sealed for *mut ffi::PyObject {} -} - -use sealed::Sealed; - pub(crate) trait FfiPtrExt: Sealed { unsafe fn assume_owned_or_err(self, py: Python<'_>) -> PyResult>; unsafe fn assume_owned_or_opt(self, py: Python<'_>) -> Option>; diff --git a/src/lib.rs b/src/lib.rs index d0c53c4a0c9..17dbd972d85 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -324,6 +324,7 @@ pub use crate::version::PythonVersionInfo; pub(crate) mod ffi_ptr_ext; pub(crate) mod py_result_ext; +pub(crate) mod sealed; /// Old module which contained some implementation details of the `#[pyproto]` module. /// diff --git a/src/py_result_ext.rs b/src/py_result_ext.rs index 66309988dc4..2ad079ed7ac 100644 --- a/src/py_result_ext.rs +++ b/src/py_result_ext.rs @@ -1,16 +1,6 @@ use crate::{types::any::PyAnyMethods, Bound, PyAny, PyResult, PyTypeCheck}; -mod sealed { - use super::*; - - pub trait Sealed {} - - impl Sealed for PyResult> {} -} - -use sealed::Sealed; - -pub(crate) trait PyResultExt<'py>: Sealed { +pub(crate) trait PyResultExt<'py>: crate::sealed::Sealed { fn downcast_into(self) -> PyResult>; unsafe fn downcast_into_unchecked(self) -> PyResult>; } diff --git a/src/sealed.rs b/src/sealed.rs new file mode 100644 index 00000000000..e2d5c5ccfed --- /dev/null +++ b/src/sealed.rs @@ -0,0 +1,34 @@ +use crate::types::{ + PyBool, PyByteArray, PyBytes, PyCapsule, PyComplex, PyDict, PyFloat, PyFrozenSet, PyList, + PyMapping, PyModule, PySequence, PySet, PySlice, PyString, PyTraceback, PyTuple, PyType, +}; +use crate::{ffi, Bound, PyAny, PyResult}; + +pub trait Sealed {} + +// for FfiPtrExt +impl Sealed for *mut ffi::PyObject {} + +// for PyResultExt +impl Sealed for PyResult> {} + +// for Py(...)Methods +impl Sealed for Bound<'_, PyAny> {} +impl Sealed for Bound<'_, PyBool> {} +impl Sealed for Bound<'_, PyByteArray> {} +impl Sealed for Bound<'_, PyBytes> {} +impl Sealed for Bound<'_, PyCapsule> {} +impl Sealed for Bound<'_, PyComplex> {} +impl Sealed for Bound<'_, PyDict> {} +impl Sealed for Bound<'_, PyFloat> {} +impl Sealed for Bound<'_, PyFrozenSet> {} +impl Sealed for Bound<'_, PyList> {} +impl Sealed for Bound<'_, PyMapping> {} +impl Sealed for Bound<'_, PyModule> {} +impl Sealed for Bound<'_, PySequence> {} +impl Sealed for Bound<'_, PySet> {} +impl Sealed for Bound<'_, PySlice> {} +impl Sealed for Bound<'_, PyString> {} +impl Sealed for Bound<'_, PyTraceback> {} +impl Sealed for Bound<'_, PyTuple> {} +impl Sealed for Bound<'_, PyType> {} diff --git a/src/types/any.rs b/src/types/any.rs index 15f2fd3135c..0207217ba96 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -939,7 +939,7 @@ impl PyAny { /// It is recommended you import this trait via `use pyo3::prelude::*` rather than /// by importing this trait directly. #[doc(alias = "PyAny")] -pub trait PyAnyMethods<'py> { +pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// Returns whether `self` and `other` point to the same object. To compare /// the equality of two objects (the `==` operator), use [`eq`](PyAny::eq). /// diff --git a/src/types/boolobject.rs b/src/types/boolobject.rs index 906a967069a..3a2f60f6fa3 100644 --- a/src/types/boolobject.rs +++ b/src/types/boolobject.rs @@ -55,7 +55,7 @@ impl PyBool { /// syntax these methods are separated into a trait, because stable Rust does not yet support /// `arbitrary_self_types`. #[doc(alias = "PyBool")] -pub trait PyBoolMethods<'py> { +pub trait PyBoolMethods<'py>: crate::sealed::Sealed { /// Gets whether this boolean is `true`. fn is_true(&self) -> bool; } diff --git a/src/types/bytearray.rs b/src/types/bytearray.rs index 2a21509d87f..a860b4d4cca 100644 --- a/src/types/bytearray.rs +++ b/src/types/bytearray.rs @@ -293,7 +293,7 @@ impl PyByteArray { /// syntax these methods are separated into a trait, because stable Rust does not yet support /// `arbitrary_self_types`. #[doc(alias = "PyByteArray")] -pub trait PyByteArrayMethods<'py> { +pub trait PyByteArrayMethods<'py>: crate::sealed::Sealed { /// Gets the length of the bytearray. fn len(&self) -> usize; diff --git a/src/types/bytes.rs b/src/types/bytes.rs index 0861af630a5..2a54c172d1f 100644 --- a/src/types/bytes.rs +++ b/src/types/bytes.rs @@ -143,7 +143,7 @@ impl PyBytes { /// syntax these methods are separated into a trait, because stable Rust does not yet support /// `arbitrary_self_types`. #[doc(alias = "PyBytes")] -pub trait PyBytesMethods<'py> { +pub trait PyBytesMethods<'py>: crate::sealed::Sealed { /// Gets the Python string as a byte slice. fn as_bytes(&self) -> &[u8]; } diff --git a/src/types/capsule.rs b/src/types/capsule.rs index aa1a910d21b..fe5a47e1796 100644 --- a/src/types/capsule.rs +++ b/src/types/capsule.rs @@ -263,7 +263,7 @@ impl PyCapsule { /// syntax these methods are separated into a trait, because stable Rust does not yet support /// `arbitrary_self_types`. #[doc(alias = "PyCapsule")] -pub trait PyCapsuleMethods<'py> { +pub trait PyCapsuleMethods<'py>: crate::sealed::Sealed { /// Sets the context pointer in the capsule. /// /// Returns an error if this capsule is not valid. diff --git a/src/types/complex.rs b/src/types/complex.rs index 6d4bc7a2244..80ecffdc5cf 100644 --- a/src/types/complex.rs +++ b/src/types/complex.rs @@ -258,7 +258,7 @@ mod not_limited_impls { /// syntax these methods are separated into a trait, because stable Rust does not yet support /// `arbitrary_self_types`. #[doc(alias = "PyComplex")] -pub trait PyComplexMethods<'py> { +pub trait PyComplexMethods<'py>: crate::sealed::Sealed { /// Returns the real part of the complex number. fn real(&self) -> c_double; /// Returns the imaginary part of the complex number. diff --git a/src/types/dict.rs b/src/types/dict.rs index daae753c6cc..5d1243463f2 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -290,7 +290,7 @@ impl PyDict { /// syntax these methods are separated into a trait, because stable Rust does not yet support /// `arbitrary_self_types`. #[doc(alias = "PyDict")] -pub trait PyDictMethods<'py> { +pub trait PyDictMethods<'py>: crate::sealed::Sealed { /// Returns a new dictionary that contains the same key-value pairs as self. /// /// This is equivalent to the Python expression `self.copy()`. diff --git a/src/types/float.rs b/src/types/float.rs index 766c597b2b4..6db1fdae038 100644 --- a/src/types/float.rs +++ b/src/types/float.rs @@ -58,7 +58,7 @@ impl PyFloat { /// syntax these methods are separated into a trait, because stable Rust does not yet support /// `arbitrary_self_types`. #[doc(alias = "PyFloat")] -pub trait PyFloatMethods<'py> { +pub trait PyFloatMethods<'py>: crate::sealed::Sealed { /// Gets the value of this float. fn value(&self) -> c_double; } diff --git a/src/types/frozenset.rs b/src/types/frozenset.rs index 15f892588e5..cc16a9341c8 100644 --- a/src/types/frozenset.rs +++ b/src/types/frozenset.rs @@ -157,7 +157,7 @@ impl PyFrozenSet { /// syntax these methods are separated into a trait, because stable Rust does not yet support /// `arbitrary_self_types`. #[doc(alias = "PyFrozenSet")] -pub trait PyFrozenSetMethods<'py> { +pub trait PyFrozenSetMethods<'py>: crate::sealed::Sealed { /// Returns the number of items in the set. /// /// This is equivalent to the Python expression `len(self)`. diff --git a/src/types/list.rs b/src/types/list.rs index 3e3942bcc13..a8df26ddbc5 100644 --- a/src/types/list.rs +++ b/src/types/list.rs @@ -285,7 +285,7 @@ index_impls!(PyList, "list", PyList::len, PyList::get_slice); /// syntax these methods are separated into a trait, because stable Rust does not yet support /// `arbitrary_self_types`. #[doc(alias = "PyList")] -pub trait PyListMethods<'py> { +pub trait PyListMethods<'py>: crate::sealed::Sealed { /// Returns the length of the list. fn len(&self) -> usize; diff --git a/src/types/mapping.rs b/src/types/mapping.rs index afbdae688b4..a5a93163bbd 100644 --- a/src/types/mapping.rs +++ b/src/types/mapping.rs @@ -109,7 +109,7 @@ impl PyMapping { /// syntax these methods are separated into a trait, because stable Rust does not yet support /// `arbitrary_self_types`. #[doc(alias = "PyMapping")] -pub trait PyMappingMethods<'py> { +pub trait PyMappingMethods<'py>: crate::sealed::Sealed { /// Returns the number of objects in the mapping. /// /// This is equivalent to the Python expression `len(self)`. diff --git a/src/types/module.rs b/src/types/module.rs index 75fc6625846..693df79cafa 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -410,7 +410,7 @@ impl PyModule { /// syntax these methods are separated into a trait, because stable Rust does not yet support /// `arbitrary_self_types`. #[doc(alias = "PyModule")] -pub trait PyModuleMethods<'py> { +pub trait PyModuleMethods<'py>: crate::sealed::Sealed { /// Returns the module's `__dict__` attribute, which contains the module's symbol table. fn dict(&self) -> Bound<'py, PyDict>; diff --git a/src/types/sequence.rs b/src/types/sequence.rs index 80306cbf44b..62abb66fa6e 100644 --- a/src/types/sequence.rs +++ b/src/types/sequence.rs @@ -192,7 +192,7 @@ impl PySequence { /// syntax these methods are separated into a trait, because stable Rust does not yet support /// `arbitrary_self_types`. #[doc(alias = "PySequence")] -pub trait PySequenceMethods<'py> { +pub trait PySequenceMethods<'py>: crate::sealed::Sealed { /// Returns the number of objects in sequence. /// /// This is equivalent to the Python expression `len(self)`. diff --git a/src/types/set.rs b/src/types/set.rs index 0c1733527cf..8e36ab81f8e 100644 --- a/src/types/set.rs +++ b/src/types/set.rs @@ -139,7 +139,7 @@ impl PySet { /// syntax these methods are separated into a trait, because stable Rust does not yet support /// `arbitrary_self_types`. #[doc(alias = "PySet")] -pub trait PySetMethods<'py> { +pub trait PySetMethods<'py>: crate::sealed::Sealed { /// Removes all elements from the set. fn clear(&self); diff --git a/src/types/slice.rs b/src/types/slice.rs index b4b6731d695..8e7545208dc 100644 --- a/src/types/slice.rs +++ b/src/types/slice.rs @@ -105,7 +105,7 @@ impl PySlice { /// syntax these methods are separated into a trait, because stable Rust does not yet support /// `arbitrary_self_types`. #[doc(alias = "PySlice")] -pub trait PySliceMethods<'py> { +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. diff --git a/src/types/string.rs b/src/types/string.rs index 88ebb30b2d1..93f3682c3a4 100644 --- a/src/types/string.rs +++ b/src/types/string.rs @@ -282,7 +282,7 @@ impl PyString { /// syntax these methods are separated into a trait, because stable Rust does not yet support /// `arbitrary_self_types`. #[doc(alias = "PyString")] -pub trait PyStringMethods<'py> { +pub trait PyStringMethods<'py>: crate::sealed::Sealed { /// Gets the Python string as a Rust UTF-8 string slice. /// /// Returns a `UnicodeEncodeError` if the input is not valid unicode diff --git a/src/types/traceback.rs b/src/types/traceback.rs index b8782463367..c4cedd791f6 100644 --- a/src/types/traceback.rs +++ b/src/types/traceback.rs @@ -56,7 +56,7 @@ impl PyTraceback { /// syntax these methods are separated into a trait, because stable Rust does not yet support /// `arbitrary_self_types`. #[doc(alias = "PyTraceback")] -pub trait PyTracebackMethods<'py> { +pub trait PyTracebackMethods<'py>: crate::sealed::Sealed { /// Formats the traceback as a string. /// /// This does not include the exception type and value. The exception type and value can be diff --git a/src/types/tuple.rs b/src/types/tuple.rs index 51d33f93460..3ffaf9c3224 100644 --- a/src/types/tuple.rs +++ b/src/types/tuple.rs @@ -248,7 +248,7 @@ index_impls!(PyTuple, "tuple", PyTuple::len, PyTuple::get_slice); /// syntax these methods are separated into a trait, because stable Rust does not yet support /// `arbitrary_self_types`. #[doc(alias = "PyTuple")] -pub trait PyTupleMethods<'py> { +pub trait PyTupleMethods<'py>: crate::sealed::Sealed { /// Gets the length of the tuple. fn len(&self) -> usize; diff --git a/src/types/typeobject.rs b/src/types/typeobject.rs index 4c1b17a2aa8..d75e39d022d 100644 --- a/src/types/typeobject.rs +++ b/src/types/typeobject.rs @@ -107,7 +107,7 @@ impl PyType { /// syntax these methods are separated into a trait, because stable Rust does not yet support /// `arbitrary_self_types`. #[doc(alias = "PyType")] -pub trait PyTypeMethods<'py> { +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; From a582fa01639df01d884d5ed8683b44a8d7bf0734 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Wed, 28 Feb 2024 20:51:40 +0000 Subject: [PATCH 053/936] docs: update discord invite to permanent one (#3913) --- .github/ISSUE_TEMPLATE/config.yml | 2 +- Contributing.md | 4 ++-- README.md | 4 ++-- guide/src/faq.md | 2 +- guide/src/getting_started.md | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index ca4239b6fa7..7919c91f0c6 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -7,5 +7,5 @@ contact_links: url: https://github.com/PyO3/pyo3/discussions about: For troubleshooting help, see the Discussions - name: 👋 Chat - url: https://discord.gg/c4BwayXQ + url: https://discord.gg/33kcChzH7f about: Engage with PyO3's users and developers on Discord diff --git a/Contributing.md b/Contributing.md index 9392c0fc2de..b34bf420072 100644 --- a/Contributing.md +++ b/Contributing.md @@ -29,7 +29,7 @@ To work and develop PyO3, you need Python & Rust installed on your system. ### Help users identify bugs -The [PyO3 Discord server](https://discord.gg/c4BwayXQ) is very active with users who are new to PyO3, and often completely new to Rust. Helping them debug is a great way to get experience with the PyO3 codebase. +The [PyO3 Discord server](https://discord.gg/33kcChzH7f) is very active with users who are new to PyO3, and often completely new to Rust. Helping them debug is a great way to get experience with the PyO3 codebase. Helping others often reveals bugs, documentation weaknesses, and missing APIs. It's a good idea to open GitHub issues for these immediately so the resolution can be designed and implemented! @@ -203,7 +203,7 @@ You can install an IDE plugin to view the coverage. For example, if you use VSCo ## Sponsor this project -At the moment there is no official organisation that accepts sponsorship on PyO3's behalf. If you're seeking to provide significant funding to the PyO3 ecosystem, please reach out to us on [GitHub](https://github.com/PyO3/pyo3/issues/new) or [Discord](https://discord.gg/c4BwayXQ) and we can discuss. +At the moment there is no official organisation that accepts sponsorship on PyO3's behalf. If you're seeking to provide significant funding to the PyO3 ecosystem, please reach out to us on [GitHub](https://github.com/PyO3/pyo3/issues/new) or [Discord](https://discord.gg/33kcChzH7f) and we can discuss. In the meanwhile, some of our maintainers have personal GitHub sponsorship pages and would be grateful for your support: diff --git a/README.md b/README.md index c139f13693e..eaf1ce8809b 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![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) -[![discord server](https://img.shields.io/discord/1209263839632424990?logo=discord)](https://discord.gg/c4BwayXQ) +[![discord server](https://img.shields.io/discord/1209263839632424990?logo=discord)](https://discord.gg/33kcChzH7f) [![contributing notes](https://img.shields.io/badge/contribute-on%20github-Green?logo=github)](https://github.com/PyO3/pyo3/blob/main/Contributing.md) [Rust](https://www.rust-lang.org/) bindings for [Python](https://www.python.org/), including tools for creating native Python extension modules. Running and interacting with Python code from a Rust binary is also supported. @@ -237,7 +237,7 @@ about this topic. Everyone is welcomed to contribute to PyO3! There are many ways to support the project, such as: -- help PyO3 users with issues on GitHub and [Discord](https://discord.gg/c4BwayXQ) +- help PyO3 users with issues on GitHub and [Discord](https://discord.gg/33kcChzH7f) - improve documentation - write features and bugfixes - publish blogs and examples of how to use PyO3 diff --git a/guide/src/faq.md b/guide/src/faq.md index 6f5188c73be..1034e9ccc2a 100644 --- a/guide/src/faq.md +++ b/guide/src/faq.md @@ -1,6 +1,6 @@ # Frequently Asked Questions and troubleshooting -Sorry that you're having trouble using PyO3. If you can't find the answer to your problem in the list below, you can also reach out for help on [GitHub Discussions](https://github.com/PyO3/pyo3/discussions) and on [Discord](https://discord.gg/c4BwayXQ). +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! diff --git a/guide/src/getting_started.md b/guide/src/getting_started.md index 235272dab20..7b76639c1c4 100644 --- a/guide/src/getting_started.md +++ b/guide/src/getting_started.md @@ -2,7 +2,7 @@ To get started using PyO3 you will need three things: a Rust toolchain, a Python environment, and a way to build. We'll cover each of these below. -> If you'd like to chat to the PyO3 maintainers and other PyO3 users, consider joining the [PyO3 Discord server](https://discord.gg/c4BwayXQ). We're keen to hear about your experience getting started, so we can make PyO3 as accessible as possible for everyone! +> If you'd like to chat to the PyO3 maintainers and other PyO3 users, consider joining the [PyO3 Discord server](https://discord.gg/33kcChzH7f). We're keen to hear about your experience getting started, so we can make PyO3 as accessible as possible for everyone! ## Rust From 68ec6de0c9196225fc5806825e04f8649341a27d Mon Sep 17 00:00:00 2001 From: Matthew Neeley Date: Wed, 28 Feb 2024 14:36:50 -0800 Subject: [PATCH 054/936] Use single-arg form of `#[pymodule]` function in docs and tests (#3899) * Use single-arg form for `#[pymodule]` functions in docs and tests * Update guide/src/function.md Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> * Add test of two-argument module function * Fix new test --------- Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> --- README.md | 2 +- examples/decorator/src/lib.rs | 2 +- examples/getitem/src/lib.rs | 2 +- examples/maturin-starter/src/submodule.rs | 2 +- examples/plugin/plugin_api/src/lib.rs | 2 +- .../setuptools-rust-starter/src/submodule.rs | 2 +- examples/word-count/src/lib.rs | 2 +- guide/src/class.md | 2 +- guide/src/class/numeric.md | 2 +- guide/src/class/object.md | 4 +- guide/src/ecosystem/async-await.md | 6 +- guide/src/ecosystem/logging.md | 4 +- guide/src/function.md | 12 ++-- guide/src/function/signature.md | 2 +- guide/src/getting_started.md | 2 +- guide/src/module.md | 4 +- guide/src/python_from_rust.md | 2 +- guide/src/trait_bounds.md | 4 +- pytests/src/awaitable.rs | 2 +- pytests/src/buf_and_str.rs | 2 +- pytests/src/comparisons.rs | 2 +- pytests/src/datetime.rs | 2 +- pytests/src/dict_iter.rs | 2 +- pytests/src/enums.rs | 4 +- 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/conversions/indexmap.rs | 2 +- src/conversions/num_bigint.rs | 2 +- src/conversions/num_complex.rs | 4 +- src/conversions/rust_decimal.rs | 2 +- src/exceptions.rs | 6 +- src/lib.rs | 2 +- src/types/module.rs | 12 ++-- tests/test_append_to_inittab.rs | 4 +- tests/test_module.rs | 68 ++++++++++++------- tests/test_text_signature.rs | 2 +- tests/ui/invalid_pymodule_args.rs | 2 +- 43 files changed, 108 insertions(+), 86 deletions(-) diff --git a/README.md b/README.md index eaf1ce8809b..21dc125d5ab 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ fn sum_as_string(a: usize, b: usize) -> PyResult { /// the `lib.name` setting in the `Cargo.toml`, else Python will not be able to /// import the module. #[pymodule] -fn string_sum(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn string_sum(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(sum_as_string, m)?)?; Ok(()) } diff --git a/examples/decorator/src/lib.rs b/examples/decorator/src/lib.rs index fb2f2932dd2..9dccabc7341 100644 --- a/examples/decorator/src/lib.rs +++ b/examples/decorator/src/lib.rs @@ -60,7 +60,7 @@ impl PyCounter { } #[pymodule] -pub fn decorator(_py: Python<'_>, module: &PyModule) -> PyResult<()> { +pub fn decorator(module: &Bound<'_, PyModule>) -> PyResult<()> { module.add_class::()?; Ok(()) } diff --git a/examples/getitem/src/lib.rs b/examples/getitem/src/lib.rs index 90a3e9fc52f..eed60076bb8 100644 --- a/examples/getitem/src/lib.rs +++ b/examples/getitem/src/lib.rs @@ -75,7 +75,7 @@ impl ExampleContainer { #[pymodule] #[pyo3(name = "getitem")] -fn example(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn example(m: &Bound<'_, PyModule>) -> PyResult<()> { // ? -https://github.com/PyO3/maturin/issues/475 m.add_class::()?; Ok(()) diff --git a/examples/maturin-starter/src/submodule.rs b/examples/maturin-starter/src/submodule.rs index 56540b2e469..f3eb174100b 100644 --- a/examples/maturin-starter/src/submodule.rs +++ b/examples/maturin-starter/src/submodule.rs @@ -16,7 +16,7 @@ impl SubmoduleClass { } #[pymodule] -pub fn submodule(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +pub fn submodule(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; Ok(()) } diff --git a/examples/plugin/plugin_api/src/lib.rs b/examples/plugin/plugin_api/src/lib.rs index 59aae55699d..580c85a8c8e 100644 --- a/examples/plugin/plugin_api/src/lib.rs +++ b/examples/plugin/plugin_api/src/lib.rs @@ -26,7 +26,7 @@ impl Gadget { /// A Python module for plugin interface types #[pymodule] -pub fn plugin_api(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +pub fn plugin_api(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; Ok(()) } diff --git a/examples/setuptools-rust-starter/src/submodule.rs b/examples/setuptools-rust-starter/src/submodule.rs index 56540b2e469..f3eb174100b 100644 --- a/examples/setuptools-rust-starter/src/submodule.rs +++ b/examples/setuptools-rust-starter/src/submodule.rs @@ -16,7 +16,7 @@ impl SubmoduleClass { } #[pymodule] -pub fn submodule(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +pub fn submodule(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; Ok(()) } diff --git a/examples/word-count/src/lib.rs b/examples/word-count/src/lib.rs index b7d3a8033a6..5bc73df97a4 100644 --- a/examples/word-count/src/lib.rs +++ b/examples/word-count/src/lib.rs @@ -33,7 +33,7 @@ fn count_line(line: &str, needle: &str) -> usize { } #[pymodule] -fn word_count(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn word_count(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(search, m)?)?; m.add_function(wrap_pyfunction!(search_sequential, m)?)?; m.add_function(wrap_pyfunction!(search_sequential_allow_threads, m)?)?; diff --git a/guide/src/class.md b/guide/src/class.md index 8ce896f5a61..1e752700613 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -181,7 +181,7 @@ The next step is to create the module initializer and add our class to it: # struct Number(i32); # #[pymodule] -fn my_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; Ok(()) } diff --git a/guide/src/class/numeric.md b/guide/src/class/numeric.md index 2ffb0a543ef..3e6a3cf47e9 100644 --- a/guide/src/class/numeric.md +++ b/guide/src/class/numeric.md @@ -326,7 +326,7 @@ impl Number { } #[pymodule] -fn my_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; Ok(()) } diff --git a/guide/src/class/object.md b/guide/src/class/object.md index 10867976df0..729815ade0b 100644 --- a/guide/src/class/object.md +++ b/guide/src/class/object.md @@ -18,7 +18,7 @@ impl Number { } #[pymodule] -fn my_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; Ok(()) } @@ -295,7 +295,7 @@ impl Number { } #[pymodule] -fn my_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; Ok(()) } diff --git a/guide/src/ecosystem/async-await.md b/guide/src/ecosystem/async-await.md index f537ab90df1..ec46e872ba7 100644 --- a/guide/src/ecosystem/async-await.md +++ b/guide/src/ecosystem/async-await.md @@ -128,7 +128,7 @@ fn rust_sleep(py: Python<'_>) -> PyResult<&PyAny> { } #[pymodule] -fn my_async_module(py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn my_async_module(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(rust_sleep, m)?)?; Ok(()) @@ -151,7 +151,7 @@ fn rust_sleep(py: Python<'_>) -> PyResult<&PyAny> { } #[pymodule] -fn my_async_module(py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn my_async_module(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(rust_sleep, m)?)?; Ok(()) } @@ -475,7 +475,7 @@ fn rust_sleep(py: Python<'_>) -> PyResult<&PyAny> { } #[pymodule] -fn my_async_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn my_async_module(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(rust_sleep, m)?)?; Ok(()) diff --git a/guide/src/ecosystem/logging.md b/guide/src/ecosystem/logging.md index 2e7d4a087c6..da95c4a7cd2 100644 --- a/guide/src/ecosystem/logging.md +++ b/guide/src/ecosystem/logging.md @@ -30,11 +30,11 @@ fn log_something() { } #[pymodule] -fn my_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> { // A good place to install the Rust -> Python logger. pyo3_log::init(); - m.add_function(wrap_pyfunction!(log_something))?; + m.add_function(wrap_pyfunction!(log_something, m)?)?; Ok(()) } ``` diff --git a/guide/src/function.md b/guide/src/function.md index f3955ba554b..2ed38f6256f 100644 --- a/guide/src/function.md +++ b/guide/src/function.md @@ -13,7 +13,7 @@ fn double(x: usize) -> usize { } #[pymodule] -fn my_extension(py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn my_extension(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(double, m)?)?; Ok(()) } @@ -55,7 +55,7 @@ The `#[pyo3]` attribute can be used to modify properties of the generated Python } #[pymodule] - fn module_with_functions(py: Python<'_>, m: &PyModule) -> PyResult<()> { + fn module_with_functions(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(no_args_py, m)?)?; Ok(()) } @@ -92,7 +92,7 @@ The `#[pyo3]` attribute can be used to modify properties of the generated Python } #[pymodule] - fn module_with_fn(py: Python<'_>, m: &PyModule) -> PyResult<()> { + fn module_with_fn(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(pyfunction_with_module, m)?) } ``` @@ -166,8 +166,8 @@ Python argument passing convention.) It then embeds the call to the Rust functio FFI-wrapper function. This wrapper handles extraction of the regular arguments and the keyword arguments from the input `PyObject`s. -The `wrap_pyfunction` macro can be used to directly get a `PyCFunction` given a -`#[pyfunction]` and a `PyModule`: `wrap_pyfunction!(rust_fun, module)`. +The `wrap_pyfunction` macro can be used to directly get a `Bound` given a +`#[pyfunction]` and a `Bound`: `wrap_pyfunction!(rust_fun, module)`. ## `#[pyfn]` shorthand @@ -197,7 +197,7 @@ documented in the rest of this chapter. The code above is expanded to the follow use pyo3::prelude::*; #[pymodule] -fn my_extension(py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn my_extension(m: &Bound<'_, PyModule>) -> PyResult<()> { #[pyfunction] fn double(x: usize) -> usize { x * 2 diff --git a/guide/src/function/signature.md b/guide/src/function/signature.md index d92767e7bde..4fcfe958b19 100644 --- a/guide/src/function/signature.md +++ b/guide/src/function/signature.md @@ -21,7 +21,7 @@ fn num_kwds(kwds: Option<&PyDict>) -> usize { } #[pymodule] -fn module_with_functions(py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn module_with_functions(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(num_kwds, m)?).unwrap(); Ok(()) } diff --git a/guide/src/getting_started.md b/guide/src/getting_started.md index 7b76639c1c4..008da8109f6 100644 --- a/guide/src/getting_started.md +++ b/guide/src/getting_started.md @@ -163,7 +163,7 @@ fn sum_as_string(a: usize, b: usize) -> PyResult { /// the `lib.name` setting in the `Cargo.toml`, else Python will not be able to /// import the module. #[pymodule] -fn pyo3_example(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn pyo3_example(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(sum_as_string, m)?)?; Ok(()) } diff --git a/guide/src/module.md b/guide/src/module.md index 53c390bec06..a403a3621e6 100644 --- a/guide/src/module.md +++ b/guide/src/module.md @@ -12,7 +12,7 @@ fn double(x: usize) -> usize { /// This module is implemented in Rust. #[pymodule] -fn my_extension(py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn my_extension(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(double, m)?)?; Ok(()) } @@ -34,7 +34,7 @@ fn double(x: usize) -> usize { #[pymodule] #[pyo3(name = "custom_name")] -fn my_extension(py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn my_extension(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(double, m)?)?; Ok(()) } diff --git a/guide/src/python_from_rust.md b/guide/src/python_from_rust.md index b51bbe592eb..d8f3214f58b 100644 --- a/guide/src/python_from_rust.md +++ b/guide/src/python_from_rust.md @@ -283,7 +283,7 @@ fn add_one(x: i64) -> i64 { } #[pymodule] -fn foo(_py: Python<'_>, foo_module: &PyModule) -> PyResult<()> { +fn foo(foo_module: &Bound<'_, PyModule>) -> PyResult<()> { foo_module.add_function(wrap_pyfunction!(add_one, foo_module)?)?; Ok(()) } diff --git a/guide/src/trait_bounds.md b/guide/src/trait_bounds.md index e0dd988412f..e05d44e982a 100644 --- a/guide/src/trait_bounds.md +++ b/guide/src/trait_bounds.md @@ -132,7 +132,7 @@ struct UserModel { } #[pymodule] -fn trait_exposure(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn trait_exposure(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; Ok(()) } @@ -489,7 +489,7 @@ pub struct UserModel { } #[pymodule] -fn trait_exposure(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn trait_exposure(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_function(wrap_pyfunction!(solve_wrapper, m)?)?; Ok(()) diff --git a/pytests/src/awaitable.rs b/pytests/src/awaitable.rs index e1a70b42bb0..5e3b98e14ea 100644 --- a/pytests/src/awaitable.rs +++ b/pytests/src/awaitable.rs @@ -79,7 +79,7 @@ impl FutureAwaitable { } #[pymodule] -pub fn awaitable(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +pub fn awaitable(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; Ok(()) diff --git a/pytests/src/buf_and_str.rs b/pytests/src/buf_and_str.rs index 23db9f0625e..e9651e0cfd9 100644 --- a/pytests/src/buf_and_str.rs +++ b/pytests/src/buf_and_str.rs @@ -48,7 +48,7 @@ fn return_memoryview(py: Python<'_>) -> PyResult> { } #[pymodule] -pub fn buf_and_str(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +pub fn buf_and_str(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_function(wrap_pyfunction!(return_memoryview, m)?)?; Ok(()) diff --git a/pytests/src/comparisons.rs b/pytests/src/comparisons.rs index b3ba293186a..fa35acf8e1a 100644 --- a/pytests/src/comparisons.rs +++ b/pytests/src/comparisons.rs @@ -101,7 +101,7 @@ impl OrderedDefaultNe { } #[pymodule] -pub fn comparisons(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +pub fn comparisons(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/pytests/src/datetime.rs b/pytests/src/datetime.rs index 9d8f32a93c9..aeb57240c15 100644 --- a/pytests/src/datetime.rs +++ b/pytests/src/datetime.rs @@ -205,7 +205,7 @@ impl TzClass { } #[pymodule] -pub fn datetime(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +pub fn datetime(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(make_date, m)?)?; m.add_function(wrap_pyfunction!(get_date_tuple, m)?)?; m.add_function(wrap_pyfunction!(date_from_timestamp, m)?)?; diff --git a/pytests/src/dict_iter.rs b/pytests/src/dict_iter.rs index 5f5992b6efc..985c929792f 100644 --- a/pytests/src/dict_iter.rs +++ b/pytests/src/dict_iter.rs @@ -3,7 +3,7 @@ use pyo3::prelude::*; use pyo3::types::PyDict; #[pymodule] -pub fn dict_iter(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +pub fn dict_iter(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; Ok(()) } diff --git a/pytests/src/enums.rs b/pytests/src/enums.rs index 11b592d3563..32478cbefc2 100644 --- a/pytests/src/enums.rs +++ b/pytests/src/enums.rs @@ -1,7 +1,7 @@ -use pyo3::{pyclass, pyfunction, pymodule, types::PyModule, wrap_pyfunction, PyResult, Python}; +use pyo3::{pyclass, pyfunction, pymodule, types::PyModule, wrap_pyfunction, Bound, PyResult}; #[pymodule] -pub fn enums(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +pub fn enums(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_wrapped(wrap_pyfunction!(do_simple_stuff))?; diff --git a/pytests/src/misc.rs b/pytests/src/misc.rs index bd941461e91..3b893ccd1f6 100644 --- a/pytests/src/misc.rs +++ b/pytests/src/misc.rs @@ -31,7 +31,7 @@ fn get_item_and_run_callback(dict: Bound<'_, PyDict>, callback: Bound<'_, PyAny> } #[pymodule] -pub fn misc(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +pub fn misc(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(issue_219, m)?)?; m.add_function(wrap_pyfunction!(get_type_full_name, m)?)?; m.add_function(wrap_pyfunction!(accepts_bool, m)?)?; diff --git a/pytests/src/objstore.rs b/pytests/src/objstore.rs index f7fc66edb84..440f29fad63 100644 --- a/pytests/src/objstore.rs +++ b/pytests/src/objstore.rs @@ -19,6 +19,6 @@ impl ObjStore { } #[pymodule] -pub fn objstore(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +pub fn objstore(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::() } diff --git a/pytests/src/othermod.rs b/pytests/src/othermod.rs index 763d38a878f..29ca8121890 100644 --- a/pytests/src/othermod.rs +++ b/pytests/src/othermod.rs @@ -29,7 +29,7 @@ fn double(x: i32) -> i32 { } #[pymodule] -pub fn othermod(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +pub fn othermod(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(double, m)?)?; m.add_class::()?; diff --git a/pytests/src/path.rs b/pytests/src/path.rs index b3e8f92bacf..0675e56d13a 100644 --- a/pytests/src/path.rs +++ b/pytests/src/path.rs @@ -12,7 +12,7 @@ fn take_pathbuf(path: PathBuf) -> PathBuf { } #[pymodule] -pub fn path(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +pub fn path(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(make_path, m)?)?; m.add_function(wrap_pyfunction!(take_pathbuf, m)?)?; diff --git a/pytests/src/pyclasses.rs b/pytests/src/pyclasses.rs index 9c7b2d250da..1f3baec2755 100644 --- a/pytests/src/pyclasses.rs +++ b/pytests/src/pyclasses.rs @@ -80,7 +80,7 @@ impl AssertingBaseClassGilRef { struct ClassWithoutConstructor; #[pymodule] -pub fn pyclasses(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +pub fn pyclasses(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; diff --git a/pytests/src/pyfunctions.rs b/pytests/src/pyfunctions.rs index a92733c2898..77496198bb9 100644 --- a/pytests/src/pyfunctions.rs +++ b/pytests/src/pyfunctions.rs @@ -68,7 +68,7 @@ fn args_kwargs<'py>( } #[pymodule] -pub fn pyfunctions(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +pub fn pyfunctions(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(none, m)?)?; m.add_function(wrap_pyfunction!(simple, m)?)?; m.add_function(wrap_pyfunction!(simple_args, m)?)?; diff --git a/pytests/src/sequence.rs b/pytests/src/sequence.rs index 5916414ee8f..0e48a161bd3 100644 --- a/pytests/src/sequence.rs +++ b/pytests/src/sequence.rs @@ -17,7 +17,7 @@ fn vec_to_vec_pystring(vec: Vec<&PyString>) -> Vec<&PyString> { } #[pymodule] -pub fn sequence(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +pub fn sequence(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(vec_to_vec_i32, m)?)?; m.add_function(wrap_pyfunction!(array_to_array_i32, m)?)?; m.add_function(wrap_pyfunction!(vec_to_vec_pystring, m)?)?; diff --git a/pytests/src/subclassing.rs b/pytests/src/subclassing.rs index 0033114ccea..8e451cd9183 100644 --- a/pytests/src/subclassing.rs +++ b/pytests/src/subclassing.rs @@ -18,7 +18,7 @@ impl Subclassable { } #[pymodule] -pub fn subclassing(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +pub fn subclassing(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; Ok(()) } diff --git a/src/conversions/indexmap.rs b/src/conversions/indexmap.rs index e908cddb621..bb9ae0126d0 100644 --- a/src/conversions/indexmap.rs +++ b/src/conversions/indexmap.rs @@ -71,7 +71,7 @@ //! } //! //! #[pymodule] -//! fn my_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +//! fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> { //! m.add_function(wrap_pyfunction!(calculate_statistics, m)?)?; //! Ok(()) //! } diff --git a/src/conversions/num_bigint.rs b/src/conversions/num_bigint.rs index e6f345fd200..69bc5493366 100644 --- a/src/conversions/num_bigint.rs +++ b/src/conversions/num_bigint.rs @@ -31,7 +31,7 @@ //! } //! //! #[pymodule] -//! fn my_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +//! fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> { //! m.add_function(wrap_pyfunction!(add_one, m)?)?; //! Ok(()) //! } diff --git a/src/conversions/num_complex.rs b/src/conversions/num_complex.rs index 369888af85f..a57b2745ec9 100644 --- a/src/conversions/num_complex.rs +++ b/src/conversions/num_complex.rs @@ -44,7 +44,7 @@ //! } //! //! #[pymodule] -//! fn my_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +//! fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> { //! m.add_function(wrap_pyfunction!(get_eigenvalues, m)?)?; //! Ok(()) //! } @@ -57,7 +57,7 @@ //! # Python::with_gil(|py| -> PyResult<()> { //! # let module = PyModule::new_bound(py, "my_module")?; //! # -//! # module.add_function(&wrap_pyfunction!(get_eigenvalues, module.as_gil_ref())?.as_borrowed())?; +//! # 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); diff --git a/src/conversions/rust_decimal.rs b/src/conversions/rust_decimal.rs index 2e3151720e6..8bf2e33752d 100644 --- a/src/conversions/rust_decimal.rs +++ b/src/conversions/rust_decimal.rs @@ -30,7 +30,7 @@ //! } //! //! #[pymodule] -//! fn my_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +//! fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> { //! m.add_function(wrap_pyfunction!(add_one, m)?)?; //! Ok(()) //! } diff --git a/src/exceptions.rs b/src/exceptions.rs index 1c952393241..3401679b25e 100644 --- a/src/exceptions.rs +++ b/src/exceptions.rs @@ -164,9 +164,9 @@ macro_rules! import_exception { /// } /// /// #[pymodule] -/// fn my_module(py: Python<'_>, m: &PyModule) -> PyResult<()> { -/// m.add("MyError", py.get_type_bound::())?; -/// m.add_function(wrap_pyfunction!(raise_myerror, py)?)?; +/// fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> { +/// m.add("MyError", m.py().get_type_bound::())?; +/// m.add_function(wrap_pyfunction!(raise_myerror, m)?)?; /// Ok(()) /// } /// # fn main() -> PyResult<()> { diff --git a/src/lib.rs b/src/lib.rs index 17dbd972d85..c2591cec91e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -168,7 +168,7 @@ //! //! /// A Python module implemented in Rust. //! #[pymodule] -//! fn string_sum(py: Python<'_>, m: &PyModule) -> PyResult<()> { +//! fn string_sum(m: &Bound<'_, PyModule>) -> PyResult<()> { //! m.add_function(wrap_pyfunction!(sum_as_string, m)?)?; //! //! Ok(()) diff --git a/src/types/module.rs b/src/types/module.rs index 693df79cafa..d6f3acb2567 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -239,7 +239,7 @@ impl PyModule { /// use pyo3::prelude::*; /// /// #[pymodule] - /// fn my_module(_py: Python<'_>, module: &PyModule) -> PyResult<()> { + /// fn my_module(module: &Bound<'_, PyModule>) -> PyResult<()> { /// module.add("c", 299_792_458)?; /// Ok(()) /// } @@ -280,7 +280,7 @@ impl PyModule { /// struct Foo { /* fields omitted */ } /// /// #[pymodule] - /// fn my_module(_py: Python<'_>, module: &PyModule) -> PyResult<()> { + /// fn my_module(module: &Bound<'_, PyModule>) -> PyResult<()> { /// module.add_class::()?; /// Ok(()) /// } @@ -377,7 +377,7 @@ impl PyModule { /// println!("Hello world!") /// } /// #[pymodule] - /// fn my_module(_py: Python<'_>, module: &PyModule) -> PyResult<()> { + /// fn my_module(module: &Bound<'_, PyModule>) -> PyResult<()> { /// module.add_function(wrap_pyfunction!(say_hello, module)?) /// } /// ``` @@ -442,7 +442,7 @@ pub trait PyModuleMethods<'py>: crate::sealed::Sealed { /// use pyo3::prelude::*; /// /// #[pymodule] - /// fn my_module(_py: Python<'_>, module: &PyModule) -> PyResult<()> { + /// fn my_module(module: &Bound<'_, PyModule>) -> PyResult<()> { /// module.add("c", 299_792_458)?; /// Ok(()) /// } @@ -481,7 +481,7 @@ pub trait PyModuleMethods<'py>: crate::sealed::Sealed { /// struct Foo { /* fields omitted */ } /// /// #[pymodule] - /// fn my_module(_py: Python<'_>, module: &PyModule) -> PyResult<()> { + /// fn my_module(module: &Bound<'_, PyModule>) -> PyResult<()> { /// module.add_class::()?; /// Ok(()) /// } @@ -570,7 +570,7 @@ pub trait PyModuleMethods<'py>: crate::sealed::Sealed { /// println!("Hello world!") /// } /// #[pymodule] - /// fn my_module(_py: Python<'_>, module: &PyModule) -> PyResult<()> { + /// fn my_module(module: &Bound<'_, PyModule>) -> PyResult<()> { /// module.add_function(wrap_pyfunction!(say_hello, module)?) /// } /// ``` diff --git a/tests/test_append_to_inittab.rs b/tests/test_append_to_inittab.rs index 59ecaf42909..da35298b4d9 100644 --- a/tests/test_append_to_inittab.rs +++ b/tests/test_append_to_inittab.rs @@ -8,8 +8,8 @@ fn foo() -> usize { } #[pymodule] -fn module_fn_with_functions(_py: Python<'_>, m: &PyModule) -> PyResult<()> { - m.add_function(wrap_pyfunction!(foo, m)?).unwrap(); +fn module_fn_with_functions(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_function(wrap_pyfunction!(foo, m)?)?; Ok(()) } diff --git a/tests/test_module.rs b/tests/test_module.rs index 5763043e57d..d4c4acca90f 100644 --- a/tests/test_module.rs +++ b/tests/test_module.rs @@ -36,7 +36,7 @@ fn double(x: usize) -> usize { /// This module is implemented in Rust. #[pymodule] -fn module_with_functions(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn module_with_functions(m: &Bound<'_, PyModule>) -> PyResult<()> { #[pyfn(m)] #[pyo3(name = "no_parameters")] fn function_with_name() -> usize { @@ -54,14 +54,14 @@ fn module_with_functions(_py: Python<'_>, m: &PyModule) -> PyResult<()> { v.value * 2 } - m.add_class::().unwrap(); - m.add_class::().unwrap(); - m.add_class::().unwrap(); + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; - m.add("foo", "bar").unwrap(); + m.add("foo", "bar")?; - m.add_function(wrap_pyfunction!(double, m)?).unwrap(); - m.add("also_double", wrap_pyfunction!(double, m)?).unwrap(); + m.add_function(wrap_pyfunction!(double, m)?)?; + m.add("also_double", wrap_pyfunction!(double, m)?)?; Ok(()) } @@ -116,9 +116,31 @@ fn test_module_with_functions() { }); } +/// This module uses a legacy two-argument module function. +#[pymodule] +fn module_with_explicit_py_arg(_py: Python<'_>, m: &PyModule) -> PyResult<()> { + m.add_function(wrap_pyfunction!(double, m)?)?; + Ok(()) +} + +#[test] +fn test_module_with_explicit_py_arg() { + use pyo3::wrap_pymodule; + + Python::with_gil(|py| { + let d = [( + "module_with_explicit_py_arg", + wrap_pymodule!(module_with_explicit_py_arg)(py), + )] + .into_py_dict_bound(py); + + py_assert!(py, *d, "module_with_explicit_py_arg.double(3) == 6"); + }); +} + #[pymodule] #[pyo3(name = "other_name")] -fn some_name(_: Python<'_>, m: &PyModule) -> PyResult<()> { +fn some_name(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add("other_name", "other_name")?; Ok(()) } @@ -166,7 +188,7 @@ fn r#move() -> usize { } #[pymodule] -fn raw_ident_module(_py: Python<'_>, module: &PyModule) -> PyResult<()> { +fn raw_ident_module(module: &Bound<'_, PyModule>) -> PyResult<()> { module.add_function(wrap_pyfunction!(r#move, module)?) } @@ -190,7 +212,7 @@ fn custom_named_fn() -> usize { #[test] fn test_custom_names() { #[pymodule] - fn custom_names(_py: Python<'_>, m: &PyModule) -> PyResult<()> { + fn custom_names(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(custom_named_fn, m)?)?; Ok(()) } @@ -206,7 +228,7 @@ fn test_custom_names() { #[test] fn test_module_dict() { #[pymodule] - fn module_dict(_py: Python<'_>, m: &PyModule) -> PyResult<()> { + fn module_dict(m: &Bound<'_, PyModule>) -> PyResult<()> { m.dict().set_item("yay", "me")?; Ok(()) } @@ -222,7 +244,7 @@ fn test_module_dict() { fn test_module_dunder_all() { Python::with_gil(|py| { #[pymodule] - fn dunder_all(_py: Python<'_>, m: &PyModule) -> PyResult<()> { + fn dunder_all(m: &Bound<'_, PyModule>) -> PyResult<()> { m.dict().set_item("yay", "me")?; m.add_function(wrap_pyfunction!(custom_named_fn, m)?)?; Ok(()) @@ -245,7 +267,7 @@ fn submodule(module: &Bound<'_, PyModule>) -> PyResult<()> { } #[pymodule] -fn submodule_with_init_fn(_py: Python<'_>, module: &PyModule) -> PyResult<()> { +fn submodule_with_init_fn(module: &Bound<'_, PyModule>) -> PyResult<()> { module.add_function(wrap_pyfunction!(subfunction, module)?)?; Ok(()) } @@ -256,14 +278,14 @@ fn superfunction() -> String { } #[pymodule] -fn supermodule(py: Python<'_>, module: &PyModule) -> PyResult<()> { +fn supermodule(module: &Bound<'_, PyModule>) -> PyResult<()> { module.add_function(wrap_pyfunction!(superfunction, module)?)?; - let module_to_add = PyModule::new_bound(py, "submodule")?; + let module_to_add = PyModule::new_bound(module.py(), "submodule")?; submodule(&module_to_add)?; - module.add_submodule(module_to_add.as_gil_ref())?; - let module_to_add = PyModule::new_bound(py, "submodule_with_init_fn")?; - submodule_with_init_fn(py, module_to_add.as_gil_ref())?; - module.add_submodule(module_to_add.as_gil_ref())?; + module.add_submodule(&module_to_add)?; + let module_to_add = PyModule::new_bound(module.py(), "submodule_with_init_fn")?; + submodule_with_init_fn(&module_to_add)?; + module.add_submodule(&module_to_add)?; Ok(()) } @@ -300,7 +322,7 @@ fn ext_vararg_fn(py: Python<'_>, a: i32, args: &Bound<'_, PyTuple>) -> PyObject } #[pymodule] -fn vararg_module(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { +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 { ext_vararg_fn(py, a, args) @@ -328,7 +350,7 @@ fn test_module_with_constant() { // Regression test for #1102 #[pymodule] - fn module_with_constant(_py: Python<'_>, m: &PyModule) -> PyResult<()> { + fn module_with_constant(m: &Bound<'_, PyModule>) -> PyResult<()> { const ANON: AnonClass = AnonClass {}; m.add("ANON", ANON)?; @@ -410,7 +432,7 @@ fn pyfunction_with_pass_module_in_attribute(module: &PyModule) -> PyResult<&str> } #[pymodule] -fn module_with_functions_with_module(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { +fn module_with_functions_with_module(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(pyfunction_with_module, m)?)?; m.add_function(wrap_pyfunction!(pyfunction_with_module_gil_ref, m)?)?; m.add_function(wrap_pyfunction!(pyfunction_with_module_owned, m)?)?; @@ -475,7 +497,7 @@ fn test_module_doc_hidden() { #[doc(hidden)] #[allow(clippy::unnecessary_wraps)] #[pymodule] - fn my_module(_py: Python<'_>, _m: &PyModule) -> PyResult<()> { + fn my_module(_m: &Bound<'_, PyModule>) -> PyResult<()> { Ok(()) } diff --git a/tests/test_text_signature.rs b/tests/test_text_signature.rs index cb2cd85e99b..6b56999c62f 100644 --- a/tests/test_text_signature.rs +++ b/tests/test_text_signature.rs @@ -335,7 +335,7 @@ fn test_auto_test_signature_opt_out() { #[test] fn test_pyfn() { #[pymodule] - fn my_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { + fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> { #[pyfn(m, signature = (a, b=None, *, c=42))] #[pyo3(text_signature = "(a, b=None, *, c=42)")] fn my_function(a: i32, b: Option, c: i32) { diff --git a/tests/ui/invalid_pymodule_args.rs b/tests/ui/invalid_pymodule_args.rs index ebd229eef2e..37e53960fd3 100644 --- a/tests/ui/invalid_pymodule_args.rs +++ b/tests/ui/invalid_pymodule_args.rs @@ -1,7 +1,7 @@ use pyo3::prelude::*; #[pymodule(some_arg)] -fn module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn module(m: &Bound<'_, PyModule>) -> PyResult<()> { Ok(()) } From 56683ed553dfa413cc623425c655514b69c3992b Mon Sep 17 00:00:00 2001 From: Lily Foote Date: Thu, 29 Feb 2024 07:15:34 +0000 Subject: [PATCH 055/936] deprecate Py::as_ref (#3864) * Deprecate Py::as_ref * Reword as_ref deprecation note Co-authored-by: David Hewitt * Tidy up remaining uses of Py::as_ref Co-authored-by: David Hewitt * Pass hello into println! explicitly --------- Co-authored-by: David Hewitt --- guide/src/class.md | 1 + guide/src/memory.md | 4 +- guide/src/performance.md | 10 +++- guide/src/trait_bounds.md | 15 ++++++ guide/src/types.md | 4 ++ src/impl_/coroutine.rs | 12 ++++- src/instance.rs | 9 ++++ src/pycell/impl_.rs | 50 +++++++++---------- src/pyclass.rs | 16 +++++- tests/ui/invalid_frozen_pyclass_borrow.rs | 4 +- tests/ui/invalid_frozen_pyclass_borrow.stderr | 28 +++++------ 11 files changed, 105 insertions(+), 48 deletions(-) diff --git a/guide/src/class.md b/guide/src/class.md index 1e752700613..cc254973370 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -256,6 +256,7 @@ fn return_myclass() -> Py { let obj = return_myclass(); Python::with_gil(|py| { + #[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. let cell = obj.as_ref(py); // Py::as_ref returns &PyCell let obj_ref = cell.borrow(); // Get PyRef assert_eq!(obj_ref.num, 1); diff --git a/guide/src/memory.md b/guide/src/memory.md index 2f5e5d9b0bd..46136e3f1a4 100644 --- a/guide/src/memory.md +++ b/guide/src/memory.md @@ -171,7 +171,9 @@ let hello: Py = Python::with_gil(|py| { // Do some stuff... // Now sometime later in the program we want to access `hello`. Python::with_gil(|py| { - println!("Python says: {}", hello.as_ref(py)); + #[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. + let hello = hello.as_ref(py); + println!("Python says: {}", hello); }); // Now we're done with `hello`. drop(hello); // Memory *not* released here. diff --git a/guide/src/performance.md b/guide/src/performance.md index 23fb59c4e90..fe362bed953 100644 --- a/guide/src/performance.md +++ b/guide/src/performance.md @@ -70,7 +70,11 @@ struct FooRef<'a>(&'a PyList); impl PartialEq for FooRef<'_> { fn eq(&self, other: &Foo) -> bool { - Python::with_gil(|py| self.0.len() == other.0.as_ref(py).len()) + Python::with_gil(|py| { + #[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. + let len = other.0.as_ref(py).len(); + self.0.len() == len + }) } } ``` @@ -88,7 +92,9 @@ impl PartialEq for FooRef<'_> { fn eq(&self, other: &Foo) -> bool { // Access to `&'a PyAny` implies access to `Python<'a>`. let py = self.0.py(); - self.0.len() == other.0.as_ref(py).len() + #[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. + let len = other.0.as_ref(py).len(); + self.0.len() == len } } ``` diff --git a/guide/src/trait_bounds.md b/guide/src/trait_bounds.md index e05d44e982a..6dfaa2e20aa 100644 --- a/guide/src/trait_bounds.md +++ b/guide/src/trait_bounds.md @@ -83,6 +83,7 @@ impl Model for UserModel { Python::with_gil(|py| { let values: Vec = var.clone(); let list: PyObject = values.into_py(py); + #[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. let py_model = self.model.as_ref(py); py_model .call_method("set_variables", (list,), None) @@ -93,6 +94,7 @@ impl Model for UserModel { fn get_results(&self) -> Vec { println!("Rust calling Python to get the results"); Python::with_gil(|py| { + #[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. self.model .as_ref(py) .call_method("get_results", (), None) @@ -105,6 +107,7 @@ impl Model for UserModel { fn compute(&mut self) { println!("Rust calling Python to perform the computation"); Python::with_gil(|py| { + #[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. self.model .as_ref(py) .call_method("compute", (), None) @@ -183,6 +186,7 @@ This wrapper will also perform the type conversions between Python and Rust. # Python::with_gil(|py| { # let values: Vec = var.clone(); # let list: PyObject = values.into_py(py); +# #[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. # let py_model = self.model.as_ref(py); # py_model # .call_method("set_variables", (list,), None) @@ -193,6 +197,7 @@ This wrapper will also perform the type conversions between Python and Rust. # fn get_results(&self) -> Vec { # println!("Rust calling Python to get the results"); # Python::with_gil(|py| { +# #[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. # self.model # .as_ref(py) # .call_method("get_results", (), None) @@ -205,6 +210,7 @@ This wrapper will also perform the type conversions between Python and Rust. # fn compute(&mut self) { # println!("Rust calling Python to perform the computation"); # Python::with_gil(|py| { +# #[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. # self.model # .as_ref(py) # .call_method("compute", (), None) @@ -349,6 +355,7 @@ We used in our `get_results` method the following call that performs the type co impl Model for UserModel { fn get_results(&self) -> Vec { println!("Rust calling Python to get the results"); + #[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. Python::with_gil(|py| { self.model .as_ref(py) @@ -363,6 +370,7 @@ impl Model for UserModel { # Python::with_gil(|py| { # let values: Vec = var.clone(); # let list: PyObject = values.into_py(py); +# #[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. # let py_model = self.model.as_ref(py); # py_model # .call_method("set_variables", (list,), None) @@ -373,6 +381,7 @@ impl Model for UserModel { # fn compute(&mut self) { # println!("Rust calling Python to perform the computation"); # Python::with_gil(|py| { +# #[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. # self.model # .as_ref(py) # .call_method("compute", (), None) @@ -403,6 +412,7 @@ impl Model for UserModel { fn get_results(&self) -> Vec { println!("Get results from Rust calling Python"); Python::with_gil(|py| { + #[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. let py_result: &PyAny = self .model .as_ref(py) @@ -424,6 +434,7 @@ impl Model for UserModel { # Python::with_gil(|py| { # let values: Vec = var.clone(); # let list: PyObject = values.into_py(py); +# #[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. # let py_model = self.model.as_ref(py); # py_model # .call_method("set_variables", (list,), None) @@ -434,6 +445,7 @@ impl Model for UserModel { # fn compute(&mut self) { # println!("Rust calling Python to perform the computation"); # Python::with_gil(|py| { +# #[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. # self.model # .as_ref(py) # .call_method("compute", (), None) @@ -523,6 +535,7 @@ impl Model for UserModel { Python::with_gil(|py| { let values: Vec = var.clone(); let list: PyObject = values.into_py(py); + #[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. let py_model = self.model.as_ref(py); py_model .call_method("set_variables", (list,), None) @@ -533,6 +546,7 @@ impl Model for UserModel { fn get_results(&self) -> Vec { println!("Get results from Rust calling Python"); Python::with_gil(|py| { + #[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. let py_result: &PyAny = self .model .as_ref(py) @@ -553,6 +567,7 @@ impl Model for UserModel { fn compute(&mut self) { println!("Rust calling Python to perform the computation"); Python::with_gil(|py| { + #[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. self.model .as_ref(py) .call_method("compute", (), None) diff --git a/guide/src/types.md b/guide/src/types.md index f768a31f019..51ea10f4545 100644 --- a/guide/src/types.md +++ b/guide/src/types.md @@ -145,6 +145,7 @@ let _ = list.repr()?; let _: &PyAny = list; // To &PyAny explicitly with .as_ref() +#[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. let _: &PyAny = list.as_ref(); // To Py with .into() or Py::from() @@ -179,6 +180,7 @@ For a `Py`, the conversions are as below: let list: Py = PyList::empty_bound(py).unbind(); // To &PyList with Py::as_ref() (borrows from the Py) +#[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. let _: &PyList = list.as_ref(py); # let list_clone = list.clone(); // Because `.into_ref()` will consume `list`. @@ -202,6 +204,7 @@ For a `#[pyclass] struct MyClass`, the conversions for `Py` are below: let my_class: Py = Py::new(py, MyClass { })?; // To &PyCell with Py::as_ref() (borrows from the Py) +#[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. let _: &PyCell = my_class.as_ref(py); # let my_class_clone = my_class.clone(); // Because `.into_ref()` will consume `my_class`. @@ -276,6 +279,7 @@ let _ = cell.repr()?; let _: &PyAny = cell; // To &PyAny explicitly with .as_ref() +#[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. let _: &PyAny = cell.as_ref(); # Ok(()) # }).unwrap(); diff --git a/src/impl_/coroutine.rs b/src/impl_/coroutine.rs index c9ca4873c6c..a59eddd0e8f 100644 --- a/src/impl_/coroutine.rs +++ b/src/impl_/coroutine.rs @@ -56,7 +56,11 @@ impl Deref for RefGuard { impl Drop for RefGuard { fn drop(&mut self) { - Python::with_gil(|gil| self.0.as_ref(gil).release_ref()) + Python::with_gil(|gil| { + #[allow(deprecated)] + let self_ref = self.0.bind(gil); + self_ref.release_ref() + }) } } @@ -87,6 +91,10 @@ impl> DerefMut for RefMutGuard { impl> Drop for RefMutGuard { fn drop(&mut self) { - Python::with_gil(|gil| self.0.as_ref(gil).release_mut()) + Python::with_gil(|gil| { + #[allow(deprecated)] + let self_ref = self.0.bind(gil); + self_ref.release_mut() + }) } } diff --git a/src/instance.rs b/src/instance.rs index cb33ed32c75..65c5ee3bc5f 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -914,6 +914,7 @@ where /// # /// 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); /// }); @@ -929,10 +930,18 @@ where /// /// Python::with_gil(|py| { /// let my_class: Py = Py::new(py, MyClass {}).unwrap(); + /// # #[allow(deprecated)] /// let my_class_cell: &PyCell = my_class.as_ref(py); /// assert!(my_class_cell.try_borrow().is_ok()); /// }); /// ``` + #[cfg_attr( + not(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; unsafe { PyNativeType::unchecked_downcast(&*any) } diff --git a/src/pycell/impl_.rs b/src/pycell/impl_.rs index 62875e67ae4..29ba7eda7eb 100644 --- a/src/pycell/impl_.rs +++ b/src/pycell/impl_.rs @@ -284,43 +284,43 @@ mod tests { ) .unwrap(); - let mmm_cell: &PyCell = mmm.as_ref(py); + let mmm_bound: &Bound<'_, MutableChildOfMutableChildOfMutableBase> = mmm.bind(py); - let mmm_refmut = mmm_cell.borrow_mut(); + let mmm_refmut = mmm_bound.borrow_mut(); // Cannot take any other mutable or immutable borrows whilst the object is borrowed mutably - assert!(mmm_cell + assert!(mmm_bound .extract::>() .is_err()); - assert!(mmm_cell + assert!(mmm_bound .extract::>() .is_err()); - assert!(mmm_cell.extract::>().is_err()); - assert!(mmm_cell + assert!(mmm_bound.extract::>().is_err()); + assert!(mmm_bound .extract::>() .is_err()); - assert!(mmm_cell + assert!(mmm_bound .extract::>() .is_err()); - assert!(mmm_cell.extract::>().is_err()); + assert!(mmm_bound.extract::>().is_err()); // With the borrow dropped, all other borrow attempts will succeed drop(mmm_refmut); - assert!(mmm_cell + assert!(mmm_bound .extract::>() .is_ok()); - assert!(mmm_cell + assert!(mmm_bound .extract::>() .is_ok()); - assert!(mmm_cell.extract::>().is_ok()); - assert!(mmm_cell + assert!(mmm_bound.extract::>().is_ok()); + assert!(mmm_bound .extract::>() .is_ok()); - assert!(mmm_cell + assert!(mmm_bound .extract::>() .is_ok()); - assert!(mmm_cell.extract::>().is_ok()); + assert!(mmm_bound.extract::>().is_ok()); }) } @@ -335,38 +335,38 @@ mod tests { ) .unwrap(); - let mmm_cell: &PyCell = mmm.as_ref(py); + let mmm_bound: &Bound<'_, MutableChildOfMutableChildOfMutableBase> = mmm.bind(py); - let mmm_refmut = mmm_cell.borrow(); + let mmm_refmut = mmm_bound.borrow(); // Further immutable borrows are ok - assert!(mmm_cell + assert!(mmm_bound .extract::>() .is_ok()); - assert!(mmm_cell + assert!(mmm_bound .extract::>() .is_ok()); - assert!(mmm_cell.extract::>().is_ok()); + assert!(mmm_bound.extract::>().is_ok()); // Further mutable borrows are not ok - assert!(mmm_cell + assert!(mmm_bound .extract::>() .is_err()); - assert!(mmm_cell + assert!(mmm_bound .extract::>() .is_err()); - assert!(mmm_cell.extract::>().is_err()); + assert!(mmm_bound.extract::>().is_err()); // With the borrow dropped, all mutable borrow attempts will succeed drop(mmm_refmut); - assert!(mmm_cell + assert!(mmm_bound .extract::>() .is_ok()); - assert!(mmm_cell + assert!(mmm_bound .extract::>() .is_ok()); - assert!(mmm_cell.extract::>().is_ok()); + assert!(mmm_bound.extract::>().is_ok()); }) } } diff --git a/src/pyclass.rs b/src/pyclass.rs index eb4a5595ca9..ebdc52dc217 100644 --- a/src/pyclass.rs +++ b/src/pyclass.rs @@ -1,7 +1,7 @@ //! `PyClass` and related traits. use crate::{ - callback::IntoPyCallbackOutput, ffi, impl_::pyclass::PyClassImpl, IntoPy, PyCell, PyObject, - PyResult, PyTypeInfo, Python, + callback::IntoPyCallbackOutput, ffi, impl_::pyclass::PyClassImpl, Bound, IntoPy, PyCell, + PyObject, PyResult, PyTypeInfo, Python, }; use std::{cmp::Ordering, os::raw::c_int}; @@ -216,6 +216,18 @@ pub trait Frozen: boolean_struct::private::Boolean {} impl Frozen for boolean_struct::True {} impl Frozen for boolean_struct::False {} +impl<'py, T: PyClass> Bound<'py, T> { + #[cfg(feature = "macros")] + pub(crate) fn release_ref(&self) { + self.get_cell().release_ref(); + } + + #[cfg(feature = "macros")] + pub(crate) fn release_mut(&self) { + self.get_cell().release_mut(); + } +} + mod tests { #[test] fn test_compare_op_matches() { diff --git a/tests/ui/invalid_frozen_pyclass_borrow.rs b/tests/ui/invalid_frozen_pyclass_borrow.rs index 1f18eab6170..aa8969ab1a6 100644 --- a/tests/ui/invalid_frozen_pyclass_borrow.rs +++ b/tests/ui/invalid_frozen_pyclass_borrow.rs @@ -12,7 +12,7 @@ impl Foo { } fn borrow_mut_fails(foo: Py, py: Python) { - let borrow = foo.as_ref(py).borrow_mut(); + let borrow = foo.bind(py).borrow_mut(); } #[pyclass(subclass)] @@ -22,7 +22,7 @@ struct MutableBase; struct ImmutableChild; fn borrow_mut_of_child_fails(child: Py, py: Python) { - let borrow = child.as_ref(py).borrow_mut(); + let borrow = child.bind(py).borrow_mut(); } fn py_get_of_mutable_class_fails(class: Py) { diff --git a/tests/ui/invalid_frozen_pyclass_borrow.stderr b/tests/ui/invalid_frozen_pyclass_borrow.stderr index 5e09d512ae7..1a8e45964ca 100644 --- a/tests/ui/invalid_frozen_pyclass_borrow.stderr +++ b/tests/ui/invalid_frozen_pyclass_borrow.stderr @@ -17,34 +17,34 @@ note: required by a bound in `extract_pyclass_ref_mut` | ^^^^^^^^^^^^^^ required by this bound in `extract_pyclass_ref_mut` error[E0271]: type mismatch resolving `::Frozen == False` - --> tests/ui/invalid_frozen_pyclass_borrow.rs:15:33 + --> tests/ui/invalid_frozen_pyclass_borrow.rs:15:31 | -15 | let borrow = foo.as_ref(py).borrow_mut(); - | ^^^^^^^^^^ expected `False`, found `True` +15 | let borrow = foo.bind(py).borrow_mut(); + | ^^^^^^^^^^ expected `False`, found `True` | -note: required by a bound in `pyo3::PyCell::::borrow_mut` - --> src/pycell.rs +note: required by a bound in `pyo3::Bound::<'py, T>::borrow_mut` + --> src/instance.rs | - | pub fn borrow_mut(&self) -> PyRefMut<'_, T> + | pub fn borrow_mut(&self) -> PyRefMut<'py, T> | ---------- required by a bound in this associated function | where | T: PyClass, - | ^^^^^^^^^^^^^^ required by this bound in `PyCell::::borrow_mut` + | ^^^^^^^^^^^^^^ required by this bound in `Bound::<'py, T>::borrow_mut` error[E0271]: type mismatch resolving `::Frozen == False` - --> tests/ui/invalid_frozen_pyclass_borrow.rs:25:35 + --> tests/ui/invalid_frozen_pyclass_borrow.rs:25:33 | -25 | let borrow = child.as_ref(py).borrow_mut(); - | ^^^^^^^^^^ expected `False`, found `True` +25 | let borrow = child.bind(py).borrow_mut(); + | ^^^^^^^^^^ expected `False`, found `True` | -note: required by a bound in `pyo3::PyCell::::borrow_mut` - --> src/pycell.rs +note: required by a bound in `pyo3::Bound::<'py, T>::borrow_mut` + --> src/instance.rs | - | pub fn borrow_mut(&self) -> PyRefMut<'_, T> + | pub fn borrow_mut(&self) -> PyRefMut<'py, T> | ---------- required by a bound in this associated function | where | T: PyClass, - | ^^^^^^^^^^^^^^ required by this bound in `PyCell::::borrow_mut` + | ^^^^^^^^^^^^^^ required by this bound in `Bound::<'py, T>::borrow_mut` error[E0271]: type mismatch resolving `::Frozen == True` --> tests/ui/invalid_frozen_pyclass_borrow.rs:29:11 From 1d224610c30b86f5125a849fcba4ad4eb969f9e7 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Fri, 1 Mar 2024 10:21:47 +0100 Subject: [PATCH 056/936] docs: update `Python classes` section of the guide (#3914) * docs: update `Python classes` section of the guide * review feedback davidhewitt * migration guide entry --- examples/decorator/src/lib.rs | 6 +-- guide/src/class.md | 69 +++++++++++++++++------------------ guide/src/class/call.md | 4 +- guide/src/class/numeric.md | 14 +++---- guide/src/class/object.md | 6 +-- guide/src/class/protocols.md | 2 +- guide/src/migration.md | 4 ++ src/types/tuple.rs | 6 +++ 8 files changed, 60 insertions(+), 51 deletions(-) diff --git a/examples/decorator/src/lib.rs b/examples/decorator/src/lib.rs index 9dccabc7341..cfb09c112d5 100644 --- a/examples/decorator/src/lib.rs +++ b/examples/decorator/src/lib.rs @@ -40,8 +40,8 @@ impl PyCounter { fn __call__( &self, py: Python<'_>, - args: &PyTuple, - kwargs: Option>, + args: &Bound<'_, PyTuple>, + kwargs: Option<&Bound<'_, PyDict>>, ) -> PyResult> { let old_count = self.count.get(); let new_count = old_count + 1; @@ -51,7 +51,7 @@ impl PyCounter { println!("{} has been called {} time(s).", name, new_count); // After doing something, we finally forward the call to the wrapped function - let ret = self.wraps.call_bound(py, args, kwargs.as_ref())?; + let ret = self.wraps.call_bound(py, args, kwargs)?; // We could do something with the return value of // the function before returning it diff --git a/guide/src/class.md b/guide/src/class.md index cc254973370..3096c9f7842 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -187,26 +187,23 @@ fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> { } ``` -## PyCell and interior mutability +## Bound and interior mutability -You sometimes need to convert your `pyclass` into a Python object and access it +You sometimes need to convert your `#[pyclass]` into a Python object and access it from Rust code (e.g., for testing it). -[`PyCell`] is the primary interface for that. +[`Bound`] is the primary interface for that. -`PyCell` is always allocated in the Python heap, so Rust doesn't have ownership of it. -In other words, Rust code can only extract a `&PyCell`, not a `PyCell`. - -Thus, to mutate data behind `&PyCell` safely, PyO3 employs the +To mutate data behind a `Bound<'_, T>` safely, PyO3 employs the [Interior Mutability Pattern](https://doc.rust-lang.org/book/ch15-05-interior-mutability.html) like [`RefCell`]. -Users who are familiar with `RefCell` can use `PyCell` just like `RefCell`. +Users who are familiar with `RefCell` can use `Bound` just like `RefCell`. For users who are not very familiar with `RefCell`, here is a reminder of Rust's rules of borrowing: - At any given time, you can have either (but not both of) one mutable reference or any number of immutable references. - References must always be valid. -`PyCell`, like `RefCell`, ensures these borrowing rules by tracking references at runtime. +`Bound`, like `RefCell`, ensures these borrowing rules by tracking references at runtime. ```rust # use pyo3::prelude::*; @@ -216,8 +213,7 @@ struct MyClass { num: i32, } Python::with_gil(|py| { -# #[allow(deprecated)] - let obj = PyCell::new(py, MyClass { num: 3 }).unwrap(); + let obj = Bound::new(py, MyClass { num: 3 }).unwrap(); { let obj_ref = obj.borrow(); // Get PyRef assert_eq!(obj_ref.num, 3); @@ -232,12 +228,12 @@ Python::with_gil(|py| { assert!(obj.try_borrow_mut().is_err()); } - // You can convert `&PyCell` to a Python object + // You can convert `Bound` to a Python object pyo3::py_run!(py, obj, "assert obj.num == 5"); }); ``` -`&PyCell` is bounded by the same lifetime as a [`GILGuard`]. +A `Bound<'py, T>` is restricted to the GIL lifetime `'py`. To make the object longer lived (for example, to store it in a struct on the Rust side), you can use `Py`, which stores an object longer than the GIL lifetime, and therefore needs a `Python<'_>` token to access. @@ -256,9 +252,8 @@ fn return_myclass() -> Py { let obj = return_myclass(); Python::with_gil(|py| { - #[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. - let cell = obj.as_ref(py); // Py::as_ref returns &PyCell - let obj_ref = cell.borrow(); // Get PyRef + let bound = obj.bind(py); // Py::bind returns &Bound<'_, MyClass> + let obj_ref = bound.borrow(); // Get PyRef assert_eq!(obj_ref.num, 1); }); ``` @@ -267,7 +262,7 @@ Python::with_gil(|py| { As detailed above, runtime borrow checking is currently enabled by default. But a class can opt of out it by declaring itself `frozen`. It can still use interior mutability via standard Rust types like `RefCell` or `Mutex`, but it is not bound to the implementation provided by PyO3 and can choose the most appropriate strategy on field-by-field basis. -Classes which are `frozen` and also `Sync`, e.g. they do use `Mutex` but not `RefCell`, can be accessed without needing the Python GIL via the `PyCell::get` and `Py::get` methods: +Classes which are `frozen` and also `Sync`, e.g. they do use `Mutex` but not `RefCell`, can be accessed without needing the Python GIL via the `Bound::get` and `Py::get` methods: ```rust use std::sync::atomic::{AtomicUsize, Ordering}; @@ -413,8 +408,9 @@ You can inherit native types such as `PyDict`, if they implement [`PySizedLayout`]({{#PYO3_DOCS_URL}}/pyo3/type_object/trait.PySizedLayout.html). This is not supported when building for the Python limited API (aka the `abi3` feature of PyO3). -However, because of some technical problems, we don't currently provide safe upcasting methods for types -that inherit native types. Even in such cases, you can unsafely get a base class by raw pointer conversion. +To convert between the Rust type and its native base class, you can take +`slf` as a Python object. To access the Rust fields use `slf.borrow()` or +`slf.borrow_mut()`, and to access the base class use `slf.downcast::()`. ```rust # #[cfg(not(Py_LIMITED_API))] { @@ -435,10 +431,9 @@ impl DictWithCounter { Self::default() } - fn set(mut self_: PyRefMut<'_, Self>, key: String, value: &PyAny) -> PyResult<()> { - self_.counter.entry(key.clone()).or_insert(0); - let py = self_.py(); - let dict: &PyDict = unsafe { py.from_borrowed_ptr_or_err(self_.as_ptr())? }; + fn set(slf: &Bound<'_, Self>, key: String, value: &PyAny) -> PyResult<()> { + slf.borrow_mut().counter.entry(key.clone()).or_insert(0); + let dict = slf.downcast::()?; dict.set_item(key, value) } } @@ -492,7 +487,7 @@ struct MyDict { impl MyDict { #[new] #[pyo3(signature = (*args, **kwargs))] - fn new(args: &PyAny, kwargs: Option<&PyAny>) -> Self { + fn new(args: &Bound<'_, PyAny>, kwargs: Option<&Bound<'_, PyAny>>) -> Self { Self { private: 0 } } @@ -703,7 +698,7 @@ Declares a class method callable from Python. * The first parameter is the type object of the class on which the method is called. This may be the type object of a derived class. -* The first parameter implicitly has type `&PyType`. +* The first parameter implicitly has type `&Bound<'_, PyType>`. * For details on `parameter-list`, see the documentation of `Method arguments` section. * The return type must be `PyResult` or `T` for some `T` that implements `IntoPy`. @@ -803,23 +798,23 @@ struct MyClass { my_field: i32, } -// Take a GIL-bound reference when the underlying `PyCell` is irrelevant. +// Take a reference when the underlying `Bound` is irrelevant. #[pyfunction] fn increment_field(my_class: &mut MyClass) { my_class.my_field += 1; } -// Take a GIL-bound reference wrapper when borrowing should be automatic, -// but interaction with the underlying `PyCell` is desired. +// Take a reference wrapper when borrowing should be automatic, +// but interaction with the underlying `Bound` is desired. #[pyfunction] fn print_field(my_class: PyRef<'_, MyClass>) { println!("{}", my_class.my_field); } -// Take a GIL-bound reference to the underlying cell +// Take a reference to the underlying Bound // when borrowing needs to be managed manually. #[pyfunction] -fn increment_then_print_field(my_class: &PyCell) { +fn increment_then_print_field(my_class: &Bound<'_, MyClass>) { my_class.borrow_mut().my_field += 1; println!("{}", my_class.borrow().my_field); @@ -878,9 +873,9 @@ impl MyClass { fn method( &mut self, num: i32, - py_args: &PyTuple, + py_args: &Bound<'_, PyTuple>, name: &str, - py_kwargs: Option<&PyDict>, + py_kwargs: Option<&Bound<'_, PyDict>>, ) -> String { let num_before = self.num; self.num = num; @@ -1232,6 +1227,9 @@ struct MyClass { # #[allow(dead_code)] num: i32, } + +impl pyo3::types::DerefToPyAny for MyClass {} + unsafe impl pyo3::type_object::HasPyGilRef for MyClass { type AsRefTarget = pyo3::PyCell; } @@ -1279,6 +1277,8 @@ impl pyo3::IntoPy for MyClass { impl pyo3::impl_::pyclass::PyClassImpl for MyClass { const IS_BASETYPE: bool = false; const IS_SUBCLASS: bool = false; + const IS_MAPPING: bool = false; + const IS_SEQUENCE: bool = false; type BaseType = PyAny; type ThreadChecker = pyo3::impl_::pyclass::SendablePyClass; type PyClassMutability = <::PyClassMutability as pyo3::impl_::pycell::PyClassMutability>::MutableChild; @@ -1304,7 +1304,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, "", None.or_else(|| collector.new_text_signature())) + build_pyclass_doc(::NAME, "\0", collector.new_text_signature()) }).map(::std::ops::Deref::deref) } } @@ -1317,11 +1317,10 @@ impl pyo3::impl_::pyclass::PyClassImpl for MyClass { ``` -[`GILGuard`]: {{#PYO3_DOCS_URL}}/pyo3/struct.GILGuard.html [`PyTypeInfo`]: {{#PYO3_DOCS_URL}}/pyo3/type_object/trait.PyTypeInfo.html [`Py`]: {{#PYO3_DOCS_URL}}/pyo3/struct.Py.html -[`PyCell`]: {{#PYO3_DOCS_URL}}/pyo3/pycell/struct.PyCell.html +[`Bound`]: {{#PYO3_DOCS_URL}}/pyo3/struct.Bound.html [`PyClass`]: {{#PYO3_DOCS_URL}}/pyo3/pyclass/trait.PyClass.html [`PyRef`]: {{#PYO3_DOCS_URL}}/pyo3/pycell/struct.PyRef.html [`PyRefMut`]: {{#PYO3_DOCS_URL}}/pyo3/pycell/struct.PyRefMut.html diff --git a/guide/src/class/call.md b/guide/src/class/call.md index 3b20986239b..0890df9561a 100644 --- a/guide/src/class/call.md +++ b/guide/src/class/call.md @@ -75,8 +75,8 @@ A [previous implementation] used a normal `u64`, which meant it required a `&mut fn __call__( &mut self, py: Python<'_>, - args: &PyTuple, - kwargs: Option<&PyDict>, + args: &Bound<'_, PyTuple>, + kwargs: Option<&Bound<'_, PyDict>>, ) -> PyResult> { self.count += 1; let name = self.wraps.getattr(py, "__name__")?; diff --git a/guide/src/class/numeric.md b/guide/src/class/numeric.md index 3e6a3cf47e9..9fb609a931d 100644 --- a/guide/src/class/numeric.md +++ b/guide/src/class/numeric.md @@ -35,7 +35,7 @@ and cast it to an `i32`. # #![allow(dead_code)] use pyo3::prelude::*; -fn wrap(obj: &PyAny) -> Result { +fn wrap(obj: &Bound<'_, PyAny>) -> PyResult { let val = obj.call_method1("__and__", (0xFFFFFFFF_u32,))?; let val: u32 = val.extract()?; // 👇 This intentionally overflows! @@ -48,7 +48,7 @@ We also add documentation, via `///` comments, which are visible to Python users # #![allow(dead_code)] use pyo3::prelude::*; -fn wrap(obj: &PyAny) -> Result { +fn wrap(obj: &Bound<'_, PyAny>) -> PyResult { let val = obj.call_method1("__and__", (0xFFFFFFFF_u32,))?; let val: u32 = val.extract()?; Ok(val as i32) @@ -212,7 +212,7 @@ use pyo3::prelude::*; use pyo3::class::basic::CompareOp; use pyo3::types::PyComplex; -fn wrap(obj: &PyAny) -> Result { +fn wrap(obj: &Bound<'_, PyAny>) -> PyResult { let val = obj.call_method1("__and__", (0xFFFFFFFF_u32,))?; let val: u32 = val.extract()?; Ok(val as i32) @@ -229,7 +229,7 @@ impl Number { Self(value) } - fn __repr__(slf: &PyCell) -> PyResult { + fn __repr__(slf: &Bound<'_, Self>) -> PyResult { // Get the class name dynamically in case `Number` is subclassed let class_name: String = slf.get_type().qualname()?; Ok(format!("{}({})", class_name, slf.borrow().0)) @@ -411,8 +411,8 @@ the contracts of this function. Let's review those contracts: - The GIL must be held. If it's not, calling this function causes a data race. - The pointer must be valid, i.e. it must be properly aligned and point to a valid Python object. -Let's create that helper function. The signature has to be `fn(&PyAny) -> PyResult`. -- `&PyAny` represents a checked borrowed reference, so the pointer derived from it is valid (and not null). +Let's create that helper function. The signature has to be `fn(&Bound<'_, PyAny>) -> PyResult`. +- `&Bound<'_, PyAny>` represents a checked borrowed reference, so the pointer derived from it is valid (and not null). - Whenever we have borrowed references to Python objects in scope, it is guaranteed that the GIL is held. This reference is also where we can get a [`Python`] token to use in our call to [`PyErr::take`]. ```rust @@ -421,7 +421,7 @@ use std::os::raw::c_ulong; use pyo3::prelude::*; use pyo3::ffi; -fn wrap(obj: &PyAny) -> Result { +fn wrap(obj: &Bound<'_, PyAny>) -> Result { let py: Python<'_> = obj.py(); unsafe { diff --git a/guide/src/class/object.md b/guide/src/class/object.md index 729815ade0b..db6cc7d3234 100644 --- a/guide/src/class/object.md +++ b/guide/src/class/object.md @@ -76,7 +76,7 @@ In the `__repr__`, we used a hard-coded class name. This is sometimes not ideal, because if the class is subclassed in Python, we would like the repr to reflect the subclass name. This is typically done in Python code by accessing `self.__class__.__name__`. In order to be able to access the Python type information -*and* the Rust struct, we need to use a `PyCell` as the `self` argument. +*and* the Rust struct, we need to use a `Bound` as the `self` argument. ```rust # use pyo3::prelude::*; @@ -86,7 +86,7 @@ the subclass name. This is typically done in Python code by accessing # #[pymethods] impl Number { - fn __repr__(slf: &PyCell) -> PyResult { + fn __repr__(slf: &Bound<'_, Self>) -> PyResult { // This is the equivalent of `self.__class__.__name__` in Python. let class_name: String = slf.get_type().qualname()?; // To access fields of the Rust struct, we need to borrow the `PyCell`. @@ -263,7 +263,7 @@ impl Number { Self(value) } - fn __repr__(slf: &PyCell) -> PyResult { + fn __repr__(slf: &Bound<'_, Self>) -> PyResult { let class_name: String = slf.get_type().qualname()?; Ok(format!("{}({})", class_name, slf.borrow().0)) } diff --git a/guide/src/class/protocols.md b/guide/src/class/protocols.md index 516c051664b..b917c6a3eaf 100644 --- a/guide/src/class/protocols.md +++ b/guide/src/class/protocols.md @@ -17,7 +17,7 @@ When PyO3 handles a magic method, a couple of changes apply compared to other `# The following sections list of all magic methods PyO3 currently handles. The given signatures should be interpreted as follows: - All methods take a receiver as first argument, shown as ``. It can be - `&self`, `&mut self` or a `PyCell` reference like `self_: PyRef<'_, Self>` and + `&self`, `&mut self` or a `Bound` reference like `self_: PyRef<'_, Self>` and `self_: PyRefMut<'_, Self>`, as described [here](../class.md#inheritance). - An optional `Python<'py>` argument is always allowed as the first argument. - Return values can be optionally wrapped in `PyResult`. diff --git a/guide/src/migration.md b/guide/src/migration.md index 9a958f35458..709e7a70488 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -203,6 +203,10 @@ impl PyClassAsyncIter { `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 + +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` 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. diff --git a/src/types/tuple.rs b/src/types/tuple.rs index 3ffaf9c3224..c379b67aa1a 100644 --- a/src/types/tuple.rs +++ b/src/types/tuple.rs @@ -631,6 +631,12 @@ impl IntoPy> for Bound<'_, PyTuple> { } } +impl IntoPy> for &'_ Bound<'_, PyTuple> { + fn into_py(self, _: Python<'_>) -> Py { + self.clone().unbind() + } +} + #[cold] fn wrong_tuple_length(t: &Bound<'_, PyTuple>, expected_length: usize) -> PyErr { let msg = format!( From 1c5265e1c263fcb194f7226cabeb93f59f9dbcd7 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Fri, 1 Mar 2024 21:51:53 +0100 Subject: [PATCH 057/936] deprecate `from_borrowed_ptr` methods (#3915) * deprecate `from_borrowed_ptr` methods This deprecates the methods on the `Python` marker, aswell as `FromPyPointer` * use `BoundRef` to defer ref cnt inc until after the error case --- pyo3-macros-backend/src/method.rs | 4 +-- src/conversion.rs | 33 ++++++++++++++++++++++++ src/impl_/coroutine.rs | 18 ++++++------- src/instance.rs | 10 ++++++-- src/lib.rs | 4 +-- src/marker.rs | 42 ++++++++++++++++++++++--------- src/pycell.rs | 15 ++++++++--- src/type_object.rs | 5 +++- src/types/boolobject.rs | 5 +++- 9 files changed, 104 insertions(+), 32 deletions(-) diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index 6ee87fb99a6..42f22204601 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -513,7 +513,7 @@ impl<'a> FnSpec<'a> { holders.pop().unwrap(); // does not actually use holder created by `self_arg` quote! {{ - let __guard = _pyo3::impl_::coroutine::RefGuard::<#cls>::new(py.from_borrowed_ptr::<_pyo3::types::PyAny>(_slf))?; + let __guard = _pyo3::impl_::coroutine::RefGuard::<#cls>::new(&_pyo3::impl_::pymethods::BoundRef::ref_from_ptr(py, &_slf))?; async move { function(&__guard, #(#args),*).await } }} } @@ -521,7 +521,7 @@ impl<'a> FnSpec<'a> { holders.pop().unwrap(); // does not actually use holder created by `self_arg` quote! {{ - let mut __guard = _pyo3::impl_::coroutine::RefMutGuard::<#cls>::new(py.from_borrowed_ptr::<_pyo3::types::PyAny>(_slf))?; + let mut __guard = _pyo3::impl_::coroutine::RefMutGuard::<#cls>::new(&_pyo3::impl_::pymethods::BoundRef::ref_from_ptr(py, &_slf))?; async move { function(&mut __guard, #(#args),*).await } }} } diff --git a/src/conversion.rs b/src/conversion.rs index fc407fa5bc8..986ac7c537c 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -425,6 +425,7 @@ impl IntoPy> for () { /// # Safety /// /// See safety notes on individual functions. +#[deprecated(since = "0.21.0")] pub unsafe trait FromPyPointer<'p>: Sized { /// Convert from an arbitrary `PyObject`. /// @@ -494,6 +495,13 @@ 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" + ) + )] unsafe fn from_borrowed_ptr_or_opt(py: Python<'p>, ptr: *mut ffi::PyObject) -> Option<&'p Self>; /// Convert from an arbitrary borrowed `PyObject`. @@ -501,7 +509,15 @@ 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" + ) + )] 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`. @@ -509,7 +525,15 @@ 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" + ) + )] 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`. @@ -517,14 +541,23 @@ 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" + ) + )] 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)) } } +#[allow(deprecated)] unsafe impl<'p, T> FromPyPointer<'p> for T where T: 'p + crate::PyNativeType, diff --git a/src/impl_/coroutine.rs b/src/impl_/coroutine.rs index a59eddd0e8f..9be95ba3303 100644 --- a/src/impl_/coroutine.rs +++ b/src/impl_/coroutine.rs @@ -8,7 +8,7 @@ use crate::{ coroutine::{cancel::ThrowCallback, Coroutine}, instance::Bound, pyclass::boolean_struct::False, - types::PyString, + types::{PyAnyMethods, PyString}, IntoPy, Py, PyAny, PyCell, PyClass, PyErr, PyObject, PyResult, Python, }; @@ -39,10 +39,10 @@ fn get_ptr(obj: &Py) -> *mut T { pub struct RefGuard(Py); impl RefGuard { - pub fn new(obj: &PyAny) -> PyResult { - let owned: Py = obj.extract()?; - mem::forget(owned.try_borrow(obj.py())?); - Ok(RefGuard(owned)) + pub fn new(obj: &Bound<'_, PyAny>) -> PyResult { + let owned = obj.downcast::()?; + mem::forget(owned.try_borrow()?); + Ok(RefGuard(owned.clone().unbind())) } } @@ -67,10 +67,10 @@ impl Drop for RefGuard { pub struct RefMutGuard>(Py); impl> RefMutGuard { - pub fn new(obj: &PyAny) -> PyResult { - let owned: Py = obj.extract()?; - mem::forget(owned.try_borrow_mut(obj.py())?); - Ok(RefMutGuard(owned)) + pub fn new(obj: &Bound<'_, PyAny>) -> PyResult { + let owned = obj.downcast::()?; + mem::forget(owned.try_borrow_mut()?); + Ok(RefMutGuard(owned.clone().unbind())) } } diff --git a/src/instance.rs b/src/instance.rs index 65c5ee3bc5f..bc655caa70f 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -488,7 +488,10 @@ impl<'py, T> Bound<'py, T> { where T: HasPyGilRef, { - unsafe { self.py().from_borrowed_ptr(self.as_ptr()) } + #[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 @@ -613,7 +616,10 @@ where { pub(crate) fn into_gil_ref(self) -> &'py T::AsRefTarget { // Safety: self is a borrow over `'py`. - unsafe { self.py().from_borrowed_ptr(self.0.as_ptr()) } + #[allow(deprecated)] + unsafe { + self.py().from_borrowed_ptr(self.0.as_ptr()) + } } } diff --git a/src/lib.rs b/src/lib.rs index c2591cec91e..8c0e6269947 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -304,9 +304,9 @@ //! [Features chapter of the guide]: https://pyo3.rs/latest/features.html#features-reference "Features Reference - PyO3 user guide" //! [`Ungil`]: crate::marker::Ungil pub use crate::class::*; -pub use crate::conversion::{AsPyPointer, FromPyObject, FromPyPointer, IntoPy, ToPyObject}; +pub use crate::conversion::{AsPyPointer, FromPyObject, IntoPy, ToPyObject}; #[allow(deprecated)] -pub use crate::conversion::{PyTryFrom, PyTryInto}; +pub use crate::conversion::{FromPyPointer, PyTryFrom, PyTryInto}; pub use crate::err::{ DowncastError, DowncastIntoError, PyDowncastError, PyErr, PyErrArguments, PyResult, ToPyErr, }; diff --git a/src/marker.rs b/src/marker.rs index 5609601f440..9b3d7329316 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -127,9 +127,9 @@ use crate::types::{ PyAny, PyDict, PyEllipsis, PyModule, PyNone, PyNotImplemented, PyString, PyType, }; use crate::version::PythonVersionInfo; -use crate::{ - ffi, Bound, FromPyPointer, IntoPy, Py, PyNativeType, PyObject, PyTypeCheck, PyTypeInfo, -}; +#[allow(deprecated)] +use crate::FromPyPointer; +use crate::{ffi, Bound, IntoPy, Py, PyNativeType, PyObject, PyTypeCheck, PyTypeInfo}; use std::ffi::{CStr, CString}; use std::marker::PhantomData; use std::os::raw::c_int; @@ -880,7 +880,7 @@ impl<'py> Python<'py> { /// # Safety /// /// Callers must ensure that ensure that the cast is valid. - #[allow(clippy::wrong_self_convention)] + #[allow(clippy::wrong_self_convention, deprecated)] #[cfg_attr( not(feature = "gil-refs"), deprecated( @@ -892,7 +892,6 @@ impl<'py> Python<'py> { where T: FromPyPointer<'py>, { - #[allow(deprecated)] FromPyPointer::from_owned_ptr(self, ptr) } @@ -904,7 +903,7 @@ impl<'py> Python<'py> { /// # Safety /// /// Callers must ensure that ensure that the cast is valid. - #[allow(clippy::wrong_self_convention)] + #[allow(clippy::wrong_self_convention, deprecated)] #[cfg_attr( not(feature = "gil-refs"), deprecated( @@ -916,7 +915,6 @@ impl<'py> Python<'py> { where T: FromPyPointer<'py>, { - #[allow(deprecated)] FromPyPointer::from_owned_ptr_or_err(self, ptr) } @@ -928,7 +926,7 @@ impl<'py> Python<'py> { /// # Safety /// /// Callers must ensure that ensure that the cast is valid. - #[allow(clippy::wrong_self_convention)] + #[allow(clippy::wrong_self_convention, deprecated)] #[cfg_attr( not(feature = "gil-refs"), deprecated( @@ -940,7 +938,6 @@ impl<'py> Python<'py> { where T: FromPyPointer<'py>, { - #[allow(deprecated)] FromPyPointer::from_owned_ptr_or_opt(self, ptr) } @@ -951,7 +948,14 @@ impl<'py> Python<'py> { /// # Safety /// /// Callers must ensure that ensure that the cast is valid. - #[allow(clippy::wrong_self_convention)] + #[allow(clippy::wrong_self_convention, deprecated)] + #[cfg_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" + ) + )] pub unsafe fn from_borrowed_ptr(self, ptr: *mut ffi::PyObject) -> &'py T where T: FromPyPointer<'py>, @@ -966,7 +970,14 @@ impl<'py> Python<'py> { /// # Safety /// /// Callers must ensure that ensure that the cast is valid. - #[allow(clippy::wrong_self_convention)] + #[allow(clippy::wrong_self_convention, deprecated)] + #[cfg_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" + ) + )] pub unsafe fn from_borrowed_ptr_or_err(self, ptr: *mut ffi::PyObject) -> PyResult<&'py T> where T: FromPyPointer<'py>, @@ -981,7 +992,14 @@ impl<'py> Python<'py> { /// # Safety /// /// Callers must ensure that ensure that the cast is valid. - #[allow(clippy::wrong_self_convention)] + #[allow(clippy::wrong_self_convention, deprecated)] + #[cfg_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" + ) + )] pub unsafe fn from_borrowed_ptr_or_opt(self, ptr: *mut ffi::PyObject) -> Option<&'py T> where T: FromPyPointer<'py>, diff --git a/src/pycell.rs b/src/pycell.rs index 29fd1b37886..9d00de95cbb 100644 --- a/src/pycell.rs +++ b/src/pycell.rs @@ -62,6 +62,7 @@ //! ) -> *mut pyo3::ffi::PyObject { //! use :: pyo3 as _pyo3; //! _pyo3::impl_::trampoline::noargs(_slf, _args, |py, _slf| { +//! # #[allow(deprecated)] //! let _cell = py //! .from_borrowed_ptr::<_pyo3::PyAny>(_slf) //! .downcast::<_pyo3::PyCell>()?; @@ -191,6 +192,8 @@ //! [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" +#[allow(deprecated)] +use crate::conversion::FromPyPointer; use crate::exceptions::PyRuntimeError; use crate::ffi_ptr_ext::FfiPtrExt; use crate::impl_::pyclass::{ @@ -205,7 +208,7 @@ use crate::type_object::{PyLayout, PySizedLayout}; use crate::types::any::PyAnyMethods; use crate::types::PyAny; use crate::{ - conversion::{AsPyPointer, FromPyPointer, ToPyObject}, + conversion::{AsPyPointer, ToPyObject}, type_object::get_tp_free, PyTypeInfo, }; @@ -573,7 +576,10 @@ impl ToPyObject for &PyCell { impl AsRef for PyCell { fn as_ref(&self) -> &PyAny { - unsafe { self.py().from_borrowed_ptr(self.as_ptr()) } + #[allow(deprecated)] + unsafe { + self.py().from_borrowed_ptr(self.as_ptr()) + } } } @@ -581,7 +587,10 @@ impl Deref for PyCell { type Target = PyAny; fn deref(&self) -> &PyAny { - unsafe { self.py().from_borrowed_ptr(self.as_ptr()) } + #[allow(deprecated)] + unsafe { + self.py().from_borrowed_ptr(self.as_ptr()) + } } } diff --git a/src/type_object.rs b/src/type_object.rs index 994781b3fc0..318810b534d 100644 --- a/src/type_object.rs +++ b/src/type_object.rs @@ -76,7 +76,10 @@ pub unsafe trait PyTypeInfo: Sized + HasPyGilRef { 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. - unsafe { py.from_borrowed_ptr(Self::type_object_raw(py) as _) } + #[allow(deprecated)] + unsafe { + py.from_borrowed_ptr(Self::type_object_raw(py) as _) + } } /// Returns the safe abstraction over the type object. diff --git a/src/types/boolobject.rs b/src/types/boolobject.rs index 3a2f60f6fa3..52e4c886aab 100644 --- a/src/types/boolobject.rs +++ b/src/types/boolobject.rs @@ -25,7 +25,10 @@ impl PyBool { )] #[inline] pub fn new(py: Python<'_>, val: bool) -> &PyBool { - unsafe { py.from_borrowed_ptr(if val { ffi::Py_True() } else { ffi::Py_False() }) } + #[allow(deprecated)] + unsafe { + py.from_borrowed_ptr(if val { ffi::Py_True() } else { ffi::Py_False() }) + } } /// Depending on `val`, returns `true` or `false`. From d94827720e5335b8281e32451bf5fed1102032cb Mon Sep 17 00:00:00 2001 From: Lily Foote Date: Sat, 2 Mar 2024 22:53:28 +0000 Subject: [PATCH 058/936] Delete duplicate test code (#3926) These used to explicitly call `.iter()`, but that was removed in b65cbb9 to remove lints. --- src/types/frozenset.rs | 6 ------ src/types/set.rs | 6 ------ 2 files changed, 12 deletions(-) diff --git a/src/types/frozenset.rs b/src/types/frozenset.rs index cc16a9341c8..34452af0e87 100644 --- a/src/types/frozenset.rs +++ b/src/types/frozenset.rs @@ -351,12 +351,6 @@ mod tests { Python::with_gil(|py| { let set = PyFrozenSet::new(py, &[1]).unwrap(); - // iter method - for el in set { - assert_eq!(1i32, el.extract::().unwrap()); - } - - // intoiterator iteration for el in set { assert_eq!(1i32, el.extract::().unwrap()); } diff --git a/src/types/set.rs b/src/types/set.rs index 8e36ab81f8e..cf10f06e699 100644 --- a/src/types/set.rs +++ b/src/types/set.rs @@ -476,12 +476,6 @@ mod tests { Python::with_gil(|py| { let set = PySet::new(py, &[1]).unwrap(); - // iter method - for el in set { - assert_eq!(1i32, el.extract::<'_, i32>().unwrap()); - } - - // intoiterator iteration for el in set { assert_eq!(1i32, el.extract::<'_, i32>().unwrap()); } From 81be11e67a958f108c50d72d7fea23e837dd1ffb Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sat, 2 Mar 2024 23:55:05 +0100 Subject: [PATCH 059/936] docs: update Python modules section of the guide (#3924) --- guide/src/module.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/guide/src/module.md b/guide/src/module.md index a403a3621e6..0444c750c9c 100644 --- a/guide/src/module.md +++ b/guide/src/module.md @@ -65,22 +65,22 @@ print(my_extension.__doc__) ## Python submodules You can create a module hierarchy within a single extension module by using -[`PyModule.add_submodule()`]({{#PYO3_DOCS_URL}}/pyo3/prelude/struct.PyModule.html#method.add_submodule). +[`Bound<'_, PyModule>::add_submodule()`]({{#PYO3_DOCS_URL}}/pyo3/prelude/trait.PyModuleMethods.html#tymethod.add_submodule). For example, you could define the modules `parent_module` and `parent_module.child_module`. ```rust use pyo3::prelude::*; #[pymodule] -fn parent_module(py: Python<'_>, m: &PyModule) -> PyResult<()> { - register_child_module(py, m)?; +fn parent_module(m: &Bound<'_, PyModule>) -> PyResult<()> { + register_child_module(m)?; Ok(()) } -fn register_child_module(py: Python<'_>, parent_module: &PyModule) -> PyResult<()> { - let child_module = PyModule::new_bound(py, "child_module")?; +fn register_child_module(parent_module: &Bound<'_, PyModule>) -> PyResult<()> { + let child_module = PyModule::new_bound(parent_module.py(), "child_module")?; child_module.add_function(wrap_pyfunction!(func, &child_module)?)?; - parent_module.add_submodule(child_module.as_gil_ref())?; + parent_module.add_submodule(&child_module)?; Ok(()) } From 2e56f659ed6a08f10f1e56a24251430878e7975f Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sun, 3 Mar 2024 07:00:59 +0000 Subject: [PATCH 060/936] split `PyCell` and `PyClassObject` concepts (#3917) * add test for refguard ref counting * split `PyCell` and `PyClassObject` concepts * rework `create_cell` to `create_class_object` * Apply suggestions from code review Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> * review: Icxolu feedback --------- Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> --- pyo3-macros-backend/src/method.rs | 3 +- src/impl_/coroutine.rs | 35 +++--- src/impl_/pycell.rs | 4 +- src/impl_/pyclass.rs | 21 ++-- src/impl_/pymethods.rs | 14 ++- src/instance.rs | 46 +++---- src/pycell.rs | 197 +++++------------------------- src/pycell/impl_.rs | 170 ++++++++++++++++++++++++-- src/pyclass.rs | 16 +-- src/pyclass/create_type_object.rs | 5 +- src/pyclass_init.rs | 66 +++++----- src/type_object.rs | 2 +- src/types/mod.rs | 2 +- tests/test_coroutine.rs | 14 ++- 14 files changed, 301 insertions(+), 294 deletions(-) diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index 42f22204601..a9ba960d513 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -645,8 +645,7 @@ impl<'a> FnSpec<'a> { #( #holders )* let result = #call; let initializer: _pyo3::PyClassInitializer::<#cls> = result.convert(py)?; - let cell = initializer.create_cell_from_subtype(py, _slf)?; - ::std::result::Result::Ok(cell as *mut _pyo3::ffi::PyObject) + _pyo3::impl_::pymethods::tp_new_impl(py, initializer, _slf) } } } diff --git a/src/impl_/coroutine.rs b/src/impl_/coroutine.rs index 9be95ba3303..1d3119400a0 100644 --- a/src/impl_/coroutine.rs +++ b/src/impl_/coroutine.rs @@ -1,15 +1,15 @@ use std::{ future::Future, - mem, ops::{Deref, DerefMut}, }; use crate::{ coroutine::{cancel::ThrowCallback, Coroutine}, instance::Bound, + pycell::impl_::PyClassBorrowChecker, pyclass::boolean_struct::False, types::{PyAnyMethods, PyString}, - IntoPy, Py, PyAny, PyCell, PyClass, PyErr, PyObject, PyResult, Python, + IntoPy, Py, PyAny, PyClass, PyErr, PyObject, PyResult, Python, }; pub fn new_coroutine( @@ -32,17 +32,16 @@ where } fn get_ptr(obj: &Py) -> *mut T { - // SAFETY: Py can be casted as *const PyCell - unsafe { &*(obj.as_ptr() as *const PyCell) }.get_ptr() + obj.get_class_object().get_ptr() } pub struct RefGuard(Py); impl RefGuard { pub fn new(obj: &Bound<'_, PyAny>) -> PyResult { - let owned = obj.downcast::()?; - mem::forget(owned.try_borrow()?); - Ok(RefGuard(owned.clone().unbind())) + let bound = obj.downcast::()?; + bound.get_class_object().borrow_checker().try_borrow()?; + Ok(RefGuard(bound.clone().unbind())) } } @@ -57,9 +56,11 @@ impl Deref for RefGuard { impl Drop for RefGuard { fn drop(&mut self) { Python::with_gil(|gil| { - #[allow(deprecated)] - let self_ref = self.0.bind(gil); - self_ref.release_ref() + self.0 + .bind(gil) + .get_class_object() + .borrow_checker() + .release_borrow() }) } } @@ -68,9 +69,9 @@ pub struct RefMutGuard>(Py); impl> RefMutGuard { pub fn new(obj: &Bound<'_, PyAny>) -> PyResult { - let owned = obj.downcast::()?; - mem::forget(owned.try_borrow_mut()?); - Ok(RefMutGuard(owned.clone().unbind())) + let bound = obj.downcast::()?; + bound.get_class_object().borrow_checker().try_borrow_mut()?; + Ok(RefMutGuard(bound.clone().unbind())) } } @@ -92,9 +93,11 @@ impl> DerefMut for RefMutGuard { impl> Drop for RefMutGuard { fn drop(&mut self) { Python::with_gil(|gil| { - #[allow(deprecated)] - let self_ref = self.0.bind(gil); - self_ref.release_mut() + self.0 + .bind(gil) + .get_class_object() + .borrow_checker() + .release_borrow_mut() }) } } diff --git a/src/impl_/pycell.rs b/src/impl_/pycell.rs index 39811aebd29..93514c7bb29 100644 --- a/src/impl_/pycell.rs +++ b/src/impl_/pycell.rs @@ -1,2 +1,4 @@ //! Externally-accessible implementation of pycell -pub use crate::pycell::impl_::{GetBorrowChecker, PyClassMutability}; +pub use crate::pycell::impl_::{ + GetBorrowChecker, PyClassMutability, PyClassObject, PyClassObjectBase, PyClassObjectLayout, +}; diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index e2204fabb02..1a144f736e0 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -2,14 +2,13 @@ use crate::{ exceptions::{PyAttributeError, PyNotImplementedError, PyRuntimeError, PyValueError}, ffi, impl_::freelist::FreeList, - impl_::pycell::{GetBorrowChecker, PyClassMutability}, + impl_::pycell::{GetBorrowChecker, PyClassMutability, PyClassObjectLayout}, internal_tricks::extract_c_string, - pycell::PyCellLayout, pyclass_init::PyObjectInit, types::any::PyAnyMethods, types::PyBool, - Borrowed, Py, PyAny, PyCell, PyClass, PyErr, PyMethodDefType, PyNativeType, PyResult, - PyTypeInfo, Python, + Borrowed, Py, PyAny, PyClass, PyErr, PyMethodDefType, PyNativeType, PyResult, PyTypeInfo, + Python, }; use std::{ borrow::Cow, @@ -26,13 +25,13 @@ pub use lazy_type_object::LazyTypeObject; /// Gets the offset of the dictionary from the start of the object in bytes. #[inline] pub fn dict_offset() -> ffi::Py_ssize_t { - PyCell::::dict_offset() + PyClassObject::::dict_offset() } /// Gets the offset of the weakref list from the start of the object in bytes. #[inline] pub fn weaklist_offset() -> ffi::Py_ssize_t { - PyCell::::weaklist_offset() + PyClassObject::::weaklist_offset() } /// Represents the `__dict__` field for `#[pyclass]`. @@ -883,6 +882,8 @@ macro_rules! generate_pyclass_richcompare_slot { } pub use generate_pyclass_richcompare_slot; +use super::pycell::PyClassObject; + /// Implements a freelist. /// /// Do not implement this trait manually. Instead, use `#[pyclass(freelist = N)]` @@ -1095,7 +1096,7 @@ impl PyClassThreadChecker for ThreadCheckerImpl { /// Trait denoting that this class is suitable to be used as a base type for PyClass. pub trait PyClassBaseType: Sized { - type LayoutAsBase: PyCellLayout; + type LayoutAsBase: PyClassObjectLayout; type BaseNativeType; type Initializer: PyObjectInit; type PyClassMutability: PyClassMutability; @@ -1105,7 +1106,7 @@ pub trait PyClassBaseType: Sized { /// /// In the future this will be extended to immutable PyClasses too. impl PyClassBaseType for T { - type LayoutAsBase = crate::pycell::PyCell; + type LayoutAsBase = crate::impl_::pycell::PyClassObject; type BaseNativeType = T::BaseNativeType; type Initializer = crate::pyclass_init::PyClassInitializer; type PyClassMutability = T::PyClassMutability; @@ -1113,7 +1114,7 @@ impl PyClassBaseType for T { /// Implementation of tp_dealloc for pyclasses without gc pub(crate) unsafe extern "C" fn tp_dealloc(obj: *mut ffi::PyObject) { - crate::impl_::trampoline::dealloc(obj, PyCell::::tp_dealloc) + crate::impl_::trampoline::dealloc(obj, PyClassObject::::tp_dealloc) } /// Implementation of tp_dealloc for pyclasses with gc @@ -1122,7 +1123,7 @@ pub(crate) unsafe extern "C" fn tp_dealloc_with_gc(obj: *mut ffi::Py { ffi::PyObject_GC_UnTrack(obj.cast()); } - crate::impl_::trampoline::dealloc(obj, PyCell::::tp_dealloc) + crate::impl_::trampoline::dealloc(obj, PyClassObject::::tp_dealloc) } pub(crate) unsafe extern "C" fn get_sequence_item_from_mapping( diff --git a/src/impl_/pymethods.rs b/src/impl_/pymethods.rs index 70c95ca0883..bc950bafee4 100644 --- a/src/impl_/pymethods.rs +++ b/src/impl_/pymethods.rs @@ -7,8 +7,8 @@ use crate::pycell::{PyBorrowError, PyBorrowMutError}; use crate::pyclass::boolean_struct::False; use crate::types::{any::PyAnyMethods, PyModule, PyType}; use crate::{ - ffi, Borrowed, Bound, DowncastError, Py, PyAny, PyCell, PyClass, PyErr, PyObject, PyRef, - PyRefMut, PyResult, PyTraverseError, PyTypeCheck, PyVisit, Python, + ffi, Borrowed, Bound, DowncastError, Py, PyAny, PyCell, PyClass, PyClassInitializer, PyErr, + PyObject, PyRef, PyRefMut, PyResult, PyTraverseError, PyTypeCheck, PyVisit, Python, }; use std::borrow::Cow; use std::ffi::CStr; @@ -569,3 +569,13 @@ impl<'py, T> std::ops::Deref for BoundRef<'_, 'py, T> { self.0 } } + +pub unsafe fn tp_new_impl( + py: Python<'_>, + initializer: PyClassInitializer, + target_type: *mut ffi::PyTypeObject, +) -> PyResult<*mut ffi::PyObject> { + initializer + .create_class_object_of_type(py, target_type) + .map(Bound::into_ptr) +} diff --git a/src/instance.rs b/src/instance.rs index bc655caa70f..66c530d5f53 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -1,5 +1,5 @@ use crate::err::{self, PyDowncastError, PyErr, PyResult}; -use crate::ffi_ptr_ext::FfiPtrExt; +use crate::impl_::pycell::PyClassObject; use crate::pycell::{PyBorrowError, PyBorrowMutError, PyCell}; use crate::pyclass::boolean_struct::{False, True}; use crate::type_object::HasPyGilRef; @@ -92,14 +92,7 @@ where py: Python<'py>, value: impl Into>, ) -> PyResult> { - let initializer = value.into(); - let obj = initializer.create_cell(py)?; - let ob = unsafe { - obj.cast::() - .assume_owned(py) - .downcast_into_unchecked() - }; - Ok(ob) + value.into().create_class_object(py) } } @@ -332,19 +325,11 @@ where where T: PyClass + Sync, { - let cell = self.get_cell(); - // SAFETY: The class itself is frozen and `Sync` and we do not access anything but `cell.contents.value`. - unsafe { &*cell.get_ptr() } + self.1.get() } - pub(crate) fn get_cell(&'py self) -> &'py PyCell { - let cell = self.as_ptr().cast::>(); - // SAFETY: Bound is known to contain an object which is laid out in memory as a - // PyCell. - // - // Strictly speaking for now `&'py PyCell` is part of the "GIL Ref" API, so this - // could use some further refactoring later to avoid going through this reference. - unsafe { &*cell } + pub(crate) fn get_class_object(&self) -> &PyClassObject { + self.1.get_class_object() } } @@ -887,10 +872,7 @@ where /// # } /// ``` pub fn new(py: Python<'_>, value: impl Into>) -> PyResult> { - let initializer = value.into(); - let obj = initializer.create_cell(py)?; - let ob = unsafe { Py::from_owned_ptr(py, obj as _) }; - Ok(ob) + Bound::new(py, value).map(Bound::unbind) } } @@ -1194,12 +1176,16 @@ where where T: PyClass + Sync, { - let any = self.as_ptr() as *const PyAny; - // SAFETY: The class itself is frozen and `Sync` and we do not access anything but `cell.contents.value`. - unsafe { - let cell: &PyCell = PyNativeType::unchecked_downcast(&*any); - &*cell.get_ptr() - } + // Safety: The class itself is frozen and `Sync` + unsafe { &*self.get_class_object().get_ptr() } + } + + /// Get a view on the underlying `PyClass` contents. + pub(crate) fn get_class_object(&self) -> &PyClassObject { + let class_object = self.as_ptr().cast::>(); + // Safety: Bound is known to contain an object which is laid out in memory as a + // PyClassObject. + unsafe { &*class_object } } } diff --git a/src/pycell.rs b/src/pycell.rs index 9d00de95cbb..43c75314f00 100644 --- a/src/pycell.rs +++ b/src/pycell.rs @@ -192,13 +192,10 @@ //! [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" -#[allow(deprecated)] -use crate::conversion::FromPyPointer; +use crate::conversion::{AsPyPointer, ToPyObject}; use crate::exceptions::PyRuntimeError; use crate::ffi_ptr_ext::FfiPtrExt; -use crate::impl_::pyclass::{ - PyClassBaseType, PyClassDict, PyClassImpl, PyClassThreadChecker, PyClassWeakRef, -}; +use crate::impl_::pyclass::PyClassImpl; use crate::pyclass::{ boolean_struct::{False, True}, PyClass, @@ -207,28 +204,15 @@ use crate::pyclass_init::PyClassInitializer; use crate::type_object::{PyLayout, PySizedLayout}; use crate::types::any::PyAnyMethods; use crate::types::PyAny; -use crate::{ - conversion::{AsPyPointer, ToPyObject}, - type_object::get_tp_free, - PyTypeInfo, -}; use crate::{ffi, Bound, IntoPy, PyErr, PyNativeType, PyObject, PyResult, PyTypeCheck, Python}; -use std::cell::UnsafeCell; use std::fmt; use std::mem::ManuallyDrop; use std::ops::{Deref, DerefMut}; pub(crate) mod impl_; -use impl_::{GetBorrowChecker, PyClassBorrowChecker, PyClassMutability}; - -/// Base layout of PyCell. -#[doc(hidden)] -#[repr(C)] -pub struct PyCellBase { - ob_base: T, -} +use impl_::PyClassBorrowChecker; -unsafe impl PyLayout for PyCellBase where U: PySizedLayout {} +use self::impl_::{PyClassObject, PyClassObjectLayout}; /// A container type for (mutably) accessing [`PyClass`] values /// @@ -266,20 +250,8 @@ unsafe impl PyLayout for PyCellBase where U: PySizedLayout {} /// ``` /// For more information on how, when and why (not) to use `PyCell` please see the /// [module-level documentation](self). -#[repr(C)] -pub struct PyCell { - ob_base: ::LayoutAsBase, - contents: PyCellContents, -} - -#[repr(C)] -pub(crate) struct PyCellContents { - pub(crate) value: ManuallyDrop>, - pub(crate) borrow_checker: ::Storage, - pub(crate) thread_checker: T::ThreadChecker, - pub(crate) dict: T::Dict, - pub(crate) weakref: T::WeakRef, -} +#[repr(transparent)] +pub struct PyCell(PyClassObject); unsafe impl PyNativeType for PyCell { type AsRefSource = T; @@ -298,12 +270,7 @@ impl PyCell { ) )] pub fn new(py: Python<'_>, value: impl Into>) -> PyResult<&Self> { - unsafe { - let initializer = value.into(); - let self_ = initializer.create_cell(py)?; - #[allow(deprecated)] - FromPyPointer::from_owned_ptr_or_err(py, self_ as _) - } + Bound::new(py, value).map(Bound::into_gil_ref) } /// Immutably borrows the value `T`. This borrow lasts as long as the returned `PyRef` exists. @@ -423,10 +390,11 @@ impl PyCell { /// }); /// ``` pub unsafe fn try_borrow_unguarded(&self) -> Result<&T, PyBorrowError> { - self.ensure_threadsafe(); - self.borrow_checker() + self.0.ensure_threadsafe(); + self.0 + .borrow_checker() .try_borrow_unguarded() - .map(|_: ()| &*self.contents.value.get()) + .map(|_: ()| &*self.0.get_ptr()) } /// Provide an immutable borrow of the value `T` without acquiring the GIL. @@ -506,45 +474,7 @@ impl PyCell { } pub(crate) fn get_ptr(&self) -> *mut T { - self.contents.value.get() - } - - /// Gets the offset of the dictionary from the start of the struct in bytes. - pub(crate) fn dict_offset() -> ffi::Py_ssize_t { - use memoffset::offset_of; - - let offset = offset_of!(PyCell, contents) + offset_of!(PyCellContents, dict); - - // Py_ssize_t may not be equal to isize on all platforms - #[allow(clippy::useless_conversion)] - offset.try_into().expect("offset should fit in Py_ssize_t") - } - - /// Gets the offset of the weakref list from the start of the struct in bytes. - pub(crate) fn weaklist_offset() -> ffi::Py_ssize_t { - use memoffset::offset_of; - - let offset = offset_of!(PyCell, contents) + offset_of!(PyCellContents, weakref); - - // Py_ssize_t may not be equal to isize on all platforms - #[allow(clippy::useless_conversion)] - offset.try_into().expect("offset should fit in Py_ssize_t") - } - - #[cfg(feature = "macros")] - pub(crate) fn release_ref(&self) { - self.borrow_checker().release_borrow(); - } - - #[cfg(feature = "macros")] - pub(crate) fn release_mut(&self) { - self.borrow_checker().release_borrow_mut(); - } -} - -impl PyCell { - fn borrow_checker(&self) -> &::Checker { - T::PyClassMutability::borrow_checker(self) + self.0.get_ptr() } } @@ -675,7 +605,7 @@ where U: PyClass, { fn as_ref(&self) -> &T::BaseType { - unsafe { &*self.inner.get_cell().ob_base.get_ptr() } + unsafe { &*self.inner.get_class_object().ob_base.get_ptr() } } } @@ -709,7 +639,7 @@ impl<'py, T: PyClass> PyRef<'py, T> { } pub(crate) fn try_borrow(obj: &Bound<'py, T>) -> Result { - let cell = obj.get_cell(); + let cell = obj.get_class_object(); cell.ensure_threadsafe(); cell.borrow_checker() .try_borrow() @@ -717,7 +647,7 @@ impl<'py, T: PyClass> PyRef<'py, T> { } pub(crate) fn try_borrow_threadsafe(obj: &Bound<'py, T>) -> Result { - let cell = obj.get_cell(); + let cell = obj.get_class_object(); cell.check_threadsafe()?; cell.borrow_checker() .try_borrow() @@ -793,13 +723,16 @@ impl<'p, T: PyClass> Deref for PyRef<'p, T> { #[inline] fn deref(&self) -> &T { - unsafe { &*self.inner.get_cell().get_ptr() } + unsafe { &*self.inner.get_class_object().get_ptr() } } } impl<'p, T: PyClass> Drop for PyRef<'p, T> { fn drop(&mut self) { - self.inner.get_cell().borrow_checker().release_borrow() + self.inner + .get_class_object() + .borrow_checker() + .release_borrow() } } @@ -856,7 +789,7 @@ where U: PyClass, { fn as_ref(&self) -> &T::BaseType { - unsafe { &*self.inner.get_cell().ob_base.get_ptr() } + unsafe { &*self.inner.get_class_object().ob_base.get_ptr() } } } @@ -866,7 +799,7 @@ where U: PyClass, { fn as_mut(&mut self) -> &mut T::BaseType { - unsafe { &mut *self.inner.get_cell().ob_base.get_ptr() } + unsafe { &mut *self.inner.get_class_object().ob_base.get_ptr() } } } @@ -900,7 +833,7 @@ impl<'py, T: PyClass> PyRefMut<'py, T> { } pub(crate) fn try_borrow(obj: &Bound<'py, T>) -> Result { - let cell = obj.get_cell(); + let cell = obj.get_class_object(); cell.ensure_threadsafe(); cell.borrow_checker() .try_borrow_mut() @@ -934,20 +867,23 @@ impl<'p, T: PyClass> Deref for PyRefMut<'p, T> { #[inline] fn deref(&self) -> &T { - unsafe { &*self.inner.get_cell().get_ptr() } + unsafe { &*self.inner.get_class_object().get_ptr() } } } impl<'p, T: PyClass> DerefMut for PyRefMut<'p, T> { #[inline] fn deref_mut(&mut self) -> &mut T { - unsafe { &mut *self.inner.get_cell().get_ptr() } + unsafe { &mut *self.inner.get_class_object().get_ptr() } } } impl<'p, T: PyClass> Drop for PyRefMut<'p, T> { fn drop(&mut self) { - self.inner.get_cell().borrow_checker().release_borrow_mut() + self.inner + .get_class_object() + .borrow_checker() + .release_borrow_mut() } } @@ -1034,81 +970,6 @@ impl From for PyErr { } } -#[doc(hidden)] -pub trait PyCellLayout: PyLayout { - fn ensure_threadsafe(&self); - fn check_threadsafe(&self) -> Result<(), PyBorrowError>; - /// Implementation of tp_dealloc. - /// # Safety - /// - slf must be a valid pointer to an instance of a T or a subclass. - /// - slf must not be used after this call (as it will be freed). - unsafe fn tp_dealloc(py: Python<'_>, slf: *mut ffi::PyObject); -} - -impl PyCellLayout for PyCellBase -where - U: PySizedLayout, - T: PyTypeInfo, -{ - fn ensure_threadsafe(&self) {} - fn check_threadsafe(&self) -> Result<(), PyBorrowError> { - Ok(()) - } - unsafe fn tp_dealloc(py: Python<'_>, slf: *mut ffi::PyObject) { - let type_obj = T::type_object_raw(py); - // For `#[pyclass]` types which inherit from PyAny, we can just call tp_free - if type_obj == std::ptr::addr_of_mut!(ffi::PyBaseObject_Type) { - return get_tp_free(ffi::Py_TYPE(slf))(slf as _); - } - - // More complex native types (e.g. `extends=PyDict`) require calling the base's dealloc. - #[cfg(not(Py_LIMITED_API))] - { - if let Some(dealloc) = (*type_obj).tp_dealloc { - // Before CPython 3.11 BaseException_dealloc would use Py_GC_UNTRACK which - // assumes the exception is currently GC tracked, so we have to re-track - // before calling the dealloc so that it can safely call Py_GC_UNTRACK. - #[cfg(not(any(Py_3_11, PyPy)))] - if ffi::PyType_FastSubclass(type_obj, ffi::Py_TPFLAGS_BASE_EXC_SUBCLASS) == 1 { - ffi::PyObject_GC_Track(slf.cast()); - } - dealloc(slf as _); - } else { - get_tp_free(ffi::Py_TYPE(slf))(slf as _); - } - } - - #[cfg(Py_LIMITED_API)] - unreachable!("subclassing native types is not possible with the `abi3` feature"); - } -} - -impl PyCellLayout for PyCell -where - ::LayoutAsBase: PyCellLayout, -{ - fn ensure_threadsafe(&self) { - self.contents.thread_checker.ensure(); - self.ob_base.ensure_threadsafe(); - } - fn check_threadsafe(&self) -> Result<(), PyBorrowError> { - if !self.contents.thread_checker.check() { - return Err(PyBorrowError { _private: () }); - } - self.ob_base.check_threadsafe() - } - unsafe fn tp_dealloc(py: Python<'_>, slf: *mut ffi::PyObject) { - // Safety: Python only calls tp_dealloc when no references to the object remain. - let cell = &mut *(slf as *mut PyCell); - if cell.contents.thread_checker.can_drop(py) { - ManuallyDrop::drop(&mut cell.contents.value); - } - cell.contents.dict.clear_dict(py); - cell.contents.weakref.clear_weakrefs(slf, py); - ::LayoutAsBase::tp_dealloc(py, slf) - } -} - #[cfg(test)] #[cfg(feature = "macros")] mod tests { diff --git a/src/pycell/impl_.rs b/src/pycell/impl_.rs index 29ba7eda7eb..378bec04993 100644 --- a/src/pycell/impl_.rs +++ b/src/pycell/impl_.rs @@ -1,11 +1,15 @@ #![allow(missing_docs)] -//! Crate-private implementation of pycell +//! Crate-private implementation of PyClassObject -use std::cell::Cell; +use std::cell::{Cell, UnsafeCell}; use std::marker::PhantomData; +use std::mem::ManuallyDrop; -use crate::impl_::pyclass::{PyClassBaseType, PyClassImpl}; -use crate::PyCell; +use crate::impl_::pyclass::{ + PyClassBaseType, PyClassDict, PyClassImpl, PyClassThreadChecker, PyClassWeakRef, +}; +use crate::type_object::{get_tp_free, PyLayout, PySizedLayout}; +use crate::{ffi, PyClass, PyTypeInfo, Python}; use super::{PyBorrowError, PyBorrowMutError}; @@ -156,29 +160,170 @@ impl PyClassBorrowChecker for BorrowChecker { } pub trait GetBorrowChecker { - fn borrow_checker(cell: &PyCell) -> &::Checker; + fn borrow_checker( + class_object: &PyClassObject, + ) -> &::Checker; } impl> GetBorrowChecker for MutableClass { - fn borrow_checker(cell: &PyCell) -> &BorrowChecker { - &cell.contents.borrow_checker + fn borrow_checker(class_object: &PyClassObject) -> &BorrowChecker { + &class_object.contents.borrow_checker } } impl> GetBorrowChecker for ImmutableClass { - fn borrow_checker(cell: &PyCell) -> &EmptySlot { - &cell.contents.borrow_checker + fn borrow_checker(class_object: &PyClassObject) -> &EmptySlot { + &class_object.contents.borrow_checker } } impl, M: PyClassMutability> GetBorrowChecker for ExtendsMutableAncestor where - T::BaseType: PyClassImpl + PyClassBaseType>, + T::BaseType: PyClassImpl + PyClassBaseType>, ::PyClassMutability: PyClassMutability, { - fn borrow_checker(cell: &PyCell) -> &BorrowChecker { - <::PyClassMutability as GetBorrowChecker>::borrow_checker(&cell.ob_base) + fn borrow_checker(class_object: &PyClassObject) -> &BorrowChecker { + <::PyClassMutability as GetBorrowChecker>::borrow_checker(&class_object.ob_base) + } +} + +/// Base layout of PyClassObject. +#[doc(hidden)] +#[repr(C)] +pub struct PyClassObjectBase { + ob_base: T, +} + +unsafe impl PyLayout for PyClassObjectBase where U: PySizedLayout {} + +#[doc(hidden)] +pub trait PyClassObjectLayout: PyLayout { + fn ensure_threadsafe(&self); + fn check_threadsafe(&self) -> Result<(), PyBorrowError>; + /// Implementation of tp_dealloc. + /// # Safety + /// - slf must be a valid pointer to an instance of a T or a subclass. + /// - slf must not be used after this call (as it will be freed). + unsafe fn tp_dealloc(py: Python<'_>, slf: *mut ffi::PyObject); +} + +impl PyClassObjectLayout for PyClassObjectBase +where + U: PySizedLayout, + T: PyTypeInfo, +{ + fn ensure_threadsafe(&self) {} + fn check_threadsafe(&self) -> Result<(), PyBorrowError> { + Ok(()) + } + unsafe fn tp_dealloc(py: Python<'_>, slf: *mut ffi::PyObject) { + let type_obj = T::type_object_raw(py); + // For `#[pyclass]` types which inherit from PyAny, we can just call tp_free + if type_obj == std::ptr::addr_of_mut!(ffi::PyBaseObject_Type) { + return get_tp_free(ffi::Py_TYPE(slf))(slf.cast()); + } + + // More complex native types (e.g. `extends=PyDict`) require calling the base's dealloc. + #[cfg(not(Py_LIMITED_API))] + { + if let Some(dealloc) = (*type_obj).tp_dealloc { + // Before CPython 3.11 BaseException_dealloc would use Py_GC_UNTRACK which + // assumes the exception is currently GC tracked, so we have to re-track + // before calling the dealloc so that it can safely call Py_GC_UNTRACK. + #[cfg(not(any(Py_3_11, PyPy)))] + if ffi::PyType_FastSubclass(type_obj, ffi::Py_TPFLAGS_BASE_EXC_SUBCLASS) == 1 { + ffi::PyObject_GC_Track(slf.cast()); + } + dealloc(slf); + } else { + get_tp_free(ffi::Py_TYPE(slf))(slf.cast()); + } + } + + #[cfg(Py_LIMITED_API)] + unreachable!("subclassing native types is not possible with the `abi3` feature"); + } +} + +/// The layout of a PyClass as a Python object +#[repr(C)] +pub struct PyClassObject { + pub(crate) ob_base: ::LayoutAsBase, + contents: PyClassObjectContents, +} + +#[repr(C)] +pub(crate) struct PyClassObjectContents { + pub(crate) value: ManuallyDrop>, + pub(crate) borrow_checker: ::Storage, + pub(crate) thread_checker: T::ThreadChecker, + pub(crate) dict: T::Dict, + pub(crate) weakref: T::WeakRef, +} + +impl PyClassObject { + pub(crate) fn get_ptr(&self) -> *mut T { + self.contents.value.get() + } + + /// Gets the offset of the dictionary from the start of the struct in bytes. + pub(crate) fn dict_offset() -> ffi::Py_ssize_t { + use memoffset::offset_of; + + let offset = + offset_of!(PyClassObject, contents) + offset_of!(PyClassObjectContents, dict); + + // Py_ssize_t may not be equal to isize on all platforms + #[allow(clippy::useless_conversion)] + offset.try_into().expect("offset should fit in Py_ssize_t") + } + + /// Gets the offset of the weakref list from the start of the struct in bytes. + pub(crate) fn weaklist_offset() -> ffi::Py_ssize_t { + use memoffset::offset_of; + + let offset = + offset_of!(PyClassObject, contents) + offset_of!(PyClassObjectContents, weakref); + + // Py_ssize_t may not be equal to isize on all platforms + #[allow(clippy::useless_conversion)] + offset.try_into().expect("offset should fit in Py_ssize_t") + } +} + +impl PyClassObject { + pub(crate) fn borrow_checker(&self) -> &::Checker { + T::PyClassMutability::borrow_checker(self) + } +} + +unsafe impl PyLayout for PyClassObject {} +impl PySizedLayout for PyClassObject {} + +impl PyClassObjectLayout for PyClassObject +where + ::LayoutAsBase: PyClassObjectLayout, +{ + fn ensure_threadsafe(&self) { + self.contents.thread_checker.ensure(); + self.ob_base.ensure_threadsafe(); + } + fn check_threadsafe(&self) -> Result<(), PyBorrowError> { + if !self.contents.thread_checker.check() { + return Err(PyBorrowError { _private: () }); + } + self.ob_base.check_threadsafe() + } + unsafe fn tp_dealloc(py: Python<'_>, slf: *mut ffi::PyObject) { + // Safety: Python only calls tp_dealloc when no references to the object remain. + let class_object = &mut *(slf.cast::>()); + if class_object.contents.thread_checker.can_drop(py) { + ManuallyDrop::drop(&mut class_object.contents.value); + } + class_object.contents.dict.clear_dict(py); + class_object.contents.weakref.clear_weakrefs(slf, py); + ::LayoutAsBase::tp_dealloc(py, slf) } } @@ -189,7 +334,6 @@ mod tests { use crate::prelude::*; use crate::pyclass::boolean_struct::{False, True}; - use crate::PyClass; #[pyclass(crate = "crate", subclass)] struct MutableBase; diff --git a/src/pyclass.rs b/src/pyclass.rs index ebdc52dc217..eb4a5595ca9 100644 --- a/src/pyclass.rs +++ b/src/pyclass.rs @@ -1,7 +1,7 @@ //! `PyClass` and related traits. use crate::{ - callback::IntoPyCallbackOutput, ffi, impl_::pyclass::PyClassImpl, Bound, IntoPy, PyCell, - PyObject, PyResult, PyTypeInfo, Python, + callback::IntoPyCallbackOutput, ffi, impl_::pyclass::PyClassImpl, IntoPy, PyCell, PyObject, + PyResult, PyTypeInfo, Python, }; use std::{cmp::Ordering, os::raw::c_int}; @@ -216,18 +216,6 @@ pub trait Frozen: boolean_struct::private::Boolean {} impl Frozen for boolean_struct::True {} impl Frozen for boolean_struct::False {} -impl<'py, T: PyClass> Bound<'py, T> { - #[cfg(feature = "macros")] - pub(crate) fn release_ref(&self) { - self.get_cell().release_ref(); - } - - #[cfg(feature = "macros")] - pub(crate) fn release_mut(&self) { - self.get_cell().release_mut(); - } -} - mod tests { #[test] fn test_compare_op_matches() { diff --git a/src/pyclass/create_type_object.rs b/src/pyclass/create_type_object.rs index d0c271cf31a..52e346212f0 100644 --- a/src/pyclass/create_type_object.rs +++ b/src/pyclass/create_type_object.rs @@ -3,6 +3,7 @@ use pyo3_ffi::PyType_IS_GC; use crate::{ exceptions::PyTypeError, ffi, + impl_::pycell::PyClassObject, impl_::pyclass::{ assign_sequence_item_from_mapping, get_sequence_item_from_mapping, tp_dealloc, tp_dealloc_with_gc, PyClassItemsIter, @@ -13,7 +14,7 @@ use crate::{ }, types::typeobject::PyTypeMethods, types::PyType, - Py, PyCell, PyClass, PyGetterDef, PyMethodDefType, PyResult, PySetterDef, PyTypeInfo, Python, + Py, PyClass, PyGetterDef, PyMethodDefType, PyResult, PySetterDef, PyTypeInfo, Python, }; use std::{ borrow::Cow, @@ -94,7 +95,7 @@ where T::items_iter(), T::NAME, T::MODULE, - std::mem::size_of::>(), + std::mem::size_of::>(), ) } } diff --git a/src/pyclass_init.rs b/src/pyclass_init.rs index 94d377a732c..923bc5b7c5a 100644 --- a/src/pyclass_init.rs +++ b/src/pyclass_init.rs @@ -1,13 +1,12 @@ //! Contains initialization utilities for `#[pyclass]`. use crate::callback::IntoPyCallbackOutput; +use crate::ffi_ptr_ext::FfiPtrExt; use crate::impl_::pyclass::{PyClassBaseType, PyClassDict, PyClassThreadChecker, PyClassWeakRef}; -use crate::{ffi, Py, PyCell, PyClass, PyErr, PyResult, Python}; +use crate::types::PyAnyMethods; +use crate::{ffi, Bound, Py, PyClass, PyErr, PyResult, Python}; use crate::{ ffi::PyTypeObject, - pycell::{ - impl_::{PyClassBorrowChecker, PyClassMutability}, - PyCellContents, - }, + pycell::impl_::{PyClassBorrowChecker, PyClassMutability, PyClassObjectContents}, type_object::{get_tp_alloc, PyTypeInfo}, }; use std::{ @@ -207,57 +206,44 @@ impl PyClassInitializer { } /// Creates a new PyCell and initializes it. - #[doc(hidden)] - pub fn create_cell(self, py: Python<'_>) -> PyResult<*mut PyCell> + pub(crate) fn create_class_object(self, py: Python<'_>) -> PyResult> where T: PyClass, { - unsafe { self.create_cell_from_subtype(py, T::type_object_raw(py)) } + unsafe { self.create_class_object_of_type(py, T::type_object_raw(py)) } } - /// Creates a new PyCell and initializes it given a typeobject `subtype`. - /// Called by the Python `tp_new` implementation generated by a `#[new]` function in a `#[pymethods]` block. + /// Creates a new class object and initializes it given a typeobject `subtype`. /// /// # Safety /// `subtype` must be a valid pointer to the type object of T or a subclass. - #[doc(hidden)] - pub unsafe fn create_cell_from_subtype( + pub(crate) unsafe fn create_class_object_of_type( self, py: Python<'_>, - subtype: *mut crate::ffi::PyTypeObject, - ) -> PyResult<*mut PyCell> + target_type: *mut crate::ffi::PyTypeObject, + ) -> PyResult> where T: PyClass, { - self.into_new_object(py, subtype).map(|obj| obj as _) - } -} - -impl PyObjectInit for PyClassInitializer { - unsafe fn into_new_object( - self, - py: Python<'_>, - subtype: *mut PyTypeObject, - ) -> PyResult<*mut ffi::PyObject> { - /// Layout of a PyCell after base new has been called, but the contents have not yet been + /// Layout of a PyClassObject after base new has been called, but the contents have not yet been /// written. #[repr(C)] - struct PartiallyInitializedPyCell { + struct PartiallyInitializedClassObject { _ob_base: ::LayoutAsBase, - contents: MaybeUninit>, + contents: MaybeUninit>, } let (init, super_init) = match self.0 { - PyClassInitializerImpl::Existing(value) => return Ok(value.into_ptr()), + PyClassInitializerImpl::Existing(value) => return Ok(value.into_bound(py)), PyClassInitializerImpl::New { init, super_init } => (init, super_init), }; - let obj = super_init.into_new_object(py, subtype)?; + let obj = super_init.into_new_object(py, target_type)?; - let cell: *mut PartiallyInitializedPyCell = obj as _; + let part_init: *mut PartiallyInitializedClassObject = obj.cast(); std::ptr::write( - (*cell).contents.as_mut_ptr(), - PyCellContents { + (*part_init).contents.as_mut_ptr(), + PyClassObjectContents { value: ManuallyDrop::new(UnsafeCell::new(init)), borrow_checker: ::Storage::new(), thread_checker: T::ThreadChecker::new(), @@ -265,7 +251,21 @@ impl PyObjectInit for PyClassInitializer { weakref: T::WeakRef::INIT, }, ); - Ok(obj) + + // Safety: obj is a valid pointer to an object of type `target_type`, which` is a known + // subclass of `T` + Ok(obj.assume_owned(py).downcast_into_unchecked()) + } +} + +impl PyObjectInit for PyClassInitializer { + unsafe fn into_new_object( + self, + py: Python<'_>, + subtype: *mut PyTypeObject, + ) -> PyResult<*mut ffi::PyObject> { + self.create_class_object_of_type(py, subtype) + .map(Bound::into_ptr) } private_impl! {} diff --git a/src/type_object.rs b/src/type_object.rs index 318810b534d..84888bee458 100644 --- a/src/type_object.rs +++ b/src/type_object.rs @@ -6,7 +6,7 @@ use crate::types::{PyAny, PyType}; use crate::{ffi, Bound, PyNativeType, Python}; /// `T: PyLayout` represents that `T` is a concrete representation of `U` in the Python heap. -/// E.g., `PyCell` is a concrete representation of all `pyclass`es, and `ffi::PyObject` +/// E.g., `PyClassObject` is a concrete representation of all `pyclass`es, and `ffi::PyObject` /// is of `PyAny`. /// /// This trait is intended to be used internally. diff --git a/src/types/mod.rs b/src/types/mod.rs index 938716f78f4..66312a98a0c 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -290,7 +290,7 @@ macro_rules! pyobject_native_type_sized { unsafe impl $crate::type_object::PyLayout<$name> for $layout {} impl $crate::type_object::PySizedLayout<$name> for $layout {} impl<$($generics,)*> $crate::impl_::pyclass::PyClassBaseType for $name { - type LayoutAsBase = $crate::pycell::PyCellBase<$layout>; + type LayoutAsBase = $crate::impl_::pycell::PyClassObjectBase<$layout>; type BaseNativeType = $name; type Initializer = $crate::pyclass_init::PyNativeTypeInitializer; type PyClassMutability = $crate::pycell::impl_::ImmutableClass; diff --git a/tests/test_coroutine.rs b/tests/test_coroutine.rs index e04aafda24d..db79c72a233 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}; +use portable_atomic::{AtomicBool, Ordering}; use pyo3::{ coroutine::CancelHandle, prelude::*, @@ -259,6 +260,15 @@ fn test_async_method_receiver() { self.0 } } + + static IS_DROPPED: AtomicBool = AtomicBool::new(false); + + impl Drop for Counter { + fn drop(&mut self) { + IS_DROPPED.store(true, Ordering::SeqCst); + } + } + Python::with_gil(|gil| { let test = r#" import asyncio @@ -291,5 +301,7 @@ fn test_async_method_receiver() { "#; let locals = [("Counter", gil.get_type_bound::())].into_py_dict_bound(gil); py_run!(gil, *locals, test); - }) + }); + + assert!(IS_DROPPED.load(Ordering::SeqCst)); } From 00eb0146237e16ae902b1f685e1d3fd2173ba104 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sun, 3 Mar 2024 10:15:46 +0100 Subject: [PATCH 061/936] docs: update Python function section of the guide (#3925) * docs: update Python function section of the guide * update `pass_module` types Co-authored-by: David Hewitt --------- Co-authored-by: David Hewitt --- guide/src/function.md | 29 ++++++++++++++-------------- guide/src/function/error_handling.md | 10 ++++------ guide/src/function/signature.md | 22 ++++++++++----------- 3 files changed, 29 insertions(+), 32 deletions(-) diff --git a/guide/src/function.md b/guide/src/function.md index 2ed38f6256f..1bba52f2235 100644 --- a/guide/src/function.md +++ b/guide/src/function.md @@ -77,7 +77,7 @@ The `#[pyo3]` attribute can be used to modify properties of the generated Python - `#[pyo3(pass_module)]` - Set this option to make PyO3 pass the containing module as the first argument to the function. It is then possible to use the module in the function body. The first argument **must** be of type `&PyModule`. + Set this option to make PyO3 pass the containing module as the first argument to the function. It is then possible to use the module in the function body. The first argument **must** be of type `&Bound<'_, PyModule>`, `Bound<'_, PyModule>`, or `Py`. The following example creates a function `pyfunction_with_module` which returns the containing module's name (i.e. `module_with_fn`): @@ -103,14 +103,14 @@ The `#[pyo3]` attribute can be used on individual arguments to modify properties - `#[pyo3(from_py_with = "...")]` - Set this on an option to specify a custom function to convert the function argument from Python to the desired Rust type, instead of using the default `FromPyObject` extraction. The function signature must be `fn(&PyAny) -> PyResult` where `T` is the Rust type of the argument. + Set this on an option to specify a custom function to convert the function argument from Python to the desired Rust type, instead of using the default `FromPyObject` extraction. The function signature must be `fn(&Bound<'_, PyAny>) -> PyResult` where `T` is the Rust type of the argument. The following example uses `from_py_with` to convert the input Python object to its length: ```rust use pyo3::prelude::*; - fn get_length(obj: &PyAny) -> PyResult { + fn get_length(obj: &Bound<'_, PyAny>) -> PyResult { let length = obj.len()?; Ok(length) } @@ -121,7 +121,7 @@ The `#[pyo3]` attribute can be used on individual arguments to modify properties } # Python::with_gil(|py| { - # let f = pyo3::wrap_pyfunction!(object_length)(py).unwrap(); + # let f = pyo3::wrap_pyfunction_bound!(object_length)(py).unwrap(); # assert_eq!(f.call1((vec![1, 2, 3],)).unwrap().extract::().unwrap(), 3); # }); ``` @@ -134,11 +134,11 @@ You can pass Python `def`'d functions and built-in functions to Rust functions [ corresponds to regular Python functions while [`PyCFunction`] describes built-ins such as `repr()`. -You can also use [`PyAny::is_callable`] to check if you have a callable object. `is_callable` will -return `true` for functions (including lambdas), methods and objects with a `__call__` method. -You can call the object with [`PyAny::call`] with the args as first parameter and the kwargs -(or `None`) as second parameter. There are also [`PyAny::call0`] with no args and [`PyAny::call1`] -with only positional args. +You can also use [`Bound<'_, PyAny>::is_callable`] to check if you have a callable object. `is_callable` +will return `true` for functions (including lambdas), methods and objects with a `__call__` method. +You can call the object with [`Bound<'_, PyAny>::call`] with the args as first parameter and the kwargs +(or `None`) as second parameter. There are also [`Bound<'_, PyAny>::call0`] with no args and +[`Bound<'_, PyAny>::call1`] with only positional args. ### Calling Rust functions in Python @@ -149,11 +149,10 @@ The ways to convert a Rust function into a Python object vary depending on the f - use a `#[pyclass]` struct which stores the function as a field and implement `__call__` to call the stored function. - use `PyCFunction::new_closure` to create an object directly from the function. -[`PyAny::is_callable`]: {{#PYO3_DOCS_URL}}/pyo3/struct.PyAny.html#tymethod.is_callable -[`PyAny::call`]: {{#PYO3_DOCS_URL}}/pyo3/struct.PyAny.html#tymethod.call -[`PyAny::call0`]: {{#PYO3_DOCS_URL}}/pyo3/struct.PyAny.html#tymethod.call0 -[`PyAny::call1`]: {{#PYO3_DOCS_URL}}/pyo3/struct.PyAny.html#tymethod.call1 -[`PyObject`]: {{#PYO3_DOCS_URL}}/pyo3/type.PyObject.html +[`Bound<'_, PyAny>::is_callable`]: {{#PYO3_DOCS_URL}}/pyo3/prelude/trait.PyAnyMethods.html#tymethod.is_callable +[`Bound<'_, PyAny>::call`]: {{#PYO3_DOCS_URL}}/pyo3/prelude/trait.PyAnyMethods.html#tymethod.call +[`Bound<'_, PyAny>::call0`]: {{#PYO3_DOCS_URL}}/pyo3/prelude/trait.PyAnyMethods.html#tymethod.call0 +[`Bound<'_, PyAny>::call1`]: {{#PYO3_DOCS_URL}}/pyo3/prelude/trait.PyAnyMethods.html#tymethod.call1 [`wrap_pyfunction!`]: {{#PYO3_DOCS_URL}}/pyo3/macro.wrap_pyfunction.html [`PyFunction`]: {{#PYO3_DOCS_URL}}/pyo3/types/struct.PyFunction.html [`PyCFunction`]: {{#PYO3_DOCS_URL}}/pyo3/types/struct.PyCFunction.html @@ -180,7 +179,7 @@ An example of `#[pyfn]` is below: use pyo3::prelude::*; #[pymodule] -fn my_extension(py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn my_extension(m: &Bound<'_, PyModule>) -> PyResult<()> { #[pyfn(m)] fn double(x: usize) -> usize { x * 2 diff --git a/guide/src/function/error_handling.md b/guide/src/function/error_handling.md index b0f63885cdf..09fe5cab27b 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!(check_positive, py).unwrap(); +# let fun = pyo3::wrap_pyfunction_bound!(check_positive, py).unwrap(); # fun.call1((-1,)).unwrap_err(); # fun.call1((1,)).unwrap(); # }); @@ -72,7 +72,7 @@ fn parse_int(x: &str) -> Result { # fn main() { # Python::with_gil(|py| { -# let fun = pyo3::wrap_pyfunction!(parse_int, py).unwrap(); +# let fun = pyo3::wrap_pyfunction_bound!(parse_int, py).unwrap(); # let value: usize = fun.call1(("5",)).unwrap().extract().unwrap(); # assert_eq!(value, 5); # }); @@ -132,7 +132,7 @@ fn connect(s: String) -> Result<(), CustomIOError> { fn main() { Python::with_gil(|py| { - let fun = pyo3::wrap_pyfunction!(connect, py).unwrap(); + let fun = pyo3::wrap_pyfunction_bound!(connect, py).unwrap(); let err = fun.call1(("0.0.0.0",)).unwrap_err(); assert!(err.is_instance_of::(py)); }); @@ -224,7 +224,7 @@ fn wrapped_get_x() -> Result { # fn main() { # Python::with_gil(|py| { -# let fun = pyo3::wrap_pyfunction!(wrapped_get_x, py).unwrap(); +# let fun = pyo3::wrap_pyfunction_bound!(wrapped_get_x, py).unwrap(); # let value: usize = fun.call0().unwrap().extract().unwrap(); # assert_eq!(value, 5); # }); @@ -234,8 +234,6 @@ fn wrapped_get_x() -> Result { [`From`]: https://doc.rust-lang.org/stable/std/convert/trait.From.html [`Result`]: https://doc.rust-lang.org/stable/std/result/enum.Result.html - -[`PyResult`]: {{#PYO3_DOCS_URL}}/pyo3/prelude/type.PyResult.html [`PyResult`]: {{#PYO3_DOCS_URL}}/pyo3/prelude/type.PyResult.html [`PyErr`]: {{#PYO3_DOCS_URL}}/pyo3/struct.PyErr.html [`pyo3::exceptions`]: {{#PYO3_DOCS_URL}}/pyo3/exceptions/index.html diff --git a/guide/src/function/signature.md b/guide/src/function/signature.md index 4fcfe958b19..b276fc457fb 100644 --- a/guide/src/function/signature.md +++ b/guide/src/function/signature.md @@ -16,7 +16,7 @@ use pyo3::types::PyDict; #[pyfunction] #[pyo3(signature = (**kwds))] -fn num_kwds(kwds: Option<&PyDict>) -> usize { +fn num_kwds(kwds: Option<&Bound<'_, PyDict>>) -> usize { kwds.map_or(0, |dict| dict.len()) } @@ -31,8 +31,8 @@ Just like in Python, the following constructs can be part of the signature:: * `/`: positional-only arguments separator, each parameter defined before `/` is a positional-only parameter. * `*`: var arguments separator, each parameter defined after `*` is a keyword-only parameter. - * `*args`: "args" is var args. Type of the `args` parameter has to be `&PyTuple`. - * `**kwargs`: "kwargs" receives keyword arguments. The type of the `kwargs` parameter has to be `Option<&PyDict>`. + * `*args`: "args" is var args. Type of the `args` parameter has to be `&Bound<'_, PyTuple>`. + * `**kwargs`: "kwargs" receives keyword arguments. The type of the `kwargs` parameter has to be `Option<&Bound<'_, PyDict>>`. * `arg=Value`: arguments with default value. If the `arg` argument is defined after var arguments, it is treated as a keyword-only argument. Note that `Value` has to be valid rust code, PyO3 just inserts it into the generated @@ -59,9 +59,9 @@ impl MyClass { fn method( &mut self, num: i32, - py_args: &PyTuple, + py_args: &Bound<'_, PyTuple>, name: &str, - py_kwargs: Option<&PyDict>, + py_kwargs: Option<&Bound<'_, PyDict>>, ) -> String { let num_before = self.num; self.num = num; @@ -136,7 +136,7 @@ fn increment(x: u64, amount: Option) -> u64 { # # fn main() -> PyResult<()> { # Python::with_gil(|py| { -# let fun = pyo3::wrap_pyfunction!(increment, py)?; +# let fun = pyo3::wrap_pyfunction_bound!(increment, py)?; # # let inspect = PyModule::import_bound(py, "inspect")?.getattr("signature")?; # let sig: String = inspect @@ -164,7 +164,7 @@ fn increment(x: u64, amount: Option) -> u64 { # # fn main() -> PyResult<()> { # Python::with_gil(|py| { -# let fun = pyo3::wrap_pyfunction!(increment, py)?; +# let fun = pyo3::wrap_pyfunction_bound!(increment, py)?; # # let inspect = PyModule::import_bound(py, "inspect")?.getattr("signature")?; # let sig: String = inspect @@ -204,7 +204,7 @@ fn add(a: u64, b: u64) -> u64 { # # fn main() -> PyResult<()> { # Python::with_gil(|py| { -# let fun = pyo3::wrap_pyfunction!(add, py)?; +# let fun = pyo3::wrap_pyfunction_bound!(add, py)?; # # let doc: String = fun.getattr("__doc__")?.extract()?; # assert_eq!(doc, "This function adds two unsigned 64-bit integers."); @@ -252,7 +252,7 @@ fn add(a: u64, b: u64) -> u64 { # # fn main() -> PyResult<()> { # Python::with_gil(|py| { -# let fun = pyo3::wrap_pyfunction!(add, py)?; +# let fun = pyo3::wrap_pyfunction_bound!(add, py)?; # # let doc: String = fun.getattr("__doc__")?.extract()?; # assert_eq!(doc, "This function adds two unsigned 64-bit integers."); @@ -269,7 +269,7 @@ fn add(a: u64, b: u64) -> u64 { # } ``` -PyO3 will include the contents of the annotation unmodified as the `__text_signature`. Below shows how IPython will now present this (see the default value of 0 for b): +PyO3 will include the contents of the annotation unmodified as the `__text_signature__`. Below shows how IPython will now present this (see the default value of 0 for b): ```text >>> pyo3_test.add.__text_signature__ @@ -294,7 +294,7 @@ fn add(a: u64, b: u64) -> u64 { # # fn main() -> PyResult<()> { # Python::with_gil(|py| { -# let fun = pyo3::wrap_pyfunction!(add, py)?; +# let fun = pyo3::wrap_pyfunction_bound!(add, py)?; # # let doc: String = fun.getattr("__doc__")?.extract()?; # assert_eq!(doc, "This function adds two unsigned 64-bit integers."); From 70a7aa808db038ef9c9670f79b50fd67082d4918 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sun, 3 Mar 2024 15:47:25 +0100 Subject: [PATCH 062/936] deprecate the use of `PyCell` in favor of `Bound` and `Py` (#3916) * deprecate the use of `PyCell` in favor of `Bound` and `Py` * update `FromPyObject` for `T: PyClass + Clone` impl * move `PyCell` deprecation to the `gil-refs` feature gate and add a migration note --- guide/src/class.md | 1 + guide/src/types.md | 1 + pyo3-macros-backend/src/pyclass.rs | 1 + src/conversion.rs | 15 ++++++------- src/conversions/std/array.rs | 2 +- src/impl_/pymethods.rs | 7 +++--- src/instance.rs | 7 +++--- src/lib.rs | 4 +++- src/prelude.rs | 4 +++- src/pycell.rs | 22 ++++++++++++++++++- src/pyclass.rs | 7 +++--- src/types/pysuper.rs | 2 +- tests/test_buffer_protocol.rs | 10 ++++----- tests/test_class_new.rs | 4 ++-- tests/test_pyself.rs | 11 ++++++---- tests/test_super.rs | 6 ++--- tests/test_text_signature.rs | 2 +- tests/test_various.rs | 2 +- tests/ui/invalid_frozen_pyclass_borrow.rs | 2 +- tests/ui/invalid_frozen_pyclass_borrow.stderr | 6 ++--- tests/ui/pyclass_send.rs | 6 ++--- 21 files changed, 77 insertions(+), 45 deletions(-) diff --git a/guide/src/class.md b/guide/src/class.md index 3096c9f7842..b9b47420ccb 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -1230,6 +1230,7 @@ struct MyClass { impl pyo3::types::DerefToPyAny for MyClass {} +# #[allow(deprecated)] unsafe impl pyo3::type_object::HasPyGilRef for MyClass { type AsRefTarget = pyo3::PyCell; } diff --git a/guide/src/types.md b/guide/src/types.md index 51ea10f4545..372c6c8632f 100644 --- a/guide/src/types.md +++ b/guide/src/types.md @@ -96,6 +96,7 @@ For a `&PyAny` object reference `any` where the underlying object is a `#[pyclas let obj: &PyAny = Py::new(py, MyClass {})?.into_ref(py); // To &PyCell with PyAny::downcast +# #[allow(deprecated)] let _: &PyCell = obj.downcast()?; // To Py (aka PyObject) with .into() diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 5ec6ac172e6..734b9565a66 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -1281,6 +1281,7 @@ fn impl_pytypeinfo( }; quote! { + #[allow(deprecated)] unsafe impl _pyo3::type_object::HasPyGilRef for #cls { type AsRefTarget = _pyo3::PyCell; } diff --git a/src/conversion.rs b/src/conversion.rs index 986ac7c537c..68633d196be 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -6,9 +6,7 @@ use crate::pyclass::boolean_struct::False; use crate::type_object::PyTypeInfo; use crate::types::any::PyAnyMethods; use crate::types::PyTuple; -use crate::{ - ffi, gil, Bound, Py, PyAny, PyCell, PyClass, PyNativeType, PyObject, PyRef, PyRefMut, Python, -}; +use crate::{ffi, gil, Bound, Py, PyAny, PyClass, PyNativeType, PyObject, PyRef, PyRefMut, Python}; use std::ptr::NonNull; /// Returns a borrowed pointer to a Python object. @@ -265,7 +263,8 @@ where } } -impl<'py, T> FromPyObject<'py> for &'py PyCell +#[allow(deprecated)] +impl<'py, T> FromPyObject<'py> for &'py crate::PyCell where T: PyClass, { @@ -278,9 +277,9 @@ impl FromPyObject<'_> for T where T: PyClass + Clone, { - fn extract(obj: &PyAny) -> PyResult { - let cell: &PyCell = obj.downcast()?; - Ok(unsafe { cell.try_borrow_unguarded()?.clone() }) + fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { + let bound = obj.downcast::()?; + Ok(bound.try_borrow()?.clone()) } } @@ -389,7 +388,7 @@ mod implementations { } } - impl<'v, T> PyTryFrom<'v> for PyCell + impl<'v, T> PyTryFrom<'v> for crate::PyCell where T: 'v + PyClass, { diff --git a/src/conversions/std/array.rs b/src/conversions/std/array.rs index d901ff4e59d..bf21244e3b2 100644 --- a/src/conversions/std/array.rs +++ b/src/conversions/std/array.rs @@ -240,7 +240,7 @@ mod tests { 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 _cell: &crate::PyCell = list.get_item(4).unwrap().extract().unwrap(); + let _bound = list.get_item(4).unwrap().downcast::().unwrap(); }); } diff --git a/src/impl_/pymethods.rs b/src/impl_/pymethods.rs index bc950bafee4..df89dba7dbd 100644 --- a/src/impl_/pymethods.rs +++ b/src/impl_/pymethods.rs @@ -7,8 +7,8 @@ use crate::pycell::{PyBorrowError, PyBorrowMutError}; use crate::pyclass::boolean_struct::False; use crate::types::{any::PyAnyMethods, PyModule, PyType}; use crate::{ - ffi, Borrowed, Bound, DowncastError, Py, PyAny, PyCell, PyClass, PyClassInitializer, PyErr, - PyObject, PyRef, PyRefMut, PyResult, PyTraverseError, PyTypeCheck, PyVisit, Python, + ffi, Borrowed, Bound, DowncastError, Py, PyAny, PyClass, PyClassInitializer, PyErr, PyObject, + PyRef, PyRefMut, PyResult, PyTraverseError, PyTypeCheck, PyVisit, Python, }; use std::borrow::Cow; use std::ffi::CStr; @@ -518,7 +518,8 @@ impl<'a> From> for &'a PyModule { } } -impl<'a, 'py, T: PyClass> From> for &'a PyCell { +#[allow(deprecated)] +impl<'a, 'py, T: PyClass> From> for &'a crate::PyCell { #[inline] fn from(bound: BoundRef<'a, 'py, T>) -> Self { bound.0.as_gil_ref() diff --git a/src/instance.rs b/src/instance.rs index 66c530d5f53..cef06062430 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -1,6 +1,6 @@ use crate::err::{self, PyDowncastError, PyErr, PyResult}; use crate::impl_::pycell::PyClassObject; -use crate::pycell::{PyBorrowError, PyBorrowMutError, PyCell}; +use crate::pycell::{PyBorrowError, PyBorrowMutError}; use crate::pyclass::boolean_struct::{False, True}; use crate::type_object::HasPyGilRef; use crate::types::{any::PyAnyMethods, string::PyStringMethods, typeobject::PyTypeMethods}; @@ -1698,11 +1698,12 @@ impl std::convert::From> for Py { } // `&PyCell` can be converted to `Py` -impl std::convert::From<&PyCell> for Py +#[allow(deprecated)] +impl std::convert::From<&crate::PyCell> for Py where T: PyClass, { - fn from(cell: &PyCell) -> Self { + fn from(cell: &crate::PyCell) -> Self { cell.as_borrowed().to_owned().unbind() } } diff --git a/src/lib.rs b/src/lib.rs index 8c0e6269947..26d2ec55da1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -315,7 +315,9 @@ pub use crate::gil::GILPool; pub use crate::gil::{prepare_freethreaded_python, with_embedded_python_interpreter}; pub use crate::instance::{Borrowed, Bound, Py, PyNativeType, PyObject}; pub use crate::marker::Python; -pub use crate::pycell::{PyCell, PyRef, PyRefMut}; +#[allow(deprecated)] +pub use crate::pycell::PyCell; +pub use crate::pycell::{PyRef, PyRefMut}; pub use crate::pyclass::PyClass; pub use crate::pyclass_init::PyClassInitializer; pub use crate::type_object::{PyTypeCheck, PyTypeInfo}; diff --git a/src/prelude.rs b/src/prelude.rs index dcf4fe71cdd..ef42b2706e9 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -14,7 +14,9 @@ pub use crate::conversion::{PyTryFrom, PyTryInto}; pub use crate::err::{PyErr, PyResult}; pub use crate::instance::{Borrowed, Bound, Py, PyObject}; pub use crate::marker::Python; -pub use crate::pycell::{PyCell, PyRef, PyRefMut}; +#[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}; pub use crate::PyNativeType; diff --git a/src/pycell.rs b/src/pycell.rs index 43c75314f00..397e7b6a22a 100644 --- a/src/pycell.rs +++ b/src/pycell.rs @@ -62,7 +62,7 @@ //! ) -> *mut pyo3::ffi::PyObject { //! use :: pyo3 as _pyo3; //! _pyo3::impl_::trampoline::noargs(_slf, _args, |py, _slf| { -//! # #[allow(deprecated)] +//! # #[allow(deprecated)] //! let _cell = py //! .from_borrowed_ptr::<_pyo3::PyAny>(_slf) //! .downcast::<_pyo3::PyCell>()?; @@ -154,6 +154,7 @@ //! # pub struct Number { //! # inner: u32, //! # } +//! # #[allow(deprecated)] //! #[pyfunction] //! fn swap_numbers(a: &PyCell, b: &PyCell) { //! // Check that the pointers are unequal @@ -250,13 +251,22 @@ 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" + ) +)] #[repr(transparent)] pub struct PyCell(PyClassObject); +#[allow(deprecated)] unsafe impl PyNativeType for PyCell { type AsRefSource = T; } +#[allow(deprecated)] impl PyCell { /// Makes a new `PyCell` on the Python heap and return the reference to it. /// @@ -478,9 +488,12 @@ impl PyCell { } } +#[allow(deprecated)] unsafe impl PyLayout for PyCell {} +#[allow(deprecated)] impl PySizedLayout for PyCell {} +#[allow(deprecated)] impl PyTypeCheck for PyCell where T: PyClass, @@ -492,18 +505,21 @@ where } } +#[allow(deprecated)] unsafe impl AsPyPointer for PyCell { fn as_ptr(&self) -> *mut ffi::PyObject { (self as *const _) as *mut _ } } +#[allow(deprecated)] impl ToPyObject for &PyCell { fn to_object(&self, py: Python<'_>) -> PyObject { unsafe { PyObject::from_borrowed_ptr(py, self.as_ptr()) } } } +#[allow(deprecated)] impl AsRef for PyCell { fn as_ref(&self) -> &PyAny { #[allow(deprecated)] @@ -513,6 +529,7 @@ impl AsRef for PyCell { } } +#[allow(deprecated)] impl Deref for PyCell { type Target = PyAny; @@ -524,6 +541,7 @@ impl Deref for PyCell { } } +#[allow(deprecated)] impl fmt::Debug for PyCell { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self.try_borrow() { @@ -748,6 +766,7 @@ impl IntoPy for &'_ PyRef<'_, T> { } } +#[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 { @@ -905,6 +924,7 @@ unsafe impl<'a, T: PyClass> AsPyPointer for PyRefMut<'a, T> { } } +#[allow(deprecated)] impl<'a, T: PyClass> std::convert::TryFrom<&'a PyCell> for crate::PyRefMut<'a, T> { diff --git a/src/pyclass.rs b/src/pyclass.rs index eb4a5595ca9..b9b01cac26a 100644 --- a/src/pyclass.rs +++ b/src/pyclass.rs @@ -1,7 +1,7 @@ //! `PyClass` and related traits. use crate::{ - callback::IntoPyCallbackOutput, ffi, impl_::pyclass::PyClassImpl, IntoPy, PyCell, PyObject, - PyResult, PyTypeInfo, Python, + callback::IntoPyCallbackOutput, ffi, impl_::pyclass::PyClassImpl, IntoPy, PyObject, PyResult, + PyTypeInfo, Python, }; use std::{cmp::Ordering, os::raw::c_int}; @@ -15,7 +15,8 @@ pub use self::gc::{PyTraverseError, PyVisit}; /// /// The `#[pyclass]` attribute implements this trait for your Rust struct - /// you shouldn't implement this trait directly. -pub trait PyClass: PyTypeInfo> + PyClassImpl { +#[allow(deprecated)] +pub trait PyClass: PyTypeInfo> + PyClassImpl { /// Whether the pyclass is frozen. /// /// This can be enabled via `#[pyclass(frozen)]`. diff --git a/src/types/pysuper.rs b/src/types/pysuper.rs index 261a91f289b..0f1a47444d6 100644 --- a/src/types/pysuper.rs +++ b/src/types/pysuper.rs @@ -62,7 +62,7 @@ impl PySuper { /// (SubClass {}, BaseClass::new()) /// } /// - /// fn method(self_: &PyCell) -> PyResult<&PyAny> { + /// fn method<'py>(self_: &Bound<'py, Self>) -> PyResult> { /// let super_ = self_.py_super()?; /// super_.call_method("method", (), None) /// } diff --git a/tests/test_buffer_protocol.rs b/tests/test_buffer_protocol.rs index 85ff6a4004e..dca900808a8 100644 --- a/tests/test_buffer_protocol.rs +++ b/tests/test_buffer_protocol.rs @@ -24,11 +24,11 @@ struct TestBufferClass { #[pymethods] impl TestBufferClass { unsafe fn __getbuffer__( - slf: &PyCell, + slf: Bound<'_, Self>, view: *mut ffi::Py_buffer, flags: c_int, ) -> PyResult<()> { - fill_view_from_readonly_data(view, flags, &slf.borrow().vec, slf) + fill_view_from_readonly_data(view, flags, &slf.borrow().vec, slf.into_any()) } unsafe fn __releasebuffer__(&self, view: *mut ffi::Py_buffer) { @@ -105,12 +105,12 @@ fn test_releasebuffer_unraisable_error() { #[pymethods] impl ReleaseBufferError { unsafe fn __getbuffer__( - slf: &PyCell, + slf: Bound<'_, Self>, view: *mut ffi::Py_buffer, flags: c_int, ) -> PyResult<()> { static BUF_BYTES: &[u8] = b"hello world"; - fill_view_from_readonly_data(view, flags, BUF_BYTES, slf) + fill_view_from_readonly_data(view, flags, BUF_BYTES, slf.into_any()) } unsafe fn __releasebuffer__(&self, _view: *mut ffi::Py_buffer) -> PyResult<()> { @@ -145,7 +145,7 @@ unsafe fn fill_view_from_readonly_data( view: *mut ffi::Py_buffer, flags: c_int, data: &[u8], - owner: &PyAny, + owner: Bound<'_, PyAny>, ) -> PyResult<()> { if view.is_null() { return Err(PyBufferError::new_err("View is null")); diff --git a/tests/test_class_new.rs b/tests/test_class_new.rs index 9e16631bb83..161c60e9489 100644 --- a/tests/test_class_new.rs +++ b/tests/test_class_new.rs @@ -23,7 +23,7 @@ fn empty_class_with_new() { assert!(typeobj .call((), None) .unwrap() - .downcast::>() + .downcast::() .is_ok()); // Calling with arbitrary args or kwargs is not ok @@ -52,7 +52,7 @@ fn unit_class_with_new() { assert!(typeobj .call((), None) .unwrap() - .downcast::>() + .downcast::() .is_ok()); }); } diff --git a/tests/test_pyself.rs b/tests/test_pyself.rs index fa736f68455..901f52de530 100644 --- a/tests/test_pyself.rs +++ b/tests/test_pyself.rs @@ -18,15 +18,18 @@ struct Reader { #[pymethods] impl Reader { - fn clone_ref(slf: &PyCell) -> &PyCell { + fn clone_ref<'a, 'py>(slf: &'a Bound<'py, Self>) -> &'a Bound<'py, Self> { slf } - fn clone_ref_with_py<'py>(slf: &'py PyCell, _py: Python<'py>) -> &'py PyCell { + fn clone_ref_with_py<'a, 'py>( + slf: &'a Bound<'py, Self>, + _py: Python<'py>, + ) -> &'a Bound<'py, Self> { slf } - fn get_iter(slf: &PyCell, keys: Py) -> Iter { + fn get_iter(slf: &Bound<'_, Self>, keys: Py) -> Iter { Iter { - reader: slf.into(), + reader: slf.clone().unbind(), keys, idx: 0, } diff --git a/tests/test_super.rs b/tests/test_super.rs index 8dcedf808a5..3647e7d5b23 100644 --- a/tests/test_super.rs +++ b/tests/test_super.rs @@ -29,14 +29,14 @@ impl SubClass { (SubClass {}, BaseClass::new()) } - fn method(self_: &PyCell) -> PyResult<&PyAny> { + fn method<'py>(self_: &Bound<'py, Self>) -> PyResult> { let super_ = self_.py_super()?; super_.call_method("method", (), None) } - fn method_super_new(self_: &PyCell) -> PyResult<&PyAny> { + fn method_super_new<'py>(self_: &Bound<'py, Self>) -> PyResult> { #[cfg_attr(not(feature = "gil-refs"), allow(deprecated))] - let super_ = PySuper::new(self_.get_type(), self_)?; + let super_ = PySuper::new_bound(&self_.get_type(), self_)?; super_.call_method("method", (), None) } } diff --git a/tests/test_text_signature.rs b/tests/test_text_signature.rs index 6b56999c62f..0b93500db7e 100644 --- a/tests/test_text_signature.rs +++ b/tests/test_text_signature.rs @@ -367,7 +367,7 @@ fn test_methods() { let _ = a; } #[pyo3(text_signature = "($self, b)")] - fn pyself_method(_this: &PyCell, b: i32) { + fn pyself_method(_this: &Bound<'_, Self>, b: i32) { let _ = b; } #[classmethod] diff --git a/tests/test_various.rs b/tests/test_various.rs index 6f1bfd908a7..250f39834d1 100644 --- a/tests/test_various.rs +++ b/tests/test_various.rs @@ -124,7 +124,7 @@ impl PickleSupport { } pub fn __reduce__<'py>( - slf: &'py PyCell, + slf: &Bound<'py, Self>, py: Python<'py>, ) -> PyResult<(PyObject, Bound<'py, PyTuple>, PyObject)> { let cls = slf.to_object(py).getattr(py, "__class__")?; diff --git a/tests/ui/invalid_frozen_pyclass_borrow.rs b/tests/ui/invalid_frozen_pyclass_borrow.rs index aa8969ab1a6..6379a8707c5 100644 --- a/tests/ui/invalid_frozen_pyclass_borrow.rs +++ b/tests/ui/invalid_frozen_pyclass_borrow.rs @@ -29,7 +29,7 @@ fn py_get_of_mutable_class_fails(class: Py) { class.get(); } -fn pyclass_get_of_mutable_class_fails(class: &PyCell) { +fn pyclass_get_of_mutable_class_fails(class: &Bound<'_, MutableBase>) { class.get(); } diff --git a/tests/ui/invalid_frozen_pyclass_borrow.stderr b/tests/ui/invalid_frozen_pyclass_borrow.stderr index 1a8e45964ca..3acfbeb1823 100644 --- a/tests/ui/invalid_frozen_pyclass_borrow.stderr +++ b/tests/ui/invalid_frozen_pyclass_borrow.stderr @@ -67,11 +67,11 @@ error[E0271]: type mismatch resolving `::Frozen == True` 33 | class.get(); | ^^^ expected `True`, found `False` | -note: required by a bound in `pyo3::PyCell::::get` - --> src/pycell.rs +note: required by a bound in `pyo3::Bound::<'py, T>::get` + --> src/instance.rs | | pub fn get(&self) -> &T | --- required by a bound in this associated function | where | T: PyClass + Sync, - | ^^^^^^^^^^^^^ required by this bound in `PyCell::::get` + | ^^^^^^^^^^^^^ required by this bound in `Bound::<'py, T>::get` diff --git a/tests/ui/pyclass_send.rs b/tests/ui/pyclass_send.rs index 533302740d7..2747c2cb3bb 100644 --- a/tests/ui/pyclass_send.rs +++ b/tests/ui/pyclass_send.rs @@ -8,15 +8,15 @@ struct NotThreadSafe { fn main() { let obj = Python::with_gil(|py| { - PyCell::new(py, NotThreadSafe { data: Rc::new(5) }) + Bound::new(py, NotThreadSafe { data: Rc::new(5) }) .unwrap() - .to_object(py) + .unbind(py) }); std::thread::spawn(move || { Python::with_gil(|py| { // Uh oh, moved Rc to a new thread! - let c: &PyCell = obj.as_ref(py).downcast().unwrap(); + let c = obj.bind(py).downcast::().unwrap(); assert_eq!(*c.borrow().data, 5); }) From 4114dcb1a04160ef98fa17da52f83cce89d1497c Mon Sep 17 00:00:00 2001 From: Bruno Kolenbrander <59372212+mejrs@users.noreply.github.com> Date: Mon, 4 Mar 2024 08:54:04 +0100 Subject: [PATCH 063/936] Thread pyo3's path through the builder functions (#3907) * Thread pyo3's path through the builder functions * preserve span of pyo3_path --------- Co-authored-by: David Hewitt --- pyo3-macros-backend/src/deprecations.rs | 19 +- pyo3-macros-backend/src/frompyobject.rs | 72 ++--- pyo3-macros-backend/src/konst.rs | 17 +- pyo3-macros-backend/src/method.rs | 179 +++++++----- pyo3-macros-backend/src/module.rs | 43 +-- pyo3-macros-backend/src/params.rs | 54 ++-- pyo3-macros-backend/src/pyclass.rs | 326 +++++++++++---------- pyo3-macros-backend/src/pyfunction.rs | 25 +- pyo3-macros-backend/src/pyimpl.rs | 55 ++-- pyo3-macros-backend/src/pymethod.rs | 367 ++++++++++++++---------- pyo3-macros-backend/src/quotes.rs | 18 +- pyo3-macros-backend/src/utils.rs | 40 ++- tests/ui/invalid_proto_pymethods.stderr | 2 +- 13 files changed, 706 insertions(+), 511 deletions(-) diff --git a/pyo3-macros-backend/src/deprecations.rs b/pyo3-macros-backend/src/deprecations.rs index ea2922737b9..3f1f34144f6 100644 --- a/pyo3-macros-backend/src/deprecations.rs +++ b/pyo3-macros-backend/src/deprecations.rs @@ -1,3 +1,4 @@ +use crate::utils::Ctx; use proc_macro2::{Span, TokenStream}; use quote::{quote_spanned, ToTokens}; @@ -14,12 +15,11 @@ impl Deprecation { } } -#[derive(Default)] -pub struct Deprecations(Vec<(Deprecation, Span)>); +pub struct Deprecations<'ctx>(Vec<(Deprecation, Span)>, &'ctx Ctx); -impl Deprecations { - pub fn new() -> Self { - Deprecations(Vec::new()) +impl<'ctx> Deprecations<'ctx> { + pub fn new(ctx: &'ctx Ctx) -> Self { + Deprecations(Vec::new(), ctx) } pub fn push(&mut self, deprecation: Deprecation, span: Span) { @@ -27,15 +27,18 @@ impl Deprecations { } } -impl ToTokens for Deprecations { +impl<'ctx> ToTokens for Deprecations<'ctx> { fn to_tokens(&self, tokens: &mut TokenStream) { - for (deprecation, span) in &self.0 { + let Self(deprecations, Ctx { pyo3_path }) = self; + + for (deprecation, span) in deprecations { + let pyo3_path = pyo3_path.to_tokens_spanned(*span); let ident = deprecation.ident(*span); quote_spanned!( *span => #[allow(clippy::let_unit_value)] { - let _ = _pyo3::impl_::deprecations::#ident; + let _ = #pyo3_path::impl_::deprecations::#ident; } ) .to_tokens(tokens) diff --git a/pyo3-macros-backend/src/frompyobject.rs b/pyo3-macros-backend/src/frompyobject.rs index c1410180d05..24471c1aae8 100644 --- a/pyo3-macros-backend/src/frompyobject.rs +++ b/pyo3-macros-backend/src/frompyobject.rs @@ -1,7 +1,5 @@ -use crate::{ - attributes::{self, get_pyo3_options, CrateAttribute, FromPyWithAttribute}, - utils::get_pyo3_crate, -}; +use crate::attributes::{self, get_pyo3_options, CrateAttribute, FromPyWithAttribute}; +use crate::utils::Ctx; use proc_macro2::TokenStream; use quote::{format_ident, quote}; use syn::{ @@ -46,14 +44,15 @@ impl<'a> Enum<'a> { } /// Build derivation body for enums. - fn build(&self) -> 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(); for var in &self.variants { - let struct_derive = var.build(); + let struct_derive = var.build(ctx); let ext = quote!({ - let maybe_ret = || -> _pyo3::PyResult { + let maybe_ret = || -> #pyo3_path::PyResult { #struct_derive }(); @@ -73,7 +72,7 @@ impl<'a> Enum<'a> { #(#var_extracts),* ]; ::std::result::Result::Err( - _pyo3::impl_::frompyobject::failed_to_extract_enum( + #pyo3_path::impl_::frompyobject::failed_to_extract_enum( obj.py(), #ty_name, &[#(#variant_names),*], @@ -239,16 +238,16 @@ impl<'a> Container<'a> { } /// Build derivation body for a struct. - fn build(&self) -> 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) + self.build_newtype_struct(Some(ident), from_py_with, ctx) } ContainerType::TupleNewtype(from_py_with) => { - self.build_newtype_struct(None, from_py_with) + self.build_newtype_struct(None, from_py_with, ctx) } - ContainerType::Tuple(tups) => self.build_tuple_struct(tups), - ContainerType::Struct(tups) => self.build_struct(tups), + ContainerType::Tuple(tups) => self.build_tuple_struct(tups, ctx), + ContainerType::Struct(tups) => self.build_struct(tups, ctx), } } @@ -256,7 +255,9 @@ impl<'a> Container<'a> { &self, field_ident: Option<&Ident>, from_py_with: &Option, + ctx: &Ctx, ) -> TokenStream { + let Ctx { pyo3_path } = ctx; let self_ty = &self.path; let struct_name = self.name(); if let Some(ident) = field_ident { @@ -264,32 +265,33 @@ impl<'a> Container<'a> { match from_py_with { None => quote! { Ok(#self_ty { - #ident: _pyo3::impl_::frompyobject::extract_struct_field(obj, #struct_name, #field_name)? + #ident: #pyo3_path::impl_::frompyobject::extract_struct_field(obj, #struct_name, #field_name)? }) }, Some(FromPyWithAttribute { value: expr_path, .. }) => quote! { Ok(#self_ty { - #ident: _pyo3::impl_::frompyobject::extract_struct_field_with(#expr_path as fn(_) -> _, obj, #struct_name, #field_name)? + #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::impl_::frompyobject::extract_tuple_struct_field(obj, #struct_name, 0).map(#self_ty) + #pyo3_path::impl_::frompyobject::extract_tuple_struct_field(obj, #struct_name, 0).map(#self_ty) ), Some(FromPyWithAttribute { value: expr_path, .. }) => quote! ( - _pyo3::impl_::frompyobject::extract_tuple_struct_field_with(#expr_path as fn(_) -> _, obj, #struct_name, 0).map(#self_ty) + #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]) -> 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(); let field_idents: Vec<_> = (0..struct_fields.len()) @@ -298,12 +300,12 @@ impl<'a> Container<'a> { let fields = struct_fields.iter().zip(&field_idents).enumerate().map(|(index, (field, ident))| { match &field.from_py_with { None => quote!( - _pyo3::impl_::frompyobject::extract_tuple_struct_field(&#ident, #struct_name, #index)? + #pyo3_path::impl_::frompyobject::extract_tuple_struct_field(&#ident, #struct_name, #index)? ), Some(FromPyWithAttribute { value: expr_path, .. }) => quote! ( - _pyo3::impl_::frompyobject::extract_tuple_struct_field_with(#expr_path as fn(_) -> _, &#ident, #struct_name, #index)? + #pyo3_path::impl_::frompyobject::extract_tuple_struct_field_with(#expr_path as fn(_) -> _, &#ident, #struct_name, #index)? ), } }); @@ -315,7 +317,8 @@ impl<'a> Container<'a> { ) } - fn build_struct(&self, struct_fields: &[NamedStructField<'_>]) -> 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(); let mut fields: Punctuated = Punctuated::new(); @@ -324,27 +327,27 @@ impl<'a> Container<'a> { let field_name = ident.to_string(); let getter = match field.getter.as_ref().unwrap_or(&FieldGetter::GetAttr(None)) { FieldGetter::GetAttr(Some(name)) => { - quote!(getattr(_pyo3::intern!(obj.py(), #name))) + quote!(getattr(#pyo3_path::intern!(obj.py(), #name))) } FieldGetter::GetAttr(None) => { - quote!(getattr(_pyo3::intern!(obj.py(), #field_name))) + quote!(getattr(#pyo3_path::intern!(obj.py(), #field_name))) } FieldGetter::GetItem(Some(syn::Lit::Str(key))) => { - quote!(get_item(_pyo3::intern!(obj.py(), #key))) + quote!(get_item(#pyo3_path::intern!(obj.py(), #key))) } FieldGetter::GetItem(Some(key)) => quote!(get_item(#key)), FieldGetter::GetItem(None) => { - quote!(get_item(_pyo3::intern!(obj.py(), #field_name))) + quote!(get_item(#pyo3_path::intern!(obj.py(), #field_name))) } }; let extractor = match &field.from_py_with { None => { - quote!(_pyo3::impl_::frompyobject::extract_struct_field(&obj.#getter?, #struct_name, #field_name)?) + quote!(#pyo3_path::impl_::frompyobject::extract_struct_field(&obj.#getter?, #struct_name, #field_name)?) } Some(FromPyWithAttribute { value: expr_path, .. }) => { - quote! (_pyo3::impl_::frompyobject::extract_struct_field_with(#expr_path as fn(_) -> _, &obj.#getter?, #struct_name, #field_name)?) + quote! (#pyo3_path::impl_::frompyobject::extract_struct_field_with(#expr_path as fn(_) -> _, &obj.#getter?, #struct_name, #field_name)?) } }; @@ -579,7 +582,9 @@ pub fn build_derive_from_pyobject(tokens: &DeriveInput) -> Result { .push(parse_quote!(#gen_ident: FromPyObject<#lt_param>)) } let options = ContainerOptions::from_attrs(&tokens.attrs)?; - let krate = get_pyo3_crate(&options.krate); + let ctx = &Ctx::new(&options.krate); + let Ctx { pyo3_path } = &ctx; + let derives = match &tokens.data { syn::Data::Enum(en) => { if options.transparent || options.annotation.is_some() { @@ -587,7 +592,7 @@ pub fn build_derive_from_pyobject(tokens: &DeriveInput) -> Result { at top level for enums"); } let en = Enum::new(en, &tokens.ident)?; - en.build() + en.build(ctx) } syn::Data::Struct(st) => { if let Some(lit_str) = &options.annotation { @@ -595,7 +600,7 @@ pub fn build_derive_from_pyobject(tokens: &DeriveInput) -> Result { } let ident = &tokens.ident; let st = Container::new(&st.fields, parse_quote!(#ident), options)?; - st.build() + st.build(ctx) } syn::Data::Union(_) => bail_spanned!( tokens.span() => "#[derive(FromPyObject)] is not supported for unions" @@ -607,12 +612,11 @@ pub fn build_derive_from_pyobject(tokens: &DeriveInput) -> Result { // FIXME https://github.com/PyO3/pyo3/issues/3903 #[allow(unknown_lints, non_local_definitions)] const _: () = { - use #krate as _pyo3; - use _pyo3::prelude::PyAnyMethods; + use #pyo3_path::prelude::PyAnyMethods; #[automatically_derived] - impl #trait_generics _pyo3::FromPyObject<#lt_param> for #ident #generics #where_clause { - fn extract_bound(obj: &_pyo3::Bound<#lt_param, _pyo3::PyAny>) -> _pyo3::PyResult { + impl #trait_generics #pyo3_path::FromPyObject<#lt_param> for #ident #generics #where_clause { + fn extract_bound(obj: &#pyo3_path::Bound<#lt_param, #pyo3_path::PyAny>) -> #pyo3_path::PyResult { #derives } } diff --git a/pyo3-macros-backend/src/konst.rs b/pyo3-macros-backend/src/konst.rs index 935c9d4a302..9a41a2b7178 100644 --- a/pyo3-macros-backend/src/konst.rs +++ b/pyo3-macros-backend/src/konst.rs @@ -1,5 +1,6 @@ use std::borrow::Cow; +use crate::utils::Ctx; use crate::{ attributes::{self, get_pyo3_options, take_attributes, NameAttribute}, deprecations::Deprecations, @@ -13,12 +14,12 @@ use syn::{ Result, }; -pub struct ConstSpec { +pub struct ConstSpec<'ctx> { pub rust_ident: syn::Ident, - pub attributes: ConstAttributes, + pub attributes: ConstAttributes<'ctx>, } -impl ConstSpec { +impl ConstSpec<'_> { pub fn python_name(&self) -> Cow<'_, Ident> { if let Some(name) = &self.attributes.name { Cow::Borrowed(&name.value.0) @@ -34,10 +35,10 @@ impl ConstSpec { } } -pub struct ConstAttributes { +pub struct ConstAttributes<'ctx> { pub is_class_attr: bool, pub name: Option, - pub deprecations: Deprecations, + pub deprecations: Deprecations<'ctx>, } pub enum PyO3ConstAttribute { @@ -55,12 +56,12 @@ impl Parse for PyO3ConstAttribute { } } -impl ConstAttributes { - pub fn from_attrs(attrs: &mut Vec) -> syn::Result { +impl<'ctx> ConstAttributes<'ctx> { + pub fn from_attrs(attrs: &mut Vec, ctx: &'ctx Ctx) -> syn::Result { let mut attributes = ConstAttributes { is_class_attr: false, name: None, - deprecations: Deprecations::new(), + deprecations: Deprecations::new(ctx), }; take_attributes(attrs, |attr| { diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index a9ba960d513..1d2d22b236b 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -4,6 +4,7 @@ use proc_macro2::{Span, TokenStream}; use quote::{quote, quote_spanned, ToTokens}; use syn::{ext::IdentExt, spanned::Spanned, Ident, Result}; +use crate::utils::Ctx; use crate::{ attributes::{TextSignatureAttribute, TextSignatureAttributeValue}, deprecations::{Deprecation, Deprecations}, @@ -108,13 +109,16 @@ impl FnType { cls: Option<&syn::Type>, error_mode: ExtractErrorMode, holders: &mut Vec, + ctx: &Ctx, ) -> TokenStream { + let Ctx { pyo3_path } = ctx; match self { FnType::Getter(st) | FnType::Setter(st) | FnType::Fn(st) => { let mut receiver = st.receiver( cls.expect("no class given for Fn with a \"self\" receiver"), error_mode, holders, + ctx, ); syn::Token![,](Span::call_site()).to_tokens(&mut receiver); receiver @@ -125,22 +129,24 @@ impl FnType { FnType::FnClass(span) | FnType::FnNewClass(span) => { let py = syn::Ident::new("py", Span::call_site()); let slf: Ident = syn::Ident::new("_slf", Span::call_site()); + let pyo3_path = pyo3_path.to_tokens_spanned(*span); quote_spanned! { *span => #[allow(clippy::useless_conversion)] ::std::convert::Into::into( - _pyo3::impl_::pymethods::BoundRef::ref_from_ptr(#py, &#slf.cast()) - .downcast_unchecked::<_pyo3::types::PyType>() + #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(#py, &#slf.cast()) + .downcast_unchecked::<#pyo3_path::types::PyType>() ), } } FnType::FnModule(span) => { let py = syn::Ident::new("py", Span::call_site()); let slf: Ident = syn::Ident::new("_slf", Span::call_site()); + let pyo3_path = pyo3_path.to_tokens_spanned(*span); quote_spanned! { *span => #[allow(clippy::useless_conversion)] ::std::convert::Into::into( - _pyo3::impl_::pymethods::BoundRef::ref_from_ptr(#py, &#slf.cast()) - .downcast_unchecked::<_pyo3::types::PyModule>() + #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(#py, &#slf.cast()) + .downcast_unchecked::<#pyo3_path::types::PyModule>() ), } } @@ -161,13 +167,14 @@ pub enum ExtractErrorMode { } impl ExtractErrorMode { - pub fn handle_error(self, extract: TokenStream) -> TokenStream { + pub fn handle_error(self, extract: TokenStream, ctx: &Ctx) -> TokenStream { + let Ctx { pyo3_path } = ctx; match self { ExtractErrorMode::Raise => quote! { #extract? }, ExtractErrorMode::NotImplemented => quote! { match #extract { ::std::result::Result::Ok(value) => value, - ::std::result::Result::Err(_) => { return _pyo3::callback::convert(py, py.NotImplemented()); }, + ::std::result::Result::Err(_) => { return #pyo3_path::callback::convert(py, py.NotImplemented()); }, } }, } @@ -180,11 +187,13 @@ impl SelfType { cls: &syn::Type, error_mode: ExtractErrorMode, holders: &mut Vec, + ctx: &Ctx, ) -> TokenStream { // Due to use of quote_spanned in this function, need to bind these idents to the // main macro callsite. let py = syn::Ident::new("py", Span::call_site()); let slf = syn::Ident::new("_slf", Span::call_site()); + let Ctx { pyo3_path } = ctx; match self { SelfType::Receiver { span, mutable } => { let method = if *mutable { @@ -193,29 +202,35 @@ impl SelfType { syn::Ident::new("extract_pyclass_ref", *span) }; let holder = syn::Ident::new(&format!("holder_{}", holders.len()), *span); + let pyo3_path = pyo3_path.to_tokens_spanned(*span); holders.push(quote_spanned! { *span => #[allow(clippy::let_unit_value)] - let mut #holder = _pyo3::impl_::extract_argument::FunctionArgumentHolder::INIT; - let mut #slf = _pyo3::impl_::pymethods::BoundRef::ref_from_ptr(#py, &#slf); + let mut #holder = #pyo3_path::impl_::extract_argument::FunctionArgumentHolder::INIT; + let mut #slf = #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(#py, &#slf); }); - error_mode.handle_error(quote_spanned! { *span => - _pyo3::impl_::extract_argument::#method::<#cls>( - &#slf, - &mut #holder, - ) - }) + error_mode.handle_error( + quote_spanned! { *span => + #pyo3_path::impl_::extract_argument::#method::<#cls>( + &#slf, + &mut #holder, + ) + }, + ctx, + ) } SelfType::TryFromBoundRef(span) => { + let pyo3_path = pyo3_path.to_tokens_spanned(*span); error_mode.handle_error( quote_spanned! { *span => - _pyo3::impl_::pymethods::BoundRef::ref_from_ptr(#py, &#slf).downcast::<#cls>() - .map_err(::std::convert::Into::<_pyo3::PyErr>::into) + #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(#py, &#slf).downcast::<#cls>() + .map_err(::std::convert::Into::<#pyo3_path::PyErr>::into) .and_then( #[allow(unknown_lints, clippy::unnecessary_fallible_conversions)] // In case slf is Py (unknown_lints can be removed when MSRV is 1.75+) |bound| ::std::convert::TryFrom::try_from(bound).map_err(::std::convert::Into::into) ) - } + }, + ctx ) } } @@ -264,7 +279,7 @@ pub struct FnSpec<'a> { pub text_signature: Option, pub asyncness: Option, pub unsafety: Option, - pub deprecations: Deprecations, + pub deprecations: Deprecations<'a>, } pub fn get_return_info(output: &syn::ReturnType) -> syn::Type { @@ -303,6 +318,7 @@ impl<'a> FnSpec<'a> { sig: &'a mut syn::Signature, meth_attrs: &mut Vec, options: PyFunctionOptions, + ctx: &'a Ctx, ) -> Result> { let PyFunctionOptions { text_signature, @@ -312,7 +328,7 @@ impl<'a> FnSpec<'a> { } = options; let mut python_name = name.map(|name| name.value.0); - let mut deprecations = Deprecations::new(); + let mut deprecations = Deprecations::new(ctx); let fn_type = Self::parse_fn_type(sig, meth_attrs, &mut python_name, &mut deprecations)?; ensure_signatures_on_valid_method(&fn_type, signature.as_ref(), text_signature.as_ref())?; @@ -366,7 +382,7 @@ impl<'a> FnSpec<'a> { sig: &syn::Signature, meth_attrs: &mut Vec, python_name: &mut Option, - deprecations: &mut Deprecations, + deprecations: &mut Deprecations<'_>, ) -> Result { let mut method_attributes = parse_method_attributes(meth_attrs, deprecations)?; @@ -480,7 +496,9 @@ impl<'a> FnSpec<'a> { &self, ident: &proc_macro2::Ident, cls: Option<&syn::Type>, + ctx: &Ctx, ) -> Result { + let Ctx { pyo3_path } = ctx; let mut cancel_handle_iter = self .signature .arguments @@ -495,7 +513,7 @@ impl<'a> FnSpec<'a> { } let rust_call = |args: Vec, holders: &mut Vec| { - let self_arg = self.tp.self_arg(cls, ExtractErrorMode::Raise, holders); + let self_arg = self.tp.self_arg(cls, ExtractErrorMode::Raise, holders, ctx); let call = if self.asyncness.is_some() { let throw_callback = if cancel_handle.is_some() { @@ -505,7 +523,7 @@ impl<'a> FnSpec<'a> { }; let python_name = &self.python_name; let qualname_prefix = match cls { - Some(cls) => quote!(Some(<#cls as _pyo3::PyTypeInfo>::NAME)), + Some(cls) => quote!(Some(<#cls as #pyo3_path::PyTypeInfo>::NAME)), None => quote!(None), }; let future = match self.tp { @@ -513,7 +531,7 @@ impl<'a> FnSpec<'a> { holders.pop().unwrap(); // does not actually use holder created by `self_arg` quote! {{ - let __guard = _pyo3::impl_::coroutine::RefGuard::<#cls>::new(&_pyo3::impl_::pymethods::BoundRef::ref_from_ptr(py, &_slf))?; + let __guard = #pyo3_path::impl_::coroutine::RefGuard::<#cls>::new(&#pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(py, &_slf))?; async move { function(&__guard, #(#args),*).await } }} } @@ -521,7 +539,7 @@ impl<'a> FnSpec<'a> { holders.pop().unwrap(); // does not actually use holder created by `self_arg` quote! {{ - let mut __guard = _pyo3::impl_::coroutine::RefMutGuard::<#cls>::new(&_pyo3::impl_::pymethods::BoundRef::ref_from_ptr(py, &_slf))?; + 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, #(#args),*).await } }} } @@ -529,16 +547,16 @@ impl<'a> FnSpec<'a> { }; let mut call = quote! {{ let future = #future; - _pyo3::impl_::coroutine::new_coroutine( - _pyo3::intern!(py, stringify!(#python_name)), + #pyo3_path::impl_::coroutine::new_coroutine( + #pyo3_path::intern!(py, stringify!(#python_name)), #qualname_prefix, #throw_callback, - async move { _pyo3::impl_::wrap::OkWrap::wrap(future.await) }, + async move { #pyo3_path::impl_::wrap::OkWrap::wrap(future.await) }, ) }}; if cancel_handle.is_some() { call = quote! {{ - let __cancel_handle = _pyo3::coroutine::CancelHandle::new(); + let __cancel_handle = #pyo3_path::coroutine::CancelHandle::new(); let __throw_callback = __cancel_handle.throw_callback(); #call }}; @@ -547,7 +565,7 @@ impl<'a> FnSpec<'a> { } else { quote! { function(#self_arg #(#args),*) } }; - quotes::map_result_into_ptr(quotes::ok_wrap(call)) + quotes::map_result_into_ptr(quotes::ok_wrap(call, ctx), ctx) }; let func_name = &self.name; @@ -578,9 +596,9 @@ impl<'a> FnSpec<'a> { quote! { unsafe fn #ident<'py>( - py: _pyo3::Python<'py>, - _slf: *mut _pyo3::ffi::PyObject, - ) -> _pyo3::PyResult<*mut _pyo3::ffi::PyObject> { + py: #pyo3_path::Python<'py>, + _slf: *mut #pyo3_path::ffi::PyObject, + ) -> #pyo3_path::PyResult<*mut #pyo3_path::ffi::PyObject> { let function = #rust_name; // Shadow the function name to avoid #3017 #( #holders )* let result = #call; @@ -590,16 +608,16 @@ impl<'a> FnSpec<'a> { } CallingConvention::Fastcall => { let mut holders = Vec::new(); - let (arg_convert, args) = impl_arg_params(self, cls, true, &mut holders)?; + let (arg_convert, args) = impl_arg_params(self, cls, true, &mut holders, ctx)?; let call = rust_call(args, &mut holders); quote! { unsafe fn #ident<'py>( - py: _pyo3::Python<'py>, - _slf: *mut _pyo3::ffi::PyObject, - _args: *const *mut _pyo3::ffi::PyObject, - _nargs: _pyo3::ffi::Py_ssize_t, - _kwnames: *mut _pyo3::ffi::PyObject - ) -> _pyo3::PyResult<*mut _pyo3::ffi::PyObject> { + py: #pyo3_path::Python<'py>, + _slf: *mut #pyo3_path::ffi::PyObject, + _args: *const *mut #pyo3_path::ffi::PyObject, + _nargs: #pyo3_path::ffi::Py_ssize_t, + _kwnames: *mut #pyo3_path::ffi::PyObject + ) -> #pyo3_path::PyResult<*mut #pyo3_path::ffi::PyObject> { let function = #rust_name; // Shadow the function name to avoid #3017 #arg_convert #( #holders )* @@ -610,15 +628,15 @@ impl<'a> FnSpec<'a> { } CallingConvention::Varargs => { let mut holders = Vec::new(); - let (arg_convert, args) = impl_arg_params(self, cls, false, &mut holders)?; + let (arg_convert, args) = impl_arg_params(self, cls, false, &mut holders, ctx)?; let call = rust_call(args, &mut holders); quote! { unsafe fn #ident<'py>( - py: _pyo3::Python<'py>, - _slf: *mut _pyo3::ffi::PyObject, - _args: *mut _pyo3::ffi::PyObject, - _kwargs: *mut _pyo3::ffi::PyObject - ) -> _pyo3::PyResult<*mut _pyo3::ffi::PyObject> { + py: #pyo3_path::Python<'py>, + _slf: *mut #pyo3_path::ffi::PyObject, + _args: *mut #pyo3_path::ffi::PyObject, + _kwargs: *mut #pyo3_path::ffi::PyObject + ) -> #pyo3_path::PyResult<*mut #pyo3_path::ffi::PyObject> { let function = #rust_name; // Shadow the function name to avoid #3017 #arg_convert #( #holders )* @@ -629,23 +647,25 @@ impl<'a> FnSpec<'a> { } CallingConvention::TpNew => { let mut holders = Vec::new(); - let (arg_convert, args) = impl_arg_params(self, cls, false, &mut holders)?; - let self_arg = self.tp.self_arg(cls, ExtractErrorMode::Raise, &mut holders); + let (arg_convert, args) = impl_arg_params(self, cls, false, &mut holders, ctx)?; + let self_arg = self + .tp + .self_arg(cls, ExtractErrorMode::Raise, &mut holders, ctx); let call = quote! { #rust_name(#self_arg #(#args),*) }; quote! { unsafe fn #ident( - py: _pyo3::Python<'_>, - _slf: *mut _pyo3::ffi::PyTypeObject, - _args: *mut _pyo3::ffi::PyObject, - _kwargs: *mut _pyo3::ffi::PyObject - ) -> _pyo3::PyResult<*mut _pyo3::ffi::PyObject> { - use _pyo3::callback::IntoPyCallbackOutput; + py: #pyo3_path::Python<'_>, + _slf: *mut #pyo3_path::ffi::PyTypeObject, + _args: *mut #pyo3_path::ffi::PyObject, + _kwargs: *mut #pyo3_path::ffi::PyObject + ) -> #pyo3_path::PyResult<*mut #pyo3_path::ffi::PyObject> { + use #pyo3_path::callback::IntoPyCallbackOutput; let function = #rust_name; // Shadow the function name to avoid #3017 #arg_convert #( #holders )* let result = #call; - let initializer: _pyo3::PyClassInitializer::<#cls> = result.convert(py)?; - _pyo3::impl_::pymethods::tp_new_impl(py, initializer, _slf) + let initializer: #pyo3_path::PyClassInitializer::<#cls> = result.convert(py)?; + #pyo3_path::impl_::pymethods::tp_new_impl(py, initializer, _slf) } } } @@ -654,19 +674,20 @@ impl<'a> FnSpec<'a> { /// Return a `PyMethodDef` constructor for this function, matching the selected /// calling convention. - pub fn get_methoddef(&self, wrapper: impl ToTokens, doc: &PythonDoc) -> TokenStream { + pub fn get_methoddef(&self, wrapper: impl ToTokens, doc: &PythonDoc, ctx: &Ctx) -> TokenStream { + let Ctx { pyo3_path } = ctx; let python_name = self.null_terminated_python_name(); match self.convention { CallingConvention::Noargs => quote! { - _pyo3::impl_::pymethods::PyMethodDef::noargs( + #pyo3_path::impl_::pymethods::PyMethodDef::noargs( #python_name, - _pyo3::impl_::pymethods::PyCFunction({ + #pyo3_path::impl_::pymethods::PyCFunction({ unsafe extern "C" fn trampoline( - _slf: *mut _pyo3::ffi::PyObject, - _args: *mut _pyo3::ffi::PyObject, - ) -> *mut _pyo3::ffi::PyObject + _slf: *mut #pyo3_path::ffi::PyObject, + _args: *mut #pyo3_path::ffi::PyObject, + ) -> *mut #pyo3_path::ffi::PyObject { - _pyo3::impl_::trampoline::noargs( + #pyo3_path::impl_::trampoline::noargs( _slf, _args, #wrapper @@ -678,17 +699,17 @@ impl<'a> FnSpec<'a> { ) }, CallingConvention::Fastcall => quote! { - _pyo3::impl_::pymethods::PyMethodDef::fastcall_cfunction_with_keywords( + #pyo3_path::impl_::pymethods::PyMethodDef::fastcall_cfunction_with_keywords( #python_name, - _pyo3::impl_::pymethods::PyCFunctionFastWithKeywords({ + #pyo3_path::impl_::pymethods::PyCFunctionFastWithKeywords({ unsafe extern "C" fn trampoline( - _slf: *mut _pyo3::ffi::PyObject, - _args: *const *mut _pyo3::ffi::PyObject, - _nargs: _pyo3::ffi::Py_ssize_t, - _kwnames: *mut _pyo3::ffi::PyObject - ) -> *mut _pyo3::ffi::PyObject + _slf: *mut #pyo3_path::ffi::PyObject, + _args: *const *mut #pyo3_path::ffi::PyObject, + _nargs: #pyo3_path::ffi::Py_ssize_t, + _kwnames: *mut #pyo3_path::ffi::PyObject + ) -> *mut #pyo3_path::ffi::PyObject { - _pyo3::impl_::trampoline::fastcall_with_keywords( + #pyo3_path::impl_::trampoline::fastcall_with_keywords( _slf, _args, _nargs, @@ -702,16 +723,16 @@ impl<'a> FnSpec<'a> { ) }, CallingConvention::Varargs => quote! { - _pyo3::impl_::pymethods::PyMethodDef::cfunction_with_keywords( + #pyo3_path::impl_::pymethods::PyMethodDef::cfunction_with_keywords( #python_name, - _pyo3::impl_::pymethods::PyCFunctionWithKeywords({ + #pyo3_path::impl_::pymethods::PyCFunctionWithKeywords({ unsafe extern "C" fn trampoline( - _slf: *mut _pyo3::ffi::PyObject, - _args: *mut _pyo3::ffi::PyObject, - _kwargs: *mut _pyo3::ffi::PyObject, - ) -> *mut _pyo3::ffi::PyObject + _slf: *mut #pyo3_path::ffi::PyObject, + _args: *mut #pyo3_path::ffi::PyObject, + _kwargs: *mut #pyo3_path::ffi::PyObject, + ) -> *mut #pyo3_path::ffi::PyObject { - _pyo3::impl_::trampoline::cfunction_with_keywords( + #pyo3_path::impl_::trampoline::cfunction_with_keywords( _slf, _args, _kwargs, @@ -783,7 +804,7 @@ impl MethodTypeAttribute { /// Otherwise will either return a parse error or the attribute. fn parse_if_matching_attribute( attr: &syn::Attribute, - deprecations: &mut Deprecations, + deprecations: &mut Deprecations<'_>, ) -> Result> { fn ensure_no_arguments(meta: &syn::Meta, ident: &str) -> syn::Result<()> { match meta { @@ -869,7 +890,7 @@ impl Display for MethodTypeAttribute { fn parse_method_attributes( attrs: &mut Vec, - deprecations: &mut Deprecations, + deprecations: &mut Deprecations<'_>, ) -> Result> { let mut new_attrs = Vec::new(); let mut found_attrs = Vec::new(); diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index 0bdb1cbc50c..7528ef8102f 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -1,10 +1,10 @@ //! Code generation for the function that initializes a python module and adds classes and function. +use crate::utils::Ctx; use crate::{ attributes::{self, take_attributes, take_pyo3_options, CrateAttribute, NameAttribute}, get_doc, pyfunction::{impl_wrap_pyfunction, PyFunctionOptions}, - utils::get_pyo3_crate, }; use proc_macro2::TokenStream; use quote::quote; @@ -73,7 +73,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 krate = get_pyo3_crate(&options.krate); + let ctx = &Ctx::new(&options.krate); + let Ctx { pyo3_path } = ctx; let doc = get_doc(attrs, None); let mut module_items = Vec::new(); @@ -164,8 +165,8 @@ pub fn pymodule_module_impl(mut module: syn::ItemMod) -> Result { #initialization impl MakeDef { - const fn make_def() -> #krate::impl_::pymodule::ModuleDef { - use #krate::impl_::pymodule as impl_; + 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( @@ -177,8 +178,8 @@ pub fn pymodule_module_impl(mut module: syn::ItemMod) -> Result { } } - fn __pyo3_pymodule(module: &#krate::Bound<'_, #krate::types::PyModule>) -> #krate::PyResult<()> { - use #krate::impl_::pymodule::PyAddToModule; + fn __pyo3_pymodule(module: &#pyo3_path::Bound<'_, #pyo3_path::types::PyModule>) -> #pyo3_path::PyResult<()> { + use #pyo3_path::impl_::pymodule::PyAddToModule; #( #(#module_items_cfg_attrs)* #module_items::add_to_module(module)?; @@ -195,7 +196,8 @@ 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 krate = get_pyo3_crate(&options.krate); + let ctx = &Ctx::new(&options.krate); + let Ctx { pyo3_path } = ctx; let ident = &function.sig.ident; let vis = &function.vis; let doc = get_doc(&function.attrs, None); @@ -222,10 +224,10 @@ pub fn pymodule_function_impl(mut function: syn::ItemFn) -> Result // FIXME https://github.com/PyO3/pyo3/issues/3903 #[allow(unknown_lints, non_local_definitions)] const _: () = { - use #krate::impl_::pymodule as impl_; - use #krate::impl_::pymethods::BoundRef; + use #pyo3_path::impl_::pymodule as impl_; + use #pyo3_path::impl_::pymethods::BoundRef; - fn __pyo3_pymodule(module: &#krate::Bound<'_, #krate::types::PyModule>) -> #krate::PyResult<()> { + fn __pyo3_pymodule(module: &#pyo3_path::Bound<'_, #pyo3_path::types::PyModule>) -> #pyo3_path::PyResult<()> { #ident(#(#module_args),*) } @@ -247,36 +249,37 @@ 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 krate = get_pyo3_crate(&options.krate); + let ctx = &Ctx::new(&options.krate); + let Ctx { pyo3_path } = ctx; let pyinit_symbol = format!("PyInit_{}", name); quote! { pub const __PYO3_NAME: &'static str = concat!(stringify!(#name), "\0"); pub(super) struct MakeDef; - pub static DEF: #krate::impl_::pymodule::ModuleDef = MakeDef::make_def(); + pub static DEF: #pyo3_path::impl_::pymodule::ModuleDef = MakeDef::make_def(); - pub fn add_to_module(module: &#krate::Bound<'_, #krate::types::PyModule>) -> #krate::PyResult<()> { - use #krate::prelude::PyModuleMethods; + pub fn add_to_module(module: &#pyo3_path::Bound<'_, #pyo3_path::types::PyModule>) -> #pyo3_path::PyResult<()> { + use #pyo3_path::prelude::PyModuleMethods; module.add_submodule(DEF.make_module(module.py())?.bind(module.py())) } /// This autogenerated function is called by the python interpreter when importing /// the module. #[export_name = #pyinit_symbol] - pub unsafe extern "C" fn __pyo3_init() -> *mut #krate::ffi::PyObject { - #krate::impl_::trampoline::module_init(|py| DEF.make_module(py)) + pub unsafe extern "C" fn __pyo3_init() -> *mut #pyo3_path::ffi::PyObject { + #pyo3_path::impl_::trampoline::module_init(|py| DEF.make_module(py)) } } } /// Finds and takes care of the #[pyfn(...)] in `#[pymodule]` fn process_functions_in_module(options: &PyModuleOptions, func: &mut syn::ItemFn) -> Result<()> { - let krate = get_pyo3_crate(&options.krate); - + 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 #krate::{PyNativeType, types::PyModuleMethods}; + use #pyo3_path::{PyNativeType, types::PyModuleMethods}; )]; for mut stmt in func.block.stmts.drain(..) { @@ -287,7 +290,7 @@ 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(#krate::wrap_pyfunction!(#name, #module_name.as_borrowed())?)?; + #module_name.as_borrowed().add_function(#pyo3_path::wrap_pyfunction!(#name, #module_name.as_borrowed())?)?; }; stmts.extend(statements); } diff --git a/pyo3-macros-backend/src/params.rs b/pyo3-macros-backend/src/params.rs index 679d3e4260a..7260362fa43 100644 --- a/pyo3-macros-backend/src/params.rs +++ b/pyo3-macros-backend/src/params.rs @@ -1,3 +1,4 @@ +use crate::utils::Ctx; use crate::{ method::{FnArg, FnSpec}, pyfunction::FunctionSignature, @@ -30,8 +31,10 @@ pub fn impl_arg_params( self_: Option<&syn::Type>, fastcall: bool, holders: &mut Vec, + ctx: &Ctx, ) -> Result<(TokenStream, Vec)> { let args_array = syn::Ident::new("output", Span::call_site()); + let Ctx { pyo3_path } = ctx; if !fastcall && is_forwarded_args(&spec.signature) { // In the varargs convention, we can just pass though if the signature @@ -40,12 +43,12 @@ pub fn impl_arg_params( .signature .arguments .iter() - .map(|arg| impl_arg_param(arg, &mut 0, &args_array, holders)) + .map(|arg| impl_arg_param(arg, &mut 0, &args_array, holders, ctx)) .collect::>()?; return Ok(( quote! { - let _args = _pyo3::impl_::pymethods::BoundRef::ref_from_ptr(py, &_args); - let _kwargs = _pyo3::impl_::pymethods::BoundRef::ref_from_ptr_or_opt(py, &_kwargs); + let _args = #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(py, &_args); + let _kwargs = #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr_or_opt(py, &_kwargs); }, arg_convert, )); @@ -64,7 +67,7 @@ pub fn impl_arg_params( .iter() .map(|(name, required)| { quote! { - _pyo3::impl_::extract_argument::KeywordOnlyParameterDescription { + #pyo3_path::impl_::extract_argument::KeywordOnlyParameterDescription { name: #name, required: #required, } @@ -78,22 +81,22 @@ pub fn impl_arg_params( .signature .arguments .iter() - .map(|arg| impl_arg_param(arg, &mut option_pos, &args_array, holders)) + .map(|arg| impl_arg_param(arg, &mut option_pos, &args_array, holders, ctx)) .collect::>()?; let args_handler = if spec.signature.python_signature.varargs.is_some() { - quote! { _pyo3::impl_::extract_argument::TupleVarargs } + quote! { #pyo3_path::impl_::extract_argument::TupleVarargs } } else { - quote! { _pyo3::impl_::extract_argument::NoVarargs } + quote! { #pyo3_path::impl_::extract_argument::NoVarargs } }; let kwargs_handler = if spec.signature.python_signature.kwargs.is_some() { - quote! { _pyo3::impl_::extract_argument::DictVarkeywords } + quote! { #pyo3_path::impl_::extract_argument::DictVarkeywords } } else { - quote! { _pyo3::impl_::extract_argument::NoVarkeywords } + quote! { #pyo3_path::impl_::extract_argument::NoVarkeywords } }; let cls_name = if let Some(cls) = self_ { - quote! { ::std::option::Option::Some(<#cls as _pyo3::type_object::PyTypeInfo>::NAME) } + quote! { ::std::option::Option::Some(<#cls as #pyo3_path::type_object::PyTypeInfo>::NAME) } } else { quote! { ::std::option::Option::None } }; @@ -123,7 +126,7 @@ pub fn impl_arg_params( // create array of arguments, and then parse Ok(( quote! { - const DESCRIPTION: _pyo3::impl_::extract_argument::FunctionDescription = _pyo3::impl_::extract_argument::FunctionDescription { + const DESCRIPTION: #pyo3_path::impl_::extract_argument::FunctionDescription = #pyo3_path::impl_::extract_argument::FunctionDescription { cls_name: #cls_name, func_name: stringify!(#python_name), positional_parameter_names: &[#(#positional_parameter_names),*], @@ -145,7 +148,11 @@ fn impl_arg_param( option_pos: &mut usize, args_array: &syn::Ident, holders: &mut Vec, + ctx: &Ctx, ) -> Result { + 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 // with the function argument macro_rules! quote_arg_span { @@ -167,7 +174,7 @@ fn impl_arg_param( let holder = syn::Ident::new(&format!("holder_{}", holders.len()), arg.ty.span()); holders.push(quote_arg_span! { #[allow(clippy::let_unit_value)] - let mut #holder = _pyo3::impl_::extract_argument::FunctionArgumentHolder::INIT; + let mut #holder = #pyo3_path::impl_::extract_argument::FunctionArgumentHolder::INIT; }); holder }; @@ -179,7 +186,7 @@ fn impl_arg_param( ); let holder = push_holder(); return Ok(quote_arg_span! { - _pyo3::impl_::extract_argument::extract_argument( + #pyo3_path::impl_::extract_argument::extract_argument( &_args, &mut #holder, #name_str @@ -192,7 +199,7 @@ fn impl_arg_param( ); let holder = push_holder(); return Ok(quote_arg_span! { - _pyo3::impl_::extract_argument::extract_optional_argument( + #pyo3_path::impl_::extract_argument::extract_optional_argument( _kwargs.as_deref(), &mut #holder, #name_str, @@ -209,14 +216,17 @@ fn impl_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.optional.is_some() { - default = Some(default.map_or_else(|| quote!(::std::option::Option::None), some_wrap)); + default = Some(default.map_or_else( + || quote!(::std::option::Option::None), + |tokens| some_wrap(tokens, ctx), + )); } let tokens = if let Some(expr_path) = arg.attrs.from_py_with.as_ref().map(|attr| &attr.value) { if let Some(default) = default { quote_arg_span! { #[allow(clippy::redundant_closure)] - _pyo3::impl_::extract_argument::from_py_with_with_default( + #pyo3_path::impl_::extract_argument::from_py_with_with_default( #arg_value.as_deref(), #name_str, #expr_path as fn(_) -> _, @@ -225,8 +235,8 @@ fn impl_arg_param( } } else { quote_arg_span! { - _pyo3::impl_::extract_argument::from_py_with( - &_pyo3::impl_::extract_argument::unwrap_required_argument(#arg_value), + #pyo3_path::impl_::extract_argument::from_py_with( + &#pyo3_path::impl_::extract_argument::unwrap_required_argument(#arg_value), #name_str, #expr_path as fn(_) -> _, )? @@ -236,7 +246,7 @@ fn impl_arg_param( let holder = push_holder(); quote_arg_span! { #[allow(clippy::redundant_closure)] - _pyo3::impl_::extract_argument::extract_optional_argument( + #pyo3_path::impl_::extract_argument::extract_optional_argument( #arg_value.as_deref(), &mut #holder, #name_str, @@ -247,7 +257,7 @@ fn impl_arg_param( let holder = push_holder(); quote_arg_span! { #[allow(clippy::redundant_closure)] - _pyo3::impl_::extract_argument::extract_argument_with_default( + #pyo3_path::impl_::extract_argument::extract_argument_with_default( #arg_value.as_deref(), &mut #holder, #name_str, @@ -257,8 +267,8 @@ fn impl_arg_param( } else { let holder = push_holder(); quote_arg_span! { - _pyo3::impl_::extract_argument::extract_argument( - &_pyo3::impl_::extract_argument::unwrap_required_argument(#arg_value), + #pyo3_path::impl_::extract_argument::extract_argument( + &#pyo3_path::impl_::extract_argument::unwrap_required_argument(#arg_value), &mut #holder, #name_str )? diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 734b9565a66..9470045733c 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -13,7 +13,8 @@ use crate::pymethod::{ impl_py_getter_def, impl_py_setter_def, MethodAndMethodDef, MethodAndSlotDef, PropertyType, SlotDef, __INT__, __REPR__, __RICHCMP__, }; -use crate::utils::{self, apply_renaming_rule, get_pyo3_crate, PythonDoc}; +use crate::utils::Ctx; +use crate::utils::{self, apply_renaming_rule, PythonDoc}; use crate::PyFunctionOptions; use proc_macro2::{Ident, Span, TokenStream}; use quote::{format_ident, quote}; @@ -189,7 +190,8 @@ pub fn build_py_class( ) -> syn::Result { args.options.take_pyo3_options(&mut class.attrs)?; let doc = utils::get_doc(&class.attrs, None); - let krate = get_pyo3_crate(&args.options.krate); + + let ctx = &Ctx::new(&args.options.krate); if let Some(lt) = class.generics.lifetimes().next() { bail_spanned!( @@ -251,7 +253,7 @@ pub fn build_py_class( } } - impl_class(&class.ident, &args, doc, field_options, methods_type, krate) + impl_class(&class.ident, &args, doc, field_options, methods_type, ctx) } enum Annotated { @@ -342,9 +344,10 @@ fn impl_class( doc: PythonDoc, field_options: Vec<(&syn::Field, FieldPyO3Options)>, methods_type: PyClassMethodsType, - krate: syn::Path, + ctx: &Ctx, ) -> syn::Result { - let pytypeinfo_impl = impl_pytypeinfo(cls, args, None); + let Ctx { pyo3_path } = ctx; + let pytypeinfo_impl = impl_pytypeinfo(cls, args, None, ctx); let py_class_impl = PyClassImplsBuilder::new( cls, @@ -355,19 +358,18 @@ fn impl_class( args.options.rename_all.as_ref(), args.options.frozen, field_options, + ctx, )?, vec![], ) .doc(doc) - .impl_all()?; + .impl_all(ctx)?; Ok(quote! { // FIXME https://github.com/PyO3/pyo3/issues/3903 #[allow(unknown_lints, non_local_definitions)] const _: () = { - use #krate as _pyo3; - - impl _pyo3::types::DerefToPyAny for #cls {} + impl #pyo3_path::types::DerefToPyAny for #cls {} #pytypeinfo_impl @@ -405,6 +407,7 @@ pub fn build_py_enum( ) -> syn::Result { args.options.take_pyo3_options(&mut enum_.attrs)?; + let ctx = &Ctx::new(&args.options.krate); if let Some(extends) = &args.options.extends { bail_spanned!(extends.span() => "enums can't extend from other classes"); } else if let Some(subclass) = &args.options.subclass { @@ -415,7 +418,7 @@ pub fn build_py_enum( let doc = utils::get_doc(&enum_.attrs, None); let enum_ = PyClassEnum::new(enum_)?; - impl_enum(enum_, &args, doc, method_type) + impl_enum(enum_, &args, doc, method_type, ctx) } struct PyClassSimpleEnum<'a> { @@ -665,11 +668,14 @@ fn impl_enum( args: &PyClassArgs, doc: PythonDoc, methods_type: PyClassMethodsType, + ctx: &Ctx, ) -> Result { match enum_ { - PyClassEnum::Simple(simple_enum) => impl_simple_enum(simple_enum, args, doc, methods_type), + PyClassEnum::Simple(simple_enum) => { + impl_simple_enum(simple_enum, args, doc, methods_type, ctx) + } PyClassEnum::Complex(complex_enum) => { - impl_complex_enum(complex_enum, args, doc, methods_type) + impl_complex_enum(complex_enum, args, doc, methods_type, ctx) } } } @@ -679,12 +685,13 @@ fn impl_simple_enum( args: &PyClassArgs, doc: PythonDoc, methods_type: PyClassMethodsType, + ctx: &Ctx, ) -> Result { - let krate = get_pyo3_crate(&args.options.krate); + let Ctx { pyo3_path } = ctx; 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); + let pytypeinfo = impl_pytypeinfo(cls, args, None, ctx); let (default_repr, default_repr_slot) = { let variants_repr = variants.iter().map(|variant| { @@ -704,7 +711,8 @@ fn impl_simple_enum( } } }; - let repr_slot = generate_default_protocol_slot(&ty, &mut repr_impl, &__REPR__).unwrap(); + let repr_slot = + generate_default_protocol_slot(&ty, &mut repr_impl, &__REPR__, ctx).unwrap(); (repr_impl, repr_slot) }; @@ -723,7 +731,7 @@ fn impl_simple_enum( } } }; - let int_slot = generate_default_protocol_slot(&ty, &mut int_impl, &__INT__).unwrap(); + let int_slot = generate_default_protocol_slot(&ty, &mut int_impl, &__INT__, ctx).unwrap(); (int_impl, int_slot) }; @@ -731,30 +739,30 @@ fn impl_simple_enum( let mut richcmp_impl: syn::ImplItemFn = syn::parse_quote! { fn __pyo3__richcmp__( &self, - py: _pyo3::Python, - other: &_pyo3::PyAny, - op: _pyo3::basic::CompareOp - ) -> _pyo3::PyResult<_pyo3::PyObject> { - use _pyo3::conversion::ToPyObject; + py: #pyo3_path::Python, + other: &#pyo3_path::PyAny, + op: #pyo3_path::basic::CompareOp + ) -> #pyo3_path::PyResult<#pyo3_path::PyObject> { + use #pyo3_path::conversion::ToPyObject; use ::core::result::Result::*; match op { - _pyo3::basic::CompareOp::Eq => { + #pyo3_path::basic::CompareOp::Eq => { let self_val = self.__pyo3__int__(); if let Ok(i) = other.extract::<#repr_type>() { return Ok((self_val == i).to_object(py)); } - if let Ok(other) = other.extract::<_pyo3::PyRef>() { + if let Ok(other) = other.extract::<#pyo3_path::PyRef>() { return Ok((self_val == other.__pyo3__int__()).to_object(py)); } return Ok(py.NotImplemented()); } - _pyo3::basic::CompareOp::Ne => { + #pyo3_path::basic::CompareOp::Ne => { let self_val = self.__pyo3__int__(); if let Ok(i) = other.extract::<#repr_type>() { return Ok((self_val != i).to_object(py)); } - if let Ok(other) = other.extract::<_pyo3::PyRef>() { + if let Ok(other) = other.extract::<#pyo3_path::PyRef>() { return Ok((self_val != other.__pyo3__int__()).to_object(py)); } @@ -765,7 +773,7 @@ fn impl_simple_enum( } }; let richcmp_slot = - generate_default_protocol_slot(&ty, &mut richcmp_impl, &__RICHCMP__).unwrap(); + generate_default_protocol_slot(&ty, &mut richcmp_impl, &__RICHCMP__, ctx).unwrap(); (richcmp_impl, richcmp_slot) }; @@ -778,18 +786,17 @@ fn impl_simple_enum( simple_enum_default_methods( cls, variants.iter().map(|v| (v.ident, v.get_python_name(args))), + ctx, ), default_slots, ) .doc(doc) - .impl_all()?; + .impl_all(ctx)?; Ok(quote! { // FIXME https://github.com/PyO3/pyo3/issues/3903 #[allow(unknown_lints, non_local_definitions)] const _: () = { - use #krate as _pyo3; - #pytypeinfo #pyclass_impls @@ -810,7 +817,10 @@ fn impl_complex_enum( args: &PyClassArgs, doc: PythonDoc, methods_type: PyClassMethodsType, + ctx: &Ctx, ) -> Result { + let Ctx { pyo3_path } = ctx; + // Need to rig the enum PyClass options let args = { let mut rigged_args = args.clone(); @@ -821,10 +831,10 @@ fn impl_complex_enum( rigged_args }; - let krate = get_pyo3_crate(&args.options.krate); + let ctx = &Ctx::new(&args.options.krate); let cls = complex_enum.ident; let variants = complex_enum.variants; - let pytypeinfo = impl_pytypeinfo(cls, &args, None); + let pytypeinfo = impl_pytypeinfo(cls, &args, None, ctx); let default_slots = vec![]; @@ -837,6 +847,7 @@ fn impl_complex_enum( variants .iter() .map(|v| (v.get_ident(), v.get_python_name(&args))), + ctx, ), default_slots, ) @@ -851,17 +862,17 @@ 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::PyClassInitializer::from(self).add_subclass(#variant_cls); - let variant_value = _pyo3::Py::new(py, pyclass_init).unwrap(); - _pyo3::IntoPy::into_py(variant_value, py) + let pyclass_init = #pyo3_path::PyClassInitializer::from(self).add_subclass(#variant_cls); + let variant_value = #pyo3_path::Py::new(py, pyclass_init).unwrap(); + #pyo3_path::IntoPy::into_py(variant_value, py) } } }) .collect(); quote! { - impl _pyo3::IntoPy<_pyo3::PyObject> for #cls { - fn into_py(self, py: _pyo3::Python) -> _pyo3::PyObject { + impl #pyo3_path::IntoPy<#pyo3_path::PyObject> for #cls { + fn into_py(self, py: #pyo3_path::Python) -> #pyo3_path::PyObject { match self { #(#match_arms)* } @@ -871,11 +882,11 @@ fn impl_complex_enum( }; let pyclass_impls: TokenStream = vec![ - impl_builder.impl_pyclass(), - impl_builder.impl_extractext(), + impl_builder.impl_pyclass(ctx), + impl_builder.impl_extractext(ctx), enum_into_py_impl, - impl_builder.impl_pyclassimpl()?, - impl_builder.impl_freelist(), + impl_builder.impl_pyclassimpl(ctx)?, + impl_builder.impl_freelist(ctx), ] .into_iter() .collect(); @@ -900,12 +911,12 @@ fn impl_complex_enum( options: parse_quote!(extends = #cls, frozen), }; - let variant_cls_pytypeinfo = impl_pytypeinfo(&variant_cls, &variant_args, None); + 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)?; + let variant_new = complex_enum_variant_new(cls, variant, ctx)?; - let (variant_cls_impl, field_getters) = impl_complex_enum_variant_cls(cls, variant)?; + let (variant_cls_impl, field_getters) = impl_complex_enum_variant_cls(cls, variant, ctx)?; variant_cls_impls.push(variant_cls_impl); let pyclass_impl = PyClassImplsBuilder::new( @@ -915,7 +926,7 @@ fn impl_complex_enum( field_getters, vec![variant_new], ) - .impl_all()?; + .impl_all(ctx)?; variant_cls_pyclass_impls.push(pyclass_impl); } @@ -924,8 +935,6 @@ fn impl_complex_enum( // FIXME https://github.com/PyO3/pyo3/issues/3903 #[allow(unknown_lints, non_local_definitions)] const _: () = { - use #krate as _pyo3; - #pytypeinfo #pyclass_impls @@ -948,10 +957,11 @@ fn impl_complex_enum( fn impl_complex_enum_variant_cls( enum_name: &syn::Ident, variant: &PyClassEnumVariant<'_>, + ctx: &Ctx, ) -> Result<(TokenStream, Vec)> { match variant { PyClassEnumVariant::Struct(struct_variant) => { - impl_complex_enum_struct_variant_cls(enum_name, struct_variant) + impl_complex_enum_struct_variant_cls(enum_name, struct_variant, ctx) } } } @@ -959,7 +969,9 @@ fn impl_complex_enum_variant_cls( fn impl_complex_enum_struct_variant_cls( enum_name: &syn::Ident, variant: &PyClassEnumStructVariant<'_>, + ctx: &Ctx, ) -> Result<(TokenStream, 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); @@ -978,10 +990,11 @@ fn impl_complex_enum_struct_variant_cls( field_name, field_type, field.span, + ctx, )?; let field_getter_impl = quote! { - fn #field_name(slf: _pyo3::PyRef) -> _pyo3::PyResult<#field_type> { + 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"), @@ -999,9 +1012,9 @@ fn impl_complex_enum_struct_variant_cls( #[doc(hidden)] #[allow(non_snake_case)] impl #variant_cls { - fn __pymethod_constructor__(py: _pyo3::Python<'_>, #(#fields_with_types,)*) -> _pyo3::PyClassInitializer<#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::PyClassInitializer::from(base_value).add_subclass(#variant_cls) + #pyo3_path::PyClassInitializer::from(base_value).add_subclass(#variant_cls) } #(#field_getter_impls)* @@ -1019,11 +1032,13 @@ fn generate_default_protocol_slot( cls: &syn::Type, method: &mut syn::ImplItemFn, slot: &SlotDef, + ctx: &Ctx, ) -> syn::Result { let spec = FnSpec::parse( &mut method.sig, &mut Vec::new(), PyFunctionOptions::default(), + ctx, ) .unwrap(); let name = spec.name.to_string(); @@ -1031,12 +1046,14 @@ fn generate_default_protocol_slot( &syn::parse_quote!(#cls), &spec, &format!("__default_{}__", name), + ctx, ) } fn simple_enum_default_methods<'a>( cls: &'a syn::Ident, unit_variant_names: impl IntoIterator)>, + ctx: &Ctx, ) -> Vec { let cls_type = syn::parse_quote!(#cls); let variant_to_attribute = |var_ident: &syn::Ident, py_ident: &syn::Ident| ConstSpec { @@ -1047,18 +1064,19 @@ fn simple_enum_default_methods<'a>( kw: syn::parse_quote! { name }, value: NameLitStr(py_ident.clone()), }), - deprecations: Default::default(), + deprecations: Deprecations::new(ctx), }, }; unit_variant_names .into_iter() - .map(|(var, py_name)| gen_py_const(&cls_type, &variant_to_attribute(var, &py_name))) + .map(|(var, py_name)| gen_py_const(&cls_type, &variant_to_attribute(var, &py_name), ctx)) .collect() } fn complex_enum_default_methods<'a>( cls: &'a syn::Ident, variant_names: impl IntoIterator)>, + ctx: &Ctx, ) -> Vec { let cls_type = syn::parse_quote!(#cls); let variant_to_attribute = |var_ident: &syn::Ident, py_ident: &syn::Ident| ConstSpec { @@ -1069,13 +1087,13 @@ fn complex_enum_default_methods<'a>( kw: syn::parse_quote! { name }, value: NameLitStr(py_ident.clone()), }), - deprecations: Default::default(), + deprecations: Deprecations::new(ctx), }, }; variant_names .into_iter() .map(|(var, py_name)| { - gen_complex_enum_variant_attr(cls, &cls_type, &variant_to_attribute(var, &py_name)) + gen_complex_enum_variant_attr(cls, &cls_type, &variant_to_attribute(var, &py_name), ctx) }) .collect() } @@ -1083,8 +1101,10 @@ 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; @@ -1092,17 +1112,17 @@ pub fn gen_complex_enum_variant_attr( let variant_cls = format_ident!("{}_{}", cls, member); let associated_method = quote! { - fn #wrapper_ident(py: _pyo3::Python<'_>) -> _pyo3::PyResult<_pyo3::PyObject> { + fn #wrapper_ident(py: #pyo3_path::Python<'_>) -> #pyo3_path::PyResult<#pyo3_path::PyObject> { #deprecations ::std::result::Result::Ok(py.get_type_bound::<#variant_cls>().into_any().unbind()) } }; let method_def = quote! { - _pyo3::class::PyMethodDefType::ClassAttribute({ - _pyo3::class::PyClassAttributeDef::new( + #pyo3_path::class::PyMethodDefType::ClassAttribute({ + #pyo3_path::class::PyClassAttributeDef::new( #python_name, - _pyo3::impl_::pymethods::PyClassAttributeFactory(#cls_type::#wrapper_ident) + #pyo3_path::impl_::pymethods::PyClassAttributeFactory(#cls_type::#wrapper_ident) ) }) }; @@ -1116,10 +1136,11 @@ pub fn gen_complex_enum_variant_attr( fn complex_enum_variant_new<'a>( cls: &'a syn::Ident, variant: &'a PyClassEnumVariant<'a>, + ctx: &Ctx, ) -> Result { match variant { PyClassEnumVariant::Struct(struct_variant) => { - complex_enum_struct_variant_new(cls, struct_variant) + complex_enum_struct_variant_new(cls, struct_variant, ctx) } } } @@ -1127,12 +1148,14 @@ fn complex_enum_variant_new<'a>( fn complex_enum_struct_variant_new<'a>( cls: &'a syn::Ident, variant: &'a PyClassEnumStructVariant<'a>, + ctx: &Ctx, ) -> Result { + let Ctx { pyo3_path } = ctx; let variant_cls = format_ident!("{}_{}", cls, variant.ident); let variant_cls_type: syn::Type = parse_quote!(#variant_cls); let arg_py_ident: syn::Ident = parse_quote!(py); - let arg_py_type: syn::Type = parse_quote!(_pyo3::Python<'_>); + let arg_py_type: syn::Type = parse_quote!(#pyo3_path::Python<'_>); let args = { let mut no_pyo3_attrs = vec![]; @@ -1180,10 +1203,10 @@ fn complex_enum_struct_variant_new<'a>( text_signature: None, asyncness: None, unsafety: None, - deprecations: Deprecations::default(), + deprecations: Deprecations::new(ctx), }; - crate::pymethod::impl_py_method_def_new(&variant_cls_type, &spec) + crate::pymethod::impl_py_method_def_new(&variant_cls_type, &spec, ctx) } fn complex_enum_variant_field_getter<'a>( @@ -1191,6 +1214,7 @@ fn complex_enum_variant_field_getter<'a>( field_name: &'a syn::Ident, field_type: &'a syn::Type, field_span: Span, + ctx: &Ctx, ) -> Result { let signature = crate::pyfunction::FunctionSignature::from_arguments(vec![])?; @@ -1206,7 +1230,7 @@ fn complex_enum_variant_field_getter<'a>( text_signature: None, asyncness: None, unsafety: None, - deprecations: Deprecations::default(), + deprecations: Deprecations::new(ctx), }; let property_type = crate::pymethod::PropertyType::Function { @@ -1215,7 +1239,7 @@ fn complex_enum_variant_field_getter<'a>( doc: crate::get_doc(&[], None), }; - let getter = crate::pymethod::impl_py_getter_def(variant_cls_type, property_type)?; + let getter = crate::pymethod::impl_py_getter_def(variant_cls_type, property_type, ctx)?; Ok(getter) } @@ -1224,6 +1248,7 @@ fn descriptors_to_items( rename_all: Option<&RenameAllAttribute>, frozen: Option, field_options: Vec<(&syn::Field, FieldPyO3Options)>, + ctx: &Ctx, ) -> syn::Result> { let ty = syn::parse_quote!(#cls); let mut items = Vec::new(); @@ -1246,6 +1271,7 @@ fn descriptors_to_items( python_name: options.name.as_ref(), renaming_rule: rename_all.map(|rename_all| rename_all.value.rule), }, + ctx, )?; items.push(getter); } @@ -1260,6 +1286,7 @@ fn descriptors_to_items( python_name: options.name.as_ref(), renaming_rule: rename_all.map(|rename_all| rename_all.value.rule), }, + ctx, )?; items.push(setter); }; @@ -1270,8 +1297,10 @@ fn descriptors_to_items( fn impl_pytypeinfo( cls: &syn::Ident, attr: &PyClassArgs, - deprecations: Option<&Deprecations>, + deprecations: Option<&Deprecations<'_>>, + ctx: &Ctx, ) -> TokenStream { + let Ctx { pyo3_path } = ctx; let cls_name = get_class_python_name(cls, attr).to_string(); let module = if let Some(ModuleAttribute { value, .. }) = &attr.options.module { @@ -1282,20 +1311,20 @@ fn impl_pytypeinfo( quote! { #[allow(deprecated)] - unsafe impl _pyo3::type_object::HasPyGilRef for #cls { - type AsRefTarget = _pyo3::PyCell; + unsafe impl #pyo3_path::type_object::HasPyGilRef for #cls { + type AsRefTarget = #pyo3_path::PyCell; } - unsafe impl _pyo3::type_object::PyTypeInfo for #cls { + unsafe impl #pyo3_path::type_object::PyTypeInfo for #cls { const NAME: &'static str = #cls_name; const MODULE: ::std::option::Option<&'static str> = #module; #[inline] - fn type_object_raw(py: _pyo3::Python<'_>) -> *mut _pyo3::ffi::PyTypeObject { - use _pyo3::prelude::PyTypeMethods; + fn type_object_raw(py: #pyo3_path::Python<'_>) -> *mut #pyo3_path::ffi::PyTypeObject { + use #pyo3_path::prelude::PyTypeMethods; #deprecations - <#cls as _pyo3::impl_::pyclass::PyClassImpl>::lazy_type_object() + <#cls as #pyo3_path::impl_::pyclass::PyClassImpl>::lazy_type_object() .get_or_init(py) .as_type_ptr() } @@ -1342,82 +1371,85 @@ impl<'a> PyClassImplsBuilder<'a> { } } - fn impl_all(&self) -> Result { + fn impl_all(&self, ctx: &Ctx) -> Result { let tokens = vec![ - self.impl_pyclass(), - self.impl_extractext(), - self.impl_into_py(), - self.impl_pyclassimpl()?, - self.impl_freelist(), + self.impl_pyclass(ctx), + self.impl_extractext(ctx), + self.impl_into_py(ctx), + self.impl_pyclassimpl(ctx)?, + self.impl_freelist(ctx), ] .into_iter() .collect(); Ok(tokens) } - fn impl_pyclass(&self) -> TokenStream { + fn impl_pyclass(&self, ctx: &Ctx) -> TokenStream { + let Ctx { pyo3_path } = ctx; let cls = self.cls; let frozen = if self.attr.options.frozen.is_some() { - quote! { _pyo3::pyclass::boolean_struct::True } + quote! { #pyo3_path::pyclass::boolean_struct::True } } else { - quote! { _pyo3::pyclass::boolean_struct::False } + quote! { #pyo3_path::pyclass::boolean_struct::False } }; quote! { - impl _pyo3::PyClass for #cls { + impl #pyo3_path::PyClass for #cls { type Frozen = #frozen; } } } - fn impl_extractext(&self) -> TokenStream { + fn impl_extractext(&self, ctx: &Ctx) -> TokenStream { + let Ctx { pyo3_path } = ctx; let cls = self.cls; if self.attr.options.frozen.is_some() { quote! { - impl<'a, 'py> _pyo3::impl_::extract_argument::PyFunctionArgument<'a, 'py> for &'a #cls + impl<'a, 'py> #pyo3_path::impl_::extract_argument::PyFunctionArgument<'a, 'py> for &'a #cls { - type Holder = ::std::option::Option<_pyo3::PyRef<'py, #cls>>; + type Holder = ::std::option::Option<#pyo3_path::PyRef<'py, #cls>>; #[inline] - fn extract(obj: &'a _pyo3::Bound<'py, _pyo3::PyAny>, holder: &'a mut Self::Holder) -> _pyo3::PyResult { - _pyo3::impl_::extract_argument::extract_pyclass_ref(obj, holder) + fn extract(obj: &'a #pyo3_path::Bound<'py, #pyo3_path::PyAny>, holder: &'a mut Self::Holder) -> #pyo3_path::PyResult { + #pyo3_path::impl_::extract_argument::extract_pyclass_ref(obj, holder) } } } } else { quote! { - impl<'a, 'py> _pyo3::impl_::extract_argument::PyFunctionArgument<'a, 'py> for &'a #cls + impl<'a, 'py> #pyo3_path::impl_::extract_argument::PyFunctionArgument<'a, 'py> for &'a #cls { - type Holder = ::std::option::Option<_pyo3::PyRef<'py, #cls>>; + type Holder = ::std::option::Option<#pyo3_path::PyRef<'py, #cls>>; #[inline] - fn extract(obj: &'a _pyo3::Bound<'py, _pyo3::PyAny>, holder: &'a mut Self::Holder) -> _pyo3::PyResult { - _pyo3::impl_::extract_argument::extract_pyclass_ref(obj, holder) + fn extract(obj: &'a #pyo3_path::Bound<'py, #pyo3_path::PyAny>, holder: &'a mut Self::Holder) -> #pyo3_path::PyResult { + #pyo3_path::impl_::extract_argument::extract_pyclass_ref(obj, holder) } } - impl<'a, 'py> _pyo3::impl_::extract_argument::PyFunctionArgument<'a, 'py> for &'a mut #cls + impl<'a, 'py> #pyo3_path::impl_::extract_argument::PyFunctionArgument<'a, 'py> for &'a mut #cls { - type Holder = ::std::option::Option<_pyo3::PyRefMut<'py, #cls>>; + type Holder = ::std::option::Option<#pyo3_path::PyRefMut<'py, #cls>>; #[inline] - fn extract(obj: &'a _pyo3::Bound<'py, _pyo3::PyAny>, holder: &'a mut Self::Holder) -> _pyo3::PyResult { - _pyo3::impl_::extract_argument::extract_pyclass_ref_mut(obj, holder) + fn extract(obj: &'a #pyo3_path::Bound<'py, #pyo3_path::PyAny>, holder: &'a mut Self::Holder) -> #pyo3_path::PyResult { + #pyo3_path::impl_::extract_argument::extract_pyclass_ref_mut(obj, holder) } } } } } - fn impl_into_py(&self) -> TokenStream { + fn impl_into_py(&self, ctx: &Ctx) -> TokenStream { + let Ctx { pyo3_path } = ctx; let cls = self.cls; let attr = self.attr; // If #cls is not extended type, we allow Self->PyObject conversion if attr.options.extends.is_none() { quote! { - impl _pyo3::IntoPy<_pyo3::PyObject> for #cls { - fn into_py(self, py: _pyo3::Python) -> _pyo3::PyObject { - _pyo3::IntoPy::into_py(_pyo3::Py::new(py, self).unwrap(), py) + impl #pyo3_path::IntoPy<#pyo3_path::PyObject> for #cls { + fn into_py(self, py: #pyo3_path::Python) -> #pyo3_path::PyObject { + #pyo3_path::IntoPy::into_py(#pyo3_path::Py::new(py, self).unwrap(), py) } } } @@ -1425,13 +1457,14 @@ impl<'a> PyClassImplsBuilder<'a> { quote! {} } } - fn impl_pyclassimpl(&self) -> Result { + fn impl_pyclassimpl(&self, ctx: &Ctx) -> Result { + let Ctx { pyo3_path } = ctx; let cls = self.cls; let doc = self.doc.as_ref().map_or(quote! {"\0"}, |doc| quote! {#doc}); let is_basetype = self.attr.options.subclass.is_some(); let base = match &self.attr.options.extends { Some(extends_attr) => extends_attr.value.clone(), - None => parse_quote! { _pyo3::PyAny }, + None => parse_quote! { #pyo3_path::PyAny }, }; let is_subclass = self.attr.options.extends.is_some(); let is_mapping: bool = self.attr.options.mapping.is_some(); @@ -1444,8 +1477,8 @@ impl<'a> PyClassImplsBuilder<'a> { let dict_offset = if self.attr.options.dict.is_some() { quote! { - fn dict_offset() -> ::std::option::Option<_pyo3::ffi::Py_ssize_t> { - ::std::option::Option::Some(_pyo3::impl_::pyclass::dict_offset::()) + fn dict_offset() -> ::std::option::Option<#pyo3_path::ffi::Py_ssize_t> { + ::std::option::Option::Some(#pyo3_path::impl_::pyclass::dict_offset::()) } } } else { @@ -1455,8 +1488,8 @@ impl<'a> PyClassImplsBuilder<'a> { // insert space for weak ref let weaklist_offset = if self.attr.options.weakref.is_some() { quote! { - fn weaklist_offset() -> ::std::option::Option<_pyo3::ffi::Py_ssize_t> { - ::std::option::Option::Some(_pyo3::impl_::pyclass::weaklist_offset::()) + fn weaklist_offset() -> ::std::option::Option<#pyo3_path::ffi::Py_ssize_t> { + ::std::option::Option::Some(#pyo3_path::impl_::pyclass::weaklist_offset::()) } } } else { @@ -1464,9 +1497,9 @@ impl<'a> PyClassImplsBuilder<'a> { }; let thread_checker = if self.attr.options.unsendable.is_some() { - quote! { _pyo3::impl_::pyclass::ThreadCheckerImpl } + quote! { #pyo3_path::impl_::pyclass::ThreadCheckerImpl } } else { - quote! { _pyo3::impl_::pyclass::SendablePyClass<#cls> } + quote! { #pyo3_path::impl_::pyclass::SendablePyClass<#cls> } }; let (pymethods_items, inventory, inventory_class) = match self.methods_type { @@ -1481,13 +1514,13 @@ impl<'a> PyClassImplsBuilder<'a> { quote! { ::std::boxed::Box::new( ::std::iter::Iterator::map( - _pyo3::inventory::iter::<::Inventory>(), - _pyo3::impl_::pyclass::PyClassInventory::items + #pyo3_path::inventory::iter::<::Inventory>(), + #pyo3_path::impl_::pyclass::PyClassInventory::items ) ) }, Some(quote! { type Inventory = #inventory_class_name; }), - Some(define_inventory_class(&inventory_class_name)), + Some(define_inventory_class(&inventory_class_name, ctx)), ) } }; @@ -1504,7 +1537,7 @@ impl<'a> PyClassImplsBuilder<'a> { let default_method_defs = self.default_methods.iter().map(|meth| &meth.method_def); let default_slot_defs = self.default_slots.iter().map(|slot| &slot.slot_def); - let freelist_slots = self.freelist_slots(); + let freelist_slots = self.freelist_slots(ctx); let class_mutability = if self.attr.options.frozen.is_some() { quote! { @@ -1519,26 +1552,26 @@ impl<'a> PyClassImplsBuilder<'a> { let cls = self.cls; let attr = self.attr; let dict = if attr.options.dict.is_some() { - quote! { _pyo3::impl_::pyclass::PyClassDictSlot } + quote! { #pyo3_path::impl_::pyclass::PyClassDictSlot } } else { - quote! { _pyo3::impl_::pyclass::PyClassDummySlot } + quote! { #pyo3_path::impl_::pyclass::PyClassDummySlot } }; // insert space for weak ref let weakref = if attr.options.weakref.is_some() { - quote! { _pyo3::impl_::pyclass::PyClassWeakRefSlot } + quote! { #pyo3_path::impl_::pyclass::PyClassWeakRefSlot } } else { - quote! { _pyo3::impl_::pyclass::PyClassDummySlot } + quote! { #pyo3_path::impl_::pyclass::PyClassDummySlot } }; let base_nativetype = if attr.options.extends.is_some() { - quote! { ::BaseNativeType } + quote! { ::BaseNativeType } } else { - quote! { _pyo3::PyAny } + quote! { #pyo3_path::PyAny } }; Ok(quote! { - impl _pyo3::impl_::pyclass::PyClassImpl for #cls { + impl #pyo3_path::impl_::pyclass::PyClassImpl for #cls { const IS_BASETYPE: bool = #is_basetype; const IS_SUBCLASS: bool = #is_subclass; const IS_MAPPING: bool = #is_mapping; @@ -1547,13 +1580,13 @@ impl<'a> PyClassImplsBuilder<'a> { type BaseType = #base; type ThreadChecker = #thread_checker; #inventory - type PyClassMutability = <<#base as _pyo3::impl_::pyclass::PyClassBaseType>::PyClassMutability as _pyo3::impl_::pycell::PyClassMutability>::#class_mutability; + type PyClassMutability = <<#base as #pyo3_path::impl_::pyclass::PyClassBaseType>::PyClassMutability as #pyo3_path::impl_::pycell::PyClassMutability>::#class_mutability; type Dict = #dict; type WeakRef = #weakref; type BaseNativeType = #base_nativetype; - fn items_iter() -> _pyo3::impl_::pyclass::PyClassItemsIter { - use _pyo3::impl_::pyclass::*; + fn items_iter() -> #pyo3_path::impl_::pyclass::PyClassItemsIter { + use #pyo3_path::impl_::pyclass::*; let collector = PyClassImplCollector::::new(); static INTRINSIC_ITEMS: PyClassItems = PyClassItems { methods: &[#(#default_method_defs),*], @@ -1562,12 +1595,12 @@ impl<'a> PyClassImplsBuilder<'a> { PyClassItemsIter::new(&INTRINSIC_ITEMS, #pymethods_items) } - fn doc(py: _pyo3::Python<'_>) -> _pyo3::PyResult<&'static ::std::ffi::CStr> { - use _pyo3::impl_::pyclass::*; - static DOC: _pyo3::sync::GILOnceCell<::std::borrow::Cow<'static, ::std::ffi::CStr>> = _pyo3::sync::GILOnceCell::new(); + fn doc(py: #pyo3_path::Python<'_>) -> #pyo3_path::PyResult<&'static ::std::ffi::CStr> { + use #pyo3_path::impl_::pyclass::*; + static DOC: #pyo3_path::sync::GILOnceCell<::std::borrow::Cow<'static, ::std::ffi::CStr>> = #pyo3_path::sync::GILOnceCell::new(); DOC.get_or_try_init(py, || { let collector = PyClassImplCollector::::new(); - build_pyclass_doc(<#cls as _pyo3::PyTypeInfo>::NAME, #doc, collector.new_text_signature()) + build_pyclass_doc(<#cls as #pyo3_path::PyTypeInfo>::NAME, #doc, collector.new_text_signature()) }).map(::std::ops::Deref::deref) } @@ -1575,8 +1608,8 @@ impl<'a> PyClassImplsBuilder<'a> { #weaklist_offset - fn lazy_type_object() -> &'static _pyo3::impl_::pyclass::LazyTypeObject { - use _pyo3::impl_::pyclass::LazyTypeObject; + fn lazy_type_object() -> &'static #pyo3_path::impl_::pyclass::LazyTypeObject { + use #pyo3_path::impl_::pyclass::LazyTypeObject; static TYPE_OBJECT: LazyTypeObject<#cls> = LazyTypeObject::new(); &TYPE_OBJECT } @@ -1592,20 +1625,21 @@ impl<'a> PyClassImplsBuilder<'a> { }) } - fn impl_freelist(&self) -> TokenStream { + fn impl_freelist(&self, ctx: &Ctx) -> TokenStream { let cls = self.cls; + let Ctx { pyo3_path } = ctx; self.attr.options.freelist.as_ref().map_or(quote!{}, |freelist| { let freelist = &freelist.value; quote! { - impl _pyo3::impl_::pyclass::PyClassWithFreeList for #cls { + impl #pyo3_path::impl_::pyclass::PyClassWithFreeList for #cls { #[inline] - fn get_free_list(py: _pyo3::Python<'_>) -> &mut _pyo3::impl_::freelist::FreeList<*mut _pyo3::ffi::PyObject> { - static mut FREELIST: *mut _pyo3::impl_::freelist::FreeList<*mut _pyo3::ffi::PyObject> = 0 as *mut _; + fn get_free_list(py: #pyo3_path::Python<'_>) -> &mut #pyo3_path::impl_::freelist::FreeList<*mut #pyo3_path::ffi::PyObject> { + static mut FREELIST: *mut #pyo3_path::impl_::freelist::FreeList<*mut #pyo3_path::ffi::PyObject> = 0 as *mut _; unsafe { if FREELIST.is_null() { FREELIST = ::std::boxed::Box::into_raw(::std::boxed::Box::new( - _pyo3::impl_::freelist::FreeList::with_capacity(#freelist))); + #pyo3_path::impl_::freelist::FreeList::with_capacity(#freelist))); } &mut *FREELIST } @@ -1615,21 +1649,22 @@ impl<'a> PyClassImplsBuilder<'a> { }) } - fn freelist_slots(&self) -> Vec { + fn freelist_slots(&self, ctx: &Ctx) -> Vec { + let Ctx { pyo3_path } = ctx; let cls = self.cls; if self.attr.options.freelist.is_some() { vec![ quote! { - _pyo3::ffi::PyType_Slot { - slot: _pyo3::ffi::Py_tp_alloc, - pfunc: _pyo3::impl_::pyclass::alloc_with_freelist::<#cls> as *mut _, + #pyo3_path::ffi::PyType_Slot { + slot: #pyo3_path::ffi::Py_tp_alloc, + pfunc: #pyo3_path::impl_::pyclass::alloc_with_freelist::<#cls> as *mut _, } }, quote! { - _pyo3::ffi::PyType_Slot { - slot: _pyo3::ffi::Py_tp_free, - pfunc: _pyo3::impl_::pyclass::free_with_freelist::<#cls> as *mut _, + #pyo3_path::ffi::PyType_Slot { + slot: #pyo3_path::ffi::Py_tp_free, + pfunc: #pyo3_path::impl_::pyclass::free_with_freelist::<#cls> as *mut _, } }, ] @@ -1639,25 +1674,26 @@ impl<'a> PyClassImplsBuilder<'a> { } } -fn define_inventory_class(inventory_class_name: &syn::Ident) -> TokenStream { +fn define_inventory_class(inventory_class_name: &syn::Ident, ctx: &Ctx) -> TokenStream { + let Ctx { pyo3_path } = ctx; quote! { #[doc(hidden)] pub struct #inventory_class_name { - items: _pyo3::impl_::pyclass::PyClassItems, + items: #pyo3_path::impl_::pyclass::PyClassItems, } impl #inventory_class_name { - pub const fn new(items: _pyo3::impl_::pyclass::PyClassItems) -> Self { + pub const fn new(items: #pyo3_path::impl_::pyclass::PyClassItems) -> Self { Self { items } } } - impl _pyo3::impl_::pyclass::PyClassInventory for #inventory_class_name { - fn items(&self) -> &_pyo3::impl_::pyclass::PyClassItems { + impl #pyo3_path::impl_::pyclass::PyClassInventory for #inventory_class_name { + fn items(&self) -> &#pyo3_path::impl_::pyclass::PyClassItems { &self.items } } - _pyo3::inventory::collect!(#inventory_class_name); + #pyo3_path::inventory::collect!(#inventory_class_name); } } diff --git a/pyo3-macros-backend/src/pyfunction.rs b/pyo3-macros-backend/src/pyfunction.rs index 45de94e4a10..4b1e0eadeba 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -1,3 +1,4 @@ +use crate::utils::Ctx; use crate::{ attributes::{ self, get_pyo3_options, take_attributes, take_pyo3_options, CrateAttribute, @@ -6,7 +7,6 @@ use crate::{ deprecations::Deprecations, method::{self, CallingConvention, FnArg}, pymethod::check_generic, - utils::get_pyo3_crate, }; use proc_macro2::TokenStream; use quote::{format_ident, quote}; @@ -205,6 +205,9 @@ pub fn impl_wrap_pyfunction( krate, } = options; + let ctx = &Ctx::new(&krate); + let Ctx { pyo3_path } = &ctx; + let python_name = name.map_or_else(|| func.sig.ident.unraw(), |name| name.value.0); let tp = if pass_module.is_some() { @@ -249,17 +252,15 @@ pub fn impl_wrap_pyfunction( text_signature, asyncness: func.sig.asyncness, unsafety: func.sig.unsafety, - deprecations: Deprecations::new(), + deprecations: Deprecations::new(ctx), }; - let krate = get_pyo3_crate(&krate); - let vis = &func.vis; let name = &func.sig.ident; let wrapper_ident = format_ident!("__pyfunction_{}", spec.name); - let wrapper = spec.get_wrapper_function(&wrapper_ident, None)?; - let methoddef = spec.get_methoddef(wrapper_ident, &spec.get_doc(&func.attrs)); + let wrapper = spec.get_wrapper_function(&wrapper_ident, None, ctx)?; + let methoddef = spec.get_methoddef(wrapper_ident, &spec.get_doc(&func.attrs), ctx); let wrapped_pyfunction = quote! { @@ -268,12 +269,12 @@ pub fn impl_wrap_pyfunction( #[doc(hidden)] #vis mod #name { pub(crate) struct MakeDef; - pub const DEF: #krate::impl_::pymethods::PyMethodDef = MakeDef::DEF; + pub const DEF: #pyo3_path::impl_::pymethods::PyMethodDef = MakeDef::DEF; - pub fn add_to_module(module: &#krate::Bound<'_, #krate::types::PyModule>) -> #krate::PyResult<()> { - use #krate::prelude::PyModuleMethods; + pub fn add_to_module(module: &#pyo3_path::Bound<'_, #pyo3_path::types::PyModule>) -> #pyo3_path::PyResult<()> { + use #pyo3_path::prelude::PyModuleMethods; use ::std::convert::Into; - module.add_function(#krate::types::PyCFunction::internal_new(module.py(), &DEF, module.into())?) + module.add_function(#pyo3_path::types::PyCFunction::internal_new(module.py(), &DEF, module.into())?) } } @@ -284,10 +285,8 @@ pub fn impl_wrap_pyfunction( // FIXME https://github.com/PyO3/pyo3/issues/3903 #[allow(unknown_lints, non_local_definitions)] const _: () = { - use #krate as _pyo3; - impl #name::MakeDef { - const DEF: #krate::impl_::pymethods::PyMethodDef = #methoddef; + const DEF: #pyo3_path::impl_::pymethods::PyMethodDef = #methoddef; } #[allow(non_snake_case)] diff --git a/pyo3-macros-backend/src/pyimpl.rs b/pyo3-macros-backend/src/pyimpl.rs index 5802638e340..30a6d6dd17e 100644 --- a/pyo3-macros-backend/src/pyimpl.rs +++ b/pyo3-macros-backend/src/pyimpl.rs @@ -1,11 +1,11 @@ use std::collections::HashSet; +use crate::utils::Ctx; use crate::{ attributes::{take_pyo3_options, CrateAttribute}, konst::{ConstAttributes, ConstSpec}, pyfunction::PyFunctionOptions, pymethod::{self, is_proto_method, MethodAndMethodDef, MethodAndSlotDef}, - utils::get_pyo3_crate, }; use proc_macro2::TokenStream; use pymethod::GeneratedPyMethod; @@ -90,6 +90,7 @@ pub fn impl_methods( methods_type: PyClassMethodsType, options: PyImplOptions, ) -> syn::Result { + let ctx = &Ctx::new(&options.krate); let mut trait_impls = Vec::new(); let mut proto_impls = Vec::new(); let mut methods = Vec::new(); @@ -102,7 +103,8 @@ pub fn impl_methods( syn::ImplItem::Fn(meth) => { let mut fun_options = PyFunctionOptions::from_attrs(&mut meth.attrs)?; fun_options.krate = fun_options.krate.or_else(|| options.krate.clone()); - match pymethod::gen_py_method(ty, &mut meth.sig, &mut meth.attrs, fun_options)? { + match pymethod::gen_py_method(ty, &mut meth.sig, &mut meth.attrs, fun_options, ctx)? + { GeneratedPyMethod::Method(MethodAndMethodDef { associated_method, method_def, @@ -127,7 +129,7 @@ pub fn impl_methods( } } syn::ImplItem::Const(konst) => { - let attributes = ConstAttributes::from_attrs(&mut konst.attrs)?; + let attributes = ConstAttributes::from_attrs(&mut konst.attrs, ctx)?; if attributes.is_class_attr { let spec = ConstSpec { rust_ident: konst.ident.clone(), @@ -137,7 +139,7 @@ pub fn impl_methods( let MethodAndMethodDef { associated_method, method_def, - } = gen_py_const(ty, &spec); + } = gen_py_const(ty, &spec, ctx); methods.push(quote!(#(#attrs)* #method_def)); associated_methods.push(quote!(#(#attrs)* #associated_method)); if is_proto_method(&spec.python_name().to_string()) { @@ -158,21 +160,19 @@ pub fn impl_methods( } } - add_shared_proto_slots(ty, &mut proto_impls, implemented_proto_fragments); + add_shared_proto_slots(ty, &mut proto_impls, implemented_proto_fragments, ctx); - let krate = get_pyo3_crate(&options.krate); + let ctx = &Ctx::new(&options.krate); let items = match methods_type { - PyClassMethodsType::Specialization => impl_py_methods(ty, methods, proto_impls), - PyClassMethodsType::Inventory => submit_methods_inventory(ty, methods, proto_impls), + PyClassMethodsType::Specialization => impl_py_methods(ty, methods, proto_impls, ctx), + PyClassMethodsType::Inventory => submit_methods_inventory(ty, methods, proto_impls, ctx), }; Ok(quote! { // FIXME https://github.com/PyO3/pyo3/issues/3903 #[allow(unknown_lints, non_local_definitions)] const _: () = { - use #krate as _pyo3; - #(#trait_impls)* #items @@ -186,24 +186,25 @@ pub fn impl_methods( }) } -pub fn gen_py_const(cls: &syn::Type, spec: &ConstSpec) -> MethodAndMethodDef { +pub fn gen_py_const(cls: &syn::Type, spec: &ConstSpec<'_>, ctx: &Ctx) -> MethodAndMethodDef { let member = &spec.rust_ident; let wrapper_ident = format_ident!("__pymethod_{}__", member); let deprecations = &spec.attributes.deprecations; let python_name = &spec.null_terminated_python_name(); + let Ctx { pyo3_path } = ctx; let associated_method = quote! { - fn #wrapper_ident(py: _pyo3::Python<'_>) -> _pyo3::PyResult<_pyo3::PyObject> { + fn #wrapper_ident(py: #pyo3_path::Python<'_>) -> #pyo3_path::PyResult<#pyo3_path::PyObject> { #deprecations - ::std::result::Result::Ok(_pyo3::IntoPy::into_py(#cls::#member, py)) + ::std::result::Result::Ok(#pyo3_path::IntoPy::into_py(#cls::#member, py)) } }; let method_def = quote! { - _pyo3::class::PyMethodDefType::ClassAttribute({ - _pyo3::class::PyClassAttributeDef::new( + #pyo3_path::class::PyMethodDefType::ClassAttribute({ + #pyo3_path::class::PyClassAttributeDef::new( #python_name, - _pyo3::impl_::pymethods::PyClassAttributeFactory(#cls::#wrapper_ident) + #pyo3_path::impl_::pymethods::PyClassAttributeFactory(#cls::#wrapper_ident) ) }) }; @@ -218,13 +219,15 @@ fn impl_py_methods( ty: &syn::Type, methods: Vec, proto_impls: Vec, + ctx: &Ctx, ) -> TokenStream { + let Ctx { pyo3_path } = ctx; quote! { - impl _pyo3::impl_::pyclass::PyMethods<#ty> - for _pyo3::impl_::pyclass::PyClassImplCollector<#ty> + impl #pyo3_path::impl_::pyclass::PyMethods<#ty> + for #pyo3_path::impl_::pyclass::PyClassImplCollector<#ty> { - fn py_methods(self) -> &'static _pyo3::impl_::pyclass::PyClassItems { - static ITEMS: _pyo3::impl_::pyclass::PyClassItems = _pyo3::impl_::pyclass::PyClassItems { + fn py_methods(self) -> &'static #pyo3_path::impl_::pyclass::PyClassItems { + static ITEMS: #pyo3_path::impl_::pyclass::PyClassItems = #pyo3_path::impl_::pyclass::PyClassItems { methods: &[#(#methods),*], slots: &[#(#proto_impls),*] }; @@ -238,13 +241,15 @@ fn add_shared_proto_slots( ty: &syn::Type, proto_impls: &mut Vec, mut implemented_proto_fragments: HashSet, + ctx: &Ctx, ) { + let Ctx { pyo3_path } = ctx; macro_rules! try_add_shared_slot { ($slot:ident, $($fragments:literal),*) => {{ let mut implemented = false; $(implemented |= implemented_proto_fragments.remove($fragments));*; if implemented { - proto_impls.push(quote! { _pyo3::impl_::pyclass::$slot!(#ty) }) + proto_impls.push(quote! { #pyo3_path::impl_::pyclass::$slot!(#ty) }) } }}; } @@ -294,11 +299,13 @@ fn submit_methods_inventory( ty: &syn::Type, methods: Vec, proto_impls: Vec, + ctx: &Ctx, ) -> TokenStream { + let Ctx { pyo3_path } = ctx; quote! { - _pyo3::inventory::submit! { - type Inventory = <#ty as _pyo3::impl_::pyclass::PyClassImpl>::Inventory; - Inventory::new(_pyo3::impl_::pyclass::PyClassItems { methods: &[#(#methods),*], slots: &[#(#proto_impls),*] }) + #pyo3_path::inventory::submit! { + type Inventory = <#ty as #pyo3_path::impl_::pyclass::PyClassImpl>::Inventory; + Inventory::new(#pyo3_path::impl_::pyclass::PyClassItems { methods: &[#(#methods),*], slots: &[#(#proto_impls),*] }) } } } diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index 4cb07a9ad2a..1c19112d183 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -2,6 +2,7 @@ use std::borrow::Cow; use crate::attributes::{NameAttribute, RenamingRule}; use crate::method::{CallingConvention, ExtractErrorMode}; +use crate::utils::Ctx; use crate::utils::PythonDoc; use crate::{ method::{FnArg, FnSpec, FnType, SelfType}, @@ -160,8 +161,9 @@ impl<'a> PyMethod<'a> { sig: &'a mut syn::Signature, meth_attrs: &mut Vec, options: PyFunctionOptions, + ctx: &'a Ctx, ) -> Result { - let spec = FnSpec::parse(sig, meth_attrs, options)?; + let spec = FnSpec::parse(sig, meth_attrs, options, ctx)?; let method_name = spec.python_name.to_string(); let kind = PyMethodKind::from_name(&method_name); @@ -186,33 +188,35 @@ pub fn gen_py_method( sig: &mut syn::Signature, meth_attrs: &mut Vec, options: PyFunctionOptions, + ctx: &Ctx, ) -> Result { check_generic(sig)?; ensure_function_options_valid(&options)?; - let method = PyMethod::parse(sig, meth_attrs, options)?; + let method = PyMethod::parse(sig, meth_attrs, options, ctx)?; let spec = &method.spec; + let Ctx { pyo3_path } = ctx; Ok(match (method.kind, &spec.tp) { // Class attributes go before protos so that class attributes can be used to set proto // method to None. (_, FnType::ClassAttribute) => { - GeneratedPyMethod::Method(impl_py_class_attribute(cls, spec)?) + GeneratedPyMethod::Method(impl_py_class_attribute(cls, spec, ctx)?) } (PyMethodKind::Proto(proto_kind), _) => { ensure_no_forbidden_protocol_attributes(&proto_kind, spec, &method.method_name)?; match proto_kind { PyMethodProtoKind::Slot(slot_def) => { - let slot = slot_def.generate_type_slot(cls, spec, &method.method_name)?; + let slot = slot_def.generate_type_slot(cls, spec, &method.method_name, ctx)?; GeneratedPyMethod::Proto(slot) } PyMethodProtoKind::Call => { - GeneratedPyMethod::Proto(impl_call_slot(cls, method.spec)?) + GeneratedPyMethod::Proto(impl_call_slot(cls, method.spec, ctx)?) } PyMethodProtoKind::Traverse => { - GeneratedPyMethod::Proto(impl_traverse_slot(cls, spec)?) + GeneratedPyMethod::Proto(impl_traverse_slot(cls, spec, ctx)?) } PyMethodProtoKind::SlotFragment(slot_fragment_def) => { - let proto = slot_fragment_def.generate_pyproto_fragment(cls, spec)?; + let proto = slot_fragment_def.generate_pyproto_fragment(cls, spec, ctx)?; GeneratedPyMethod::SlotTraitImpl(method.method_name, proto) } } @@ -223,22 +227,25 @@ pub fn gen_py_method( spec, &spec.get_doc(meth_attrs), None, + ctx, )?), (_, FnType::FnClass(_)) => GeneratedPyMethod::Method(impl_py_method_def( cls, spec, &spec.get_doc(meth_attrs), - Some(quote!(_pyo3::ffi::METH_CLASS)), + Some(quote!(#pyo3_path::ffi::METH_CLASS)), + ctx, )?), (_, FnType::FnStatic) => GeneratedPyMethod::Method(impl_py_method_def( cls, spec, &spec.get_doc(meth_attrs), - Some(quote!(_pyo3::ffi::METH_STATIC)), + Some(quote!(#pyo3_path::ffi::METH_STATIC)), + ctx, )?), // special prototypes (_, FnType::FnNew) | (_, FnType::FnNewClass(_)) => { - GeneratedPyMethod::Proto(impl_py_method_def_new(cls, spec)?) + GeneratedPyMethod::Proto(impl_py_method_def_new(cls, spec, ctx)?) } (_, FnType::Getter(self_type)) => GeneratedPyMethod::Method(impl_py_getter_def( @@ -248,6 +255,7 @@ pub fn gen_py_method( spec, doc: spec.get_doc(meth_attrs), }, + ctx, )?), (_, FnType::Setter(self_type)) => GeneratedPyMethod::Method(impl_py_setter_def( cls, @@ -256,6 +264,7 @@ pub fn gen_py_method( spec, doc: spec.get_doc(meth_attrs), }, + ctx, )?), (_, FnType::FnModule(_)) => { unreachable!("methods cannot be FnModule") @@ -305,18 +314,20 @@ pub fn impl_py_method_def( spec: &FnSpec<'_>, doc: &PythonDoc, flags: Option, + ctx: &Ctx, ) -> Result { + let Ctx { pyo3_path } = ctx; let wrapper_ident = format_ident!("__pymethod_{}__", spec.python_name); - let associated_method = spec.get_wrapper_function(&wrapper_ident, Some(cls))?; + let associated_method = spec.get_wrapper_function(&wrapper_ident, Some(cls), ctx)?; let add_flags = flags.map(|flags| quote!(.flags(#flags))); let methoddef_type = match spec.tp { FnType::FnStatic => quote!(Static), FnType::FnClass(_) => quote!(Class), _ => quote!(Method), }; - let methoddef = spec.get_methoddef(quote! { #cls::#wrapper_ident }, doc); + let methoddef = spec.get_methoddef(quote! { #cls::#wrapper_ident }, doc, ctx); let method_def = quote! { - _pyo3::class::PyMethodDefType::#methoddef_type(#methoddef #add_flags) + #pyo3_path::class::PyMethodDefType::#methoddef_type(#methoddef #add_flags) }; Ok(MethodAndMethodDef { associated_method, @@ -325,9 +336,14 @@ pub fn impl_py_method_def( } /// Also used by pyclass. -pub fn impl_py_method_def_new(cls: &syn::Type, spec: &FnSpec<'_>) -> Result { +pub fn impl_py_method_def_new( + cls: &syn::Type, + spec: &FnSpec<'_>, + ctx: &Ctx, +) -> Result { + let Ctx { pyo3_path } = ctx; let wrapper_ident = syn::Ident::new("__pymethod___new____", Span::call_site()); - let associated_method = spec.get_wrapper_function(&wrapper_ident, Some(cls))?; + let associated_method = spec.get_wrapper_function(&wrapper_ident, Some(cls), ctx)?; // Use just the text_signature_call_signature() because the class' Python name // isn't known to `#[pymethods]` - that has to be attached at runtime from the PyClassImpl // trait implementation created by `#[pyclass]`. @@ -337,18 +353,18 @@ pub fn impl_py_method_def_new(cls: &syn::Type, spec: &FnSpec<'_>) -> Result *mut _pyo3::ffi::PyObject + subtype: *mut #pyo3_path::ffi::PyTypeObject, + args: *mut #pyo3_path::ffi::PyObject, + kwargs: *mut #pyo3_path::ffi::PyObject, + ) -> *mut #pyo3_path::ffi::PyObject { #deprecations - use _pyo3::impl_::pyclass::*; + use #pyo3_path::impl_::pyclass::*; impl PyClassNewTextSignature<#cls> for PyClassImplCollector<#cls> { #[inline] fn new_text_signature(self) -> ::std::option::Option<&'static str> { @@ -356,7 +372,7 @@ pub fn impl_py_method_def_new(cls: &syn::Type, spec: &FnSpec<'_>) -> Result) -> Result) -> Result) -> Result { +fn impl_call_slot(cls: &syn::Type, mut spec: FnSpec<'_>, ctx: &Ctx) -> Result { + let Ctx { pyo3_path } = ctx; + // HACK: __call__ proto slot must always use varargs calling convention, so change the spec. // Probably indicates there's a refactoring opportunity somewhere. spec.convention = CallingConvention::Varargs; let wrapper_ident = syn::Ident::new("__pymethod___call____", Span::call_site()); - let associated_method = spec.get_wrapper_function(&wrapper_ident, Some(cls))?; + let associated_method = spec.get_wrapper_function(&wrapper_ident, Some(cls), ctx)?; let slot_def = quote! { - _pyo3::ffi::PyType_Slot { - slot: _pyo3::ffi::Py_tp_call, + #pyo3_path::ffi::PyType_Slot { + slot: #pyo3_path::ffi::Py_tp_call, pfunc: { unsafe extern "C" fn trampoline( - slf: *mut _pyo3::ffi::PyObject, - args: *mut _pyo3::ffi::PyObject, - kwargs: *mut _pyo3::ffi::PyObject, - ) -> *mut _pyo3::ffi::PyObject + slf: *mut #pyo3_path::ffi::PyObject, + args: *mut #pyo3_path::ffi::PyObject, + kwargs: *mut #pyo3_path::ffi::PyObject, + ) -> *mut #pyo3_path::ffi::PyObject { - _pyo3::impl_::trampoline::ternaryfunc( + #pyo3_path::impl_::trampoline::ternaryfunc( slf, args, kwargs, @@ -398,7 +416,7 @@ fn impl_call_slot(cls: &syn::Type, mut spec: FnSpec<'_>) -> Result) -> Result) -> syn::Result { +fn impl_traverse_slot( + cls: &syn::Type, + spec: &FnSpec<'_>, + ctx: &Ctx, +) -> syn::Result { + let Ctx { pyo3_path } = ctx; if let (Some(py_arg), _) = split_off_python_arg(&spec.signature.arguments) { return Err(syn::Error::new_spanned(py_arg.ty, "__traverse__ may not take `Python`. \ Usually, an implementation of `__traverse__` should do nothing but calls to `visit.call`. \ @@ -419,17 +442,17 @@ fn impl_traverse_slot(cls: &syn::Type, spec: &FnSpec<'_>) -> syn::Result ::std::os::raw::c_int { - _pyo3::impl_::pymethods::_call_traverse::<#cls>(slf, #cls::#rust_fn_ident, visit, arg) + #pyo3_path::impl_::pymethods::_call_traverse::<#cls>(slf, #cls::#rust_fn_ident, visit, arg) } }; let slot_def = quote! { - _pyo3::ffi::PyType_Slot { - slot: _pyo3::ffi::Py_tp_traverse, - pfunc: #cls::__pymethod_traverse__ as _pyo3::ffi::traverseproc as _ + #pyo3_path::ffi::PyType_Slot { + slot: #pyo3_path::ffi::Py_tp_traverse, + pfunc: #cls::__pymethod_traverse__ as #pyo3_path::ffi::traverseproc as _ } }; Ok(MethodAndSlotDef { @@ -438,7 +461,12 @@ fn impl_traverse_slot(cls: &syn::Type, spec: &FnSpec<'_>) -> syn::Result) -> syn::Result { +fn impl_py_class_attribute( + cls: &syn::Type, + spec: &FnSpec<'_>, + ctx: &Ctx, +) -> syn::Result { + let Ctx { pyo3_path } = ctx; let (py_arg, args) = split_off_python_arg(&spec.signature.arguments); ensure_spanned!( args.is_empty(), @@ -454,20 +482,20 @@ fn impl_py_class_attribute(cls: &syn::Type, spec: &FnSpec<'_>) -> syn::Result) -> _pyo3::PyResult<_pyo3::PyObject> { + fn #wrapper_ident(py: #pyo3_path::Python<'_>) -> #pyo3_path::PyResult<#pyo3_path::PyObject> { let function = #cls::#name; // Shadow the method name to avoid #3017 - _pyo3::impl_::wrap::map_result_into_py(py, #body) + #pyo3_path::impl_::wrap::map_result_into_py(py, #body) } }; let method_def = quote! { - _pyo3::class::PyMethodDefType::ClassAttribute({ - _pyo3::class::PyClassAttributeDef::new( + #pyo3_path::class::PyMethodDefType::ClassAttribute({ + #pyo3_path::class::PyClassAttributeDef::new( #python_name, - _pyo3::impl_::pymethods::PyClassAttributeFactory(#cls::#wrapper_ident) + #pyo3_path::impl_::pymethods::PyClassAttributeFactory(#cls::#wrapper_ident) ) }) }; @@ -483,9 +511,10 @@ fn impl_call_setter( spec: &FnSpec<'_>, self_type: &SelfType, holders: &mut Vec, + ctx: &Ctx, ) -> syn::Result { let (py_arg, args) = split_off_python_arg(&spec.signature.arguments); - let slf = self_type.receiver(cls, ExtractErrorMode::Raise, holders); + let slf = self_type.receiver(cls, ExtractErrorMode::Raise, holders, ctx); if args.is_empty() { bail_spanned!(spec.name.span() => "setter function expected to have one argument"); @@ -510,7 +539,9 @@ fn impl_call_setter( pub fn impl_py_setter_def( cls: &syn::Type, property_type: PropertyType<'_>, + ctx: &Ctx, ) -> Result { + let Ctx { pyo3_path } = ctx; let python_name = property_type.null_terminated_python_name()?; let doc = property_type.doc(); let mut holders = Vec::new(); @@ -522,7 +553,7 @@ pub fn impl_py_setter_def( mutable: true, span: Span::call_site(), } - .receiver(cls, ExtractErrorMode::Raise, &mut holders); + .receiver(cls, ExtractErrorMode::Raise, &mut holders, ctx); if let Some(ident) = &field.ident { // named struct field quote!({ #slf.#ident = _val; }) @@ -534,7 +565,7 @@ pub fn impl_py_setter_def( } PropertyType::Function { spec, self_type, .. - } => impl_call_setter(cls, spec, self_type, &mut holders)?, + } => impl_call_setter(cls, spec, self_type, &mut holders, ctx)?, }; let wrapper_ident = match property_type { @@ -568,27 +599,27 @@ pub fn impl_py_setter_def( let associated_method = quote! { #cfg_attrs unsafe fn #wrapper_ident( - py: _pyo3::Python<'_>, - _slf: *mut _pyo3::ffi::PyObject, - _value: *mut _pyo3::ffi::PyObject, - ) -> _pyo3::PyResult<::std::os::raw::c_int> { + py: #pyo3_path::Python<'_>, + _slf: *mut #pyo3_path::ffi::PyObject, + _value: *mut #pyo3_path::ffi::PyObject, + ) -> #pyo3_path::PyResult<::std::os::raw::c_int> { use ::std::convert::Into; - let _value = _pyo3::impl_::pymethods::BoundRef::ref_from_ptr_or_opt(py, &_value) + let _value = #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr_or_opt(py, &_value) .ok_or_else(|| { - _pyo3::exceptions::PyAttributeError::new_err("can't delete attribute") + #pyo3_path::exceptions::PyAttributeError::new_err("can't delete attribute") })?; - let _val = _pyo3::FromPyObject::extract_bound(_value.into())?; + let _val = #pyo3_path::FromPyObject::extract_bound(_value.into())?; #( #holders )* - _pyo3::callback::convert(py, #setter_impl) + #pyo3_path::callback::convert(py, #setter_impl) } }; let method_def = quote! { #cfg_attrs - _pyo3::class::PyMethodDefType::Setter( - _pyo3::class::PySetterDef::new( + #pyo3_path::class::PyMethodDefType::Setter( + #pyo3_path::class::PySetterDef::new( #python_name, - _pyo3::impl_::pymethods::PySetter(#cls::#wrapper_ident), + #pyo3_path::impl_::pymethods::PySetter(#cls::#wrapper_ident), #doc ) ) @@ -605,9 +636,10 @@ fn impl_call_getter( spec: &FnSpec<'_>, self_type: &SelfType, holders: &mut Vec, + ctx: &Ctx, ) -> syn::Result { let (py_arg, args) = split_off_python_arg(&spec.signature.arguments); - let slf = self_type.receiver(cls, ExtractErrorMode::Raise, holders); + let slf = self_type.receiver(cls, ExtractErrorMode::Raise, holders, ctx); ensure_spanned!( args.is_empty(), args[0].ty.span() => "getter function can only have one argument (of type pyo3::Python)" @@ -627,7 +659,9 @@ fn impl_call_getter( pub fn impl_py_getter_def( cls: &syn::Type, property_type: PropertyType<'_>, + ctx: &Ctx, ) -> Result { + let Ctx { pyo3_path } = ctx; let python_name = property_type.null_terminated_python_name()?; let doc = property_type.doc(); @@ -640,7 +674,7 @@ pub fn impl_py_getter_def( mutable: false, span: Span::call_site(), } - .receiver(cls, ExtractErrorMode::Raise, &mut holders); + .receiver(cls, ExtractErrorMode::Raise, &mut holders, ctx); let field_token = if let Some(ident) = &field.ident { // named struct field ident.to_token_stream() @@ -648,17 +682,23 @@ pub fn impl_py_getter_def( // tuple struct field syn::Index::from(field_index).to_token_stream() }; - quotes::map_result_into_ptr(quotes::ok_wrap(quote! { - ::std::clone::Clone::clone(&(#slf.#field_token)) - })) + quotes::map_result_into_ptr( + quotes::ok_wrap( + quote! { + ::std::clone::Clone::clone(&(#slf.#field_token)) + }, + ctx, + ), + ctx, + ) } // Forward to `IntoPyCallbackOutput`, to handle `#[getter]`s returning results. PropertyType::Function { spec, self_type, .. } => { - let call = impl_call_getter(cls, spec, self_type, &mut holders)?; + let call = impl_call_getter(cls, spec, self_type, &mut holders, ctx)?; quote! { - _pyo3::callback::convert(py, #call) + #pyo3_path::callback::convert(py, #call) } } }; @@ -694,9 +734,9 @@ pub fn impl_py_getter_def( let associated_method = quote! { #cfg_attrs unsafe fn #wrapper_ident( - py: _pyo3::Python<'_>, - _slf: *mut _pyo3::ffi::PyObject - ) -> _pyo3::PyResult<*mut _pyo3::ffi::PyObject> { + py: #pyo3_path::Python<'_>, + _slf: *mut #pyo3_path::ffi::PyObject + ) -> #pyo3_path::PyResult<*mut #pyo3_path::ffi::PyObject> { #( #holders )* let result = #body; result @@ -705,10 +745,10 @@ pub fn impl_py_getter_def( let method_def = quote! { #cfg_attrs - _pyo3::class::PyMethodDefType::Getter( - _pyo3::class::PyGetterDef::new( + #pyo3_path::class::PyMethodDefType::Getter( + #pyo3_path::class::PyGetterDef::new( #python_name, - _pyo3::impl_::pymethods::PyGetter(#cls::#wrapper_ident), + #pyo3_path::impl_::pymethods::PyGetter(#cls::#wrapper_ident), #doc ) ) @@ -786,7 +826,7 @@ pub const __REPR__: SlotDef = SlotDef::new("Py_tp_repr", "reprfunc"); const __HASH__: SlotDef = SlotDef::new("Py_tp_hash", "hashfunc") .ret_ty(Ty::PyHashT) .return_conversion(TokenGenerator( - || quote! { _pyo3::callback::HashCallbackOutput }, + |Ctx { pyo3_path }: &Ctx| quote! { #pyo3_path::callback::HashCallbackOutput }, )); pub const __RICHCMP__: SlotDef = SlotDef::new("Py_tp_richcompare", "richcmpfunc") .extract_error_mode(ExtractErrorMode::NotImplemented) @@ -796,14 +836,16 @@ const __GET__: SlotDef = SlotDef::new("Py_tp_descr_get", "descrgetfunc") const __ITER__: SlotDef = SlotDef::new("Py_tp_iter", "getiterfunc"); const __NEXT__: SlotDef = SlotDef::new("Py_tp_iternext", "iternextfunc") .return_specialized_conversion( - TokenGenerator(|| quote! { IterBaseKind, IterOptionKind, IterResultOptionKind }), - TokenGenerator(|| quote! { iter_tag }), + TokenGenerator(|_| quote! { IterBaseKind, IterOptionKind, IterResultOptionKind }), + TokenGenerator(|_| quote! { iter_tag }), ); const __AWAIT__: SlotDef = SlotDef::new("Py_am_await", "unaryfunc"); const __AITER__: SlotDef = SlotDef::new("Py_am_aiter", "unaryfunc"); const __ANEXT__: SlotDef = SlotDef::new("Py_am_anext", "unaryfunc").return_specialized_conversion( - TokenGenerator(|| quote! { AsyncIterBaseKind, AsyncIterOptionKind, AsyncIterResultOptionKind }), - TokenGenerator(|| quote! { async_iter_tag }), + TokenGenerator( + |_| quote! { AsyncIterBaseKind, AsyncIterOptionKind, AsyncIterResultOptionKind }, + ), + TokenGenerator(|_| quote! { async_iter_tag }), ); const __LEN__: SlotDef = SlotDef::new("Py_mp_length", "lenfunc").ret_ty(Ty::PySsizeT); const __CONTAINS__: SlotDef = SlotDef::new("Py_sq_contains", "objobjproc") @@ -905,16 +947,17 @@ enum Ty { } impl Ty { - fn ffi_type(self) -> TokenStream { + fn ffi_type(self, ctx: &Ctx) -> TokenStream { + let Ctx { pyo3_path } = ctx; match self { - Ty::Object | Ty::MaybeNullObject => quote! { *mut _pyo3::ffi::PyObject }, - Ty::NonNullObject => quote! { ::std::ptr::NonNull<_pyo3::ffi::PyObject> }, - Ty::IPowModulo => quote! { _pyo3::impl_::pymethods::IPowModulo }, + Ty::Object | Ty::MaybeNullObject => quote! { *mut #pyo3_path::ffi::PyObject }, + Ty::NonNullObject => quote! { ::std::ptr::NonNull<#pyo3_path::ffi::PyObject> }, + Ty::IPowModulo => quote! { #pyo3_path::impl_::pymethods::IPowModulo }, Ty::Int | Ty::CompareOp => quote! { ::std::os::raw::c_int }, - Ty::PyHashT => quote! { _pyo3::ffi::Py_hash_t }, - Ty::PySsizeT => quote! { _pyo3::ffi::Py_ssize_t }, + Ty::PyHashT => quote! { #pyo3_path::ffi::Py_hash_t }, + Ty::PySsizeT => quote! { #pyo3_path::ffi::Py_ssize_t }, Ty::Void => quote! { () }, - Ty::PyBuffer => quote! { *mut _pyo3::ffi::Py_buffer }, + Ty::PyBuffer => quote! { *mut #pyo3_path::ffi::Py_buffer }, } } @@ -924,14 +967,16 @@ impl Ty { arg: &FnArg<'_>, extract_error_mode: ExtractErrorMode, holders: &mut Vec, + 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, - quote! { #ident }, + quote! { #ident },ctx ), Ty::MaybeNullObject => extract_object( extract_error_mode, @@ -939,36 +984,36 @@ impl Ty { &name_str, quote! { if #ident.is_null() { - _pyo3::ffi::Py_None() + #pyo3_path::ffi::Py_None() } else { #ident } - }, + },ctx ), Ty::NonNullObject => extract_object( extract_error_mode, holders, &name_str, - quote! { #ident.as_ptr() }, + quote! { #ident.as_ptr() },ctx ), Ty::IPowModulo => extract_object( extract_error_mode, holders, &name_str, - quote! { #ident.as_ptr() }, + quote! { #ident.as_ptr() },ctx ), Ty::CompareOp => extract_error_mode.handle_error( quote! { - _pyo3::class::basic::CompareOp::from_raw(#ident) - .ok_or_else(|| _pyo3::exceptions::PyValueError::new_err("invalid comparison operator")) - }, + #pyo3_path::class::basic::CompareOp::from_raw(#ident) + .ok_or_else(|| #pyo3_path::exceptions::PyValueError::new_err("invalid comparison operator")) + },ctx ), Ty::PySsizeT => { let ty = arg.ty; extract_error_mode.handle_error( quote! { - ::std::convert::TryInto::<#ty>::try_into(#ident).map_err(|e| _pyo3::exceptions::PyValueError::new_err(e.to_string())) - }, + ::std::convert::TryInto::<#ty>::try_into(#ident).map_err(|e| #pyo3_path::exceptions::PyValueError::new_err(e.to_string())) + },ctx ) } // Just pass other types through unmodified @@ -982,19 +1027,24 @@ fn extract_object( holders: &mut Vec, name: &str, source_ptr: TokenStream, + ctx: &Ctx, ) -> TokenStream { + let Ctx { pyo3_path } = ctx; let holder = syn::Ident::new(&format!("holder_{}", holders.len()), Span::call_site()); holders.push(quote! { #[allow(clippy::let_unit_value)] - let mut #holder = _pyo3::impl_::extract_argument::FunctionArgumentHolder::INIT; + let mut #holder = #pyo3_path::impl_::extract_argument::FunctionArgumentHolder::INIT; }); - extract_error_mode.handle_error(quote! { - _pyo3::impl_::extract_argument::extract_argument( - &_pyo3::impl_::pymethods::BoundRef::ref_from_ptr(py, &#source_ptr), - &mut #holder, - #name - ) - }) + extract_error_mode.handle_error( + quote! { + #pyo3_path::impl_::extract_argument::extract_argument( + &#pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(py, &#source_ptr), + &mut #holder, + #name + ) + }, + ctx, + ) } enum ReturnMode { @@ -1004,21 +1054,29 @@ enum ReturnMode { } impl ReturnMode { - fn return_call_output(&self, call: TokenStream) -> TokenStream { + fn return_call_output(&self, call: TokenStream, ctx: &Ctx) -> TokenStream { + let Ctx { pyo3_path } = ctx; match self { - ReturnMode::Conversion(conversion) => quote! { - let _result: _pyo3::PyResult<#conversion> = _pyo3::callback::convert(py, #call); - _pyo3::callback::convert(py, _result) - }, - ReturnMode::SpecializedConversion(traits, tag) => quote! { - let _result = #call; - use _pyo3::impl_::pymethods::{#traits}; - (&_result).#tag().convert(py, _result) - }, + ReturnMode::Conversion(conversion) => { + let conversion = TokenGeneratorCtx(*conversion, ctx); + quote! { + let _result: #pyo3_path::PyResult<#conversion> = #pyo3_path::callback::convert(py, #call); + #pyo3_path::callback::convert(py, _result) + } + } + ReturnMode::SpecializedConversion(traits, tag) => { + let traits = TokenGeneratorCtx(*traits, ctx); + let tag = TokenGeneratorCtx(*tag, ctx); + quote! { + let _result = #call; + use #pyo3_path::impl_::pymethods::{#traits}; + (&_result).#tag().convert(py, _result) + } + } ReturnMode::ReturnSelf => quote! { - let _result: _pyo3::PyResult<()> = _pyo3::callback::convert(py, #call); + let _result: #pyo3_path::PyResult<()> = #pyo3_path::callback::convert(py, #call); _result?; - _pyo3::ffi::Py_XINCREF(_raw_slf); + #pyo3_path::ffi::Py_XINCREF(_raw_slf); ::std::result::Result::Ok(_raw_slf) }, } @@ -1094,7 +1152,9 @@ impl SlotDef { cls: &syn::Type, spec: &FnSpec<'_>, method_name: &str, + ctx: &Ctx, ) -> Result { + let Ctx { pyo3_path } = ctx; let SlotDef { slot, func_ty, @@ -1110,12 +1170,12 @@ impl SlotDef { spec.name.span() => format!("`{}` must be `unsafe fn`", method_name) ); } - let arg_types: &Vec<_> = &arguments.iter().map(|arg| arg.ffi_type()).collect(); + let arg_types: &Vec<_> = &arguments.iter().map(|arg| arg.ffi_type(ctx)).collect(); let arg_idents: &Vec<_> = &(0..arguments.len()) .map(|i| format_ident!("arg{}", i)) .collect(); let wrapper_ident = format_ident!("__pymethod_{}__", method_name); - let ret_ty = ret_ty.ffi_type(); + let ret_ty = ret_ty.ffi_type(ctx); let mut holders = Vec::new(); let body = generate_method_body( cls, @@ -1124,14 +1184,15 @@ impl SlotDef { *extract_error_mode, &mut holders, return_mode.as_ref(), + ctx, )?; let name = spec.name; let associated_method = quote! { unsafe fn #wrapper_ident( - py: _pyo3::Python<'_>, - _raw_slf: *mut _pyo3::ffi::PyObject, + py: #pyo3_path::Python<'_>, + _raw_slf: *mut #pyo3_path::ffi::PyObject, #(#arg_idents: #arg_types),* - ) -> _pyo3::PyResult<#ret_ty> { + ) -> #pyo3_path::PyResult<#ret_ty> { let function = #cls::#name; // Shadow the method name to avoid #3017 let _slf = _raw_slf; #( #holders )* @@ -1140,20 +1201,20 @@ impl SlotDef { }; let slot_def = quote! {{ unsafe extern "C" fn trampoline( - _slf: *mut _pyo3::ffi::PyObject, + _slf: *mut #pyo3_path::ffi::PyObject, #(#arg_idents: #arg_types),* ) -> #ret_ty { - _pyo3::impl_::trampoline:: #func_ty ( + #pyo3_path::impl_::trampoline:: #func_ty ( _slf, #(#arg_idents,)* #cls::#wrapper_ident ) } - _pyo3::ffi::PyType_Slot { - slot: _pyo3::ffi::#slot, - pfunc: trampoline as _pyo3::ffi::#func_ty as _ + #pyo3_path::ffi::PyType_Slot { + slot: #pyo3_path::ffi::#slot, + pfunc: trampoline as #pyo3_path::ffi::#func_ty as _ } }}; Ok(MethodAndSlotDef { @@ -1170,15 +1231,19 @@ fn generate_method_body( extract_error_mode: ExtractErrorMode, holders: &mut Vec, return_mode: Option<&ReturnMode>, + ctx: &Ctx, ) -> Result { - let self_arg = spec.tp.self_arg(Some(cls), extract_error_mode, holders); + let Ctx { pyo3_path } = ctx; + let self_arg = spec + .tp + .self_arg(Some(cls), extract_error_mode, holders, ctx); let rust_name = spec.name; - let args = extract_proto_arguments(spec, arguments, extract_error_mode, holders)?; + let args = extract_proto_arguments(spec, arguments, extract_error_mode, holders, ctx)?; let call = quote! { #cls::#rust_name(#self_arg #(#args),*) }; Ok(if let Some(return_mode) = return_mode { - return_mode.return_call_output(call) + return_mode.return_call_output(call, ctx) } else { - quote! { _pyo3::callback::convert(py, #call) } + quote! { #pyo3_path::callback::convert(py, #call) } }) } @@ -1209,7 +1274,13 @@ impl SlotFragmentDef { self } - fn generate_pyproto_fragment(&self, cls: &syn::Type, spec: &FnSpec<'_>) -> Result { + fn generate_pyproto_fragment( + &self, + cls: &syn::Type, + spec: &FnSpec<'_>, + ctx: &Ctx, + ) -> Result { + let Ctx { pyo3_path } = ctx; let SlotFragmentDef { fragment, arguments, @@ -1219,7 +1290,7 @@ impl SlotFragmentDef { let fragment_trait = format_ident!("PyClass{}SlotFragment", fragment); let method = syn::Ident::new(fragment, Span::call_site()); let wrapper_ident = format_ident!("__pymethod_{}__", fragment); - let arg_types: &Vec<_> = &arguments.iter().map(|arg| arg.ffi_type()).collect(); + let arg_types: &Vec<_> = &arguments.iter().map(|arg| arg.ffi_type(ctx)).collect(); let arg_idents: &Vec<_> = &(0..arguments.len()) .map(|i| format_ident!("arg{}", i)) .collect(); @@ -1231,30 +1302,31 @@ impl SlotFragmentDef { *extract_error_mode, &mut holders, None, + ctx, )?; - let ret_ty = ret_ty.ffi_type(); + let ret_ty = ret_ty.ffi_type(ctx); Ok(quote! { impl #cls { unsafe fn #wrapper_ident( - py: _pyo3::Python, - _raw_slf: *mut _pyo3::ffi::PyObject, + py: #pyo3_path::Python, + _raw_slf: *mut #pyo3_path::ffi::PyObject, #(#arg_idents: #arg_types),* - ) -> _pyo3::PyResult<#ret_ty> { + ) -> #pyo3_path::PyResult<#ret_ty> { let _slf = _raw_slf; #( #holders )* #body } } - impl _pyo3::impl_::pyclass::#fragment_trait<#cls> for _pyo3::impl_::pyclass::PyClassImplCollector<#cls> { + impl #pyo3_path::impl_::pyclass::#fragment_trait<#cls> for #pyo3_path::impl_::pyclass::PyClassImplCollector<#cls> { #[inline] unsafe fn #method( self, - py: _pyo3::Python, - _raw_slf: *mut _pyo3::ffi::PyObject, + py: #pyo3_path::Python, + _raw_slf: *mut #pyo3_path::ffi::PyObject, #(#arg_idents: #arg_types),* - ) -> _pyo3::PyResult<#ret_ty> { + ) -> #pyo3_path::PyResult<#ret_ty> { #cls::#wrapper_ident(py, _raw_slf, #(#arg_idents),*) } } @@ -1341,6 +1413,7 @@ fn extract_proto_arguments( proto_args: &[Ty], extract_error_mode: ExtractErrorMode, holders: &mut Vec, + ctx: &Ctx, ) -> Result> { let mut args = Vec::with_capacity(spec.signature.arguments.len()); let mut non_python_args = 0; @@ -1352,7 +1425,7 @@ fn extract_proto_arguments( let ident = syn::Ident::new(&format!("arg{}", non_python_args), Span::call_site()); let conversions = proto_args.get(non_python_args) .ok_or_else(|| err_spanned!(arg.ty.span() => format!("Expected at most {} non-python arguments", proto_args.len())))? - .extract(&ident, arg, extract_error_mode, holders); + .extract(&ident, arg, extract_error_mode, holders, ctx); non_python_args += 1; args.push(conversions); } @@ -1372,10 +1445,14 @@ impl ToTokens for StaticIdent { } } -struct TokenGenerator(fn() -> TokenStream); +#[derive(Clone, Copy)] +struct TokenGenerator(fn(&Ctx) -> TokenStream); + +struct TokenGeneratorCtx<'ctx>(TokenGenerator, &'ctx Ctx); -impl ToTokens for TokenGenerator { +impl ToTokens for TokenGeneratorCtx<'_> { fn to_tokens(&self, tokens: &mut TokenStream) { - self.0().to_tokens(tokens) + let Self(TokenGenerator(gen), ctx) = self; + (gen)(ctx).to_tokens(tokens) } } diff --git a/pyo3-macros-backend/src/quotes.rs b/pyo3-macros-backend/src/quotes.rs index 239036ef3ca..0219cb9ae7f 100644 --- a/pyo3-macros-backend/src/quotes.rs +++ b/pyo3-macros-backend/src/quotes.rs @@ -1,21 +1,25 @@ +use crate::utils::Ctx; use proc_macro2::TokenStream; use quote::quote; -pub(crate) fn some_wrap(obj: TokenStream) -> TokenStream { +pub(crate) fn some_wrap(obj: TokenStream, ctx: &Ctx) -> TokenStream { + let Ctx { pyo3_path } = ctx; quote! { - _pyo3::impl_::wrap::SomeWrap::wrap(#obj) + #pyo3_path::impl_::wrap::SomeWrap::wrap(#obj) } } -pub(crate) fn ok_wrap(obj: TokenStream) -> TokenStream { +pub(crate) fn ok_wrap(obj: TokenStream, ctx: &Ctx) -> TokenStream { + let Ctx { pyo3_path } = ctx; quote! { - _pyo3::impl_::wrap::OkWrap::wrap(#obj) - .map_err(::core::convert::Into::<_pyo3::PyErr>::into) + #pyo3_path::impl_::wrap::OkWrap::wrap(#obj) + .map_err(::core::convert::Into::<#pyo3_path::PyErr>::into) } } -pub(crate) fn map_result_into_ptr(result: TokenStream) -> TokenStream { +pub(crate) fn map_result_into_ptr(result: TokenStream, ctx: &Ctx) -> TokenStream { + let Ctx { pyo3_path } = ctx; quote! { - _pyo3::impl_::wrap::map_result_into_ptr(py, #result) + #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 9f0f2678476..ca32abb42b3 100644 --- a/pyo3-macros-backend/src/utils.rs +++ b/pyo3-macros-backend/src/utils.rs @@ -144,11 +144,41 @@ pub fn unwrap_ty_group(mut ty: &syn::Type) -> &syn::Type { ty } -/// Extract the path to the pyo3 crate, or use the default (`::pyo3`). -pub(crate) fn get_pyo3_crate(attr: &Option) -> syn::Path { - match attr { - Some(attr) => attr.value.0.clone(), - None => syn::parse_str("::pyo3").unwrap(), +pub struct Ctx { + pub pyo3_path: PyO3CratePath, +} + +impl Ctx { + pub(crate) fn new(attr: &Option) -> Self { + let pyo3_path = match attr { + Some(attr) => PyO3CratePath::Given(attr.value.0.clone()), + None => PyO3CratePath::Default, + }; + + Self { pyo3_path } + } +} + +pub enum PyO3CratePath { + Given(syn::Path), + Default, +} + +impl PyO3CratePath { + pub fn to_tokens_spanned(&self, span: Span) -> TokenStream { + match self { + Self::Given(path) => quote::quote_spanned! { span => #path }, + Self::Default => quote::quote_spanned! { span => ::pyo3 }, + } + } +} + +impl quote::ToTokens for PyO3CratePath { + fn to_tokens(&self, tokens: &mut TokenStream) { + match self { + Self::Given(path) => path.to_tokens(tokens), + Self::Default => quote::quote! { ::pyo3 }.to_tokens(tokens), + } } } diff --git a/tests/ui/invalid_proto_pymethods.stderr b/tests/ui/invalid_proto_pymethods.stderr index 275a6b93c46..c9f6adff3a1 100644 --- a/tests/ui/invalid_proto_pymethods.stderr +++ b/tests/ui/invalid_proto_pymethods.stderr @@ -31,4 +31,4 @@ error[E0592]: duplicate definitions with name `__pymethod___richcmp____` | duplicate definitions for `__pymethod___richcmp____` | other definition for `__pymethod___richcmp____` | - = note: this error originates in the macro `_pyo3::impl_::pyclass::generate_pyclass_richcompare_slot` which comes from the expansion of the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) + = note: this error originates in the macro `::pyo3::impl_::pyclass::generate_pyclass_richcompare_slot` which comes from the expansion of the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) From 811a3e5d009e72eb41e0594b3f65e3228a2d4902 Mon Sep 17 00:00:00 2001 From: Lily Foote Date: Mon, 4 Mar 2024 21:24:38 +0000 Subject: [PATCH 064/936] Add IntoIterator for &Bound types (#3923) * Add IntoIterator for &Bound<'py, PyList> * Add a test for Bound<'_, PyList>.into_iter * Implement IntoIterator for more &Bound types * Remove some explicit .iter() calls * Implement IntoIterator for &Bound<'py, PyIterator> --- pyo3-benches/benches/bench_dict.rs | 2 +- pyo3-benches/benches/bench_list.rs | 2 +- pyo3-benches/benches/bench_set.rs | 2 +- src/conversions/hashbrown.rs | 2 +- src/conversions/indexmap.rs | 2 +- src/conversions/std/map.rs | 4 +-- src/types/dict.rs | 29 +++++++++++++++++++++ src/types/frozenset.rs | 21 +++++++++++++++ src/types/iterator.rs | 42 ++++++++++++++++++++++++++++++ src/types/list.rs | 23 ++++++++++++++++ src/types/mod.rs | 2 +- src/types/set.rs | 27 +++++++++++++++++++ src/types/tuple.rs | 26 ++++++++++++++++++ 13 files changed, 176 insertions(+), 8 deletions(-) diff --git a/pyo3-benches/benches/bench_dict.rs b/pyo3-benches/benches/bench_dict.rs index b63e010c1ff..e6a1e2e6b0b 100644 --- a/pyo3-benches/benches/bench_dict.rs +++ b/pyo3-benches/benches/bench_dict.rs @@ -11,7 +11,7 @@ fn iter_dict(b: &mut Bencher<'_>) { let dict = (0..LEN as u64).map(|i| (i, i * 2)).into_py_dict_bound(py); let mut sum = 0; b.iter(|| { - for (k, _v) in dict.iter() { + for (k, _v) in &dict { let i: u64 = k.extract().unwrap(); sum += i; } diff --git a/pyo3-benches/benches/bench_list.rs b/pyo3-benches/benches/bench_list.rs index dddd04d81ee..774dddcea38 100644 --- a/pyo3-benches/benches/bench_list.rs +++ b/pyo3-benches/benches/bench_list.rs @@ -11,7 +11,7 @@ fn iter_list(b: &mut Bencher<'_>) { let list = PyList::new_bound(py, 0..LEN); let mut sum = 0; b.iter(|| { - for x in list.iter() { + for x in &list { let i: u64 = x.extract().unwrap(); sum += i; } diff --git a/pyo3-benches/benches/bench_set.rs b/pyo3-benches/benches/bench_set.rs index 49243a63fd4..b04cb491304 100644 --- a/pyo3-benches/benches/bench_set.rs +++ b/pyo3-benches/benches/bench_set.rs @@ -20,7 +20,7 @@ fn iter_set(b: &mut Bencher<'_>) { let set = PySet::new_bound(py, &(0..LEN).collect::>()).unwrap(); let mut sum = 0; b.iter(|| { - for x in set.iter() { + for x in &set { let i: u64 = x.extract().unwrap(); sum += i; } diff --git a/src/conversions/hashbrown.rs b/src/conversions/hashbrown.rs index 8ac95d87f76..9eea7734bfc 100644 --- a/src/conversions/hashbrown.rs +++ b/src/conversions/hashbrown.rs @@ -60,7 +60,7 @@ where fn extract_bound(ob: &Bound<'py, PyAny>) -> Result { let dict = ob.downcast::()?; let mut ret = hashbrown::HashMap::with_capacity_and_hasher(dict.len(), S::default()); - for (k, v) in dict.iter() { + for (k, v) in dict { ret.insert(k.extract()?, v.extract()?); } Ok(ret) diff --git a/src/conversions/indexmap.rs b/src/conversions/indexmap.rs index bb9ae0126d0..fdbe057f32d 100644 --- a/src/conversions/indexmap.rs +++ b/src/conversions/indexmap.rs @@ -125,7 +125,7 @@ where fn extract_bound(ob: &Bound<'py, PyAny>) -> Result { let dict = ob.downcast::()?; let mut ret = indexmap::IndexMap::with_capacity_and_hasher(dict.len(), S::default()); - for (k, v) in dict.iter() { + for (k, v) in dict { ret.insert(k.extract()?, v.extract()?); } Ok(ret) diff --git a/src/conversions/std/map.rs b/src/conversions/std/map.rs index c17caa8b252..49eff4c1e8d 100644 --- a/src/conversions/std/map.rs +++ b/src/conversions/std/map.rs @@ -76,7 +76,7 @@ where fn extract_bound(ob: &Bound<'py, PyAny>) -> Result { let dict = ob.downcast::()?; let mut ret = collections::HashMap::with_capacity_and_hasher(dict.len(), S::default()); - for (k, v) in dict.iter() { + for (k, v) in dict { ret.insert(k.extract()?, v.extract()?); } Ok(ret) @@ -96,7 +96,7 @@ where fn extract_bound(ob: &Bound<'py, PyAny>) -> Result { let dict = ob.downcast::()?; let mut ret = collections::BTreeMap::new(); - for (k, v) in dict.iter() { + for (k, v) in dict { ret.insert(k.extract()?, v.extract()?); } Ok(ret) diff --git a/src/types/dict.rs b/src/types/dict.rs index 5d1243463f2..2302004d238 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -672,6 +672,15 @@ impl<'py> IntoIterator for Bound<'py, PyDict> { } } +impl<'py> IntoIterator for &Bound<'py, PyDict> { + type Item = (Bound<'py, PyAny>, Bound<'py, PyAny>); + type IntoIter = BoundDictIterator<'py>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + mod borrowed_iter { use super::*; @@ -1123,6 +1132,26 @@ mod tests { }); } + #[test] + fn test_iter_bound() { + Python::with_gil(|py| { + let mut v = HashMap::new(); + v.insert(7, 32); + v.insert(8, 42); + v.insert(9, 123); + let ob = v.to_object(py); + let dict: &Bound<'_, PyDict> = ob.downcast_bound(py).unwrap(); + let mut key_sum = 0; + let mut value_sum = 0; + for (key, value) in dict { + key_sum += key.extract::().unwrap(); + value_sum += value.extract::().unwrap(); + } + assert_eq!(7 + 8 + 9, key_sum); + assert_eq!(32 + 42 + 123, value_sum); + }); + } + #[test] fn test_iter_value_mutated() { Python::with_gil(|py| { diff --git a/src/types/frozenset.rs b/src/types/frozenset.rs index 34452af0e87..8a60efb8caa 100644 --- a/src/types/frozenset.rs +++ b/src/types/frozenset.rs @@ -249,6 +249,16 @@ impl<'py> IntoIterator for Bound<'py, PyFrozenSet> { } } +impl<'py> IntoIterator for &Bound<'py, PyFrozenSet> { + type Item = Bound<'py, PyAny>; + type IntoIter = BoundFrozenSetIterator<'py>; + + /// Returns an iterator of values in this set. + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + /// PyO3 implementation of an iterator for a Python `frozenset` object. pub struct BoundFrozenSetIterator<'p> { it: Bound<'p, PyIterator>, @@ -357,6 +367,17 @@ mod tests { }); } + #[test] + fn test_frozenset_iter_bound() { + Python::with_gil(|py| { + let set = PyFrozenSet::new_bound(py, &[1]).unwrap(); + + for el in &set { + assert_eq!(1i32, el.extract::().unwrap()); + } + }); + } + #[test] fn test_frozenset_iter_size_hint() { Python::with_gil(|py| { diff --git a/src/types/iterator.rs b/src/types/iterator.rs index 848a8e692ca..bd01fe81a78 100644 --- a/src/types/iterator.rs +++ b/src/types/iterator.rs @@ -111,6 +111,15 @@ impl<'py> Borrowed<'_, 'py, PyIterator> { } } +impl<'py> IntoIterator for &Bound<'py, PyIterator> { + type Item = PyResult>; + type IntoIter = Bound<'py, PyIterator>; + + fn into_iter(self) -> Self::IntoIter { + self.clone() + } +} + impl PyTypeCheck for PyIterator { const NAME: &'static str = "Iterator"; @@ -246,6 +255,39 @@ def fibonacci(target): }); } + #[test] + fn fibonacci_generator_bound() { + use crate::types::any::PyAnyMethods; + use crate::Bound; + + let fibonacci_generator = r#" +def fibonacci(target): + a = 1 + b = 1 + for _ in range(target): + yield a + a, b = b, a + b +"#; + + Python::with_gil(|py| { + let context = PyDict::new_bound(py); + py.run_bound(fibonacci_generator, None, Some(&context)) + .unwrap(); + + let generator: Bound<'_, PyIterator> = py + .eval_bound("fibonacci(5)", None, Some(&context)) + .unwrap() + .downcast_into() + .unwrap(); + let mut items = vec![]; + for actual in &generator { + let actual = actual.unwrap().extract::().unwrap(); + items.push(actual); + } + assert_eq!(items, [1, 1, 2, 3, 5]); + }); + } + #[test] fn int_not_iterable() { Python::with_gil(|py| { diff --git a/src/types/list.rs b/src/types/list.rs index a8df26ddbc5..3aa7ddca3f0 100644 --- a/src/types/list.rs +++ b/src/types/list.rs @@ -703,6 +703,15 @@ impl<'py> IntoIterator for Bound<'py, PyList> { } } +impl<'py> IntoIterator for &Bound<'py, PyList> { + type Item = Bound<'py, PyAny>; + type IntoIter = BoundListIterator<'py>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + #[cfg(test)] #[cfg_attr(not(feature = "gil-refs"), allow(deprecated))] mod tests { @@ -911,6 +920,20 @@ mod tests { }); } + #[test] + fn test_into_iter_bound() { + use crate::types::any::PyAnyMethods; + + Python::with_gil(|py| { + let list = PyList::new_bound(py, [1, 2, 3, 4]); + let mut items = vec![]; + for item in &list { + items.push(item.extract::().unwrap()); + } + assert_eq!(items, vec![1, 2, 3, 4]); + }); + } + #[test] fn test_extract() { Python::with_gil(|py| { diff --git a/src/types/mod.rs b/src/types/mod.rs index 66312a98a0c..f0f025ee8e1 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -64,7 +64,7 @@ pub use self::typeobject::{PyType, PyTypeMethods}; /// Python::with_gil(|py| { /// let dict = py.eval_bound("{'a':'b', 'c':'d'}", None, None)?.downcast_into::()?; /// -/// for (key, value) in dict.iter() { +/// for (key, value) in &dict { /// println!("key: {}, value: {}", key, value); /// } /// diff --git a/src/types/set.rs b/src/types/set.rs index cf10f06e699..c043fa5ba4f 100644 --- a/src/types/set.rs +++ b/src/types/set.rs @@ -309,6 +309,20 @@ impl<'py> IntoIterator for Bound<'py, PySet> { } } +impl<'py> IntoIterator for &Bound<'py, PySet> { + type Item = Bound<'py, PyAny>; + type IntoIter = BoundSetIterator<'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 { + self.iter() + } +} + /// PyO3 implementation of an iterator for a Python `set` object. pub struct BoundSetIterator<'p> { it: Bound<'p, PyIterator>, @@ -482,6 +496,19 @@ mod tests { }); } + #[test] + fn test_set_iter_bound() { + use crate::types::any::PyAnyMethods; + + Python::with_gil(|py| { + let set = PySet::new_bound(py, &[1]).unwrap(); + + for el in &set { + assert_eq!(1i32, el.extract::().unwrap()); + } + }); + } + #[test] #[should_panic] fn test_set_iter_mutation() { diff --git a/src/types/tuple.rs b/src/types/tuple.rs index c379b67aa1a..d8053e1441a 100644 --- a/src/types/tuple.rs +++ b/src/types/tuple.rs @@ -554,6 +554,15 @@ impl<'py> IntoIterator for Bound<'py, PyTuple> { } } +impl<'py> IntoIterator for &Bound<'py, PyTuple> { + type Item = Bound<'py, PyAny>; + type IntoIter = BoundTupleIterator<'py>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + /// Used by `PyTuple::iter_borrowed()`. pub struct BorrowedTupleIterator<'a, 'py> { tuple: Borrowed<'a, 'py, PyTuple>, @@ -976,6 +985,23 @@ 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(); + assert_eq!(3, tuple.len()); + + let mut items = vec![]; + for item in tuple { + items.push(item.extract::().unwrap()); + } + assert_eq!(items, vec![1, 2, 3]); + }); + } + #[test] #[cfg(not(Py_LIMITED_API))] fn test_as_slice() { From b08ee4b7e17b88988f2d2ab94e24596c580222ac Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Mon, 4 Mar 2024 22:23:35 +0000 Subject: [PATCH 065/936] remove `gil-refs` feature from `full` feature (#3930) --- Cargo.toml | 3 +-- noxfile.py | 18 +++++++++++++----- src/conversions/chrono.rs | 4 ++-- src/conversions/num_bigint.rs | 8 ++++---- src/conversions/rust_decimal.rs | 14 +++----------- 5 files changed, 23 insertions(+), 24 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e7364a7c9f5..32fa5a006b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -110,7 +110,6 @@ nightly = [] # This is mostly intended for testing purposes - activating *all* of these isn't particularly useful. full = [ "macros", - "gil-refs", # "multiple-pymethods", # TODO re-add this when MSRV is greater than 1.62 "anyhow", "chrono", @@ -140,7 +139,7 @@ members = [ [package.metadata.docs.rs] no-default-features = true -features = ["full"] +features = ["full", "gil-refs"] rustdoc-args = ["--cfg", "docsrs"] [workspace.lints.clippy] diff --git a/noxfile.py b/noxfile.py index add737e5f15..2aa0f464b13 100644 --- a/noxfile.py +++ b/noxfile.py @@ -46,6 +46,7 @@ def test_rust(session: nox.Session): _run_cargo_test(session, features="abi3") if "skip-full" not in session.posargs: _run_cargo_test(session, features="full") + _run_cargo_test(session, features="full gil-refs") _run_cargo_test(session, features="abi3 full") @@ -617,6 +618,7 @@ def check_feature_powerset(session: nox.Session): EXCLUDED_FROM_FULL = { "nightly", + "gil-refs", "extension-module", "full", "default", @@ -658,10 +660,15 @@ def check_feature_powerset(session: nox.Session): session.error("no experimental features exist; please simplify the noxfile") features_to_skip = [ - *EXCLUDED_FROM_FULL, + *(EXCLUDED_FROM_FULL - {"gil-refs"}), *abi3_version_features, ] + # deny warnings + env = os.environ.copy() + rust_flags = env.get("RUSTFLAGS", "") + env["RUSTFLAGS"] = f"{rust_flags} -Dwarnings" + comma_join = ",".join _run_cargo( session, @@ -672,6 +679,7 @@ def check_feature_powerset(session: nox.Session): *(f"--group-features={comma_join(group)}" for group in features_to_group), "check", "--all-targets", + env=env, ) @@ -715,8 +723,8 @@ def _get_feature_sets() -> Tuple[Tuple[str, ...], ...]: "--no-default-features", "--features=abi3", ), - ("--features=full multiple-pymethods",), - ("--features=abi3 full multiple-pymethods",), + ("--features=full gil-refs multiple-pymethods",), + ("--features=abi3 full gil-refs multiple-pymethods",), ) else: return ( @@ -725,8 +733,8 @@ def _get_feature_sets() -> Tuple[Tuple[str, ...], ...]: "--no-default-features", "--features=abi3", ), - ("--features=full",), - ("--features=abi3 full",), + ("--features=full gil-refs",), + ("--features=abi3 full gil-refs",), ) diff --git a/src/conversions/chrono.rs b/src/conversions/chrono.rs index 3aa6dbeb4ca..134724b2249 100644 --- a/src/conversions/chrono.rs +++ b/src/conversions/chrono.rs @@ -980,13 +980,13 @@ mod tests { let td = new_py_datetime_ob(py, "timedelta", (0, 3600, 0)); let py_timedelta = new_py_datetime_ob(py, "timezone", (td,)); // Should be equal - assert!(offset.as_ref(py).eq(py_timedelta).unwrap()); + assert!(offset.bind(py).eq(py_timedelta).unwrap()); // Same but with negative values let offset = FixedOffset::east_opt(-3600).unwrap().to_object(py); let td = new_py_datetime_ob(py, "timedelta", (0, -3600, 0)); let py_timedelta = new_py_datetime_ob(py, "timezone", (td,)); - assert!(offset.as_ref(py).eq(py_timedelta).unwrap()); + assert!(offset.bind(py).eq(py_timedelta).unwrap()); }) } diff --git a/src/conversions/num_bigint.rs b/src/conversions/num_bigint.rs index 69bc5493366..743df8a9923 100644 --- a/src/conversions/num_bigint.rs +++ b/src/conversions/num_bigint.rs @@ -282,7 +282,7 @@ mod tests { let mut f0 = 1.to_object(py); let mut f1 = 1.to_object(py); std::iter::from_fn(move || { - let f2 = f0.call_method1(py, "__add__", (f1.as_ref(py),)).unwrap(); + let f2 = f0.call_method1(py, "__add__", (f1.bind(py),)).unwrap(); Some(std::mem::replace(&mut f0, std::mem::replace(&mut f1, f2))) }) } @@ -295,7 +295,7 @@ mod tests { // Python -> Rust assert_eq!(py_result.extract::(py).unwrap(), rs_result); // Rust -> Python - assert!(py_result.as_ref(py).eq(rs_result).unwrap()); + assert!(py_result.bind(py).eq(rs_result).unwrap()); } }); } @@ -308,7 +308,7 @@ mod tests { // Python -> Rust assert_eq!(py_result.extract::(py).unwrap(), rs_result); // Rust -> Python - assert!(py_result.as_ref(py).eq(&rs_result).unwrap()); + assert!(py_result.bind(py).eq(&rs_result).unwrap()); // negate @@ -318,7 +318,7 @@ mod tests { // Python -> Rust assert_eq!(py_result.extract::(py).unwrap(), rs_result); // Rust -> Python - assert!(py_result.as_ref(py).eq(rs_result).unwrap()); + assert!(py_result.bind(py).eq(rs_result).unwrap()); } }); } diff --git a/src/conversions/rust_decimal.rs b/src/conversions/rust_decimal.rs index 8bf2e33752d..b75cd875128 100644 --- a/src/conversions/rust_decimal.rs +++ b/src/conversions/rust_decimal.rs @@ -54,9 +54,7 @@ use crate::sync::GILOnceCell; use crate::types::any::PyAnyMethods; use crate::types::string::PyStringMethods; use crate::types::PyType; -use crate::{ - intern, Bound, FromPyObject, IntoPy, Py, PyAny, PyObject, PyResult, Python, ToPyObject, -}; +use crate::{Bound, FromPyObject, IntoPy, Py, PyAny, PyObject, PyResult, Python, ToPyObject}; use rust_decimal::Decimal; use std::str::FromStr; @@ -74,14 +72,8 @@ impl FromPyObject<'_> for Decimal { static DECIMAL_CLS: GILOnceCell> = GILOnceCell::new(); -fn get_decimal_cls(py: Python<'_>) -> PyResult<&PyType> { - DECIMAL_CLS - .get_or_try_init(py, || { - py.import_bound(intern!(py, "decimal"))? - .getattr(intern!(py, "Decimal"))? - .extract() - }) - .map(|ty| ty.as_ref(py)) +fn get_decimal_cls(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> { + DECIMAL_CLS.get_or_try_init_type_ref(py, "decimal", "Decimal") } impl ToPyObject for Decimal { From fe84fed966e7217963dbee9ad1189ebc3df5c2db Mon Sep 17 00:00:00 2001 From: Thomas Tanon Date: Wed, 6 Mar 2024 01:31:56 +0100 Subject: [PATCH 066/936] Allow inline struct, enum, fn and mod inside of declarative modules (#3902) * Inline struct, enum, fn and mod inside of declarative modules * remove news fragment --------- Co-authored-by: David Hewitt --- pyo3-macros-backend/src/module.rs | 161 +++++++++++++++--- tests/test_declarative_module.rs | 36 +++- tests/ui/invalid_pymodule_trait.stderr | 2 +- .../invalid_pymodule_two_pymodule_init.stderr | 2 +- 4 files changed, 169 insertions(+), 32 deletions(-) diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index 7528ef8102f..7173fa8647d 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -115,19 +115,10 @@ pub fn pymodule_module_impl(mut module: syn::ItemMod) -> Result { for item in &mut *items { match item { Item::Use(item_use) => { - let mut is_pyo3 = false; - item_use.attrs.retain(|attr| { - let found = attr.path().is_ident("pymodule_export"); - is_pyo3 |= found; - !found - }); - if is_pyo3 { - let cfg_attrs = item_use - .attrs - .iter() - .filter(|attr| attr.path().is_ident("cfg")) - .cloned() - .collect::>(); + let is_pymodule_export = + find_and_remove_attribute(&mut item_use.attrs, "pymodule_export"); + if is_pymodule_export { + let cfg_attrs = get_cfg_attributes(&item_use.attrs); extract_use_items( &item_use.tree, &cfg_attrs, @@ -137,23 +128,116 @@ pub fn pymodule_module_impl(mut module: syn::ItemMod) -> Result { } } Item::Fn(item_fn) => { - let mut is_module_init = false; - item_fn.attrs.retain(|attr| { - let found = attr.path().is_ident("pymodule_init"); - is_module_init |= found; - !found - }); - if is_module_init { - ensure_spanned!(pymodule_init.is_none(), item_fn.span() => "only one pymodule_init may be specified"); - let ident = &item_fn.sig.ident; + ensure_spanned!( + !has_attribute(&item_fn.attrs, "pymodule_export"), + item.span() => "`#[pymodule_export]` may only be used on `use` statements" + ); + let is_pymodule_init = + find_and_remove_attribute(&mut item_fn.attrs, "pymodule_init"); + let ident = &item_fn.sig.ident; + if is_pymodule_init { + ensure_spanned!( + !has_attribute(&item_fn.attrs, "pyfunction"), + item_fn.span() => "`#[pyfunction]` cannot be used alongside `#[pymodule_init]`" + ); + ensure_spanned!(pymodule_init.is_none(), item_fn.span() => "only one `#[pymodule_init]` may be specified"); pymodule_init = Some(quote! { #ident(module)?; }); - } else { - bail_spanned!(item.span() => "only 'use' statements and and pymodule_init functions are allowed in #[pymodule]") + } else if has_attribute(&item_fn.attrs, "pyfunction") { + module_items.push(ident.clone()); + module_items_cfg_attrs.push(get_cfg_attributes(&item_fn.attrs)); } } - item => { - bail_spanned!(item.span() => "only 'use' statements and and pymodule_init functions are allowed in #[pymodule]") + Item::Struct(item_struct) => { + ensure_spanned!( + !has_attribute(&item_struct.attrs, "pymodule_export"), + item.span() => "`#[pymodule_export]` may only be used on `use` statements" + ); + if has_attribute(&item_struct.attrs, "pyclass") { + module_items.push(item_struct.ident.clone()); + module_items_cfg_attrs.push(get_cfg_attributes(&item_struct.attrs)); + } + } + Item::Enum(item_enum) => { + ensure_spanned!( + !has_attribute(&item_enum.attrs, "pymodule_export"), + item.span() => "`#[pymodule_export]` may only be used on `use` statements" + ); + if has_attribute(&item_enum.attrs, "pyclass") { + module_items.push(item_enum.ident.clone()); + module_items_cfg_attrs.push(get_cfg_attributes(&item_enum.attrs)); + } + } + Item::Mod(item_mod) => { + ensure_spanned!( + !has_attribute(&item_mod.attrs, "pymodule_export"), + item.span() => "`#[pymodule_export]` may only be used on `use` statements" + ); + if has_attribute(&item_mod.attrs, "pymodule") { + module_items.push(item_mod.ident.clone()); + module_items_cfg_attrs.push(get_cfg_attributes(&item_mod.attrs)); + } + } + Item::ForeignMod(item) => { + ensure_spanned!( + !has_attribute(&item.attrs, "pymodule_export"), + item.span() => "`#[pymodule_export]` may only be used on `use` statements" + ); + } + Item::Trait(item) => { + ensure_spanned!( + !has_attribute(&item.attrs, "pymodule_export"), + item.span() => "`#[pymodule_export]` may only be used on `use` statements" + ); + } + Item::Const(item) => { + ensure_spanned!( + !has_attribute(&item.attrs, "pymodule_export"), + item.span() => "`#[pymodule_export]` may only be used on `use` statements" + ); + } + Item::Static(item) => { + ensure_spanned!( + !has_attribute(&item.attrs, "pymodule_export"), + item.span() => "`#[pymodule_export]` may only be used on `use` statements" + ); + } + Item::Macro(item) => { + ensure_spanned!( + !has_attribute(&item.attrs, "pymodule_export"), + item.span() => "`#[pymodule_export]` may only be used on `use` statements" + ); + } + Item::ExternCrate(item) => { + ensure_spanned!( + !has_attribute(&item.attrs, "pymodule_export"), + item.span() => "`#[pymodule_export]` may only be used on `use` statements" + ); + } + Item::Impl(item) => { + ensure_spanned!( + !has_attribute(&item.attrs, "pymodule_export"), + item.span() => "`#[pymodule_export]` may only be used on `use` statements" + ); } + Item::TraitAlias(item) => { + ensure_spanned!( + !has_attribute(&item.attrs, "pymodule_export"), + item.span() => "`#[pymodule_export]` may only be used on `use` statements" + ); + } + Item::Type(item) => { + ensure_spanned!( + !has_attribute(&item.attrs, "pymodule_export"), + item.span() => "`#[pymodule_export]` may only be used on `use` statements" + ); + } + Item::Union(item) => { + ensure_spanned!( + !has_attribute(&item.attrs, "pymodule_export"), + item.span() => "`#[pymodule_export]` may only be used on `use` statements" + ); + } + _ => (), } } @@ -355,6 +439,31 @@ fn get_pyfn_attr(attrs: &mut Vec) -> syn::Result Vec { + attrs + .iter() + .filter(|attr| attr.path().is_ident("cfg")) + .cloned() + .collect() +} + +fn find_and_remove_attribute(attrs: &mut Vec, ident: &str) -> bool { + let mut found = false; + attrs.retain(|attr| { + if attr.path().is_ident(ident) { + found = true; + false + } else { + true + } + }); + found +} + +fn has_attribute(attrs: &[syn::Attribute], ident: &str) -> bool { + attrs.iter().any(|attr| attr.path().is_ident(ident)) +} + enum PyModulePyO3Option { Crate(CrateAttribute), Name(NameAttribute), diff --git a/tests/test_declarative_module.rs b/tests/test_declarative_module.rs index 86913d9b800..3cb93765173 100644 --- a/tests/test_declarative_module.rs +++ b/tests/test_declarative_module.rs @@ -15,8 +15,8 @@ struct ValueClass { #[pymethods] impl ValueClass { #[new] - fn new(value: usize) -> ValueClass { - ValueClass { value } + fn new(value: usize) -> Self { + Self { value } } } @@ -48,6 +48,33 @@ mod declarative_module { #[pymodule_export] use super::{declarative_module2, double, MyError, ValueClass as Value}; + #[pymodule] + mod inner { + use super::*; + + #[pyfunction] + fn triple(x: usize) -> usize { + x * 3 + } + + #[pyclass] + struct Struct; + + #[pymethods] + impl Struct { + #[new] + fn new() -> Self { + Self + } + } + + #[pyclass] + enum Enum { + A, + B, + } + } + #[pymodule_init] fn init(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add("double2", m.getattr("double")?) @@ -65,7 +92,6 @@ mod declarative_submodule { use super::{double, double_value}; } -/// A module written using declarative syntax. #[pymodule] #[pyo3(name = "declarative_module_renamed")] mod declarative_module2 { @@ -84,7 +110,7 @@ fn test_declarative_module() { ); py_assert!(py, m, "m.double(2) == 4"); - py_assert!(py, m, "m.double2(3) == 6"); + py_assert!(py, m, "m.inner.triple(3) == 9"); py_assert!(py, m, "m.declarative_submodule.double(4) == 8"); py_assert!( py, @@ -97,5 +123,7 @@ fn test_declarative_module() { py_assert!(py, m, "not hasattr(m, 'LocatedClass')"); #[cfg(not(Py_LIMITED_API))] py_assert!(py, m, "hasattr(m, 'LocatedClass')"); + py_assert!(py, m, "isinstance(m.inner.Struct(), m.inner.Struct)"); + py_assert!(py, m, "isinstance(m.inner.Enum.A, m.inner.Enum)"); }) } diff --git a/tests/ui/invalid_pymodule_trait.stderr b/tests/ui/invalid_pymodule_trait.stderr index 3ed128617f5..4b02f14a540 100644 --- a/tests/ui/invalid_pymodule_trait.stderr +++ b/tests/ui/invalid_pymodule_trait.stderr @@ -1,4 +1,4 @@ -error: only 'use' statements and and pymodule_init functions are allowed in #[pymodule] +error: `#[pymodule_export]` may only be used on `use` statements --> tests/ui/invalid_pymodule_trait.rs:5:5 | 5 | #[pymodule_export] diff --git a/tests/ui/invalid_pymodule_two_pymodule_init.stderr b/tests/ui/invalid_pymodule_two_pymodule_init.stderr index 9f0900f9348..c117ebd573f 100644 --- a/tests/ui/invalid_pymodule_two_pymodule_init.stderr +++ b/tests/ui/invalid_pymodule_two_pymodule_init.stderr @@ -1,4 +1,4 @@ -error: only one pymodule_init may be specified +error: only one `#[pymodule_init]` may be specified --> tests/ui/invalid_pymodule_two_pymodule_init.rs:11:5 | 11 | fn init2(m: &PyModule) -> PyResult<()> { From 57bbc32e7c98a8266a81ca7db517d714832ad916 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Wed, 6 Mar 2024 00:54:45 +0000 Subject: [PATCH 067/936] add `experimental-async` feature (#3931) * add `experimental-async` feature * gate async doctests on feature --- Cargo.toml | 6 +- guide/src/async-await.md | 6 +- guide/src/features.md | 6 ++ newsfragments/3931.added.md | 1 + pyo3-macros-backend/Cargo.toml | 3 + pyo3-macros-backend/src/method.rs | 7 ++ pyo3-macros/Cargo.toml | 1 + src/impl_.rs | 2 +- src/lib.rs | 2 +- tests/test_compile_error.rs | 5 +- tests/test_coroutine.rs | 2 +- tests/ui/invalid_argument_attributes.rs | 25 ------- tests/ui/invalid_argument_attributes.stderr | 79 --------------------- tests/ui/invalid_cancel_handle.rs | 22 ++++++ tests/ui/invalid_cancel_handle.stderr | 72 +++++++++++++++++++ 15 files changed, 129 insertions(+), 110 deletions(-) create mode 100644 newsfragments/3931.added.md create mode 100644 tests/ui/invalid_cancel_handle.rs create mode 100644 tests/ui/invalid_cancel_handle.stderr diff --git a/Cargo.toml b/Cargo.toml index 32fa5a006b6..32dc0ba75ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -65,6 +65,9 @@ pyo3-build-config = { path = "pyo3-build-config", version = "=0.21.0-dev", featu [features] default = ["macros"] +# Enables support for `async fn` for `#[pyfunction]` and `#[pymethods]`. +experimental-async = ["macros", "pyo3-macros/experimental-async"] + # Enables pyo3::inspect module and additional type information on FromPyObject # and IntoPy traits experimental-inspect = [] @@ -115,8 +118,9 @@ full = [ "chrono", "chrono-tz", "either", - "experimental-inspect", + "experimental-async", "experimental-declarative-modules", + "experimental-inspect", "eyre", "hashbrown", "indexmap", diff --git a/guide/src/async-await.md b/guide/src/async-await.md index c14b5d93d84..688f0a65bc4 100644 --- a/guide/src/async-await.md +++ b/guide/src/async-await.md @@ -6,6 +6,7 @@ ```rust # #![allow(dead_code)] +# #[cfg(feature = "experimental-async")] { use std::{thread, time::Duration}; use futures::channel::oneshot; use pyo3::prelude::*; @@ -20,6 +21,7 @@ async fn sleep(seconds: f64, result: Option) -> Option { rx.await.unwrap(); result } +# } ``` *Python awaitables instantiated with this method can only be awaited in *asyncio* context. Other Python async runtime may be supported in the future.* @@ -72,6 +74,7 @@ Cancellation on the Python side can be caught using [`CancelHandle`]({{#PYO3_DOC ```rust # #![allow(dead_code)] +# #[cfg(feature = "experimental-async")] { use futures::FutureExt; use pyo3::prelude::*; use pyo3::coroutine::CancelHandle; @@ -83,11 +86,12 @@ async fn cancellable(#[pyo3(cancel_handle)] mut cancel: CancelHandle) { _ = cancel.cancelled().fuse() => println!("cancelled"), } } +# } ``` ## The `Coroutine` type -To make a Rust future awaitable in Python, PyO3 defines a [`Coroutine`]({{#PYO3_DOCS_URL}}/pyo3/coroutine/struct.Coroutine.html) type, which implements the Python [coroutine protocol](https://docs.python.org/3/library/collections.abc.html#collections.abc.Coroutine). +To make a Rust future awaitable in Python, PyO3 defines a [`Coroutine`]({{#PYO3_DOCS_URL}}/pyo3/coroutine/struct.Coroutine.html) type, which implements the Python [coroutine protocol](https://docs.python.org/3/library/collections.abc.html#collections.abc.Coroutine). Each `coroutine.send` call is translated to a `Future::poll` call. If a [`CancelHandle`]({{#PYO3_DOCS_URL}}/pyo3/coroutine/struct.CancelHandle.html) parameter is declared, the exception passed to `coroutine.throw` call is stored in it and can be retrieved with [`CancelHandle::cancelled`]({{#PYO3_DOCS_URL}}/pyo3/coroutine/struct.CancelHandle.html#method.cancelled); otherwise, it cancels the Rust future, and the exception is reraised; diff --git a/guide/src/features.md b/guide/src/features.md index 43124e0076e..118284959d6 100644 --- a/guide/src/features.md +++ b/guide/src/features.md @@ -51,6 +51,12 @@ If you do not enable this feature, you should call `pyo3::prepare_freethreaded_p ## Advanced Features +### `experimental-async` + +This feature adds support for `async fn` in `#[pyfunction]` and `#[pymethods]`. + +The feature has some unfinished refinements and performance improvements. To help finish this off, see [issue #1632](https://github.com/PyO3/pyo3/issues/1632) and its associated draft PRs. + ### `experimental-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/newsfragments/3931.added.md b/newsfragments/3931.added.md new file mode 100644 index 00000000000..b532adeeae5 --- /dev/null +++ b/newsfragments/3931.added.md @@ -0,0 +1 @@ +Add `experimental-async` feature. diff --git a/pyo3-macros-backend/Cargo.toml b/pyo3-macros-backend/Cargo.toml index 0e83cc29fd4..665c8c3510d 100644 --- a/pyo3-macros-backend/Cargo.toml +++ b/pyo3-macros-backend/Cargo.toml @@ -26,3 +26,6 @@ features = ["derive", "parsing", "printing", "clone-impls", "full", "extra-trait [lints] workspace = true + +[features] +experimental-async = [] diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index 1d2d22b236b..9a8cb828b22 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -512,6 +512,13 @@ impl<'a> FnSpec<'a> { } } + if self.asyncness.is_some() { + ensure_spanned!( + cfg!(feature = "experimental-async"), + self.asyncness.span() => "async functions are only supported with the `experimental-async` feature" + ); + } + let rust_call = |args: Vec, holders: &mut Vec| { let self_arg = self.tp.self_arg(cls, ExtractErrorMode::Raise, holders, ctx); diff --git a/pyo3-macros/Cargo.toml b/pyo3-macros/Cargo.toml index a0368a5f364..97d2de07cba 100644 --- a/pyo3-macros/Cargo.toml +++ b/pyo3-macros/Cargo.toml @@ -15,6 +15,7 @@ proc-macro = true [features] multiple-pymethods = [] +experimental-async = ["pyo3-macros-backend/experimental-async"] experimental-declarative-modules = [] [dependencies] diff --git a/src/impl_.rs b/src/impl_.rs index 77f9ff4ea1f..ea71b257c0e 100644 --- a/src/impl_.rs +++ b/src/impl_.rs @@ -6,7 +6,7 @@ //! APIs may may change at any time without documentation in the CHANGELOG and without //! breaking semver guarantees. -#[cfg(feature = "macros")] +#[cfg(feature = "experimental-async")] pub mod coroutine; pub mod deprecations; pub mod extract_argument; diff --git a/src/lib.rs b/src/lib.rs index 26d2ec55da1..c2c7d96a40a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -418,7 +418,7 @@ pub mod buffer; pub mod callback; pub mod conversion; mod conversions; -#[cfg(feature = "macros")] +#[cfg(feature = "experimental-async")] pub mod coroutine; #[macro_use] #[doc(hidden)] diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index 5f2d25db92f..f5fbec7a533 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -27,7 +27,8 @@ fn test_compile_errors() { t.compile_fail("tests/ui/wrong_aspyref_lifetimes.rs"); t.compile_fail("tests/ui/invalid_pyfunctions.rs"); t.compile_fail("tests/ui/invalid_pymethods.rs"); - #[cfg(Py_LIMITED_API)] + // output changes with async feature + #[cfg(all(Py_LIMITED_API, feature = "experimental-async"))] t.compile_fail("tests/ui/abi3_nativetype_inheritance.rs"); t.compile_fail("tests/ui/invalid_intern_arg.rs"); t.compile_fail("tests/ui/invalid_frozen_pyclass_borrow.rs"); @@ -48,4 +49,6 @@ fn test_compile_errors() { 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")] + t.compile_fail("tests/ui/invalid_cancel_handle.rs"); } diff --git a/tests/test_coroutine.rs b/tests/test_coroutine.rs index db79c72a233..17539fa113e 100644 --- a/tests/test_coroutine.rs +++ b/tests/test_coroutine.rs @@ -1,4 +1,4 @@ -#![cfg(feature = "macros")] +#![cfg(feature = "experimental-async")] #![cfg(not(target_arch = "wasm32"))] use std::{task::Poll, thread, time::Duration}; diff --git a/tests/ui/invalid_argument_attributes.rs b/tests/ui/invalid_argument_attributes.rs index ed9d6ce6971..311c6c03e0e 100644 --- a/tests/ui/invalid_argument_attributes.rs +++ b/tests/ui/invalid_argument_attributes.rs @@ -15,29 +15,4 @@ fn from_py_with_value_not_a_string(#[pyo3(from_py_with = func)] param: String) { #[pyfunction] fn from_py_with_repeated(#[pyo3(from_py_with = "func", from_py_with = "func")] param: String) {} -#[pyfunction] -async fn from_py_with_value_and_cancel_handle( - #[pyo3(from_py_with = "func", cancel_handle)] _param: String, -) { -} - -#[pyfunction] -async fn cancel_handle_repeated(#[pyo3(cancel_handle, cancel_handle)] _param: String) {} - -#[pyfunction] -async fn cancel_handle_repeated2( - #[pyo3(cancel_handle)] _param: String, - #[pyo3(cancel_handle)] _param2: String, -) { -} - -#[pyfunction] -fn cancel_handle_synchronous(#[pyo3(cancel_handle)] _param: String) {} - -#[pyfunction] -async fn cancel_handle_wrong_type(#[pyo3(cancel_handle)] _param: String) {} - -#[pyfunction] -async fn missing_cancel_handle_attribute(_param: pyo3::coroutine::CancelHandle) {} - fn main() {} diff --git a/tests/ui/invalid_argument_attributes.stderr b/tests/ui/invalid_argument_attributes.stderr index c122dd25f8c..d27c25fd179 100644 --- a/tests/ui/invalid_argument_attributes.stderr +++ b/tests/ui/invalid_argument_attributes.stderr @@ -27,82 +27,3 @@ 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: `from_py_with` and `cancel_handle` cannot be specified together - --> tests/ui/invalid_argument_attributes.rs:20:35 - | -20 | #[pyo3(from_py_with = "func", cancel_handle)] _param: String, - | ^^^^^^^^^^^^^ - -error: `cancel_handle` may only be specified once per argument - --> tests/ui/invalid_argument_attributes.rs:25:55 - | -25 | async fn cancel_handle_repeated(#[pyo3(cancel_handle, cancel_handle)] _param: String) {} - | ^^^^^^^^^^^^^ - -error: `cancel_handle` may only be specified once - --> tests/ui/invalid_argument_attributes.rs:30:28 - | -30 | #[pyo3(cancel_handle)] _param2: String, - | ^^^^^^^ - -error: `cancel_handle` attribute can only be used with `async fn` - --> tests/ui/invalid_argument_attributes.rs:35:53 - | -35 | fn cancel_handle_synchronous(#[pyo3(cancel_handle)] _param: String) {} - | ^^^^^^ - -error[E0308]: mismatched types - --> tests/ui/invalid_argument_attributes.rs:37:1 - | -37 | #[pyfunction] - | ^^^^^^^^^^^^^ - | | - | expected `String`, found `CancelHandle` - | arguments to this function are incorrect - | -note: function defined here - --> tests/ui/invalid_argument_attributes.rs:38:10 - | -38 | async fn cancel_handle_wrong_type(#[pyo3(cancel_handle)] _param: String) {} - | ^^^^^^^^^^^^^^^^^^^^^^^^ -------------- - = note: this error originates in the attribute macro `pyfunction` (in Nightly builds, run with -Z macro-backtrace for more info) - -error[E0277]: the trait bound `CancelHandle: PyClass` is not satisfied - --> tests/ui/invalid_argument_attributes.rs:41:50 - | -41 | async fn missing_cancel_handle_attribute(_param: pyo3::coroutine::CancelHandle) {} - | ^^^^ the trait `PyClass` is not implemented for `CancelHandle` - | - = help: the trait `PyClass` is implemented for `pyo3::coroutine::Coroutine` - = note: required for `CancelHandle` to implement `FromPyObject<'_>` - = note: required for `CancelHandle` to implement `PyFunctionArgument<'_, '_>` -note: required by a bound in `extract_argument` - --> src/impl_/extract_argument.rs - | - | pub fn extract_argument<'a, 'py, T>( - | ---------------- required by a bound in this function -... - | T: PyFunctionArgument<'a, 'py>, - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `extract_argument` - -error[E0277]: the trait bound `CancelHandle: Clone` is not satisfied - --> tests/ui/invalid_argument_attributes.rs:41:50 - | -41 | async fn missing_cancel_handle_attribute(_param: pyo3::coroutine::CancelHandle) {} - | ^^^^ the trait `Clone` is not implemented for `CancelHandle` - | - = help: the following other types implement trait `PyFunctionArgument<'a, 'py>`: - &'a 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 `PyFunctionArgument<'_, '_>` -note: required by a bound in `extract_argument` - --> src/impl_/extract_argument.rs - | - | pub fn extract_argument<'a, 'py, T>( - | ---------------- required by a bound in this function -... - | T: PyFunctionArgument<'a, 'py>, - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `extract_argument` diff --git a/tests/ui/invalid_cancel_handle.rs b/tests/ui/invalid_cancel_handle.rs new file mode 100644 index 00000000000..59076b14418 --- /dev/null +++ b/tests/ui/invalid_cancel_handle.rs @@ -0,0 +1,22 @@ +use pyo3::prelude::*; + +#[pyfunction] +async fn cancel_handle_repeated(#[pyo3(cancel_handle, cancel_handle)] _param: String) {} + +#[pyfunction] +async fn cancel_handle_repeated2( + #[pyo3(cancel_handle)] _param: String, + #[pyo3(cancel_handle)] _param2: String, +) { +} + +#[pyfunction] +fn cancel_handle_synchronous(#[pyo3(cancel_handle)] _param: String) {} + +#[pyfunction] +async fn cancel_handle_wrong_type(#[pyo3(cancel_handle)] _param: String) {} + +#[pyfunction] +async fn missing_cancel_handle_attribute(_param: pyo3::coroutine::CancelHandle) {} + +fn main() {} diff --git a/tests/ui/invalid_cancel_handle.stderr b/tests/ui/invalid_cancel_handle.stderr new file mode 100644 index 00000000000..6dc3ff3ccab --- /dev/null +++ b/tests/ui/invalid_cancel_handle.stderr @@ -0,0 +1,72 @@ +error: `cancel_handle` may only be specified once per argument + --> tests/ui/invalid_cancel_handle.rs:4:55 + | +4 | async fn cancel_handle_repeated(#[pyo3(cancel_handle, cancel_handle)] _param: String) {} + | ^^^^^^^^^^^^^ + +error: `cancel_handle` may only be specified once + --> tests/ui/invalid_cancel_handle.rs:9:28 + | +9 | #[pyo3(cancel_handle)] _param2: String, + | ^^^^^^^ + +error: `cancel_handle` attribute can only be used with `async fn` + --> tests/ui/invalid_cancel_handle.rs:14:53 + | +14 | fn cancel_handle_synchronous(#[pyo3(cancel_handle)] _param: String) {} + | ^^^^^^ + +error[E0308]: mismatched types + --> tests/ui/invalid_cancel_handle.rs:16:1 + | +16 | #[pyfunction] + | ^^^^^^^^^^^^^ + | | + | expected `String`, found `CancelHandle` + | arguments to this function are incorrect + | +note: function defined here + --> tests/ui/invalid_cancel_handle.rs:17:10 + | +17 | async fn cancel_handle_wrong_type(#[pyo3(cancel_handle)] _param: String) {} + | ^^^^^^^^^^^^^^^^^^^^^^^^ -------------- + = note: this error originates in the attribute macro `pyfunction` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0277]: the trait bound `CancelHandle: PyClass` 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` + | + = help: the trait `PyClass` is implemented for `pyo3::coroutine::Coroutine` + = note: required for `CancelHandle` to implement `FromPyObject<'_>` + = note: required for `CancelHandle` to implement `PyFunctionArgument<'_, '_>` +note: required by a bound in `extract_argument` + --> src/impl_/extract_argument.rs + | + | pub fn extract_argument<'a, 'py, T>( + | ---------------- required by a bound in this function +... + | T: PyFunctionArgument<'a, 'py>, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `extract_argument` + +error[E0277]: the trait bound `CancelHandle: Clone` is not satisfied + --> tests/ui/invalid_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 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 `PyFunctionArgument<'_, '_>` +note: required by a bound in `extract_argument` + --> src/impl_/extract_argument.rs + | + | pub fn extract_argument<'a, 'py, T>( + | ---------------- required by a bound in this function +... + | T: PyFunctionArgument<'a, 'py>, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `extract_argument` From d35f41e0bf158911efc844652683ee615c0f1d16 Mon Sep 17 00:00:00 2001 From: Thomas Tanon Date: Wed, 6 Mar 2024 18:49:43 +0100 Subject: [PATCH 068/936] Chrono: allow deprecation warning until upgrading to 0.4.35+ (#3935) * Chrono: allow deprecation warning until upgrading to 0.4.35+ Requiring 0.4.35 is blocked on MSRV (Chrono: 1.61, PyO3: 1.56) * also allow deprecated chrono in example --------- Co-authored-by: David Hewitt --- src/conversions/chrono.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/conversions/chrono.rs b/src/conversions/chrono.rs index 134724b2249..2e46a9e54ff 100644 --- a/src/conversions/chrono.rs +++ b/src/conversions/chrono.rs @@ -19,6 +19,9 @@ //! # 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}; //! @@ -39,6 +42,11 @@ //! }); //! } //! ``` + +// `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; From 0f7ddb19a5a67eb9266319987a39b5efb07d6e05 Mon Sep 17 00:00:00 2001 From: Thomas Tanon Date: Wed, 6 Mar 2024 19:19:43 +0100 Subject: [PATCH 069/936] PyPy: remove PyCode (PyCode_Type does not exist) (#3934) --- newsfragments/3934.removed.md | 1 + pyo3-ffi/src/cpython/code.rs | 1 + src/types/code.rs | 14 ++++++++++++++ src/types/mod.rs | 4 ++-- 4 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 newsfragments/3934.removed.md diff --git a/newsfragments/3934.removed.md b/newsfragments/3934.removed.md new file mode 100644 index 00000000000..66741d3f6b3 --- /dev/null +++ b/newsfragments/3934.removed.md @@ -0,0 +1 @@ +Remove `PyCode` and `PyCode_Type` on PyPy: `PyCode_Type` is not exposed by PyPy. \ No newline at end of file diff --git a/pyo3-ffi/src/cpython/code.rs b/pyo3-ffi/src/cpython/code.rs index 05f21a137b5..498eab59cce 100644 --- a/pyo3-ffi/src/cpython/code.rs +++ b/pyo3-ffi/src/cpython/code.rs @@ -230,6 +230,7 @@ pub const CO_FUTURE_GENERATOR_STOP: c_int = 0x8_0000; pub const CO_MAXBLOCKS: usize = 20; +#[cfg(not(PyPy))] #[cfg_attr(windows, link(name = "pythonXY"))] extern "C" { pub static mut PyCode_Type: PyTypeObject; diff --git a/src/types/code.rs b/src/types/code.rs index 8956deb1a71..f60e7783aa4 100644 --- a/src/types/code.rs +++ b/src/types/code.rs @@ -10,3 +10,17 @@ pyobject_native_type_core!( pyobject_native_static_type_object!(ffi::PyCode_Type), #checkfunction=ffi::PyCode_Check ); + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::PyTypeMethods; + use crate::{PyTypeInfo, Python}; + + #[test] + fn test_type_object() { + Python::with_gil(|py| { + assert_eq!(PyCode::type_object_bound(py).name().unwrap(), "code"); + }) + } +} diff --git a/src/types/mod.rs b/src/types/mod.rs index f0f025ee8e1..fc74b03de5b 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -5,7 +5,7 @@ pub use self::boolobject::{PyBool, PyBoolMethods}; pub use self::bytearray::{PyByteArray, PyByteArrayMethods}; pub use self::bytes::{PyBytes, PyBytesMethods}; pub use self::capsule::{PyCapsule, PyCapsuleMethods}; -#[cfg(not(Py_LIMITED_API))] +#[cfg(all(not(Py_LIMITED_API), not(PyPy)))] pub use self::code::PyCode; pub use self::complex::{PyComplex, PyComplexMethods}; #[allow(deprecated)] @@ -316,7 +316,7 @@ pub(crate) mod boolobject; pub(crate) mod bytearray; pub(crate) mod bytes; pub(crate) mod capsule; -#[cfg(not(Py_LIMITED_API))] +#[cfg(all(not(Py_LIMITED_API), not(PyPy)))] mod code; pub(crate) mod complex; #[cfg(not(Py_LIMITED_API))] From fbd531195aeb18eb23a679bc30946285120376ce Mon Sep 17 00:00:00 2001 From: Thomas Tanon Date: Wed, 6 Mar 2024 19:20:02 +0100 Subject: [PATCH 070/936] PyAddToModule: Properly propagate initialization error (#3919) Better than panics --- pyo3-macros-backend/src/pyclass.rs | 19 ++++++++++++-- src/impl_/pymodule.rs | 9 +------ src/types/mod.rs | 12 +++++++++ tests/test_declarative_module.rs | 41 ++++++++++++++++++++++++++++++ 4 files changed, 71 insertions(+), 10 deletions(-) diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 9470045733c..3eca80861b7 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -881,11 +881,12 @@ fn impl_complex_enum( } }; - let pyclass_impls: TokenStream = vec![ + let pyclass_impls: TokenStream = [ impl_builder.impl_pyclass(ctx), impl_builder.impl_extractext(ctx), enum_into_py_impl, impl_builder.impl_pyclassimpl(ctx)?, + impl_builder.impl_add_to_module(ctx), impl_builder.impl_freelist(ctx), ] .into_iter() @@ -1372,11 +1373,12 @@ impl<'a> PyClassImplsBuilder<'a> { } fn impl_all(&self, ctx: &Ctx) -> Result { - let tokens = vec![ + let tokens = [ self.impl_pyclass(ctx), self.impl_extractext(ctx), self.impl_into_py(ctx), self.impl_pyclassimpl(ctx)?, + self.impl_add_to_module(ctx), self.impl_freelist(ctx), ] .into_iter() @@ -1625,6 +1627,19 @@ impl<'a> PyClassImplsBuilder<'a> { }) } + fn impl_add_to_module(&self, ctx: &Ctx) -> TokenStream { + let Ctx { pyo3_path } = ctx; + let cls = self.cls; + quote! { + impl #pyo3_path::impl_::pymodule::PyAddToModule for #cls { + fn add_to_module(module: &#pyo3_path::Bound<'_, #pyo3_path::types::PyModule>) -> #pyo3_path::PyResult<()> { + use #pyo3_path::types::PyModuleMethods; + module.add_class::() + } + } + } + } + fn impl_freelist(&self, ctx: &Ctx) -> TokenStream { let cls = self.cls; let Ctx { pyo3_path } = ctx; diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index 9fff799c37b..a19cbda526f 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -7,8 +7,7 @@ use portable_atomic::{AtomicI64, Ordering}; #[cfg(not(PyPy))] use crate::exceptions::PyImportError; -use crate::types::module::PyModuleMethods; -use crate::{ffi, sync::GILOnceCell, types::PyModule, Bound, Py, PyResult, PyTypeInfo, Python}; +use crate::{ffi, sync::GILOnceCell, types::PyModule, Bound, Py, PyResult, Python}; /// `Sync` wrapper of `ffi::PyModuleDef`. pub struct ModuleDef { @@ -141,12 +140,6 @@ pub trait PyAddToModule { fn add_to_module(module: &Bound<'_, PyModule>) -> PyResult<()>; } -impl PyAddToModule for T { - fn add_to_module(module: &Bound<'_, PyModule>) -> PyResult<()> { - module.add(Self::NAME, Self::type_object_bound(module.py())) - } -} - #[cfg(test)] mod tests { use std::sync::atomic::{AtomicBool, Ordering}; diff --git a/src/types/mod.rs b/src/types/mod.rs index fc74b03de5b..cee45e8678d 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -249,6 +249,18 @@ macro_rules! pyobject_native_type_info( } )? } + + impl<$($generics,)*> $crate::impl_::pymodule::PyAddToModule for $name { + fn add_to_module( + module: &$crate::Bound<'_, $crate::types::PyModule>, + ) -> $crate::PyResult<()> { + use $crate::types::PyModuleMethods; + module.add( + ::NAME, + ::type_object_bound(module.py()), + ) + } + } }; ); diff --git a/tests/test_declarative_module.rs b/tests/test_declarative_module.rs index 3cb93765173..9eea8e2df07 100644 --- a/tests/test_declarative_module.rs +++ b/tests/test_declarative_module.rs @@ -3,6 +3,8 @@ use pyo3::create_exception; use pyo3::exceptions::PyException; use pyo3::prelude::*; +#[cfg(not(Py_LIMITED_API))] +use pyo3::types::PyBool; #[path = "../src/tests/common.rs"] mod common; @@ -127,3 +129,42 @@ fn test_declarative_module() { py_assert!(py, m, "isinstance(m.inner.Enum.A, m.inner.Enum)"); }) } + +#[cfg(not(Py_LIMITED_API))] +#[pyclass(extends = PyBool)] +struct ExtendsBool; + +#[cfg(not(Py_LIMITED_API))] +#[pymodule] +mod class_initialization_module { + #[pymodule_export] + use super::ExtendsBool; +} + +#[test] +#[cfg(not(Py_LIMITED_API))] +fn test_class_initialization_fails() { + Python::with_gil(|py| { + let err = class_initialization_module::DEF + .make_module(py) + .unwrap_err(); + assert_eq!( + err.to_string(), + "RuntimeError: An error occurred while initializing class ExtendsBool" + ); + }) +} + +#[pymodule] +mod r#type { + #[pymodule_export] + use super::double; +} + +#[test] +fn test_raw_ident_module() { + Python::with_gil(|py| { + let m = pyo3::wrap_pymodule!(r#type)(py).into_bound(py); + py_assert!(py, m, "m.double(2) == 4"); + }) +} From 31c4820010cc1b5e65ea27d024e5a330496ccece Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Fri, 8 Mar 2024 01:28:11 +0100 Subject: [PATCH 071/936] deprecate `&PyModule` as `#[pymodule]` argument type (#3936) * deprecate `&PyModule` as `#[pymodule]` argument type * cleanup * add ui tests * fix deprecations in tests * fix maturin and setuptools-rust starters * run `deprecated` ui test only when `gil-refs` as disabled --- examples/maturin-starter/src/lib.rs | 2 +- examples/setuptools-rust-starter/src/lib.rs | 2 +- guide/src/exception.md | 2 +- guide/src/migration.md | 2 +- pyo3-macros-backend/src/module.rs | 25 +++++++++++++ pytests/src/lib.rs | 2 +- src/impl_/pymethods.rs | 40 +++++++++++++++++++++ src/tests/hygiene/pymodule.rs | 2 ++ src/types/module.rs | 8 ++--- tests/test_compile_error.rs | 1 + tests/test_module.rs | 2 +- tests/test_no_imports.rs | 1 + tests/ui/deprecations.rs | 35 ++++++++++++++++++ tests/ui/deprecations.stderr | 12 +++++++ 14 files changed, 126 insertions(+), 10 deletions(-) diff --git a/examples/maturin-starter/src/lib.rs b/examples/maturin-starter/src/lib.rs index 8b0748f8311..faa147b2a10 100644 --- a/examples/maturin-starter/src/lib.rs +++ b/examples/maturin-starter/src/lib.rs @@ -20,7 +20,7 @@ impl ExampleClass { /// An example module implemented in Rust using PyO3. #[pymodule] -fn maturin_starter(py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn maturin_starter(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_wrapped(wrap_pymodule!(submodule::submodule))?; diff --git a/examples/setuptools-rust-starter/src/lib.rs b/examples/setuptools-rust-starter/src/lib.rs index 2bcd411c238..d31284be7a3 100644 --- a/examples/setuptools-rust-starter/src/lib.rs +++ b/examples/setuptools-rust-starter/src/lib.rs @@ -20,7 +20,7 @@ impl ExampleClass { /// An example module implemented in Rust using PyO3. #[pymodule] -fn _setuptools_rust_starter(py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn _setuptools_rust_starter(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_wrapped(wrap_pymodule!(submodule::submodule))?; diff --git a/guide/src/exception.md b/guide/src/exception.md index 0be44167760..04550bd4b92 100644 --- a/guide/src/exception.md +++ b/guide/src/exception.md @@ -44,7 +44,7 @@ use pyo3::exceptions::PyException; pyo3::create_exception!(mymodule, CustomError, PyException); #[pymodule] -fn mymodule(py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn mymodule(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { // ... other elements added to module ... m.add("CustomError", py.get_type_bound::())?; diff --git a/guide/src/migration.md b/guide/src/migration.md index 709e7a70488..8537f9a1a69 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -847,7 +847,7 @@ fn my_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { To fix it, make the private submodule visible, e.g. with `pub` or `pub(crate)`. -```rust +```rust,ignore mod foo { use pyo3::prelude::*; diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index 7173fa8647d..fb02c074996 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -11,6 +11,7 @@ use quote::quote; use syn::{ ext::IdentExt, parse::{Parse, ParseStream}, + parse_quote, parse_quote_spanned, spanned::Spanned, token::Comma, Item, Path, Result, @@ -281,6 +282,7 @@ pub fn pymodule_function_impl(mut function: syn::ItemFn) -> Result let options = PyModuleOptions::from_attrs(&mut function.attrs)?; process_functions_in_module(&options, &mut function)?; let ctx = &Ctx::new(&options.krate); + let stmts = std::mem::take(&mut function.block.stmts); let Ctx { pyo3_path } = ctx; let ident = &function.sig.ident; let vis = &function.vis; @@ -295,6 +297,29 @@ pub fn pymodule_function_impl(mut function: syn::ItemFn) -> Result } module_args.push(quote!(::std::convert::Into::into(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 = &pat_ident.ident; + return Some([ + parse_quote! { let (#ident, e) = #pyo3_path::impl_::pymethods::inspect_type(#ident); }, + parse_quote_spanned! { pat_type.span() => e.extract_gil_ref(); }, + ]); + } + } + None + }) + .flatten(); + + function.block.stmts = extractors.chain(stmts).collect(); + function + .attrs + .push(parse_quote!(#[allow(clippy::used_underscore_binding)])); + Ok(quote! { #function #vis mod #ident { diff --git a/pytests/src/lib.rs b/pytests/src/lib.rs index bfd80edb719..cbd65c8012c 100644 --- a/pytests/src/lib.rs +++ b/pytests/src/lib.rs @@ -18,7 +18,7 @@ pub mod sequence; pub mod subclassing; #[pymodule] -fn pyo3_pytests(py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn pyo3_pytests(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_wrapped(wrap_pymodule!(awaitable::awaitable))?; #[cfg(not(Py_LIMITED_API))] m.add_wrapped(wrap_pymodule!(buf_and_str::buf_and_str))?; diff --git a/src/impl_/pymethods.rs b/src/impl_/pymethods.rs index df89dba7dbd..a7df90b572d 100644 --- a/src/impl_/pymethods.rs +++ b/src/impl_/pymethods.rs @@ -580,3 +580,43 @@ pub unsafe fn tp_new_impl( .create_class_object_of_type(py, target_type) .map(Bound::into_ptr) } + +pub fn inspect_type(t: T) -> (T, Extractor) { + (t, Extractor::new()) +} + +pub struct Extractor(NotAGilRef); +pub struct NotAGilRef(std::marker::PhantomData); + +pub trait IsGilRef {} + +impl IsGilRef for &'_ T {} + +impl Extractor { + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + Extractor(NotAGilRef(std::marker::PhantomData)) + } +} + +impl Extractor { + #[cfg_attr( + not(feature = "gil-refs"), + deprecated( + since = "0.21.0", + note = "use `&Bound<'_, T>` instead for this function argument" + ) + )] + pub fn extract_gil_ref(&self) {} +} + +impl NotAGilRef { + pub fn extract_gil_ref(&self) {} +} + +impl std::ops::Deref for Extractor { + type Target = NotAGilRef; + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/src/tests/hygiene/pymodule.rs b/src/tests/hygiene/pymodule.rs index bb49d3823c1..6229708dd06 100644 --- a/src/tests/hygiene/pymodule.rs +++ b/src/tests/hygiene/pymodule.rs @@ -7,12 +7,14 @@ fn do_something(x: i32) -> crate::PyResult { ::std::result::Result::Ok(x) } +#[allow(deprecated)] #[crate::pymodule] #[pyo3(crate = "crate")] fn foo(_py: crate::Python<'_>, _m: &crate::types::PyModule) -> crate::PyResult<()> { ::std::result::Result::Ok(()) } +#[allow(deprecated)] #[crate::pymodule] #[pyo3(crate = "crate")] fn my_module(_py: crate::Python<'_>, m: &crate::types::PyModule) -> crate::PyResult<()> { diff --git a/src/types/module.rs b/src/types/module.rs index d6f3acb2567..993060703e6 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -335,11 +335,11 @@ impl PyModule { /// use pyo3::prelude::*; /// /// #[pymodule] - /// fn my_module(py: Python<'_>, module: &PyModule) -> PyResult<()> { + /// 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.as_gil_ref())?; + /// module.add_submodule(&submodule)?; /// Ok(()) /// } /// ``` @@ -530,11 +530,11 @@ pub trait PyModuleMethods<'py>: crate::sealed::Sealed { /// use pyo3::prelude::*; /// /// #[pymodule] - /// fn my_module(py: Python<'_>, module: &PyModule) -> PyResult<()> { + /// 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.as_gil_ref())?; + /// module.add_submodule(&submodule)?; /// Ok(()) /// } /// ``` diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index f5fbec7a533..3f1052c3077 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -18,6 +18,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"))] 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/test_module.rs b/tests/test_module.rs index d4c4acca90f..0eef3c0e2d1 100644 --- a/tests/test_module.rs +++ b/tests/test_module.rs @@ -118,7 +118,7 @@ fn test_module_with_functions() { /// This module uses a legacy two-argument module function. #[pymodule] -fn module_with_explicit_py_arg(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +fn module_with_explicit_py_arg(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(double, m)?)?; Ok(()) } diff --git a/tests/test_no_imports.rs b/tests/test_no_imports.rs index 69f4b6e4651..4c77cc8e2a0 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()) } +#[allow(deprecated)] #[pyo3::pymodule] fn basic_module(_py: pyo3::Python<'_>, m: &pyo3::types::PyModule) -> pyo3::PyResult<()> { #[pyfn(m)] diff --git a/tests/ui/deprecations.rs b/tests/ui/deprecations.rs index 4177dd6da22..06bcf8197a7 100644 --- a/tests/ui/deprecations.rs +++ b/tests/ui/deprecations.rs @@ -14,3 +14,38 @@ impl MyClass { } fn main() {} + +#[pyfunction] +fn double(x: usize) -> usize { + x * 2 +} + +#[pymodule] +fn module_gil_ref(m: &PyModule) -> PyResult<()> { + m.add_function(wrap_pyfunction!(double, m)?)?; + Ok(()) +} + +#[pymodule] +fn module_gil_ref_with_explicit_py_arg(_py: Python<'_>, m: &PyModule) -> PyResult<()> { + m.add_function(wrap_pyfunction!(double, m)?)?; + 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(()) +} diff --git a/tests/ui/deprecations.stderr b/tests/ui/deprecations.stderr index a857b5ee420..2440f909019 100644 --- a/tests/ui/deprecations.stderr +++ b/tests/ui/deprecations.stderr @@ -9,3 +9,15 @@ note: the lint level is defined here | 1 | #![deny(deprecated)] | ^^^^^^^^^^ + +error: use of deprecated method `pyo3::methods::Extractor::::extract_gil_ref`: use `&Bound<'_, T>` instead for this function argument + --> tests/ui/deprecations.rs:24:19 + | +24 | fn module_gil_ref(m: &PyModule) -> PyResult<()> { + | ^ + +error: use of deprecated method `pyo3::methods::Extractor::::extract_gil_ref`: use `&Bound<'_, T>` instead for this function argument + --> tests/ui/deprecations.rs:30:57 + | +30 | fn module_gil_ref_with_explicit_py_arg(_py: Python<'_>, m: &PyModule) -> PyResult<()> { + | ^ From 770d9b7f014cc70213950065b9b60bb3679464cd Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 8 Mar 2024 07:43:48 +0000 Subject: [PATCH 072/936] add `FromPyObjectBound` trait for extracting `&str` without GIL Refs (#3928) * add `FromPyObjectBound` adjustment for `&str` without GIL Refs * review: alex, Icxolu feedback * add newsfragment * add newsfragment for `FromPyObject` trait change * make some examples compatible with abi3 < 3.10 * seal `FromPyObjectBound` * fixup chrono_tz conversion --- guide/src/migration.md | 10 ++++ newsfragments/3928.added.md | 1 + newsfragments/3928.changed.md | 1 + noxfile.py | 3 +- src/conversion.rs | 83 ++++++++++++++++++++++++++- src/conversions/chrono_tz.rs | 8 ++- src/conversions/either.rs | 4 +- src/conversions/std/slice.rs | 43 ++++++++++++-- src/conversions/std/string.rs | 55 +++++++++++++++--- src/impl_/extract_argument.rs | 19 +++++- src/impl_/pymodule.rs | 9 ++- src/inspect/types.rs | 5 +- src/instance.rs | 7 ++- src/types/any.rs | 32 +++++------ src/types/bytes.rs | 9 ++- src/types/module.rs | 10 +++- tests/test_class_conversion.rs | 2 +- tests/test_compile_error.rs | 1 + tests/test_pyfunction.rs | 2 +- tests/ui/invalid_cancel_handle.stderr | 2 + 20 files changed, 251 insertions(+), 55 deletions(-) create mode 100644 newsfragments/3928.added.md create mode 100644 newsfragments/3928.changed.md diff --git a/guide/src/migration.md b/guide/src/migration.md index 8537f9a1a69..fa4c23cc969 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -211,6 +211,8 @@ Interactions with Python objects implemented in Rust no longer need to go though To minimise breakage of code using the GIL-Refs API, the `Bound` smart pointer has been introduced by adding complements to all functions which accept or return GIL Refs. This allows code to migrate by replacing the deprecated APIs with the new ones. +To identify what to migrate, temporarily switch off the `gil-refs` feature to see deprecation warnings on all uses of APIs accepting and producing GIL Refs. Over one or more PRs it should be possible to follow the deprecation hints to update code. Depending on your development environment, switching off the `gil-refs` feature may introduce [some very targeted breakages](#deactivating-the-gil-refs-feature), so you may need to fixup those first. + For example, the following APIs have gained updated variants: - `PyList::new`, `PyTyple::new` and similar constructors have replacements `PyList::new_bound`, `PyTuple::new_bound` etc. - `FromPyObject::extract` has a new `FromPyObject::extract_bound` (see the section below) @@ -272,7 +274,15 @@ impl<'py> FromPyObject<'py> for MyType { The expectation is that in 0.22 `extract_bound` will have the default implementation removed and in 0.23 `extract` will be removed. +### Deactivating the `gil-refs` feature + +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. + +There is one notable API removed when this feature is disabled. `FromPyObject` trait implementations for types which borrow directly from the input data cannot be implemented by PyO3 without GIL Refs (while the migration is ongoing). These types are `&str`, `Cow<'_, str>`, `&[u8]`, `Cow<'_, u8>`. + +To ease pain during migration, these types instead implement a new temporary trait `FromPyObjectBound` which is the expected future form of `FromPyObject`. The new temporary trait ensures is that `obj.extract::<&str>()` continues to work (with the new constraint that the extracted value now depends on the input `obj` lifetime), as well for these types in `#[pyfunction]` arguments. +An unfortunate final point here is that PyO3 cannot offer this new implementation for `&str` on `abi3` builds for Python older than 3.10. On code which needs `abi3` builds for these older Python versions, many cases of `.extract::<&str>()` may need to be replaced with `.extract::()`, which is string data which borrows from the Python `str` object. Alternatively, use `.extract::>()`, `.extract::()` to copy the data into Rust for these versions. ## from 0.19.* to 0.20 diff --git a/newsfragments/3928.added.md b/newsfragments/3928.added.md new file mode 100644 index 00000000000..e768e6b0163 --- /dev/null +++ b/newsfragments/3928.added.md @@ -0,0 +1 @@ +Implement `FromPyObject` for `Cow`. diff --git a/newsfragments/3928.changed.md b/newsfragments/3928.changed.md new file mode 100644 index 00000000000..a4734c41ed3 --- /dev/null +++ b/newsfragments/3928.changed.md @@ -0,0 +1 @@ +Move implementations of `FromPyObject` for `&str`, `Cow`, `&[u8]` and `Cow<[u8]>` onto a temporary trait `FromPyObjectBound` when `gil-refs` feature is deactivated. diff --git a/noxfile.py b/noxfile.py index 2aa0f464b13..3bba2574885 100644 --- a/noxfile.py +++ b/noxfile.py @@ -613,8 +613,7 @@ def check_feature_powerset(session: nox.Session): if toml is None: session.error("requires Python 3.11 or `toml` to be installed") - with (PYO3_DIR / "Cargo.toml").open("rb") as cargo_toml_file: - cargo_toml = toml.load(cargo_toml_file) + cargo_toml = toml.loads((PYO3_DIR / "Cargo.toml").read_text()) EXCLUDED_FROM_FULL = { "nightly", diff --git a/src/conversion.rs b/src/conversion.rs index 68633d196be..efb09b6b341 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -178,12 +178,12 @@ pub trait IntoPy: Sized { /// Python::with_gil(|py| { /// // Calling `.extract()` on a `Bound` smart pointer /// let obj: Bound<'_, PyString> = PyString::new_bound(py, "blah"); -/// let s: &str = obj.extract()?; +/// let s: String = obj.extract()?; /// # assert_eq!(s, "blah"); /// /// // Calling `.extract(py)` on a `Py` smart pointer /// let obj: Py = obj.unbind(); -/// let s: &str = obj.extract(py)?; +/// let s: String = obj.extract(py)?; /// # assert_eq!(s, "blah"); /// # Ok(()) /// }) @@ -237,6 +237,85 @@ pub trait FromPyObject<'py>: Sized { } } +mod from_py_object_bound_sealed { + /// Private seal for the `FromPyObjectBound` trait. + /// + /// This prevents downstream types from implementing the trait before + /// PyO3 is ready to declare the trait as public API. + pub trait Sealed {} + + // This generic implementation is why the seal is separate from + // `crate::sealed::Sealed`. + impl<'py, T> Sealed for T where T: super::FromPyObject<'py> {} + #[cfg(not(feature = "gil-refs"))] + impl Sealed for &'_ str {} + #[cfg(not(feature = "gil-refs"))] + impl Sealed for std::borrow::Cow<'_, str> {} + #[cfg(not(feature = "gil-refs"))] + impl Sealed for &'_ [u8] {} + #[cfg(not(feature = "gil-refs"))] + impl Sealed for std::borrow::Cow<'_, [u8]> {} +} + +/// Expected form of [`FromPyObject`] to be used in a future PyO3 release. +/// +/// The difference between this and `FromPyObject` is that this trait takes an +/// additional lifetime `'a`, which is the lifetime of the input `Bound`. +/// +/// This allows implementations for `&'a str` and `&'a [u8]`, which could not +/// be expressed by the existing `FromPyObject` trait once the GIL Refs API was +/// removed. +/// +/// # Usage +/// +/// Users are prevented from implementing this trait, instead they should implement +/// the normal `FromPyObject` trait. This trait has a blanket implementation +/// for `T: FromPyObject`. +/// +/// The only case where this trait may have a use case to be implemented is when the +/// lifetime of the extracted value is tied to the lifetime `'a` of the input `Bound` +/// instead of the GIL lifetime `py`, as is the case for the `&'a str` implementation. +/// +/// Please contact the PyO3 maintainers if you believe you have a use case for implementing +/// this trait before PyO3 is ready to change the main `FromPyObject` trait to take an +/// additional lifetime. +/// +/// Similarly, users should typically not call these trait methods and should instead +/// use this via the `extract` method on `Bound` and `Py`. +pub trait FromPyObjectBound<'a, 'py>: Sized + from_py_object_bound_sealed::Sealed { + /// Extracts `Self` from the bound smart pointer `obj`. + /// + /// Users are advised against calling this method directly: instead, use this via + /// [`Bound<'_, PyAny>::extract`] or [`Py::extract`]. + fn from_py_object_bound(ob: &'a Bound<'py, PyAny>) -> PyResult; + + /// Extracts the type hint information for this type when it appears as an argument. + /// + /// For example, `Vec` would return `Sequence[int]`. + /// The default implementation returns `Any`, which is correct for any type. + /// + /// For most types, the return value for this method will be identical to that of [`IntoPy::type_output`]. + /// It may be different for some types, such as `Dict`, to allow duck-typing: functions return `Dict` but take `Mapping` as argument. + #[cfg(feature = "experimental-inspect")] + fn type_input() -> TypeInfo { + TypeInfo::Any + } +} + +impl<'py, T> FromPyObjectBound<'_, 'py> for T +where + T: FromPyObject<'py>, +{ + fn from_py_object_bound(ob: &Bound<'py, PyAny>) -> PyResult { + Self::extract_bound(ob) + } + + #[cfg(feature = "experimental-inspect")] + fn type_input() -> TypeInfo { + ::type_input() + } +} + /// Identity conversion: allows using existing `PyObject` instances where /// `T: ToPyObject` is expected. impl ToPyObject for &'_ T { diff --git a/src/conversions/chrono_tz.rs b/src/conversions/chrono_tz.rs index b4d0e7edf8c..845814c4dab 100644 --- a/src/conversions/chrono_tz.rs +++ b/src/conversions/chrono_tz.rs @@ -34,6 +34,7 @@ //! } //! ``` use crate::exceptions::PyValueError; +use crate::pybacked::PyBackedStr; use crate::sync::GILOnceCell; use crate::types::{any::PyAnyMethods, PyType}; use crate::{ @@ -62,8 +63,11 @@ impl IntoPy for Tz { impl FromPyObject<'_> for Tz { fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { - Tz::from_str(ob.getattr(intern!(ob.py(), "key"))?.extract()?) - .map_err(|e| PyValueError::new_err(e.to_string())) + Tz::from_str( + &ob.getattr(intern!(ob.py(), "key"))? + .extract::()?, + ) + .map_err(|e| PyValueError::new_err(e.to_string())) } } diff --git a/src/conversions/either.rs b/src/conversions/either.rs index 16d5ad491eb..84ec88ea009 100644 --- a/src/conversions/either.rs +++ b/src/conversions/either.rs @@ -113,6 +113,8 @@ where #[cfg(test)] mod tests { + use std::borrow::Cow; + use crate::exceptions::PyTypeError; use crate::{Python, ToPyObject}; @@ -132,7 +134,7 @@ mod tests { let r = E::Right("foo".to_owned()); let obj_r = r.to_object(py); - assert_eq!(obj_r.extract::<&str>(py).unwrap(), "foo"); + assert_eq!(obj_r.extract::>(py).unwrap(), "foo"); assert_eq!(obj_r.extract::(py).unwrap(), r); let obj_s = "foo".to_object(py); diff --git a/src/conversions/std/slice.rs b/src/conversions/std/slice.rs index dad27f081cf..18bb52cdd1a 100644 --- a/src/conversions/std/slice.rs +++ b/src/conversions/std/slice.rs @@ -2,9 +2,11 @@ use std::borrow::Cow; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; +#[cfg(not(feature = "gil-refs"))] +use crate::types::PyBytesMethods; use crate::{ - types::{PyByteArray, PyBytes}, - FromPyObject, IntoPy, Py, PyAny, PyObject, PyResult, Python, ToPyObject, + types::{PyAnyMethods, PyByteArray, PyByteArrayMethods, PyBytes}, + Bound, IntoPy, Py, PyAny, PyObject, PyResult, Python, ToPyObject, }; impl<'a> IntoPy for &'a [u8] { @@ -18,7 +20,8 @@ impl<'a> IntoPy for &'a [u8] { } } -impl<'py> FromPyObject<'py> for &'py [u8] { +#[cfg(feature = "gil-refs")] +impl<'py> crate::FromPyObject<'py> for &'py [u8] { fn extract(obj: &'py PyAny) -> PyResult { Ok(obj.downcast::()?.as_bytes()) } @@ -29,13 +32,38 @@ impl<'py> FromPyObject<'py> for &'py [u8] { } } +#[cfg(not(feature = "gil-refs"))] +impl<'a> crate::conversion::FromPyObjectBound<'a, '_> for &'a [u8] { + fn from_py_object_bound(obj: &'a Bound<'_, PyAny>) -> PyResult { + Ok(obj.downcast::()?.as_bytes()) + } + + #[cfg(feature = "experimental-inspect")] + fn type_input() -> TypeInfo { + Self::type_output() + } +} + /// Special-purpose trait impl to efficiently handle both `bytes` and `bytearray` /// /// If the source object is a `bytes` object, the `Cow` will be borrowed and /// pointing into the source object, and no copying or heap allocations will happen. /// If it is a `bytearray`, its contents will be copied to an owned `Cow`. -impl<'py> FromPyObject<'py> for Cow<'py, [u8]> { - fn extract(ob: &'py PyAny) -> PyResult { +#[cfg(feature = "gil-refs")] +impl<'py> crate::FromPyObject<'py> for Cow<'py, [u8]> { + fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { + 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: &'a Bound<'_, PyAny>) -> PyResult { if let Ok(bytes) = ob.downcast::() { return Ok(Cow::Borrowed(bytes.as_bytes())); } @@ -43,6 +71,11 @@ impl<'py> FromPyObject<'py> for Cow<'py, [u8]> { let byte_array = ob.downcast::()?; Ok(Cow::Owned(byte_array.to_vec())) } + + #[cfg(feature = "experimental-inspect")] + fn type_input() -> TypeInfo { + Self::type_output() + } } impl ToPyObject for Cow<'_, [u8]> { diff --git a/src/conversions/std/string.rs b/src/conversions/std/string.rs index 60e8ff7eb53..d013890a77e 100644 --- a/src/conversions/std/string.rs +++ b/src/conversions/std/string.rs @@ -113,7 +113,8 @@ impl<'a> IntoPy for &'a String { } /// Allows extracting strings from Python objects. -/// Accepts Python `str` and `unicode` objects. +/// 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() @@ -125,6 +126,42 @@ impl<'py> FromPyObject<'py> for &'py str { } } +#[cfg(all(not(feature = "gil-refs"), any(Py_3_10, not(Py_LIMITED_API))))] +impl<'a> crate::conversion::FromPyObjectBound<'a, '_> for &'a str { + fn from_py_object_bound(ob: &'a Bound<'_, PyAny>) -> PyResult { + ob.downcast::()?.to_str() + } + + #[cfg(feature = "experimental-inspect")] + fn type_input() -> TypeInfo { + ::type_input() + } +} + +#[cfg(feature = "gil-refs")] +impl<'py> FromPyObject<'py> for Cow<'py, str> { + fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { + ob.extract().map(Cow::Owned) + } + + #[cfg(feature = "experimental-inspect")] + fn type_input() -> TypeInfo { + ::type_input() + } +} + +#[cfg(not(feature = "gil-refs"))] +impl<'a> crate::conversion::FromPyObjectBound<'a, '_> for Cow<'a, str> { + fn from_py_object_bound(ob: &'a Bound<'_, PyAny>) -> PyResult { + ob.downcast::()?.to_cow() + } + + #[cfg(feature = "experimental-inspect")] + fn type_input() -> TypeInfo { + ::type_input() + } +} + /// Allows extracting strings from Python objects. /// Accepts Python `str` and `unicode` objects. impl FromPyObject<'_> for String { @@ -169,9 +206,9 @@ mod tests { Python::with_gil(|py| { let s = "Hello Python"; let py_string: PyObject = Cow::Borrowed(s).into_py(py); - assert_eq!(s, py_string.extract::<&str>(py).unwrap()); + assert_eq!(s, py_string.extract::>(py).unwrap()); let py_string: PyObject = Cow::::Owned(s.into()).into_py(py); - assert_eq!(s, py_string.extract::<&str>(py).unwrap()); + assert_eq!(s, py_string.extract::>(py).unwrap()); }) } @@ -180,9 +217,9 @@ mod tests { Python::with_gil(|py| { let s = "Hello Python"; let py_string = Cow::Borrowed(s).to_object(py); - assert_eq!(s, py_string.extract::<&str>(py).unwrap()); + assert_eq!(s, py_string.extract::>(py).unwrap()); let py_string = Cow::::Owned(s.into()).to_object(py); - assert_eq!(s, py_string.extract::<&str>(py).unwrap()); + assert_eq!(s, py_string.extract::>(py).unwrap()); }) } @@ -201,7 +238,7 @@ mod tests { let s = "Hello Python"; let py_string = s.to_object(py); - let s2: &str = py_string.bind(py).extract().unwrap(); + let s2: Cow<'_, str> = py_string.bind(py).extract().unwrap(); assert_eq!(s, s2); }) } @@ -238,19 +275,19 @@ mod tests { assert_eq!( s, IntoPy::::into_py(s3, py) - .extract::<&str>(py) + .extract::>(py) .unwrap() ); assert_eq!( s, IntoPy::::into_py(s2, py) - .extract::<&str>(py) + .extract::>(py) .unwrap() ); assert_eq!( s, IntoPy::::into_py(s, py) - .extract::<&str>(py) + .extract::>(py) .unwrap() ); }) diff --git a/src/impl_/extract_argument.rs b/src/impl_/extract_argument.rs index d2e2b18cf05..82285b3d50c 100644 --- a/src/impl_/extract_argument.rs +++ b/src/impl_/extract_argument.rs @@ -1,10 +1,10 @@ use crate::{ + conversion::FromPyObjectBound, exceptions::PyTypeError, ffi, pyclass::boolean_struct::False, types::{any::PyAnyMethods, dict::PyDictMethods, tuple::PyTupleMethods, PyDict, PyTuple}, - Borrowed, Bound, FromPyObject, PyAny, PyClass, PyErr, PyRef, PyRefMut, PyResult, PyTypeCheck, - Python, + Borrowed, Bound, PyAny, PyClass, PyErr, PyRef, PyRefMut, PyResult, PyTypeCheck, Python, }; /// Helper type used to keep implementation more concise. @@ -27,7 +27,7 @@ pub trait PyFunctionArgument<'a, 'py>: Sized + 'a { impl<'a, 'py, T> PyFunctionArgument<'a, 'py> for T where - T: FromPyObject<'py> + 'a, + T: FromPyObjectBound<'a, 'py> + 'a, { type Holder = (); @@ -52,6 +52,19 @@ where } } +#[cfg(all(Py_LIMITED_API, not(any(feature = "gil-refs", Py_3_10))))] +impl<'a> PyFunctionArgument<'a, '_> for &'a str { + type Holder = Option>; + + #[inline] + fn extract( + obj: &'a Bound<'_, PyAny>, + holder: &'a mut Option>, + ) -> PyResult { + Ok(holder.insert(obj.extract()?)) + } +} + /// Trait for types which can be a function argument holder - they should /// to be able to const-initialize to an empty value. pub trait FunctionArgumentHolder: Sized { diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index a19cbda526f..9ca80e3918c 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -142,7 +142,10 @@ pub trait PyAddToModule { #[cfg(test)] mod tests { - use std::sync::atomic::{AtomicBool, Ordering}; + use std::{ + borrow::Cow, + sync::atomic::{AtomicBool, Ordering}, + }; use crate::{ types::{any::PyAnyMethods, module::PyModuleMethods, PyModule}, @@ -169,7 +172,7 @@ mod tests { module .getattr("__name__") .unwrap() - .extract::<&str>() + .extract::>() .unwrap(), "test_module", ); @@ -177,7 +180,7 @@ mod tests { module .getattr("__doc__") .unwrap() - .extract::<&str>() + .extract::>() .unwrap(), "some doc", ); diff --git a/src/inspect/types.rs b/src/inspect/types.rs index d8f8e6a19ea..13ce62585b1 100644 --- a/src/inspect/types.rs +++ b/src/inspect/types.rs @@ -464,7 +464,10 @@ mod conversion { assert_display(&String::type_input(), "str"); assert_display(&<&[u8]>::type_output(), "bytes"); - assert_display(&<&[u8]>::type_input(), "bytes"); + assert_display( + &<&[u8] as crate::conversion::FromPyObjectBound>::type_input(), + "bytes", + ); } #[test] diff --git a/src/instance.rs b/src/instance.rs index cef06062430..069bea69f8c 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -1315,9 +1315,12 @@ impl Py { /// Extracts some type from the Python object. /// /// This is a wrapper function around `FromPyObject::extract()`. - pub fn extract<'py, D>(&self, py: Python<'py>) -> PyResult + pub fn extract<'a, 'py, D>(&'a self, py: Python<'py>) -> PyResult where - D: FromPyObject<'py>, + D: crate::conversion::FromPyObjectBound<'a, 'py>, + // TODO it might be possible to relax this bound in future, to allow + // e.g. `.extract::<&str>(py)` where `py` is short-lived. + 'py: 'a, { self.bind(py).as_any().extract() } diff --git a/src/types/any.rs b/src/types/any.rs index 0207217ba96..e4dbaf80200 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -1,5 +1,5 @@ use crate::class::basic::CompareOp; -use crate::conversion::{AsPyPointer, FromPyObject, IntoPy, ToPyObject}; +use crate::conversion::{AsPyPointer, FromPyObject, FromPyObjectBound, IntoPy, ToPyObject}; use crate::err::{DowncastError, DowncastIntoError, PyDowncastError, PyErr, PyResult}; use crate::exceptions::{PyAttributeError, PyTypeError}; use crate::ffi_ptr_ext::FfiPtrExt; @@ -391,7 +391,7 @@ impl PyAny { /// let kwargs = PyDict::new_bound(py); /// kwargs.set_item("cruel", "world")?; /// let result = fun.call(args, Some(&kwargs))?; - /// assert_eq!(result.extract::<&str>()?, "called with args and kwargs"); + /// assert_eq!(result.extract::()?, "called with args and kwargs"); /// Ok(()) /// }) /// # } @@ -452,7 +452,7 @@ impl PyAny { /// let fun = module.getattr("function")?; /// let args = ("hello",); /// let result = fun.call1(args)?; - /// assert_eq!(result.extract::<&str>()?, "called with args"); + /// assert_eq!(result.extract::()?, "called with args"); /// Ok(()) /// }) /// # } @@ -491,7 +491,7 @@ impl PyAny { /// let kwargs = PyDict::new_bound(py); /// kwargs.set_item("cruel", "world")?; /// let result = instance.call_method("method", args, Some(&kwargs))?; - /// assert_eq!(result.extract::<&str>()?, "called with args and kwargs"); + /// assert_eq!(result.extract::()?, "called with args and kwargs"); /// Ok(()) /// }) /// # } @@ -532,7 +532,7 @@ impl PyAny { /// let module = PyModule::from_code_bound(py, CODE, "", "")?; /// let instance = module.getattr("a")?; /// let result = instance.call_method0("method")?; - /// assert_eq!(result.extract::<&str>()?, "called with no arguments"); + /// assert_eq!(result.extract::()?, "called with no arguments"); /// Ok(()) /// }) /// # } @@ -573,7 +573,7 @@ impl PyAny { /// let instance = module.getattr("a")?; /// let args = ("hello",); /// let result = instance.call_method1("method", args)?; - /// assert_eq!(result.extract::<&str>()?, "called with args"); + /// assert_eq!(result.extract::()?, "called with args"); /// Ok(()) /// }) /// # } @@ -1271,7 +1271,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// let kwargs = PyDict::new_bound(py); /// kwargs.set_item("cruel", "world")?; /// let result = fun.call(args, Some(&kwargs))?; - /// assert_eq!(result.extract::<&str>()?, "called with args and kwargs"); + /// assert_eq!(result.extract::()?, "called with args and kwargs"); /// Ok(()) /// }) /// # } @@ -1326,7 +1326,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// let fun = module.getattr("function")?; /// let args = ("hello",); /// let result = fun.call1(args)?; - /// assert_eq!(result.extract::<&str>()?, "called with args"); + /// assert_eq!(result.extract::()?, "called with args"); /// Ok(()) /// }) /// # } @@ -1363,7 +1363,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// let kwargs = PyDict::new_bound(py); /// kwargs.set_item("cruel", "world")?; /// let result = instance.call_method("method", args, Some(&kwargs))?; - /// assert_eq!(result.extract::<&str>()?, "called with args and kwargs"); + /// assert_eq!(result.extract::()?, "called with args and kwargs"); /// Ok(()) /// }) /// # } @@ -1404,7 +1404,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// let module = PyModule::from_code_bound(py, CODE, "", "")?; /// let instance = module.getattr("a")?; /// let result = instance.call_method0("method")?; - /// assert_eq!(result.extract::<&str>()?, "called with no arguments"); + /// assert_eq!(result.extract::()?, "called with no arguments"); /// Ok(()) /// }) /// # } @@ -1440,7 +1440,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// let instance = module.getattr("a")?; /// let args = ("hello",); /// let result = instance.call_method1("method", args)?; - /// assert_eq!(result.extract::<&str>()?, "called with args"); + /// assert_eq!(result.extract::()?, "called with args"); /// Ok(()) /// }) /// # } @@ -1642,9 +1642,9 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// Extracts some type from the Python object. /// /// This is a wrapper function around [`FromPyObject::extract()`]. - fn extract(&self) -> PyResult + fn extract<'a, T>(&'a self) -> PyResult where - D: FromPyObject<'py>; + T: FromPyObjectBound<'a, 'py>; /// Returns the reference count for the Python object. fn get_refcnt(&self) -> isize; @@ -2178,11 +2178,11 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { std::mem::transmute(self) } - fn extract(&self) -> PyResult + fn extract<'a, T>(&'a self) -> PyResult where - D: FromPyObject<'py>, + T: FromPyObjectBound<'a, 'py>, { - FromPyObject::extract_bound(self) + FromPyObjectBound::from_py_object_bound(self) } fn get_refcnt(&self) -> isize { diff --git a/src/types/bytes.rs b/src/types/bytes.rs index 2a54c172d1f..55c295c416f 100644 --- a/src/types/bytes.rs +++ b/src/types/bytes.rs @@ -196,14 +196,13 @@ impl> Index for Bound<'_, PyBytes> { } #[cfg(test)] -#[cfg_attr(not(feature = "gil-refs"), allow(deprecated))] mod tests { use super::*; #[test] fn test_bytes_index() { Python::with_gil(|py| { - let bytes = PyBytes::new(py, b"Hello World"); + let bytes = PyBytes::new_bound(py, b"Hello World"); assert_eq!(bytes[1], b'e'); }); } @@ -222,7 +221,7 @@ mod tests { #[test] fn test_bytes_new_with() -> super::PyResult<()> { Python::with_gil(|py| -> super::PyResult<()> { - let py_bytes = PyBytes::new_with(py, 10, |b: &mut [u8]| { + let py_bytes = PyBytes::new_bound_with(py, 10, |b: &mut [u8]| { b.copy_from_slice(b"Hello Rust"); Ok(()) })?; @@ -235,7 +234,7 @@ mod tests { #[test] fn test_bytes_new_with_zero_initialised() -> super::PyResult<()> { Python::with_gil(|py| -> super::PyResult<()> { - let py_bytes = PyBytes::new_with(py, 10, |_b: &mut [u8]| Ok(()))?; + let py_bytes = PyBytes::new_bound_with(py, 10, |_b: &mut [u8]| Ok(()))?; let bytes: &[u8] = py_bytes.extract()?; assert_eq!(bytes, &[0; 10]); Ok(()) @@ -246,7 +245,7 @@ mod tests { fn test_bytes_new_with_error() { use crate::exceptions::PyValueError; Python::with_gil(|py| { - let py_bytes_result = PyBytes::new_with(py, 10, |_b: &mut [u8]| { + let py_bytes_result = PyBytes::new_bound_with(py, 10, |_b: &mut [u8]| { Err(PyValueError::new_err("Hello Crustaceans!")) }); assert!(py_bytes_result.is_err()); diff --git a/src/types/module.rs b/src/types/module.rs index 993060703e6..11afcf76c83 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -10,6 +10,8 @@ use crate::{exceptions, ffi, Bound, IntoPy, Py, PyNativeType, PyObject, Python}; use std::ffi::CString; use std::str; +use super::PyStringMethods; + /// Represents a Python [`module`][1] object. /// /// As with all other Python objects, modules are first class citizens. @@ -399,8 +401,12 @@ impl PyModule { /// [1]: crate::prelude::pyfunction /// [2]: crate::wrap_pyfunction pub fn add_function<'a>(&'a self, fun: &'a PyCFunction) -> PyResult<()> { - let name = fun.getattr(__name__(self.py()))?.extract()?; - self.add(name, fun) + let name = fun + .as_borrowed() + .getattr(__name__(self.py()))? + .downcast_into::()?; + let name = name.to_cow()?; + self.add(&name, fun) } } diff --git a/tests/test_class_conversion.rs b/tests/test_class_conversion.rs index a09195278b0..ede8928f865 100644 --- a/tests/test_class_conversion.rs +++ b/tests/test_class_conversion.rs @@ -151,7 +151,7 @@ fn test_pycell_deref() { // Should be able to deref as PyAny assert_eq!( obj.call_method0("foo") - .and_then(|e| e.extract::<&str>()) + .and_then(|e| e.extract::()) .unwrap(), "SubClass" ); diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index 3f1052c3077..38b5c4727c6 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -51,5 +51,6 @@ fn test_compile_errors() { #[cfg(feature = "experimental-declarative-modules")] t.compile_fail("tests/ui/invalid_pymodule_two_pymodule_init.rs"); #[cfg(feature = "experimental-async")] + #[cfg(any(not(Py_LIMITED_API), Py_3_10))] // to avoid PyFunctionArgument for &str t.compile_fail("tests/ui/invalid_cancel_handle.rs"); } diff --git a/tests/test_pyfunction.rs b/tests/test_pyfunction.rs index 838f7353737..32c3ede6309 100644 --- a/tests/test_pyfunction.rs +++ b/tests/test_pyfunction.rs @@ -215,7 +215,7 @@ struct ValueClass { fn conversion_error( str_arg: &str, int_arg: i64, - tuple_arg: (&str, f64), + tuple_arg: (String, f64), option_arg: Option, struct_arg: Option, ) { diff --git a/tests/ui/invalid_cancel_handle.stderr b/tests/ui/invalid_cancel_handle.stderr index 6dc3ff3ccab..9e617bca988 100644 --- a/tests/ui/invalid_cancel_handle.stderr +++ b/tests/ui/invalid_cancel_handle.stderr @@ -40,6 +40,7 @@ error[E0277]: the trait bound `CancelHandle: PyClass` is not satisfied | = 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 by a bound in `extract_argument` --> src/impl_/extract_argument.rs @@ -61,6 +62,7 @@ error[E0277]: the trait bound `CancelHandle: Clone` is not satisfied &'a pyo3::coroutine::Coroutine &'a mut pyo3::coroutine::Coroutine = note: required for `CancelHandle` to implement `FromPyObject<'_>` + = note: required for `CancelHandle` to implement `FromPyObjectBound<'_, '_>` = note: required for `CancelHandle` to implement `PyFunctionArgument<'_, '_>` note: required by a bound in `extract_argument` --> src/impl_/extract_argument.rs From 14d1d2a9eec6433fbe6bb552294a99e4bc19e50c Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 8 Mar 2024 14:10:47 +0000 Subject: [PATCH 073/936] docs: use `lychee` to check link URLs (#3941) * guide: install `mdbook-linkcheck` * use `shutil` to copy license files * move from `mdbook-linkcheck` to `lychee` * clean guide & doc build products before build * fix more broken links * review: mejrs --- .github/workflows/gh-pages.yml | 8 +-- .gitignore | 2 + .netlify/build.sh | 10 ++- CHANGELOG.md | 5 +- Contributing.md | 9 ++- guide/book.toml | 2 +- guide/pyclass_parameters.md | 2 +- guide/src/async-await.md | 4 +- guide/src/building_and_distribution.md | 12 ++-- .../multiple_python_versions.md | 6 +- guide/src/class.md | 10 ++- guide/src/class/numeric.md | 2 +- guide/src/class/protocols.md | 2 +- guide/src/conversions/tables.md | 2 +- guide/src/ecosystem/async-await.md | 4 +- guide/src/exception.md | 4 +- guide/src/features.md | 10 +-- guide/src/function.md | 10 +-- guide/src/function/error_handling.md | 2 +- guide/src/memory.md | 2 +- guide/src/migration.md | 4 +- guide/src/module.md | 2 +- guide/src/parallelism.md | 2 +- guide/src/python_from_rust.md | 16 ++--- guide/src/trait_bounds.md | 2 +- noxfile.py | 64 +++++++++++++++++-- src/gil.rs | 2 +- 27 files changed, 135 insertions(+), 65 deletions(-) diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index ec8874d5841..a9a7669054a 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -26,9 +26,9 @@ jobs: - uses: dtolnay/rust-toolchain@nightly - name: Setup mdBook - uses: peaceiris/actions-mdbook@v1 + uses: taiki-e/install-action@v2 with: - mdbook-version: "0.4.19" + tool: mdbook,lychee - name: Prepare tag id: prepare_tag @@ -36,11 +36,11 @@ jobs: TAG_NAME="${GITHUB_REF##*/}" echo "::set-output name=tag_name::${TAG_NAME}" - # This builds the book in target/guide. + # This builds the book in target/guide/. - name: Build the guide run: | python -m pip install --upgrade pip && pip install nox - nox -s build-guide + nox -s check-guide env: PYO3_VERSION_TAG: ${{ steps.prepare_tag.outputs.tag_name }} diff --git a/.gitignore b/.gitignore index d27dfa6f9a5..3ad8328cd15 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,8 @@ dist/ .eggs/ venv* guide/book/ +guide/src/LICENSE-APACHE +guide/src/LICENSE-MIT *.so *.out *.egg-info diff --git a/.netlify/build.sh b/.netlify/build.sh index 10ec241c1d7..dc68c0310cd 100755 --- a/.netlify/build.sh +++ b/.netlify/build.sh @@ -71,9 +71,17 @@ if [ "${INSTALLED_MDBOOK_VERSION}" != "mdbook v${MDBOOK_VERSION}" ]; then cargo install mdbook@${MDBOOK_VERSION} --force fi +# Install latest mdbook-linkcheck. Netlify will cache the cargo bin dir, so this will +# only build mdbook-linkcheck if needed. +MDBOOK_LINKCHECK_VERSION=$(cargo search mdbook-linkcheck --limit 1 | head -1 | tr -s ' ' | cut -d ' ' -f 3 | tr -d '"') +INSTALLED_MDBOOK_LINKCHECK_VERSION=$(mdbook-linkcheck --version || echo "none") +if [ "${INSTALLED_MDBOOK_LINKCHECK_VERSION}" != "mdbook v${MDBOOK_LINKCHECK_VERSION}" ]; then + cargo install mdbook-linkcheck@${MDBOOK_LINKCHECK_VERSION} --force +fi + pip install nox nox -s build-guide -mv target/guide netlify_build/main/ +mv target/guide/ netlify_build/main/ ## Build public docs diff --git a/CHANGELOG.md b/CHANGELOG.md index 295a035370a..fcd4ca4f495 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1687,7 +1687,7 @@ Yanked [0.9.2]: https://github.com/pyo3/pyo3/compare/v0.9.1...v0.9.2 [0.9.1]: https://github.com/pyo3/pyo3/compare/v0.9.0...v0.9.1 [0.9.0]: https://github.com/pyo3/pyo3/compare/v0.8.5...v0.9.0 -[0.8.4]: https://github.com/pyo3/pyo3/compare/v0.8.4...v0.8.5 +[0.8.5]: https://github.com/pyo3/pyo3/compare/v0.8.4...v0.8.5 [0.8.4]: https://github.com/pyo3/pyo3/compare/v0.8.3...v0.8.4 [0.8.3]: https://github.com/pyo3/pyo3/compare/v0.8.2...v0.8.3 [0.8.2]: https://github.com/pyo3/pyo3/compare/v0.8.1...v0.8.2 @@ -1696,7 +1696,8 @@ Yanked [0.7.0]: https://github.com/pyo3/pyo3/compare/v0.6.0...v0.7.0 [0.6.0]: https://github.com/pyo3/pyo3/compare/v0.5.3...v0.6.0 [0.5.3]: https://github.com/pyo3/pyo3/compare/v0.5.2...v0.5.3 -[0.5.2]: https://github.com/pyo3/pyo3/compare/v0.5.0...v0.5.2 +[0.5.2]: https://github.com/pyo3/pyo3/compare/v0.5.1...v0.5.2 +[0.5.1]: https://github.com/pyo3/pyo3/compare/v0.5.0...v0.5.1 [0.5.0]: https://github.com/pyo3/pyo3/compare/v0.4.1...v0.5.0 [0.4.1]: https://github.com/pyo3/pyo3/compare/v0.4.0...v0.4.1 [0.4.0]: https://github.com/pyo3/pyo3/compare/v0.3.2...v0.4.0 diff --git a/Contributing.md b/Contributing.md index b34bf420072..2abdd80d47f 100644 --- a/Contributing.md +++ b/Contributing.md @@ -70,6 +70,12 @@ First, install [`mdbook`][mdbook] and [`nox`][nox]. Then, run nox -s build-guide -- --open ``` +To check all links in the guide are valid, also install [`lychee`][lychee] and use the `check-guide` session instead: + +```shell +nox -s check-guide +``` + ### Help design the next PyO3 Issues which don't yet have a clear solution use the [needs-design](https://github.com/PyO3/pyo3/issues?q=is%3Aissue+is%3Aopen+label%3Aneeds-design) label. @@ -171,7 +177,7 @@ First, there are Rust-based benchmarks located in the `pyo3-benches` subdirector nox -s bench -Second, there is a Python-based benchmark contained in the `pytests` subdirectory. You can read more about it [here](pytests). +Second, there is a Python-based benchmark contained in the `pytests` subdirectory. You can read more about it [here](https://github.com/PyO3/pyo3/tree/main/pytests). ## Code coverage @@ -211,4 +217,5 @@ In the meanwhile, some of our maintainers have personal GitHub sponsorship pages - [messense](https://github.com/sponsors/messense) [mdbook]: https://rust-lang.github.io/mdBook/cli/index.html +[lychee]: https://github.com/lycheeverse/lychee [nox]: https://github.com/theacodes/nox diff --git a/guide/book.toml b/guide/book.toml index 31fa4bb1587..bccc3506098 100644 --- a/guide/book.toml +++ b/guide/book.toml @@ -9,4 +9,4 @@ command = "python3 guide/pyo3_version.py" [output.html] git-repository-url = "https://github.com/PyO3/pyo3/tree/main/guide" edit-url-template = "https://github.com/PyO3/pyo3/edit/main/guide/%7Bpath%7D" -playground.runnable = false \ No newline at end of file +playground.runnable = false diff --git a/guide/pyclass_parameters.md b/guide/pyclass_parameters.md index 35c54147df5..6951a5b5e15 100644 --- a/guide/pyclass_parameters.md +++ b/guide/pyclass_parameters.md @@ -33,7 +33,7 @@ struct MyClass {} struct MyClass {} ``` -[params-1]: https://docs.rs/pyo3/latest/pyo3/struct.PyAny.html +[params-1]: https://docs.rs/pyo3/latest/pyo3/types/struct.PyAny.html [params-2]: https://en.wikipedia.org/wiki/Free_list [params-3]: https://doc.rust-lang.org/std/marker/trait.Send.html [params-4]: https://doc.rust-lang.org/std/rc/struct.Rc.html diff --git a/guide/src/async-await.md b/guide/src/async-await.md index 688f0a65bc4..c354be7f1b7 100644 --- a/guide/src/async-await.md +++ b/guide/src/async-await.md @@ -38,7 +38,7 @@ However, there is an exception for method receiver, so async methods can accept Even if it is not possible to pass a `py: Python<'_>` parameter to `async fn`, the GIL is still held during the execution of the future – it's also the case for regular `fn` without `Python<'_>`/`&PyAny` parameter, yet the GIL is held. -It is still possible to get a `Python` marker using [`Python::with_gil`]({{#PYO3_DOCS_URL}}/pyo3/struct.Python.html#method.with_gil); because `with_gil` is reentrant and optimized, the cost will be negligible. +It is still possible to get a `Python` marker using [`Python::with_gil`]({{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.with_gil); because `with_gil` is reentrant and optimized, the cost will be negligible. ## Release the GIL across `.await` @@ -70,7 +70,7 @@ where ## Cancellation -Cancellation on the Python side can be caught using [`CancelHandle`]({{#PYO3_DOCS_URL}}/pyo3/coroutine/struct.CancelHandle.html) type, by annotating a function parameter with `#[pyo3(cancel_handle)]. +Cancellation on the Python side can be caught using [`CancelHandle`]({{#PYO3_DOCS_URL}}/pyo3/coroutine/struct.CancelHandle.html) type, by annotating a function parameter with `#[pyo3(cancel_handle)]`. ```rust # #![allow(dead_code)] diff --git a/guide/src/building_and_distribution.md b/guide/src/building_and_distribution.md index 97e77d24c34..8caa63e688d 100644 --- a/guide/src/building_and_distribution.md +++ b/guide/src/building_and_distribution.md @@ -4,7 +4,7 @@ This chapter of the guide goes into detail on how to build and distribute projec The material in this chapter is intended for users who have already read the PyO3 [README](./index.md). It covers in turn the choices that can be made for Python modules and for Rust binaries. There is also a section at the end about cross-compiling projects using PyO3. -There is an additional sub-chapter dedicated to [supporting multiple Python versions](./building_and_distribution/multiple_python_versions.html). +There is an additional sub-chapter dedicated to [supporting multiple Python versions](./building_and_distribution/multiple_python_versions.md). ## Configuring the Python version @@ -177,7 +177,7 @@ The downside of not linking to `libpython` is that binaries, tests, and examples By default, Python extension modules can only be used with the same Python version they were compiled against. For example, an extension module built for Python 3.5 can't be imported in Python 3.8. [PEP 384](https://www.python.org/dev/peps/pep-0384/) introduced the idea of the limited Python API, which would have a stable ABI enabling extension modules built with it to be used against multiple Python versions. This is also known as `abi3`. -The advantage of building extension modules using the limited Python API is that package vendors only need to build and distribute a single copy (for each OS / architecture), and users can install it on all Python versions from the [minimum version](#minimum-python-version-for-abi3) and up. The downside of this is that PyO3 can't use optimizations which rely on being compiled against a known exact Python version. It's up to you to decide whether this matters for your extension module. It's also possible to design your extension module such that you can distribute `abi3` wheels but allow users compiling from source to benefit from additional optimizations - see the [support for multiple python versions](./building_and_distribution/multiple_python_versions.html) section of this guide, in particular the `#[cfg(Py_LIMITED_API)]` flag. +The advantage of building extension modules using the limited Python API is that package vendors only need to build and distribute a single copy (for each OS / architecture), and users can install it on all Python versions from the [minimum version](#minimum-python-version-for-abi3) and up. The downside of this is that PyO3 can't use optimizations which rely on being compiled against a known exact Python version. It's up to you to decide whether this matters for your extension module. It's also possible to design your extension module such that you can distribute `abi3` wheels but allow users compiling from source to benefit from additional optimizations - see the [support for multiple python versions](./building_and_distribution/multiple_python_versions.md) section of this guide, in particular the `#[cfg(Py_LIMITED_API)]` flag. There are three steps involved in making use of `abi3` when building Python packages as wheels: @@ -198,7 +198,7 @@ See the [corresponding](https://github.com/PyO3/maturin/pull/353) [PRs](https:// Because a single `abi3` wheel can be used with many different Python versions, PyO3 has feature flags `abi3-py37`, `abi3-py38`, `abi3-py39` etc. to set the minimum required Python version for your `abi3` wheel. For example, if you set the `abi3-py37` feature, your extension wheel can be used on all Python 3 versions from Python 3.7 and up. `maturin` and `setuptools-rust` will give the wheel a name like `my-extension-1.0-cp37-abi3-manylinux2020_x86_64.whl`. -As your extension module may be run with multiple different Python versions you may occasionally find you need to check the Python version at runtime to customize behavior. See [the relevant section of this guide](./building_and_distribution/multiple_python_versions.html#checking-the-python-version-at-runtime) on supporting multiple Python versions at runtime. +As your extension module may be run with multiple different Python versions you may occasionally find you need to check the Python version at runtime to customize behavior. See [the relevant section of this guide](./building_and_distribution/multiple_python_versions.md#checking-the-python-version-at-runtime) on supporting multiple Python versions at runtime. PyO3 is only able to link your extension module to abi3 version up to and including your host Python version. E.g., if you set `abi3-py38` and try to compile the crate with a host of Python 3.7, the build will fail. @@ -232,7 +232,7 @@ not work when compiling for `abi3`. These are: If you want to embed the Python interpreter inside a Rust program, there are two modes in which this can be done: dynamically and statically. We'll cover each of these modes in the following sections. Each of them affect how you must distribute your program. Instead of learning how to do this yourself, you might want to consider using a project like [PyOxidizer] to ship your application and all of its dependencies in a single file. -PyO3 automatically switches between the two linking modes depending on whether the Python distribution you have configured PyO3 to use ([see above](#python-version)) contains a shared library or a static library. The static library is most often seen in Python distributions compiled from source without the `--enable-shared` configuration option. For example, this is the default for `pyenv` on macOS. +PyO3 automatically switches between the two linking modes depending on whether the Python distribution you have configured PyO3 to use ([see above](#configuring-the-python-version)) contains a shared library or a static library. The static library is most often seen in Python distributions compiled from source without the `--enable-shared` configuration option. For example, this is the default for `pyenv` on macOS. ### Dynamically embedding the Python interpreter @@ -242,7 +242,7 @@ This mode of embedding works well for Rust tests which need access to the Python For distributing your program to non-technical users, you will have to consider including the Python shared library in your distribution as well as setting up wrapper scripts to set the right environment variables (such as `LD_LIBRARY_PATH` on UNIX, or `PATH` on Windows). -Note that PyPy cannot be embedded in Rust (or any other software). Support for this is tracked on the [PyPy issue tracker](https://foss.heptapod.net/pypy/pypy/-/issues/3286). +Note that PyPy cannot be embedded in Rust (or any other software). Support for this is tracked on the [PyPy issue tracker](https://github.com/pypy/pypy/issues/3836). ### Statically embedding the Python interpreter @@ -285,7 +285,7 @@ Thanks to Rust's great cross-compilation support, cross-compiling using PyO3 is * A toolchain for your target. * The appropriate options in your Cargo `.config` for the platform you're targeting and the toolchain you are using. * A Python interpreter that's already been compiled for your target (optional when building "abi3" extension modules). -* A Python interpreter that is built for your host and available through the `PATH` or setting the [`PYO3_PYTHON`](#python-version) variable (optional when building "abi3" extension modules). +* A Python interpreter that is built for your host and available through the `PATH` or setting the [`PYO3_PYTHON`](#configuring-the-python-version) variable (optional when building "abi3" extension modules). After you've obtained the above, you can build a cross-compiled PyO3 module by using Cargo's `--target` flag. PyO3's build script will detect that you are attempting a cross-compile based on your host machine and the desired target. diff --git a/guide/src/building_and_distribution/multiple_python_versions.md b/guide/src/building_and_distribution/multiple_python_versions.md index 43203686fc8..4e1799a215a 100644 --- a/guide/src/building_and_distribution/multiple_python_versions.md +++ b/guide/src/building_and_distribution/multiple_python_versions.md @@ -85,7 +85,7 @@ This `#[cfg]` marks code which is running on PyPy. ## Checking the Python version at runtime -When building with PyO3's `abi3` feature, your extension module will be compiled against a specific [minimum version](../building_and_distribution.html#minimum-python-version-for-abi3) of Python, but may be running on newer Python versions. +When building with PyO3's `abi3` feature, your extension module will be compiled against a specific [minimum version](../building_and_distribution.md#minimum-python-version-for-abi3) of Python, but may be running on newer Python versions. For example with PyO3's `abi3-py38` feature, your extension will be compiled as if it were for Python 3.8. If you were using `pyo3-build-config`, `#[cfg(Py_3_8)]` would be present. Your user could freely install and run your abi3 extension on Python 3.9. @@ -104,5 +104,5 @@ Python::with_gil(|py| { }); ``` -[`Python::version()`]: {{#PYO3_DOCS_URL}}/pyo3/struct.Python.html#method.version -[`Python::version_info()`]: {{#PYO3_DOCS_URL}}/pyo3/struct.Python.html#method.version_info +[`Python::version()`]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.version +[`Python::version_info()`]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.version_info diff --git a/guide/src/class.md b/guide/src/class.md index b9b47420ccb..bfcdc108f32 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -16,7 +16,7 @@ This chapter will discuss the functionality and configuration these attributes o - [`#[classmethod]`](#class-methods) - [`#[classattr]`](#class-attributes) - [`#[args]`](#method-arguments) -- [Magic methods and slots](class/protocols.html) +- [Magic methods and slots](class/protocols.md) - [Classes as function arguments](#classes-as-function-arguments) ## Defining a new class @@ -82,7 +82,7 @@ When you need to share ownership of data between Python and Rust, instead of usi A Rust `struct Foo` with a generic parameter `T` generates new compiled implementations each time it is used with a different concrete type for `T`. These new implementations are generated by the compiler at each usage site. This is incompatible with wrapping `Foo` in Python, where there needs to be a single compiled implementation of `Foo` which is integrated with the Python interpreter. -Currently, the best alternative is to write a macro which expands to a new #[pyclass] for each instantiation you want: +Currently, the best alternative is to write a macro which expands to a new `#[pyclass]` for each instantiation you want: ```rust # #![allow(dead_code)] @@ -898,9 +898,7 @@ py_args=('World', 666), py_kwargs=Some({'x': 44, 'y': 55}), name=Hello, num=44, py_args=(), py_kwargs=None, name=World, num=-1, num_before=44 ``` -## Making class method signatures available to Python - -The [`text_signature = "..."`](./function.md#text_signature) option for `#[pyfunction]` also works for `#[pymethods]`: +The [`#[pyo3(text_signature = "...")`](./function/signature.md#overriding-the-generated-signature) option for `#[pyfunction]` also works for `#[pymethods]`. ```rust # #![allow(dead_code)] @@ -1002,7 +1000,7 @@ impl MyClass { Note that `text_signature` on `#[new]` is not compatible with compilation in `abi3` mode until Python 3.10 or greater. -## #[pyclass] enums +## `#[pyclass]` enums Enum support in PyO3 comes in two flavors, depending on what kind of variants the enum has: simple and complex. diff --git a/guide/src/class/numeric.md b/guide/src/class/numeric.md index 9fb609a931d..361d2fb6d36 100644 --- a/guide/src/class/numeric.md +++ b/guide/src/class/numeric.md @@ -440,6 +440,6 @@ fn wrap(obj: &Bound<'_, PyAny>) -> Result { ``` [`PyErr::take`]: {{#PYO3_DOCS_URL}}/pyo3/prelude/struct.PyErr.html#method.take -[`Python`]: {{#PYO3_DOCS_URL}}/pyo3/struct.Python.html +[`Python`]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html [`FromPyObject`]: {{#PYO3_DOCS_URL}}/pyo3/conversion/trait.FromPyObject.html [`pyo3::ffi::PyLong_AsUnsignedLongMask`]: {{#PYO3_DOCS_URL}}/pyo3/ffi/fn.PyLong_AsUnsignedLongMask.html diff --git a/guide/src/class/protocols.md b/guide/src/class/protocols.md index b917c6a3eaf..0a77cd7f2a9 100644 --- a/guide/src/class/protocols.md +++ b/guide/src/class/protocols.md @@ -450,7 +450,7 @@ Usually, an implementation of `__traverse__` should do nothing but calls to `vis Most importantly, safe access to the GIL is prohibited inside implementations of `__traverse__`, i.e. `Python::with_gil` will panic. -> Note: these methods are part of the C API, PyPy does not necessarily honor them. If you are building for PyPy you should measure memory consumption to make sure you do not have runaway memory growth. See [this issue on the PyPy bug tracker](https://foss.heptapod.net/pypy/pypy/-/issues/3899). +> Note: these methods are part of the C API, PyPy does not necessarily honor them. If you are building for PyPy you should measure memory consumption to make sure you do not have runaway memory growth. See [this issue on the PyPy bug tracker](https://github.com/pypy/pypy/issues/3848). [`IterNextOutput`]: {{#PYO3_DOCS_URL}}/pyo3/pyclass/enum.IterNextOutput.html [`PySequence`]: {{#PYO3_DOCS_URL}}/pyo3/types/struct.PySequence.html diff --git a/guide/src/conversions/tables.md b/guide/src/conversions/tables.md index b0582431156..b6a2d30e55a 100644 --- a/guide/src/conversions/tables.md +++ b/guide/src/conversions/tables.md @@ -43,7 +43,7 @@ The table below contains the Python type and the corresponding function argument | `typing.Sequence[T]` | `Vec` | `&PySequence` | | `typing.Mapping[K, V]` | `HashMap`, `BTreeMap`, `hashbrown::HashMap`[^3], `indexmap::IndexMap`[^4] | `&PyMapping` | | `typing.Iterator[Any]` | - | `&PyIterator` | -| `typing.Union[...]` | See [`#[derive(FromPyObject)]`](traits.html#deriving-a-hrefhttpsdocsrspyo3latestpyo3conversiontraitfrompyobjecthtmlfrompyobjecta-for-enums) | - | +| `typing.Union[...]` | See [`#[derive(FromPyObject)]`](traits.md#deriving-frompyobject-for-enums) | - | There are also a few special types related to the GIL and Rust-defined `#[pyclass]`es which may come in useful: diff --git a/guide/src/ecosystem/async-await.md b/guide/src/ecosystem/async-await.md index ec46e872ba7..1e4ea4ab4b9 100644 --- a/guide/src/ecosystem/async-await.md +++ b/guide/src/ecosystem/async-await.md @@ -197,7 +197,7 @@ to do something special with the object that it returns. Normally in Python, that something special is the `await` keyword, but in order to await this coroutine in Rust, we first need to convert it into Rust's version of a `coroutine`: a `Future`. That's where `pyo3-asyncio` comes in. -[`pyo3_asyncio::into_future`](https://docs.rs/pyo3-asyncio/latest/pyo3_asyncio/fn.into_future.html) +[`pyo3_asyncio::async_std::into_future`](https://docs.rs/pyo3-asyncio/latest/pyo3_asyncio/async_std/fn.into_future.html) performs this conversion for us. The following example uses `into_future` to call the `py_sleep` function shown above and then await the @@ -349,7 +349,7 @@ implementations _prefer_ control over the main thread, this can still make some Because Python needs to control the main thread, we can't use the convenient proc macros from Rust runtimes to handle the `main` function or `#[test]` functions. Instead, the initialization for PyO3 has to be done from the `main` function and the main -thread must block on [`pyo3_asyncio::run_forever`](https://docs.rs/pyo3-asyncio/latest/pyo3_asyncio/fn.run_forever.html) or [`pyo3_asyncio::async_std::run_until_complete`](https://docs.rs/pyo3-asyncio/latest/pyo3_asyncio/async_std/fn.run_until_complete.html). +thread must block on [`pyo3_asyncio::async_std::run_until_complete`](https://docs.rs/pyo3-asyncio/latest/pyo3_asyncio/async_std/fn.run_until_complete.html). Because we have to block on one of those functions, we can't use [`#[async_std::main]`](https://docs.rs/async-std/latest/async_std/attr.main.html) or [`#[tokio::main]`](https://docs.rs/tokio/1.1.0/tokio/attr.main.html) since it's not a good idea to make long blocking calls during an async function. diff --git a/guide/src/exception.md b/guide/src/exception.md index 04550bd4b92..9e8cb780606 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/struct.PyAny.html#method.is_instance -[`PyAny::is_instance_of`]: {{#PYO3_DOCS_URL}}/pyo3/struct.PyAny.html#method.is_instance_of +[`PyAny::is_instance`]: {{#PYO3_DOCS_URL}}/pyo3/types/struct.PyAny.html#method.is_instance +[`PyAny::is_instance_of`]: {{#PYO3_DOCS_URL}}/pyo3/types/struct.PyAny.html#method.is_instance_of diff --git a/guide/src/features.md b/guide/src/features.md index 118284959d6..3ee620729b3 100644 --- a/guide/src/features.md +++ b/guide/src/features.md @@ -12,7 +12,7 @@ This feature is required when building a Python extension module using PyO3. It tells PyO3's build script to skip linking against `libpython.so` on Unix platforms, where this must not be done. -See the [building and distribution](building_and_distribution.md#linking) section for further detail. +See the [building and distribution](building_and_distribution.md#the-extension-module-feature) section for further detail. ### `abi3` @@ -45,7 +45,7 @@ section for further detail. ### `auto-initialize` -This feature changes [`Python::with_gil`]({{#PYO3_DOCS_URL}}/pyo3/struct.Python.html#method.with_gil) to automatically initialize a Python interpreter (by calling [`prepare_freethreaded_python`]({{#PYO3_DOCS_URL}}/pyo3/fn.prepare_freethreaded_python.html)) if needed. +This feature changes [`Python::with_gil`]({{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.with_gil) to automatically initialize a Python interpreter (by calling [`prepare_freethreaded_python`]({{#PYO3_DOCS_URL}}/pyo3/fn.prepare_freethreaded_python.html)) if needed. If you do not enable this feature, you should call `pyo3::prepare_freethreaded_python()` before attempting to call any other Python APIs. @@ -114,7 +114,7 @@ Adds a dependency on [anyhow](https://docs.rs/anyhow). Enables a conversion from ### `chrono` Adds a dependency on [chrono](https://docs.rs/chrono). Enables a conversion from [chrono](https://docs.rs/chrono)'s types to python: -- [Duration](https://docs.rs/chrono/latest/chrono/struct.Duration.html) -> [`PyDelta`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyDelta.html) +- [TimeDelta](https://docs.rs/chrono/latest/chrono/struct.TimeDelta.html) -> [`PyDelta`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyDelta.html) - [FixedOffset](https://docs.rs/chrono/latest/chrono/offset/struct.FixedOffset.html) -> [`PyDelta`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyDelta.html) - [Utc](https://docs.rs/chrono/latest/chrono/offset/struct.Utc.html) -> [`PyTzInfo`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyTzInfo.html) - [NaiveDate](https://docs.rs/chrono/latest/chrono/naive/struct.NaiveDate.html) -> [`PyDate`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyDate.html) @@ -129,7 +129,7 @@ It requires at least Python 3.9. ### `either` -Adds a dependency on [either](https://docs.rs/either). Enables a conversions into [either](https://docs.rs/either)’s [`Either`](https://docs.rs/either/latest/either/struct.Report.html) type. +Adds a dependency on [either](https://docs.rs/either). Enables a conversions into [either](https://docs.rs/either)’s [`Either`](https://docs.rs/either/latest/either/enum.Either.html) type. ### `eyre` @@ -145,7 +145,7 @@ Adds a dependency on [indexmap](https://docs.rs/indexmap) and enables conversion ### `num-bigint` -Adds a dependency on [num-bigint](https://docs.rs/num-bigint) and enables conversions into its [`BigInt`](https://docs.rs/num-bigint/latest/num_bigint/struct.BigInt.html) and [`BigUint`](https://docs.rs/num-bigint/latest/num_bigint/struct.BigUInt.html) types. +Adds a dependency on [num-bigint](https://docs.rs/num-bigint) and enables conversions into its [`BigInt`](https://docs.rs/num-bigint/latest/num_bigint/struct.BigInt.html) and [`BigUint`](https://docs.rs/num-bigint/latest/num_bigint/struct.BigUint.html) types. ### `num-complex` diff --git a/guide/src/function.md b/guide/src/function.md index 1bba52f2235..cfb1e5ef81e 100644 --- a/guide/src/function.md +++ b/guide/src/function.md @@ -38,7 +38,7 @@ There are also additional sections on the following topics: The `#[pyo3]` attribute can be used to modify properties of the generated Python function. It can take any combination of the following options: - - `#[pyo3(name = "...")]` + - `#[pyo3(name = "...")]` Overrides the name exposed to Python. @@ -67,15 +67,15 @@ The `#[pyo3]` attribute can be used to modify properties of the generated Python # }); ``` - - `#[pyo3(signature = (...))]` + - `#[pyo3(signature = (...))]` Defines the function signature in Python. See [Function Signatures](./function/signature.md). - - `#[pyo3(text_signature = "...")]` + - `#[pyo3(text_signature = "...")]` Overrides the PyO3-generated function signature visible in Python tooling (such as via [`inspect.signature`]). See the [corresponding topic in the Function Signatures subchapter](./function/signature.md#making-the-function-signature-available-to-python). - - `#[pyo3(pass_module)]` + - `#[pyo3(pass_module)]` Set this option to make PyO3 pass the containing module as the first argument to the function. It is then possible to use the module in the function body. The first argument **must** be of type `&Bound<'_, PyModule>`, `Bound<'_, PyModule>`, or `Py`. @@ -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. diff --git a/guide/src/function/error_handling.md b/guide/src/function/error_handling.md index 09fe5cab27b..f55fee90e54 100644 --- a/guide/src/function/error_handling.md +++ b/guide/src/function/error_handling.md @@ -23,7 +23,7 @@ In summary: As indicated in the previous section, when a `PyResult` containing an `Err` crosses from Rust to Python, PyO3 will raise the exception contained within. -Accordingly, to raise an exception from a `#[pyfunction]`, change the return type `T` to `PyResult`. When the function returns an `Err` it will raise a Python exception. (Other `Result` types can be used as long as the error `E` has a `From` conversion for `PyErr`, see [implementing a conversion](#implementing-an-error-conversion) below.) +Accordingly, to raise an exception from a `#[pyfunction]`, change the return type `T` to `PyResult`. When the function returns an `Err` it will raise a Python exception. (Other `Result` types can be used as long as the error `E` has a `From` conversion for `PyErr`, see [custom Rust error types](#custom-rust-error-types) below.) This also works for functions in `#[pymethods]`. diff --git a/guide/src/memory.md b/guide/src/memory.md index 46136e3f1a4..b14ce4496ad 100644 --- a/guide/src/memory.md +++ b/guide/src/memory.md @@ -115,7 +115,7 @@ at the end of each loop iteration, before the `with_gil()` closure ends. When doing this, you must be very careful to ensure that once the `GILPool` is dropped you do not retain access to any owned references created after the `GILPool` was created. Read the -[documentation for `Python::new_pool()`]({{#PYO3_DOCS_URL}}/pyo3/prelude/struct.Python.html#method.new_pool) +[documentation for `Python::new_pool()`]({{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.new_pool) for more information on safety. This memory management can also be applicable when writing extension modules. diff --git a/guide/src/migration.md b/guide/src/migration.md index fa4c23cc969..3ed3e015b3b 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -44,7 +44,7 @@ pyo3 = { version = "0.21", features = ["gil-refs"] } ### `PyTypeInfo` and `PyTryFrom` have been adjusted -The `PyTryFrom` trait has aged poorly, its [`try_from`] method now conflicts with `try_from` in the 2021 edition prelude. A lot of its functionality was also duplicated with `PyTypeInfo`. +The `PyTryFrom` trait has aged poorly, its `try_from` method now conflicts with `TryFrom::try_from` in the 2021 edition prelude. A lot of its functionality was also duplicated with `PyTypeInfo`. To tighten up the PyO3 traits 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. @@ -1299,7 +1299,7 @@ impl MyClass { ``` Basically you can return `Self` or `Result` directly. -For more, see [the constructor section](class.html#constructor) of this guide. +For more, see [the constructor section](class.md#constructor) of this guide. ### PyCell PyO3 0.9 introduces [`PyCell`], which is a [`RefCell`]-like object wrapper diff --git a/guide/src/module.md b/guide/src/module.md index 0444c750c9c..410716fdd39 100644 --- a/guide/src/module.md +++ b/guide/src/module.md @@ -45,7 +45,7 @@ file. Otherwise, you will get an import error in Python with the following messa `ImportError: dynamic module does not define module export function (PyInit_name_of_your_module)` To import the module, either: - - copy the shared library as described in [Manual builds](building_and_distribution.html#manual-builds), or + - copy the shared library as described in [Manual builds](building_and_distribution.md#manual-builds), or - use a tool, e.g. `maturin develop` with [maturin](https://github.com/PyO3/maturin) or `python setup.py develop` with [setuptools-rust](https://github.com/PyO3/setuptools-rust). diff --git a/guide/src/parallelism.md b/guide/src/parallelism.md index 195106b2420..792e0ed8de4 100644 --- a/guide/src/parallelism.md +++ b/guide/src/parallelism.md @@ -117,4 +117,4 @@ test_word_count_python_sequential 27.3985 (15.82) 45.452 You can see that the Python threaded version is not much slower than the Rust sequential version, which means compared to an execution on a single CPU core the speed has doubled. -[`Python::allow_threads`]: {{#PYO3_DOCS_URL}}/pyo3/struct.Python.html#method.allow_threads +[`Python::allow_threads`]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.allow_threads diff --git a/guide/src/python_from_rust.md b/guide/src/python_from_rust.md index d8f3214f58b..4a81d9a668f 100644 --- a/guide/src/python_from_rust.md +++ b/guide/src/python_from_rust.md @@ -10,15 +10,15 @@ Any Python-native object reference (such as `&PyAny`, `&PyList`, or `&PyCell`](types.html#pyt-and-pyobject) smart pointer also exposes these same six API methods, but needs a `Python` token as an additional first argument to prove the GIL is held. +For convenience the [`Py`](types.md#pyt-and-pyobject) smart pointer also exposes these same six API methods, but needs a `Python` token as an additional first argument to prove the GIL is held. The example below calls a Python function behind a `PyObject` (aka `Py`) reference: @@ -113,9 +113,9 @@ 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#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`]({{#PYO3_DOCS_URL}}/pyo3/struct.Py.html#method.call) is temporarily named `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/prelude/trait.pyanymethods#tymethod.call) already use follow the newest API conventions.) +(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.)
@@ -428,7 +428,7 @@ fn main() -> PyResult<()> { ``` -[`Python::run`]: {{#PYO3_DOCS_URL}}/pyo3/struct.Python.html#method.run +[`Python::run`]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.run [`py_run!`]: {{#PYO3_DOCS_URL}}/pyo3/macro.py_run.html ## Need to use a context manager from Rust? diff --git a/guide/src/trait_bounds.md b/guide/src/trait_bounds.md index 6dfaa2e20aa..b0eee80c80a 100644 --- a/guide/src/trait_bounds.md +++ b/guide/src/trait_bounds.md @@ -1,6 +1,6 @@ # Using in Python a Rust function with trait bounds -PyO3 allows for easy conversion from Rust to Python for certain functions and classes (see the [conversion table](conversions/tables.html). +PyO3 allows for easy conversion from Rust to Python for certain functions and classes (see the [conversion table](conversions/tables.md). However, it is not always straightforward to convert Rust code that requires a given trait implementation as an argument. This tutorial explains how to convert a Rust function that takes a trait as argument for use in Python with classes implementing the same methods as the trait. diff --git a/noxfile.py b/noxfile.py index 3bba2574885..29fe0f916cd 100644 --- a/noxfile.py +++ b/noxfile.py @@ -2,6 +2,7 @@ import json import os import re +import shutil import subprocess import sys import tempfile @@ -25,6 +26,10 @@ PYO3_DIR = Path(__file__).parent +PYO3_TARGET = Path(os.environ.get("CARGO_TARGET_DIR", PYO3_DIR / "target")).absolute() +PYO3_GUIDE_SRC = PYO3_DIR / "guide" / "src" +PYO3_GUIDE_TARGET = PYO3_TARGET / "guide" +PYO3_DOCS_TARGET = PYO3_TARGET / "doc" PY_VERSIONS = ("3.7", "3.8", "3.9", "3.10", "3.11", "3.12") PYPY_VERSIONS = ("3.7", "3.8", "3.9", "3.10") @@ -356,6 +361,7 @@ def docs(session: nox.Session) -> None: rustdoc_flags.append(session.env.get("RUSTDOCFLAGS", "")) session.env["RUSTDOCFLAGS"] = " ".join(rustdoc_flags) + shutil.rmtree(PYO3_DOCS_TARGET, ignore_errors=True) _run_cargo( session, *toolchain_flags, @@ -371,7 +377,49 @@ def docs(session: nox.Session) -> None: @nox.session(name="build-guide", venv_backend="none") def build_guide(session: nox.Session): - _run(session, "mdbook", "build", "-d", "../target/guide", "guide", *session.posargs) + shutil.rmtree(PYO3_GUIDE_TARGET, ignore_errors=True) + _run(session, "mdbook", "build", "-d", PYO3_GUIDE_TARGET, "guide", *session.posargs) + for license in ("LICENSE-APACHE", "LICENSE-MIT"): + target_file = PYO3_GUIDE_TARGET / license + target_file.unlink(missing_ok=True) + shutil.copy(PYO3_DIR / license, target_file) + + +@nox.session(name="check-guide", venv_backend="none") +def check_guide(session: nox.Session): + # reuse other sessions, but with default args + posargs = [*session.posargs] + del session.posargs[:] + build_guide(session) + docs(session) + session.posargs.extend(posargs) + + remaps = { + f"file://{PYO3_GUIDE_SRC}/([^/]*/)*?%7B%7B#PYO3_DOCS_URL\}}\}}": f"file://{PYO3_DOCS_TARGET}", + "%7B%7B#PYO3_DOCS_VERSION\}\}": "latest", + } + remap_args = [] + for key, value in remaps.items(): + remap_args.extend(("--remap", f"{key} {value}")) + # check all links in the guide + _run( + session, + "lychee", + "--include-fragments", + PYO3_GUIDE_SRC, + *remap_args, + *session.posargs, + ) + # check external links in the docs + # (intra-doc links are checked by rustdoc) + _run( + session, + "lychee", + PYO3_DOCS_TARGET, + f"--remap=https://pyo3.rs/main/ file://{PYO3_GUIDE_TARGET}/", + f"--exclude=file://{PYO3_DOCS_TARGET}", + *session.posargs, + ) @nox.session(name="format-guide", venv_backend="none") @@ -451,10 +499,11 @@ def address_sanitizer(session: nox.Session): @nox.session(name="check-changelog") def check_changelog(session: nox.Session): - event_path = os.environ.get("GITHUB_EVENT_PATH") - if event_path is None: + if not _is_github_actions(): session.error("Can only check changelog on github actions") + event_path = os.environ["GITHUB_EVENT_PATH"] + with open(event_path) as event_file: event = json.load(event_file) @@ -762,11 +811,12 @@ def _get_coverage_env() -> Dict[str, str]: def _run(session: nox.Session, *args: str, **kwargs: Any) -> None: """Wrapper for _run(session, which creates nice groups on GitHub Actions.""" - if "GITHUB_ACTIONS" in os.environ: + is_github_actions = _is_github_actions() + if is_github_actions: # Insert ::group:: at the start of nox's command line output print("::group::", end="", flush=True, file=sys.stderr) session.run(*args, **kwargs) - if "GITHUB_ACTIONS" in os.environ: + if is_github_actions: print("::endgroup::", file=sys.stderr) @@ -869,5 +919,9 @@ def _config_file() -> Iterator[_ConfigFile]: yield _ConfigFile(config) +def _is_github_actions() -> bool: + return "GITHUB_ACTIONS" in os.environ + + _BENCHES = "--manifest-path=pyo3-benches/Cargo.toml" _FFI_CHECK = "--manifest-path=pyo3-ffi-check/Cargo.toml" diff --git a/src/gil.rs b/src/gil.rs index 08b1b3f745b..91c3d1cdd27 100644 --- a/src/gil.rs +++ b/src/gil.rs @@ -67,7 +67,7 @@ fn gil_is_acquired() -> bool { /// /// This function is unavailable under PyPy because PyPy cannot be embedded in Rust (or any other /// software). Support for this is tracked on the -/// [PyPy issue tracker](https://foss.heptapod.net/pypy/pypy/-/issues/3286). +/// [PyPy issue tracker](https://github.com/pypy/pypy/issues/3836). /// /// # Examples /// ```rust From 908e661237a4bc31939473f80047b67a7d129bda Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sat, 9 Mar 2024 10:52:12 +0100 Subject: [PATCH 074/936] deprecate gil-refs in "self" position (#3943) * deprecate gil-refs in "self" position * feature gate explicit gil-ref tests * fix MSRV * adjust bracketing --------- Co-authored-by: David Hewitt --- pyo3-macros-backend/src/method.rs | 72 ++++++++++++++++++++++++++----- tests/test_class_basics.rs | 8 +++- tests/test_methods.rs | 1 + tests/test_module.rs | 8 +++- tests/ui/deprecations.rs | 23 ++++++++++ tests/ui/deprecations.stderr | 36 +++++++++++++--- 6 files changed, 128 insertions(+), 20 deletions(-) diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index 9a8cb828b22..3cc2a96e899 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -128,24 +128,24 @@ impl FnType { } FnType::FnClass(span) | FnType::FnNewClass(span) => { let py = syn::Ident::new("py", Span::call_site()); - let slf: Ident = syn::Ident::new("_slf", Span::call_site()); + let slf: Ident = syn::Ident::new("_slf_ref", Span::call_site()); let pyo3_path = pyo3_path.to_tokens_spanned(*span); quote_spanned! { *span => #[allow(clippy::useless_conversion)] ::std::convert::Into::into( - #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(#py, &#slf.cast()) + #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(#py, &*(#slf as *const _ as *const *mut _)) .downcast_unchecked::<#pyo3_path::types::PyType>() ), } } FnType::FnModule(span) => { let py = syn::Ident::new("py", Span::call_site()); - let slf: Ident = syn::Ident::new("_slf", Span::call_site()); + let slf: Ident = syn::Ident::new("_slf_ref", Span::call_site()); let pyo3_path = pyo3_path.to_tokens_spanned(*span); quote_spanned! { *span => #[allow(clippy::useless_conversion)] ::std::convert::Into::into( - #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(#py, &#slf.cast()) + #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(#py, &*(#slf as *const _ as *const *mut _)) .downcast_unchecked::<#pyo3_path::types::PyModule>() ), } @@ -519,7 +519,9 @@ impl<'a> FnSpec<'a> { ); } - let rust_call = |args: Vec, holders: &mut Vec| { + let rust_call = |args: Vec, + self_e: &syn::Ident, + holders: &mut Vec| { let self_arg = self.tp.self_arg(cls, ExtractErrorMode::Raise, holders, ctx); let call = if self.asyncness.is_some() { @@ -538,6 +540,7 @@ impl<'a> FnSpec<'a> { holders.pop().unwrap(); // does not actually use holder created by `self_arg` quote! {{ + #self_e = #pyo3_path::impl_::pymethods::Extractor::<()>::new(); let __guard = #pyo3_path::impl_::coroutine::RefGuard::<#cls>::new(&#pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(py, &_slf))?; async move { function(&__guard, #(#args),*).await } }} @@ -546,11 +549,25 @@ impl<'a> FnSpec<'a> { holders.pop().unwrap(); // does not actually use holder created by `self_arg` quote! {{ + #self_e = #pyo3_path::impl_::pymethods::Extractor::<()>::new(); 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, #(#args),*).await } }} } - _ => quote! { function(#self_arg #(#args),*) }, + _ => { + if self_arg.is_empty() { + quote! {{ + #self_e = #pyo3_path::impl_::pymethods::Extractor::<()>::new(); + function(#(#args),*) + }} + } else { + quote! { function({ + let (self_arg, e) = #pyo3_path::impl_::pymethods::inspect_type(#self_arg); + #self_e = e; + self_arg + }, #(#args),*) } + } + } }; let mut call = quote! {{ let future = #future; @@ -569,10 +586,24 @@ impl<'a> FnSpec<'a> { }}; } call + } else if self_arg.is_empty() { + quote! {{ + #self_e = #pyo3_path::impl_::pymethods::Extractor::<()>::new(); + function(#(#args),*) + }} } else { - quote! { function(#self_arg #(#args),*) } + quote! { + function({ + let (self_arg, e) = #pyo3_path::impl_::pymethods::inspect_type(#self_arg); + #self_e = e; + self_arg + }, #(#args),*) + } }; - quotes::map_result_into_ptr(quotes::ok_wrap(call, ctx), ctx) + ( + quotes::map_result_into_ptr(quotes::ok_wrap(call, ctx), ctx), + self_arg.span(), + ) }; let func_name = &self.name; @@ -582,6 +613,7 @@ impl<'a> FnSpec<'a> { quote!(#func_name) }; + let self_e = syn::Ident::new("self_e", Span::call_site()); Ok(match self.convention { CallingConvention::Noargs => { let mut holders = Vec::new(); @@ -599,16 +631,21 @@ impl<'a> FnSpec<'a> { } }) .collect(); - let call = rust_call(args, &mut holders); + let (call, self_arg_span) = rust_call(args, &self_e, &mut holders); + let extract_gil_ref = + quote_spanned! { self_arg_span => #self_e.extract_gil_ref(); }; quote! { unsafe fn #ident<'py>( py: #pyo3_path::Python<'py>, _slf: *mut #pyo3_path::ffi::PyObject, ) -> #pyo3_path::PyResult<*mut #pyo3_path::ffi::PyObject> { + let _slf_ref = &_slf; let function = #rust_name; // Shadow the function name to avoid #3017 + let #self_e; #( #holders )* let result = #call; + #extract_gil_ref result } } @@ -616,7 +653,10 @@ impl<'a> FnSpec<'a> { CallingConvention::Fastcall => { let mut holders = Vec::new(); let (arg_convert, args) = impl_arg_params(self, cls, true, &mut holders, ctx)?; - let call = rust_call(args, &mut holders); + let (call, self_arg_span) = rust_call(args, &self_e, &mut holders); + let extract_gil_ref = + quote_spanned! { self_arg_span => #self_e.extract_gil_ref(); }; + quote! { unsafe fn #ident<'py>( py: #pyo3_path::Python<'py>, @@ -625,10 +665,13 @@ 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> { + let _slf_ref = &_slf; let function = #rust_name; // Shadow the function name to avoid #3017 + let #self_e; #arg_convert #( #holders )* let result = #call; + #extract_gil_ref result } } @@ -636,7 +679,10 @@ impl<'a> FnSpec<'a> { CallingConvention::Varargs => { let mut holders = Vec::new(); let (arg_convert, args) = impl_arg_params(self, cls, false, &mut holders, ctx)?; - let call = rust_call(args, &mut holders); + let (call, self_arg_span) = rust_call(args, &self_e, &mut holders); + let extract_gil_ref = + quote_spanned! { self_arg_span => #self_e.extract_gil_ref(); }; + quote! { unsafe fn #ident<'py>( py: #pyo3_path::Python<'py>, @@ -644,10 +690,13 @@ impl<'a> FnSpec<'a> { _args: *mut #pyo3_path::ffi::PyObject, _kwargs: *mut #pyo3_path::ffi::PyObject ) -> #pyo3_path::PyResult<*mut #pyo3_path::ffi::PyObject> { + let _slf_ref = &_slf; let function = #rust_name; // Shadow the function name to avoid #3017 + let #self_e; #arg_convert #( #holders )* let result = #call; + #extract_gil_ref result } } @@ -667,6 +716,7 @@ impl<'a> FnSpec<'a> { _kwargs: *mut #pyo3_path::ffi::PyObject ) -> #pyo3_path::PyResult<*mut #pyo3_path::ffi::PyObject> { use #pyo3_path::callback::IntoPyCallbackOutput; + let _slf_ref = &_slf; let function = #rust_name; // Shadow the function name to avoid #3017 #arg_convert #( #holders )* diff --git a/tests/test_class_basics.rs b/tests/test_class_basics.rs index 21ccc4a6bb6..8c6a1c04915 100644 --- a/tests/test_class_basics.rs +++ b/tests/test_class_basics.rs @@ -307,6 +307,7 @@ impl ClassWithFromPyWithMethods { } #[classmethod] + #[cfg(feature = "gil-refs")] fn classmethod_gil_ref( _cls: &PyType, #[pyo3(from_py_with = "PyAny::len")] argument: usize, @@ -324,16 +325,19 @@ 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, + instance + has_gil_refs, r#" arg = {1: 1, 2: 3} assert instance.instance_method(arg) == 2 assert instance.classmethod(arg) == 2 - assert instance.classmethod_gil_ref(arg) == 2 + if has_gil_refs: + assert instance.classmethod_gil_ref(arg) == 2 assert instance.staticmethod(arg) == 2 "# ); diff --git a/tests/test_methods.rs b/tests/test_methods.rs index 68a004bd4bc..a9e7d37d64a 100644 --- a/tests/test_methods.rs +++ b/tests/test_methods.rs @@ -78,6 +78,7 @@ impl ClassMethod { #[classmethod] /// Test class method. + #[cfg(feature = "gil-refs")] fn method_gil_ref(cls: &PyType) -> PyResult { Ok(format!("{}.method()!", cls.qualname()?)) } diff --git a/tests/test_module.rs b/tests/test_module.rs index 0eef3c0e2d1..5760c3ebaf3 100644 --- a/tests/test_module.rs +++ b/tests/test_module.rs @@ -45,7 +45,7 @@ fn module_with_functions(m: &Bound<'_, PyModule>) -> PyResult<()> { #[pyfn(m)] #[pyo3(pass_module)] - fn with_module(module: &PyModule) -> PyResult<&str> { + fn with_module<'py>(module: &Bound<'py, PyModule>) -> PyResult> { module.name() } @@ -373,6 +373,7 @@ fn pyfunction_with_module<'py>(module: &Bound<'py, PyModule>) -> PyResult PyResult<&str> { module.name() } @@ -427,6 +428,7 @@ fn pyfunction_with_module_and_args_kwargs<'py>( #[pyfunction] #[pyo3(pass_module)] +#[cfg(feature = "gil-refs")] fn pyfunction_with_pass_module_in_attribute(module: &PyModule) -> PyResult<&str> { module.name() } @@ -434,12 +436,14 @@ fn pyfunction_with_pass_module_in_attribute(module: &PyModule) -> PyResult<&str> #[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 @@ -457,6 +461,7 @@ fn test_module_functions_with_module() { m, "m.pyfunction_with_module() == 'module_with_functions_with_module'" ); + #[cfg(feature = "gil-refs")] py_assert!( py, m, @@ -484,6 +489,7 @@ 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, diff --git a/tests/ui/deprecations.rs b/tests/ui/deprecations.rs index 06bcf8197a7..c062cead3ad 100644 --- a/tests/ui/deprecations.rs +++ b/tests/ui/deprecations.rs @@ -1,6 +1,7 @@ #![deny(deprecated)] use pyo3::prelude::*; +use pyo3::types::{PyString, PyType}; #[pyclass] struct MyClass; @@ -11,10 +12,32 @@ 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>) {} } 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> { + module.name() +} + #[pyfunction] fn double(x: usize) -> usize { x * 2 diff --git a/tests/ui/deprecations.stderr b/tests/ui/deprecations.stderr index 2440f909019..edb74f97b74 100644 --- a/tests/ui/deprecations.stderr +++ b/tests/ui/deprecations.stderr @@ -1,7 +1,7 @@ error: use of deprecated constant `pyo3::impl_::deprecations::PYMETHODS_NEW_DEPRECATED_FORM`: use `#[new]` instead of `#[__new__]` - --> tests/ui/deprecations.rs:10:7 + --> tests/ui/deprecations.rs:11:7 | -10 | #[__new__] +11 | #[__new__] | ^^^^^^^ | note: the lint level is defined here @@ -10,14 +10,38 @@ 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:22:30 + | +22 | fn method_gil_ref(_slf: &PyCell) {} + | ^^^^^^ + +error: use of deprecated method `pyo3::methods::Extractor::::extract_gil_ref`: use `&Bound<'_, T>` instead for this function argument + --> tests/ui/deprecations.rs:17:33 + | +17 | fn cls_method_gil_ref(_cls: &PyType) {} + | ^ + +error: use of deprecated method `pyo3::methods::Extractor::::extract_gil_ref`: use `&Bound<'_, T>` instead for this function argument + --> tests/ui/deprecations.rs:22:29 + | +22 | fn method_gil_ref(_slf: &PyCell) {} + | ^ + +error: use of deprecated method `pyo3::methods::Extractor::::extract_gil_ref`: use `&Bound<'_, T>` instead for this function argument + --> tests/ui/deprecations.rs:37:43 + | +37 | fn pyfunction_with_module_gil_ref(module: &PyModule) -> PyResult<&str> { + | ^ + error: use of deprecated method `pyo3::methods::Extractor::::extract_gil_ref`: use `&Bound<'_, T>` instead for this function argument - --> tests/ui/deprecations.rs:24:19 + --> tests/ui/deprecations.rs:47:19 | -24 | fn module_gil_ref(m: &PyModule) -> PyResult<()> { +47 | fn module_gil_ref(m: &PyModule) -> PyResult<()> { | ^ error: use of deprecated method `pyo3::methods::Extractor::::extract_gil_ref`: use `&Bound<'_, T>` instead for this function argument - --> tests/ui/deprecations.rs:30:57 + --> tests/ui/deprecations.rs:53:57 | -30 | fn module_gil_ref_with_explicit_py_arg(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +53 | fn module_gil_ref_with_explicit_py_arg(_py: Python<'_>, m: &PyModule) -> PyResult<()> { | ^ From 75af678f4330db682e9ee99989a1bf7cdd4cbdd9 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sat, 9 Mar 2024 21:10:58 +0100 Subject: [PATCH 075/936] docs: use kebab-case instead of snake_case for guide URLs (#3942) * guide: use kebab-case instead of snake_case * fixup doctest names Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> * review: Icxolu * fix relative url * also remap latest pyo3 * fixup python_from_rust --------- Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> --- .netlify/build.sh | 9 +++++++++ Architecture.md | 2 +- README.md | 4 ++-- build.rs | 2 +- ...ass_parameters.md => pyclass-parameters.md} | 0 guide/src/SUMMARY.md | 14 +++++++------- ...ibution.md => building-and-distribution.md} | 6 +++--- .../multiple-python-versions.md} | 2 +- guide/src/class.md | 2 +- guide/src/exception.md | 2 +- guide/src/features.md | 8 ++++---- .../{error_handling.md => error-handling.md} | 0 .../{getting_started.md => getting-started.md} | 4 ++-- guide/src/module.md | 2 +- ...python_from_rust.md => python-from-rust.md} | 0 ..._typing_hints.md => python-typing-hints.md} | 0 guide/src/{trait_bounds.md => trait-bounds.md} | 0 noxfile.py | 1 + pyo3-build-config/src/impl_.rs | 2 +- pyo3-build-config/src/lib.rs | 2 +- pyo3-ffi/README.md | 2 +- pyo3-ffi/src/lib.rs | 2 +- src/lib.rs | 18 +++++++++--------- 23 files changed, 47 insertions(+), 37 deletions(-) rename guide/{pyclass_parameters.md => pyclass-parameters.md} (100%) rename guide/src/{building_and_distribution.md => building-and-distribution.md} (99%) rename guide/src/{building_and_distribution/multiple_python_versions.md => building-and-distribution/multiple-python-versions.md} (98%) rename guide/src/function/{error_handling.md => error-handling.md} (100%) rename guide/src/{getting_started.md => getting-started.md} (92%) rename guide/src/{python_from_rust.md => python-from-rust.md} (100%) rename guide/src/{python_typing_hints.md => python-typing-hints.md} (100%) rename guide/src/{trait_bounds.md => trait-bounds.md} (100%) diff --git a/.netlify/build.sh b/.netlify/build.sh index dc68c0310cd..e1d86788ca1 100755 --- a/.netlify/build.sh +++ b/.netlify/build.sh @@ -48,6 +48,15 @@ done # Add latest redirect echo "/latest/* /v${PYO3_VERSION}/:splat 302" >> netlify_build/_redirects +# some backwards compatbiility redirects +echo "/latest/building_and_distribution/* /latest/building-and-distribution/:splat 302" >> netlify_build/_redirects +echo "/latest/building-and-distribution/multiple_python_versions/* /latest/building-and-distribution/multiple-python-versions:splat 302" >> netlify_build/_redirects +echo "/latest/function/error_handling/* /latest/function/error-handling/:splat 302" >> netlify_build/_redirects +echo "/latest/getting_started/* /latest/getting-started/:splat 302" >> netlify_build/_redirects +echo "/latest/python_from_rust/* /latest/python-from-rust/:splat 302" >> netlify_build/_redirects +echo "/latest/python_typing_hints/* /latest/python-typing-hints/:splat 302" >> netlify_build/_redirects +echo "/latest/trait_bounds/* /latest/trait-bounds/:splat 302" >> netlify_build/_redirects + ## Add landing page redirect if [ "${CONTEXT}" == "deploy-preview" ]; then echo "/ /main/" >> netlify_build/_redirects diff --git a/Architecture.md b/Architecture.md index 9ec20931c10..60083d71c98 100644 --- a/Architecture.md +++ b/Architecture.md @@ -48,7 +48,7 @@ In the [`pyo3-ffi`] crate, there is lots of conditional compilation such as `#[c `#[cfg(Py_3_7)]`, and `#[cfg(PyPy)]`. `Py_LIMITED_API` corresponds to `#define Py_LIMITED_API` macro in Python/C API. With `Py_LIMITED_API`, we can build a Python-version-agnostic binary called an -[abi3 wheel](https://pyo3.rs/latest/building_and_distribution.html#py_limited_apiabi3). +[abi3 wheel](https://pyo3.rs/latest/building-and-distribution.html#py_limited_apiabi3). `Py_3_7` means that the API is available from Python >= 3.7. There are also `Py_3_8`, `Py_3_9`, and so on. `PyPy` means that the API definition is for PyPy. diff --git a/README.md b/README.md index 21dc125d5ab..c11754c3ca1 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ maturin develop If you want to be able to run `cargo test` or use this project in a Cargo workspace and are running into linker issues, there are some workarounds in [the FAQ](https://pyo3.rs/latest/faq.html#i-cant-run-cargo-test-or-i-cant-build-in-a-cargo-workspace-im-having-linker-issues-like-symbol-not-found-or-undefined-reference-to-_pyexc_systemerror). -As well as with `maturin`, it is possible to build using [`setuptools-rust`](https://github.com/PyO3/setuptools-rust) or [manually](https://pyo3.rs/latest/building_and_distribution.html#manual-builds). Both offer more flexibility than `maturin` but require more configuration to get started. +As well as with `maturin`, it is possible to build using [`setuptools-rust`](https://github.com/PyO3/setuptools-rust) or [manually](https://pyo3.rs/latest/building-and-distribution.html#manual-builds). Both offer more flexibility than `maturin` but require more configuration to get started. ### Using Python from Rust @@ -162,7 +162,7 @@ fn main() -> PyResult<()> { } ``` -The guide has [a section](https://pyo3.rs/latest/python_from_rust.html) with lots of examples +The guide has [a section](https://pyo3.rs/latest/python-from-rust.html) with lots of examples about this topic. ## Tools and libraries diff --git a/build.rs b/build.rs index e20206310db..7f0ae6e31c8 100644 --- a/build.rs +++ b/build.rs @@ -16,7 +16,7 @@ fn ensure_auto_initialize_ok(interpreter_config: &InterpreterConfig) -> Result<( \n\ For more information, see \ https://pyo3.rs/v{pyo3_version}/\ - building_and_distribution.html#embedding-python-in-rust", + building-and-distribution.html#embedding-python-in-rust", pyo3_version = env::var("CARGO_PKG_VERSION").unwrap() ); } diff --git a/guide/pyclass_parameters.md b/guide/pyclass-parameters.md similarity index 100% rename from guide/pyclass_parameters.md rename to guide/pyclass-parameters.md diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md index 34775a0d185..6aea71b1722 100644 --- a/guide/src/SUMMARY.md +++ b/guide/src/SUMMARY.md @@ -4,11 +4,11 @@ --- -- [Getting started](getting_started.md) +- [Getting started](getting-started.md) - [Python modules](module.md) - [Python functions](function.md) - [Function signatures](function/signature.md) - - [Error handling](function/error_handling.md) + - [Error handling](function/error-handling.md) - [Python classes](class.md) - [Class customizations](class/protocols.md) - [Basic object customization](class/object.md) @@ -18,7 +18,7 @@ - [Mapping of Rust types to Python types](conversions/tables.md) - [Conversion traits](conversions/traits.md) - [Python exceptions](exception.md) -- [Calling Python from Rust](python_from_rust.md) +- [Calling Python from Rust](python-from-rust.md) - [Using `async` and `await`](async-await.md) - [GIL, mutability and object types](types.md) - [Parallelism](parallelism.md) @@ -27,8 +27,8 @@ - [Memory management](memory.md) - [Performance](performance.md) - [Advanced topics](advanced.md) -- [Building and distribution](building_and_distribution.md) - - [Supporting multiple Python versions](building_and_distribution/multiple_python_versions.md) +- [Building and distribution](building-and-distribution.md) + - [Supporting multiple Python versions](building-and-distribution/multiple-python-versions.md) - [Useful crates](ecosystem.md) - [Logging](ecosystem/logging.md) - [Using `async` and `await`](ecosystem/async-await.md) @@ -37,8 +37,8 @@ --- [Appendix A: Migration guide](migration.md) -[Appendix B: Trait bounds](trait_bounds.md) -[Appendix C: Python typing hints](python_typing_hints.md) +[Appendix B: Trait bounds](trait-bounds.md) +[Appendix C: Python typing hints](python-typing-hints.md) [CHANGELOG](changelog.md) --- diff --git a/guide/src/building_and_distribution.md b/guide/src/building-and-distribution.md similarity index 99% rename from guide/src/building_and_distribution.md rename to guide/src/building-and-distribution.md index 8caa63e688d..780f135e211 100644 --- a/guide/src/building_and_distribution.md +++ b/guide/src/building-and-distribution.md @@ -4,7 +4,7 @@ This chapter of the guide goes into detail on how to build and distribute projec The material in this chapter is intended for users who have already read the PyO3 [README](./index.md). It covers in turn the choices that can be made for Python modules and for Rust binaries. There is also a section at the end about cross-compiling projects using PyO3. -There is an additional sub-chapter dedicated to [supporting multiple Python versions](./building_and_distribution/multiple_python_versions.md). +There is an additional sub-chapter dedicated to [supporting multiple Python versions](./building-and-distribution/multiple-python-versions.md). ## Configuring the Python version @@ -177,7 +177,7 @@ The downside of not linking to `libpython` is that binaries, tests, and examples By default, Python extension modules can only be used with the same Python version they were compiled against. For example, an extension module built for Python 3.5 can't be imported in Python 3.8. [PEP 384](https://www.python.org/dev/peps/pep-0384/) introduced the idea of the limited Python API, which would have a stable ABI enabling extension modules built with it to be used against multiple Python versions. This is also known as `abi3`. -The advantage of building extension modules using the limited Python API is that package vendors only need to build and distribute a single copy (for each OS / architecture), and users can install it on all Python versions from the [minimum version](#minimum-python-version-for-abi3) and up. The downside of this is that PyO3 can't use optimizations which rely on being compiled against a known exact Python version. It's up to you to decide whether this matters for your extension module. It's also possible to design your extension module such that you can distribute `abi3` wheels but allow users compiling from source to benefit from additional optimizations - see the [support for multiple python versions](./building_and_distribution/multiple_python_versions.md) section of this guide, in particular the `#[cfg(Py_LIMITED_API)]` flag. +The advantage of building extension modules using the limited Python API is that package vendors only need to build and distribute a single copy (for each OS / architecture), and users can install it on all Python versions from the [minimum version](#minimum-python-version-for-abi3) and up. The downside of this is that PyO3 can't use optimizations which rely on being compiled against a known exact Python version. It's up to you to decide whether this matters for your extension module. It's also possible to design your extension module such that you can distribute `abi3` wheels but allow users compiling from source to benefit from additional optimizations - see the [support for multiple python versions](./building-and-distribution/multiple-python-versions.md) section of this guide, in particular the `#[cfg(Py_LIMITED_API)]` flag. There are three steps involved in making use of `abi3` when building Python packages as wheels: @@ -198,7 +198,7 @@ See the [corresponding](https://github.com/PyO3/maturin/pull/353) [PRs](https:// Because a single `abi3` wheel can be used with many different Python versions, PyO3 has feature flags `abi3-py37`, `abi3-py38`, `abi3-py39` etc. to set the minimum required Python version for your `abi3` wheel. For example, if you set the `abi3-py37` feature, your extension wheel can be used on all Python 3 versions from Python 3.7 and up. `maturin` and `setuptools-rust` will give the wheel a name like `my-extension-1.0-cp37-abi3-manylinux2020_x86_64.whl`. -As your extension module may be run with multiple different Python versions you may occasionally find you need to check the Python version at runtime to customize behavior. See [the relevant section of this guide](./building_and_distribution/multiple_python_versions.md#checking-the-python-version-at-runtime) on supporting multiple Python versions at runtime. +As your extension module may be run with multiple different Python versions you may occasionally find you need to check the Python version at runtime to customize behavior. See [the relevant section of this guide](./building-and-distribution/multiple-python-versions.md#checking-the-python-version-at-runtime) on supporting multiple Python versions at runtime. PyO3 is only able to link your extension module to abi3 version up to and including your host Python version. E.g., if you set `abi3-py38` and try to compile the crate with a host of Python 3.7, the build will fail. diff --git a/guide/src/building_and_distribution/multiple_python_versions.md b/guide/src/building-and-distribution/multiple-python-versions.md similarity index 98% rename from guide/src/building_and_distribution/multiple_python_versions.md rename to guide/src/building-and-distribution/multiple-python-versions.md index 4e1799a215a..b328d236c51 100644 --- a/guide/src/building_and_distribution/multiple_python_versions.md +++ b/guide/src/building-and-distribution/multiple-python-versions.md @@ -85,7 +85,7 @@ This `#[cfg]` marks code which is running on PyPy. ## Checking the Python version at runtime -When building with PyO3's `abi3` feature, your extension module will be compiled against a specific [minimum version](../building_and_distribution.md#minimum-python-version-for-abi3) of Python, but may be running on newer Python versions. +When building with PyO3's `abi3` feature, your extension module will be compiled against a specific [minimum version](../building-and-distribution.md#minimum-python-version-for-abi3) of Python, but may be running on newer Python versions. For example with PyO3's `abi3-py38` feature, your extension will be compiled as if it were for Python 3.8. If you were using `pyo3-build-config`, `#[cfg(Py_3_8)]` would be present. Your user could freely install and run your abi3 extension on Python 3.9. diff --git a/guide/src/class.md b/guide/src/class.md index bfcdc108f32..93920a40d88 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -288,7 +288,7 @@ Frozen classes are likely to become the default thereby guiding the PyO3 ecosyst ## Customizing the class -{{#include ../pyclass_parameters.md}} +{{#include ../pyclass-parameters.md}} These parameters are covered in various sections of this guide. diff --git a/guide/src/exception.md b/guide/src/exception.md index 9e8cb780606..ee36f544d74 100644 --- a/guide/src/exception.md +++ b/guide/src/exception.md @@ -54,7 +54,7 @@ fn mymodule(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { ## Raising an exception -As described in the [function error handling](./function/error_handling.md) chapter, to raise an exception from a `#[pyfunction]` or `#[pymethods]`, return an `Err(PyErr)`. PyO3 will automatically raise this exception for you when returning the result to Python. +As described in the [function error handling](./function/error-handling.md) chapter, to raise an exception from a `#[pyfunction]` or `#[pymethods]`, return an `Err(PyErr)`. PyO3 will automatically raise this exception for you when returning the result to Python. You can also manually write and fetch errors in the Python interpreter's global state: diff --git a/guide/src/features.md b/guide/src/features.md index 3ee620729b3..6e4e5ab70b1 100644 --- a/guide/src/features.md +++ b/guide/src/features.md @@ -12,7 +12,7 @@ This feature is required when building a Python extension module using PyO3. It tells PyO3's build script to skip linking against `libpython.so` on Unix platforms, where this must not be done. -See the [building and distribution](building_and_distribution.md#the-extension-module-feature) section for further detail. +See the [building and distribution](building-and-distribution.md#the-extension-module-feature) section for further detail. ### `abi3` @@ -20,7 +20,7 @@ This feature is used when building Python extension modules to create wheels whi It restricts PyO3's API to a subset of the full Python API which is guaranteed by [PEP 384](https://www.python.org/dev/peps/pep-0384/) to be forwards-compatible with future Python versions. -See the [building and distribution](building_and_distribution.md#py_limited_apiabi3) section for further detail. +See the [building and distribution](building-and-distribution.md#py_limited_apiabi3) section for further detail. ### The `abi3-pyXY` features @@ -28,7 +28,7 @@ See the [building and distribution](building_and_distribution.md#py_limited_apia These features are extensions of the `abi3` feature to specify the exact minimum Python version which the multiple-version-wheel will support. -See the [building and distribution](building_and_distribution.md#minimum-python-version-for-abi3) section for further detail. +See the [building and distribution](building-and-distribution.md#minimum-python-version-for-abi3) section for further detail. ### `generate-import-lib` @@ -38,7 +38,7 @@ for MinGW-w64 and MSVC (cross-)compile targets. Enabling it allows to (cross-)compile extension modules to any Windows targets without having to install the Windows Python distribution files for the target. -See the [building and distribution](building_and_distribution.md#building-abi3-extensions-without-a-python-interpreter) +See the [building and distribution](building-and-distribution.md#building-abi3-extensions-without-a-python-interpreter) section for further detail. ## Features for embedding Python in Rust diff --git a/guide/src/function/error_handling.md b/guide/src/function/error-handling.md similarity index 100% rename from guide/src/function/error_handling.md rename to guide/src/function/error-handling.md diff --git a/guide/src/getting_started.md b/guide/src/getting-started.md similarity index 92% rename from guide/src/getting_started.md rename to guide/src/getting-started.md index 008da8109f6..ef9ca5254b8 100644 --- a/guide/src/getting_started.md +++ b/guide/src/getting-started.md @@ -33,7 +33,7 @@ You can read more about `pyenv`'s configuration options [here](https://github.co ### Building -There are a number of build and Python package management systems such as [`setuptools-rust`](https://github.com/PyO3/setuptools-rust) or [manually](https://pyo3.rs/latest/building_and_distribution.html#manual-builds). We recommend the use of `maturin`, which you can install [here](https://maturin.rs/installation.html). It is developed to work with PyO3 and provides the most "batteries included" experience, especially if you are aiming to publish to PyPI. `maturin` is just a Python package, so you can add it in the same you already install Python packages. +There are a number of build and Python package management systems such as [`setuptools-rust`](https://github.com/PyO3/setuptools-rust) or [manually](./building-and-distribution.md#manual-builds). We recommend the use of `maturin`, which you can install [here](https://maturin.rs/installation.html). It is developed to work with PyO3 and provides the most "batteries included" experience, especially if you are aiming to publish to PyPI. `maturin` is just a Python package, so you can add it in the same you already install Python packages. System Python: ```bash @@ -180,4 +180,4 @@ $ python '25' ``` -For more instructions on how to use Python code from Rust, see the [Python from Rust](python_from_rust.md) page. +For more instructions on how to use Python code from Rust, see the [Python from Rust](python-from-rust.md) page. diff --git a/guide/src/module.md b/guide/src/module.md index 410716fdd39..8cac9a5be4c 100644 --- a/guide/src/module.md +++ b/guide/src/module.md @@ -45,7 +45,7 @@ file. Otherwise, you will get an import error in Python with the following messa `ImportError: dynamic module does not define module export function (PyInit_name_of_your_module)` To import the module, either: - - copy the shared library as described in [Manual builds](building_and_distribution.md#manual-builds), or + - copy the shared library as described in [Manual builds](building-and-distribution.md#manual-builds), or - use a tool, e.g. `maturin develop` with [maturin](https://github.com/PyO3/maturin) or `python setup.py develop` with [setuptools-rust](https://github.com/PyO3/setuptools-rust). diff --git a/guide/src/python_from_rust.md b/guide/src/python-from-rust.md similarity index 100% rename from guide/src/python_from_rust.md rename to guide/src/python-from-rust.md diff --git a/guide/src/python_typing_hints.md b/guide/src/python-typing-hints.md similarity index 100% rename from guide/src/python_typing_hints.md rename to guide/src/python-typing-hints.md diff --git a/guide/src/trait_bounds.md b/guide/src/trait-bounds.md similarity index 100% rename from guide/src/trait_bounds.md rename to guide/src/trait-bounds.md diff --git a/noxfile.py b/noxfile.py index 29fe0f916cd..f70fced76d6 100644 --- a/noxfile.py +++ b/noxfile.py @@ -417,6 +417,7 @@ def check_guide(session: nox.Session): "lychee", 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}", *session.posargs, ) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 698c58a18ec..7f02f744fc2 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -1365,7 +1365,7 @@ fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result "Documentation about the `serde` feature." -//! [calling_rust]: https://pyo3.rs/latest/python_from_rust.html "Calling Python from Rust - PyO3 user guide" +//! [calling_rust]: https://pyo3.rs/latest/python-from-rust.html "Calling Python from Rust - PyO3 user guide" //! [examples subdirectory]: https://github.com/PyO3/pyo3/tree/main/examples //! [feature flags]: https://doc.rust-lang.org/cargo/reference/features.html "Features - The Cargo Book" //! [global interpreter lock]: https://docs.python.org/3/glossary.html#term-global-interpreter-lock //! [hashbrown]: https://docs.rs/hashbrown //! [smallvec]: https://docs.rs/smallvec //! [indexmap]: https://docs.rs/indexmap -//! [manual_builds]: https://pyo3.rs/latest/building_and_distribution.html#manual-builds "Manual builds - Building and Distribution - PyO3 user guide" +//! [manual_builds]: https://pyo3.rs/latest/building-and-distribution.html#manual-builds "Manual builds - Building and Distribution - PyO3 user guide" //! [num-bigint]: https://docs.rs/num-bigint //! [num-complex]: https://docs.rs/num-complex //! [serde]: https://docs.rs/serde @@ -453,7 +453,7 @@ pub use pyo3_macros::{pyfunction, pymethods, pymodule, FromPyObject}; /// A proc macro used to expose Rust structs and fieldless enums as Python objects. /// -#[doc = include_str!("../guide/pyclass_parameters.md")] +#[doc = include_str!("../guide/pyclass-parameters.md")] /// /// For more on creating Python classes, /// see the [class section of the guide][1]. @@ -485,8 +485,8 @@ pub mod doc_test { "README.md" => readme_md, "guide/src/advanced.md" => guide_advanced_md, "guide/src/async-await.md" => guide_async_await_md, - "guide/src/building_and_distribution.md" => guide_building_and_distribution_md, - "guide/src/building_and_distribution/multiple_python_versions.md" => guide_bnd_multiple_python_versions_md, + "guide/src/building-and-distribution.md" => guide_building_and_distribution_md, + "guide/src/building-and-distribution/multiple-python-versions.md" => guide_bnd_multiple_python_versions_md, "guide/src/class.md" => guide_class_md, "guide/src/class/call.md" => guide_class_call, "guide/src/class/object.md" => guide_class_object, @@ -504,16 +504,16 @@ pub mod doc_test { "guide/src/faq.md" => guide_faq_md, "guide/src/features.md" => guide_features_md, "guide/src/function.md" => guide_function_md, - "guide/src/function/error_handling.md" => guide_function_error_handling_md, + "guide/src/function/error-handling.md" => guide_function_error_handling_md, "guide/src/function/signature.md" => guide_function_signature_md, "guide/src/memory.md" => guide_memory_md, "guide/src/migration.md" => guide_migration_md, "guide/src/module.md" => guide_module_md, "guide/src/parallelism.md" => guide_parallelism_md, "guide/src/performance.md" => guide_performance_md, - "guide/src/python_from_rust.md" => guide_python_from_rust_md, - "guide/src/python_typing_hints.md" => guide_python_typing_hints_md, - "guide/src/trait_bounds.md" => guide_trait_bounds_md, + "guide/src/python-from-rust.md" => guide_python_from_rust_md, + "guide/src/python-typing-hints.md" => guide_python_typing_hints_md, + "guide/src/trait-bounds.md" => guide_trait_bounds_md, "guide/src/types.md" => guide_types_md, } } From 9145fcfe19ad114d3bd7b4c509d900e6781b7c40 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sun, 10 Mar 2024 15:51:51 +0000 Subject: [PATCH 076/936] docs: major rewrite for Bound API (#3906) * wip bound docs * Update guide/src/python_from_rust/calling-existing-code.md Co-authored-by: Lily Foote * continue to move and tidy up * Apply suggestions from code review Co-authored-by: Lily Foote * update URL * complete python-from-rust.md * progress on types.md; probably more to go * update doctest paths * review: Icxolu * finish updating `types.md` to Bound API * update remainder of the guide to Bound API * Update guide/src/performance.md Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> * Update guide/src/types.md Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> * Update src/lib.rs * review: Icxolu * Update guide/src/python-from-rust.md Co-authored-by: Adam Reichold * Update guide/src/async-await.md Co-authored-by: Adam Reichold * review: adamreichold --------- Co-authored-by: Lily Foote Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> Co-authored-by: Adam Reichold --- Architecture.md | 29 +- guide/src/SUMMARY.md | 27 +- guide/src/advanced.md | 7 - guide/src/async-await.md | 12 +- guide/src/class.md | 40 +- guide/src/conversions/tables.md | 75 +-- guide/src/conversions/traits.md | 2 +- guide/src/ecosystem/async-await.md | 12 +- guide/src/exception.md | 2 +- guide/src/function-calls.md | 1 + guide/src/function.md | 4 +- guide/src/index.md | 16 + guide/src/memory.md | 63 ++- guide/src/migration.md | 16 +- guide/src/performance.md | 44 +- guide/src/python-from-rust.md | 521 +----------------- .../python-from-rust/calling-existing-code.md | 397 +++++++++++++ guide/src/python-from-rust/function-calls.md | 114 ++++ guide/src/rust-from-python.md | 13 + guide/src/trait-bounds.md | 81 +-- guide/src/types.md | 456 +++++++++------ src/lib.rs | 52 +- src/sync.rs | 5 + 23 files changed, 1114 insertions(+), 875 deletions(-) create mode 100644 guide/src/function-calls.md create mode 100644 guide/src/python-from-rust/calling-existing-code.md create mode 100644 guide/src/python-from-rust/function-calls.md create mode 100644 guide/src/rust-from-python.md diff --git a/Architecture.md b/Architecture.md index 60083d71c98..a4218a7f71f 100644 --- a/Architecture.md +++ b/Architecture.md @@ -59,6 +59,7 @@ Those flags are set in [`build.rs`](#6-buildrs-and-pyo3-build-config). [`src/types`] contains bindings to [built-in types](https://docs.python.org/3/library/stdtypes.html) of Python, such as `dict` and `list`. For historical reasons, Python's `object` is called `PyAny` in PyO3 and located in [`src/types/any.rs`]. + Currently, `PyAny` is a straightforward wrapper of `ffi::PyObject`, defined as: ```rust @@ -66,38 +67,16 @@ Currently, `PyAny` is a straightforward wrapper of `ffi::PyObject`, defined as: pub struct PyAny(UnsafeCell); ``` -All built-in types are defined as a C struct. -For example, `dict` is defined as: - -```c -typedef struct { - /* Base object */ - PyObject ob_base; - /* Number of items in the dictionary */ - Py_ssize_t ma_used; - /* Dictionary version */ - uint64_t ma_version_tag; - PyDictKeysObject *ma_keys; - PyObject **ma_values; -} PyDictObject; -``` - -However, we cannot access such a specific data structure with `#[cfg(Py_LIMITED_API)]` set. -Thus, all builtin objects are implemented as opaque types by wrapping `PyAny`, e.g.,: +Concrete Python objects are implemented by wrapping `PyAny`, e.g.,: ```rust #[repr(transparent)] pub struct PyDict(PyAny); ``` -Note that `PyAny` is not a pointer, and it is usually used as a pointer to the object in the -Python heap, as `&PyAny`. -This design choice can be changed -(see the discussion in [#1056](https://github.com/PyO3/pyo3/issues/1056)). +These types are not intended to be accessed directly, and instead are used through the `Py` and `Bound` smart pointers. -Since we need lots of boilerplate for implementing common traits for these types -(e.g., `AsPyPointer`, `AsRef`, and `Debug`), we have some macros in -[`src/types/mod.rs`]. +We have some macros in [`src/types/mod.rs`] which make it easier to implement APIs for concrete Python types. ## 3. `PyClass` and related functionalities diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md index 6aea71b1722..4c22c26f587 100644 --- a/guide/src/SUMMARY.md +++ b/guide/src/SUMMARY.md @@ -5,22 +5,25 @@ --- - [Getting started](getting-started.md) -- [Python modules](module.md) -- [Python functions](function.md) - - [Function signatures](function/signature.md) - - [Error handling](function/error-handling.md) -- [Python classes](class.md) - - [Class customizations](class/protocols.md) - - [Basic object customization](class/object.md) - - [Emulating numeric types](class/numeric.md) - - [Emulating callable objects](class/call.md) +- [Using Rust from Python](rust-from-python.md) + - [Python modules](module.md) + - [Python functions](function.md) + - [Function signatures](function/signature.md) + - [Error handling](function/error-handling.md) + - [Python classes](class.md) + - [Class customizations](class/protocols.md) + - [Basic object customization](class/object.md) + - [Emulating numeric types](class/numeric.md) + - [Emulating callable objects](class/call.md) +- [Calling Python from Rust](python-from-rust.md) + - [Python object types](types.md) + - [Python exceptions](exception.md) + - [Calling Python functions](python-from-rust/function-calls.md) + - [Executing existing Python code](python-from-rust/calling-existing-code.md) - [Type conversions](conversions.md) - [Mapping of Rust types to Python types](conversions/tables.md) - [Conversion traits](conversions/traits.md) -- [Python exceptions](exception.md) -- [Calling Python from Rust](python-from-rust.md) - [Using `async` and `await`](async-await.md) -- [GIL, mutability and object types](types.md) - [Parallelism](parallelism.md) - [Debugging](debugging.md) - [Features reference](features.md) diff --git a/guide/src/advanced.md b/guide/src/advanced.md index 8264c14dd17..61dc66382f4 100644 --- a/guide/src/advanced.md +++ b/guide/src/advanced.md @@ -5,10 +5,3 @@ PyO3 exposes much of Python's C API through the `ffi` module. The C API is naturally unsafe and requires you to manage reference counts, errors and specific invariants yourself. Please refer to the [C API Reference Manual](https://docs.python.org/3/c-api/) and [The Rustonomicon](https://doc.rust-lang.org/nightly/nomicon/ffi.html) before using any function from that API. - -## Memory management - -PyO3's `&PyAny` "owned references" and `Py` smart pointers are used to -access memory stored in Python's heap. This memory sometimes lives for longer -than expected because of differences in Rust and Python's memory models. See -the chapter on [memory management](./memory.md) for more information. diff --git a/guide/src/async-await.md b/guide/src/async-await.md index c354be7f1b7..06fa1580ad7 100644 --- a/guide/src/async-await.md +++ b/guide/src/async-await.md @@ -30,13 +30,13 @@ async fn sleep(seconds: f64, result: Option) -> Option { Resulting future of an `async fn` decorated by `#[pyfunction]` must be `Send + 'static` to be embedded in a Python object. -As a consequence, `async fn` parameters and return types must also be `Send + 'static`, so it is not possible to have a signature like `async fn does_not_compile(arg: &PyAny, py: Python<'_>) -> &PyAny`. +As a consequence, `async fn` parameters and return types must also be `Send + 'static`, so it is not possible to have a signature like `async fn does_not_compile<'py>(arg: Bound<'py, PyAny>) -> Bound<'py, PyAny>`. -However, there is an exception for method receiver, so async methods can accept `&self`/`&mut self`. Note that this means that the class instance is borrowed for as long as the returned future is not completed, even across yield points and while waiting for I/O operations to complete. Hence, other methods cannot obtain exclusive borrows while the future is still being polled. This is the same as how async methods in Rust generally work but it is more problematic for Rust code interfacing with Python code due to pervasive shared mutability. This strongly suggests to prefer shared borrows `&self` to exclusive ones `&mut self` to avoid racy borrow check failures at runtime. +However, there is an exception for method receivers, so async methods can accept `&self`/`&mut self`. Note that this means that the class instance is borrowed for as long as the returned future is not completed, even across yield points and while waiting for I/O operations to complete. Hence, other methods cannot obtain exclusive borrows while the future is still being polled. This is the same as how async methods in Rust generally work but it is more problematic for Rust code interfacing with Python code due to pervasive shared mutability. This strongly suggests to prefer shared borrows `&self` over exclusive ones `&mut self` to avoid racy borrow check failures at runtime. ## Implicit GIL holding -Even if it is not possible to pass a `py: Python<'_>` parameter to `async fn`, the GIL is still held during the execution of the future – it's also the case for regular `fn` without `Python<'_>`/`&PyAny` parameter, yet the GIL is held. +Even if it is not possible to pass a `py: Python<'py>` parameter to `async fn`, the GIL is still held during the execution of the future – it's also the case for regular `fn` without `Python<'py>`/`Bound<'py, PyAny>` parameter, yet the GIL is held. It is still possible to get a `Python` marker using [`Python::with_gil`]({{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.with_gil); because `with_gil` is reentrant and optimized, the cost will be negligible. @@ -47,7 +47,11 @@ There is currently no simple way to release the GIL when awaiting a future, *but Here is the advised workaround for now: ```rust,ignore -use std::{future::Future, pin::{Pin, pin}, task::{Context, Poll}}; +use std::{ + future::Future, + pin::{Pin, pin}, + task::{Context, Poll}, +}; use pyo3::prelude::*; struct AllowThreads(F); diff --git a/guide/src/class.md b/guide/src/class.md index 93920a40d88..2c6d854ff08 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -60,7 +60,7 @@ enum Shape { Circle { radius: f64 }, Rectangle { width: f64, height: f64 }, RegularPolygon { side_count: u32, radius: f64 }, - Nothing { }, + Nothing {}, } ``` @@ -89,7 +89,7 @@ Currently, the best alternative is to write a macro which expands to a new `#[py use pyo3::prelude::*; struct GenericClass { - data: T + data: T, } macro_rules! create_interface { @@ -102,7 +102,9 @@ macro_rules! create_interface { impl $name { #[new] pub fn new(data: $type) -> Self { - Self { inner: GenericClass { data: data } } + Self { + inner: GenericClass { data: data }, + } } } }; @@ -187,23 +189,21 @@ fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> { } ``` -## Bound and interior mutability +## Bound and interior mutability + +Often is 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. -You sometimes need to convert your `#[pyclass]` into a Python object and access it -from Rust code (e.g., for testing it). -[`Bound`] is the primary interface for that. +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. -To mutate data behind a `Bound<'_, T>` safely, PyO3 employs the -[Interior Mutability Pattern](https://doc.rust-lang.org/book/ch15-05-interior-mutability.html) -like [`RefCell`]. +The Rust borrow checker cannot reason about `&mut` references once an object's ownership has been passed to the Python interpreter. This means that borrow checking is done at runtime using with a scheme very similar to `std::cell::RefCell`. This is known as [interior mutability](https://doc.rust-lang.org/book/ch15-05-interior-mutability.html). -Users who are familiar with `RefCell` can use `Bound` just like `RefCell`. +Users who are familiar with `RefCell` can use `Py` and `Bound<'py, T>` just like `RefCell`. -For users who are not very familiar with `RefCell`, here is a reminder of Rust's rules of borrowing: +For users who are not very familiar with `RefCell`, here is a reminder of Rust's rules of borrowing: - At any given time, you can have either (but not both of) one mutable reference or any number of immutable references. -- References must always be valid. +- References can never outlast the data they refer to. -`Bound`, like `RefCell`, ensures these borrowing rules by tracking references at runtime. +`Py` and `Bound<'py, T>`, like `RefCell`, ensure these borrowing rules by tracking references at runtime. ```rust # use pyo3::prelude::*; @@ -233,10 +233,8 @@ Python::with_gil(|py| { }); ``` -A `Bound<'py, T>` is restricted to the GIL lifetime `'py`. -To make the object longer lived (for example, to store it in a struct on the -Rust side), you can use `Py`, which stores an object longer than the GIL -lifetime, and therefore needs a `Python<'_>` token to access. +A `Bound<'py, T>` is restricted to the GIL lifetime `'py`. To make the object longer lived (for example, to store it in a struct on the +Rust side), use `Py`. `Py` needs a `Python<'_>` token to allow access: ```rust # use pyo3::prelude::*; @@ -252,7 +250,7 @@ fn return_myclass() -> Py { let obj = return_myclass(); Python::with_gil(|py| { - let bound = obj.bind(py); // Py::bind returns &Bound<'_, MyClass> + let bound = obj.bind(py); // Py::bind returns &Bound<'py, MyClass> let obj_ref = bound.borrow(); // Get PyRef assert_eq!(obj_ref.num, 1); }); @@ -431,7 +429,7 @@ impl DictWithCounter { Self::default() } - fn set(slf: &Bound<'_, Self>, key: String, value: &PyAny) -> PyResult<()> { + fn set(slf: &Bound<'_, Self>, key: String, value: Bound<'_, PyAny>) -> PyResult<()> { slf.borrow_mut().counter.entry(key.clone()).or_insert(0); let dict = slf.downcast::()?; dict.set_item(key, value) @@ -1319,7 +1317,7 @@ impl pyo3::impl_::pyclass::PyClassImpl for MyClass { [`PyTypeInfo`]: {{#PYO3_DOCS_URL}}/pyo3/type_object/trait.PyTypeInfo.html [`Py`]: {{#PYO3_DOCS_URL}}/pyo3/struct.Py.html -[`Bound`]: {{#PYO3_DOCS_URL}}/pyo3/struct.Bound.html +[`Bound<'_, T>`]: {{#PYO3_DOCS_URL}}/pyo3/struct.Bound.html [`PyClass`]: {{#PYO3_DOCS_URL}}/pyo3/pyclass/trait.PyClass.html [`PyRef`]: {{#PYO3_DOCS_URL}}/pyo3/pycell/struct.PyRef.html [`PyRefMut`]: {{#PYO3_DOCS_URL}}/pyo3/pycell/struct.PyRefMut.html diff --git a/guide/src/conversions/tables.md b/guide/src/conversions/tables.md index b6a2d30e55a..eb33b17acf7 100644 --- a/guide/src/conversions/tables.md +++ b/guide/src/conversions/tables.md @@ -12,49 +12,49 @@ The table below contains the Python type and the corresponding function argument | Python | Rust | Rust (Python-native) | | ------------- |:-------------------------------:|:--------------------:| -| `object` | - | `&PyAny` | -| `str` | `String`, `Cow`, `&str`, `char`, `OsString`, `PathBuf`, `Path` | `&PyString`, `&PyUnicode` | -| `bytes` | `Vec`, `&[u8]`, `Cow<[u8]>` | `&PyBytes` | -| `bool` | `bool` | `&PyBool` | -| `int` | `i8`, `u8`, `i16`, `u16`, `i32`, `u32`, `i64`, `u64`, `i128`, `u128`, `isize`, `usize`, `num_bigint::BigInt`[^1], `num_bigint::BigUint`[^1] | `&PyLong` | -| `float` | `f32`, `f64` | `&PyFloat` | -| `complex` | `num_complex::Complex`[^2] | `&PyComplex` | -| `list[T]` | `Vec` | `&PyList` | -| `dict[K, V]` | `HashMap`, `BTreeMap`, `hashbrown::HashMap`[^3], `indexmap::IndexMap`[^4] | `&PyDict` | -| `tuple[T, U]` | `(T, U)`, `Vec` | `&PyTuple` | -| `set[T]` | `HashSet`, `BTreeSet`, `hashbrown::HashSet`[^3] | `&PySet` | -| `frozenset[T]` | `HashSet`, `BTreeSet`, `hashbrown::HashSet`[^3] | `&PyFrozenSet` | -| `bytearray` | `Vec`, `Cow<[u8]>` | `&PyByteArray` | -| `slice` | - | `&PySlice` | -| `type` | - | `&PyType` | -| `module` | - | `&PyModule` | +| `object` | - | `PyAny` | +| `str` | `String`, `Cow`, `&str`, `char`, `OsString`, `PathBuf`, `Path` | `PyString`, `PyUnicode` | +| `bytes` | `Vec`, `&[u8]`, `Cow<[u8]>` | `PyBytes` | +| `bool` | `bool` | `PyBool` | +| `int` | `i8`, `u8`, `i16`, `u16`, `i32`, `u32`, `i64`, `u64`, `i128`, `u128`, `isize`, `usize`, `num_bigint::BigInt`[^1], `num_bigint::BigUint`[^1] | `PyLong` | +| `float` | `f32`, `f64` | `PyFloat` | +| `complex` | `num_complex::Complex`[^2] | `PyComplex` | +| `list[T]` | `Vec` | `PyList` | +| `dict[K, V]` | `HashMap`, `BTreeMap`, `hashbrown::HashMap`[^3], `indexmap::IndexMap`[^4] | `PyDict` | +| `tuple[T, U]` | `(T, U)`, `Vec` | `PyTuple` | +| `set[T]` | `HashSet`, `BTreeSet`, `hashbrown::HashSet`[^3] | `PySet` | +| `frozenset[T]` | `HashSet`, `BTreeSet`, `hashbrown::HashSet`[^3] | `PyFrozenSet` | +| `bytearray` | `Vec`, `Cow<[u8]>` | `PyByteArray` | +| `slice` | - | `PySlice` | +| `type` | - | `PyType` | +| `module` | - | `PyModule` | | `collections.abc.Buffer` | - | `PyBuffer` | -| `datetime.datetime` | `SystemTime`, `chrono::DateTime`[^5], `chrono::NaiveDateTime`[^5] | `&PyDateTime` | -| `datetime.date` | `chrono::NaiveDate`[^5] | `&PyDate` | -| `datetime.time` | `chrono::NaiveTime`[^5] | `&PyTime` | -| `datetime.tzinfo` | `chrono::FixedOffset`[^5], `chrono::Utc`[^5], `chrono_tz::TimeZone`[^6] | `&PyTzInfo` | -| `datetime.timedelta` | `Duration`, `chrono::Duration`[^5] | `&PyDelta` | +| `datetime.datetime` | `SystemTime`, `chrono::DateTime`[^5], `chrono::NaiveDateTime`[^5] | `PyDateTime` | +| `datetime.date` | `chrono::NaiveDate`[^5] | `PyDate` | +| `datetime.time` | `chrono::NaiveTime`[^5] | `PyTime` | +| `datetime.tzinfo` | `chrono::FixedOffset`[^5], `chrono::Utc`[^5], `chrono_tz::TimeZone`[^6] | `PyTzInfo` | +| `datetime.timedelta` | `Duration`, `chrono::Duration`[^5] | `PyDelta` | | `decimal.Decimal` | `rust_decimal::Decimal`[^7] | - | | `ipaddress.IPv4Address` | `std::net::IpAddr`, `std::net::IpV4Addr` | - | | `ipaddress.IPv6Address` | `std::net::IpAddr`, `std::net::IpV6Addr` | - | -| `os.PathLike ` | `PathBuf`, `Path` | `&PyString`, `&PyUnicode` | -| `pathlib.Path` | `PathBuf`, `Path` | `&PyString`, `&PyUnicode` | +| `os.PathLike ` | `PathBuf`, `Path` | `PyString`, `PyUnicode` | +| `pathlib.Path` | `PathBuf`, `Path` | `PyString`, `PyUnicode` | | `typing.Optional[T]` | `Option` | - | -| `typing.Sequence[T]` | `Vec` | `&PySequence` | +| `typing.Sequence[T]` | `Vec` | `PySequence` | | `typing.Mapping[K, V]` | `HashMap`, `BTreeMap`, `hashbrown::HashMap`[^3], `indexmap::IndexMap`[^4] | `&PyMapping` | -| `typing.Iterator[Any]` | - | `&PyIterator` | +| `typing.Iterator[Any]` | - | `PyIterator` | | `typing.Union[...]` | See [`#[derive(FromPyObject)]`](traits.md#deriving-frompyobject-for-enums) | - | -There are also a few special types related to the GIL and Rust-defined `#[pyclass]`es which may come in useful: +It is also worth remembering the following special types: -| What | Description | -| ------------- | ------------------------------------- | -| `Python` | A GIL token, used to pass to PyO3 constructors to prove ownership of the GIL | -| `Py` | A Python object isolated from the GIL lifetime. This can be sent to other threads. | -| `PyObject` | An alias for `Py` | -| `&PyCell` | A `#[pyclass]` value owned by Python. | -| `PyRef` | A `#[pyclass]` borrowed immutably. | -| `PyRefMut` | A `#[pyclass]` borrowed mutably. | +| What | Description | +| ---------------- | ------------------------------------- | +| `Python<'py>` | A GIL token, used to pass to PyO3 constructors to prove ownership of the GIL. | +| `Bound<'py, T>` | A Python object connected to the GIL lifetime. This provides access to most of PyO3's APIs. | +| `Py` | A Python object isolated from the GIL lifetime. This can be sent to other threads. | +| `PyObject` | An alias for `Py` | +| `PyRef` | A `#[pyclass]` borrowed immutably. | +| `PyRefMut` | A `#[pyclass]` borrowed mutably. | For more detail on accepting `#[pyclass]` values as function arguments, see [the section of this guide on Python Classes](../class.md). @@ -72,9 +72,9 @@ For most PyO3 usage the conversion cost is worth paying to get these benefits. A ### Returning Rust values to Python -When returning values from functions callable from Python, Python-native types (`&PyAny`, `&PyDict` etc.) can be used with zero cost. +When returning values from functions callable from Python, [PyO3's smart pointers](../types.md#pyo3s-smart-pointers) (`Py`, `Bound<'py, T>`, and `Borrowed<'a, 'py, T>`) can be used with zero cost. -Because these types are references, in some situations the Rust compiler may ask for lifetime annotations. If this is the case, you should use `Py`, `Py` etc. instead - which are also zero-cost. For all of these Python-native types `T`, `Py` can be created from `T` with an `.into()` conversion. +Because `Bound<'py, T>` and `Borrowed<'a, 'py, T>` have lifetime parameters, the Rust compiler may ask for lifetime annotations to be added to your function. See the [section of the guide dedicated to this](../types.md#function-argument-lifetimes). If your function is fallible, it should return `PyResult` or `Result` where `E` implements `From for PyErr`. This will raise a `Python` exception if the `Err` variant is returned. @@ -95,7 +95,8 @@ Finally, the following Rust types are also able to convert to Python as return v | `BTreeMap` | `Dict[K, V]` | | `HashSet` | `Set[T]` | | `BTreeSet` | `Set[T]` | -| `&PyCell` | `T` | +| `Py` | `T` | +| `Bound` | `T` | | `PyRef` | `T` | | `PyRefMut` | `T` | diff --git a/guide/src/conversions/traits.md b/guide/src/conversions/traits.md index 5cdf2c590b9..3a00a160c65 100644 --- a/guide/src/conversions/traits.md +++ b/guide/src/conversions/traits.md @@ -484,7 +484,7 @@ If the input is neither a string nor an integer, the error message will be: - `pyo3(from_py_with = "...")` - apply a custom function to convert the field from Python the desired Rust type. - the argument must be the name of the function as a string. - - the function signature must be `fn(&PyAny) -> PyResult` where `T` is the Rust type of the argument. + - the function signature must be `fn(&Bound) -> PyResult` where `T` is the Rust type of the argument. ### `IntoPy` diff --git a/guide/src/ecosystem/async-await.md b/guide/src/ecosystem/async-await.md index 1e4ea4ab4b9..0319fa05063 100644 --- a/guide/src/ecosystem/async-await.md +++ b/guide/src/ecosystem/async-await.md @@ -120,7 +120,7 @@ Export an async function that makes use of `async-std`: use pyo3::{prelude::*, wrap_pyfunction}; #[pyfunction] -fn rust_sleep(py: Python<'_>) -> PyResult<&PyAny> { +fn rust_sleep(py: Python<'_>) -> PyResult<&Bound<'_, PyAny>>> { pyo3_asyncio::async_std::future_into_py(py, async { async_std::task::sleep(std::time::Duration::from_secs(1)).await; Ok(Python::with_gil(|py| py.None())) @@ -143,7 +143,7 @@ If you want to use `tokio` instead, here's what your module should look like: use pyo3::{prelude::*, wrap_pyfunction}; #[pyfunction] -fn rust_sleep(py: Python<'_>) -> PyResult<&PyAny> { +fn rust_sleep(py: Python<'_>) -> PyResult<&Bound<'_, PyAny>>> { pyo3_asyncio::tokio::future_into_py(py, async { tokio::time::sleep(std::time::Duration::from_secs(1)).await; Ok(Python::with_gil(|py| py.None())) @@ -233,7 +233,7 @@ a coroutine argument: ```rust #[pyfunction] -fn await_coro(coro: &PyAny) -> PyResult<()> { +fn await_coro(coro: &Bound<'_, PyAny>>) -> PyResult<()> { // convert the coroutine into a Rust future using the // async_std runtime let f = pyo3_asyncio::async_std::into_future(coro)?; @@ -261,7 +261,7 @@ If for you wanted to pass a callable function to the `#[pyfunction]` instead, (i ```rust #[pyfunction] -fn await_coro(callable: &PyAny) -> PyResult<()> { +fn await_coro(callable: &Bound<'_, PyAny>>) -> PyResult<()> { // get the coroutine by calling the callable let coro = callable.call0()?; @@ -317,7 +317,7 @@ async fn rust_sleep() { } #[pyfunction] -fn call_rust_sleep(py: Python<'_>) -> PyResult<&PyAny> { +fn call_rust_sleep(py: Python<'_>) -> PyResult<&Bound<'_, PyAny>>> { pyo3_asyncio::async_std::future_into_py(py, async move { rust_sleep().await; Ok(Python::with_gil(|py| py.None())) @@ -467,7 +467,7 @@ tokio = "1.4" use pyo3::{prelude::*, wrap_pyfunction}; #[pyfunction] -fn rust_sleep(py: Python<'_>) -> PyResult<&PyAny> { +fn rust_sleep(py: Python<'_>) -> PyResult<&Bound<'_, PyAny>>> { pyo3_asyncio::tokio::future_into_py(py, async { tokio::time::sleep(std::time::Duration::from_secs(1)).await; Ok(Python::with_gil(|py| py.None())) diff --git a/guide/src/exception.md b/guide/src/exception.md index ee36f544d74..3e2f5034897 100644 --- a/guide/src/exception.md +++ b/guide/src/exception.md @@ -111,7 +111,7 @@ mod io { pyo3::import_exception!(io, UnsupportedOperation); } -fn tell(file: &PyAny) -> PyResult { +fn tell(file: &Bound<'_, PyAny>) -> PyResult { match file.call_method0("tell") { Err(_) => Err(io::UnsupportedOperation::new_err("not supported: tell")), Ok(x) => x.extract::(), diff --git a/guide/src/function-calls.md b/guide/src/function-calls.md new file mode 100644 index 00000000000..e5c9c1ed9b3 --- /dev/null +++ b/guide/src/function-calls.md @@ -0,0 +1 @@ +# Calling Python functions diff --git a/guide/src/function.md b/guide/src/function.md index cfb1e5ef81e..86ac4c89b46 100644 --- a/guide/src/function.md +++ b/guide/src/function.md @@ -87,7 +87,9 @@ The `#[pyo3]` attribute can be used to modify properties of the generated Python #[pyfunction] #[pyo3(pass_module)] - fn pyfunction_with_module<'py>(module: &Bound<'py, PyModule>) -> PyResult> { + fn pyfunction_with_module<'py>( + module: &Bound<'py, PyModule>, + ) -> PyResult> { module.name() } diff --git a/guide/src/index.md b/guide/src/index.md index 80534caea5f..87914975afe 100644 --- a/guide/src/index.md +++ b/guide/src/index.md @@ -2,6 +2,22 @@ Welcome to the PyO3 user guide! This book is a companion to [PyO3's API docs](https://docs.rs/pyo3). It contains examples and documentation to explain all of PyO3's use cases in detail. +The rough order of material in this user guide is as follows: + 1. Getting started + 2. Wrapping Rust code for use from Python + 3. How to use Python code from Rust + 4. Remaining topics which go into advanced concepts in detail + Please choose from the chapters on the left to jump to individual topics, or continue below to start with PyO3's README. +
+⚠️ Warning: API update in progress 🛠️ + +PyO3 0.21 has introduced a significant new API, termed the "Bound" API after the new smart pointer `Bound`. + +While most of this guide has been updated to the new API, it is possible some stray references to the older "GIL Refs" API such as `&PyAny` remain. +
+ +
+ {{#include ../../README.md}} diff --git a/guide/src/memory.md b/guide/src/memory.md index b14ce4496ad..fe98184e3e1 100644 --- a/guide/src/memory.md +++ b/guide/src/memory.md @@ -1,5 +1,15 @@ # Memory management +
+⚠️ Warning: API update in progress 🛠️ + +PyO3 0.21 has introduced a significant new API, termed the "Bound" API after the new smart pointer `Bound`. + +This section on memory management is heavily weighted towards the now-deprecated "GIL Refs" API, which suffered from the drawbacks detailed here as well as CPU overheads. + +See [the smart pointer types](./types.md#pyo3s-smart-pointers) for description on the new, simplified, memory model of the Bound API, which is built as a thin wrapper on Python reference counting. +
+ Rust and Python have very different notions of memory management. Rust has a strict memory model with concepts of ownership, borrowing, and lifetimes, where memory is freed at predictable points in program execution. Python has @@ -10,12 +20,12 @@ Memory in Python is freed eventually by the garbage collector, but not usually in a predictable way. PyO3 bridges the Rust and Python memory models with two different strategies for -accessing memory allocated on Python's heap from inside Rust. These are -GIL-bound, or "owned" references, and GIL-independent `Py` smart pointers. +accessing memory allocated on Python's heap from inside Rust. These are +GIL Refs such as `&'py PyAny`, and GIL-independent `Py` smart pointers. ## GIL-bound memory -PyO3's GIL-bound, "owned references" (`&PyAny` etc.) make PyO3 more ergonomic to +PyO3's GIL Refs such as `&'py PyAny` make PyO3 more ergonomic to use by ensuring that their lifetime can never be longer than the duration the Python GIL is held. This means that most of PyO3's API can assume the GIL is held. (If PyO3 could not assume this, every PyO3 API would need to take a @@ -27,7 +37,10 @@ very simple and easy-to-understand programs like this: # use pyo3::types::PyString; # fn main() -> PyResult<()> { Python::with_gil(|py| -> PyResult<()> { - let hello = py.eval_bound("\"Hello World!\"", None, None)?.downcast_into::()?; + #[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(()) })?; @@ -48,7 +61,10 @@ of the time we don't have to think about this, but consider the following: # fn main() -> PyResult<()> { Python::with_gil(|py| -> PyResult<()> { for _ in 0..10 { - let hello = py.eval_bound("\"Hello World!\"", None, None)?.downcast_into::()?; + #[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. @@ -76,7 +92,10 @@ is to acquire and release the GIL with each iteration of the loop. # fn main() -> PyResult<()> { for _ in 0..10 { Python::with_gil(|py| -> PyResult<()> { - let hello = py.eval_bound("\"Hello World!\"", None, None)?.downcast_into::()?; + #[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 @@ -97,7 +116,10 @@ Python::with_gil(|py| -> PyResult<()> { for _ in 0..10 { let pool = unsafe { py.new_pool() }; let py = pool.python(); - let hello = py.eval_bound("\"Hello World!\"", None, None)?.downcast_into::()?; + #[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(()) @@ -144,8 +166,12 @@ reference count reaches zero? It depends whether or not we are holding the GIL. # use pyo3::types::PyString; # fn main() -> PyResult<()> { Python::with_gil(|py| -> PyResult<()> { - let hello: Py = py.eval_bound("\"Hello World!\"", None, None)?.extract()?; - println!("Python says: {}", hello.bind(py)); + #[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(()) @@ -166,7 +192,8 @@ we are *not* holding the GIL? # use pyo3::types::PyString; # fn main() -> PyResult<()> { let hello: Py = Python::with_gil(|py| { - py.eval_bound("\"Hello World!\"", None, None)?.extract() + #[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`. @@ -198,12 +225,16 @@ We can avoid the delay in releasing memory if we are careful to drop the # use pyo3::prelude::*; # use pyo3::types::PyString; # fn main() -> PyResult<()> { +#[allow(deprecated)] // py.eval() is part of the GIL Refs API let hello: Py = - Python::with_gil(|py| py.eval_bound("\"Hello World!\"", None, None)?.extract())?; + Python::with_gil(|py| py.eval("\"Hello World!\"", None, None)?.extract())?; // Do some stuff... // Now sometime later in the program: Python::with_gil(|py| { - println!("Python says: {}", hello.bind(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(()) @@ -220,12 +251,16 @@ until the GIL is dropped. # use pyo3::prelude::*; # use pyo3::types::PyString; # fn main() -> PyResult<()> { +#[allow(deprecated)] // py.eval() is part of the GIL Refs API let hello: Py = - Python::with_gil(|py| py.eval_bound("\"Hello World!\"", None, None)?.extract())?; + Python::with_gil(|py| py.eval("\"Hello World!\"", None, None)?.extract())?; // Do some stuff... // Now sometime later in the program: Python::with_gil(|py| { - println!("Python says: {}", hello.into_bound(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. diff --git a/guide/src/migration.md b/guide/src/migration.md index 3ed3e015b3b..5af6eb2336c 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -190,7 +190,9 @@ struct PyClassAsyncIter { impl PyClassAsyncIter { fn __anext__(&mut self) -> PyClassAwaitable { self.number += 1; - PyClassAwaitable { number: self.number } + PyClassAwaitable { + number: self.number, + } } fn __aiter__(slf: Py) -> Py { @@ -312,7 +314,10 @@ Python::with_gil(|py| { // `b` is not in the dictionary assert!(dict.get_item("b").is_none()); // `dict` is not hashable, so this fails with a `TypeError` - assert!(dict.get_item_with_error(dict).unwrap_err().is_instance_of::(py)); + assert!(dict + .get_item_with_error(dict) + .unwrap_err() + .is_instance_of::(py)); }); # } ``` @@ -333,7 +338,10 @@ Python::with_gil(|py| -> PyResult<()> { // `b` is not in the dictionary assert!(dict.get_item("b")?.is_none()); // `dict` is not hashable, so this fails with a `TypeError` - assert!(dict.get_item(dict).unwrap_err().is_instance_of::(py)); + assert!(dict + .get_item(dict) + .unwrap_err() + .is_instance_of::(py)); Ok(()) }); @@ -495,7 +503,7 @@ drop(first); drop(second); ``` -The replacement is [`Python::with_gil`]() which is more cumbersome but enforces the proper nesting by design, e.g. +The replacement is [`Python::with_gil`](https://docs.rs/pyo3/0.18.3/pyo3/marker/struct.Python.html#method.with_gil) which is more cumbersome but enforces the proper nesting by design, e.g. ```rust # #![allow(dead_code)] diff --git a/guide/src/performance.md b/guide/src/performance.md index fe362bed953..c47a91deee5 100644 --- a/guide/src/performance.md +++ b/guide/src/performance.md @@ -4,26 +4,26 @@ To achieve the best possible performance, it is useful to be aware of several tr ## `extract` versus `downcast` -Pythonic API implemented using PyO3 are often polymorphic, i.e. they will accept `&PyAny` and try to turn this into multiple more concrete types to which the requested operation is applied. This often leads to chains of calls to `extract`, e.g. +Pythonic API implemented using PyO3 are often polymorphic, i.e. they will accept `&Bound<'_, PyAny>` and try to turn this into multiple more concrete types to which the requested operation is applied. This often leads to chains of calls to `extract`, e.g. ```rust # #![allow(dead_code)] # use pyo3::prelude::*; # use pyo3::{exceptions::PyTypeError, types::PyList}; -fn frobnicate_list(list: &PyList) -> PyResult<&PyAny> { +fn frobnicate_list<'py>(list: &Bound<'_, PyList>) -> PyResult> { todo!() } -fn frobnicate_vec(vec: Vec<&PyAny>) -> PyResult<&PyAny> { +fn frobnicate_vec<'py>(vec: Vec>) -> PyResult> { todo!() } #[pyfunction] -fn frobnicate(value: &PyAny) -> PyResult<&PyAny> { - if let Ok(list) = value.extract::<&PyList>() { - frobnicate_list(list) - } else if let Ok(vec) = value.extract::>() { +fn frobnicate<'py>(value: &Bound<'py, PyAny>) -> PyResult> { + if let Ok(list) = value.extract::>() { + frobnicate_list(&list) + } else if let Ok(vec) = value.extract::>>() { frobnicate_vec(vec) } else { Err(PyTypeError::new_err("Cannot frobnicate that type.")) @@ -37,15 +37,15 @@ This suboptimal as the `FromPyObject` trait requires `extract` to have a `Res # #![allow(dead_code)] # use pyo3::prelude::*; # use pyo3::{exceptions::PyTypeError, types::PyList}; -# fn frobnicate_list(list: &PyList) -> PyResult<&PyAny> { todo!() } -# fn frobnicate_vec(vec: Vec<&PyAny>) -> PyResult<&PyAny> { todo!() } +# fn frobnicate_list<'py>(list: &Bound<'_, PyList>) -> PyResult> { todo!() } +# fn frobnicate_vec<'py>(vec: Vec>) -> PyResult> { todo!() } # #[pyfunction] -fn frobnicate(value: &PyAny) -> PyResult<&PyAny> { +fn frobnicate<'py>(value: &Bound<'py, PyAny>) -> PyResult> { // Use `downcast` instead of `extract` as turning `PyDowncastError` into `PyErr` is quite costly. if let Ok(list) = value.downcast::() { frobnicate_list(list) - } else if let Ok(vec) = value.extract::>() { + } else if let Ok(vec) = value.extract::>>() { frobnicate_vec(vec) } else { Err(PyTypeError::new_err("Cannot frobnicate that type.")) @@ -53,9 +53,9 @@ fn frobnicate(value: &PyAny) -> PyResult<&PyAny> { } ``` -## Access to GIL-bound reference implies access to GIL token +## Access to Bound implies access to GIL token -Calling `Python::with_gil` is effectively a no-op when the GIL is already held, but checking that this is the case still has a cost. If an existing GIL token can not be accessed, for example when implementing a pre-existing trait, but a GIL-bound reference is available, this cost can be avoided by exploiting that access to GIL-bound reference gives zero-cost access to a GIL token via `PyAny::py`. +Calling `Python::with_gil` is effectively a no-op when the GIL is already held, but checking that this is the case still has a cost. If an existing GIL token can not be accessed, for example when implementing a pre-existing trait, but a GIL-bound reference is available, this cost can be avoided by exploiting that access to GIL-bound reference gives zero-cost access to a GIL token via `Bound::py`. For example, instead of writing @@ -66,34 +66,32 @@ For example, instead of writing struct Foo(Py); -struct FooRef<'a>(&'a PyList); +struct FooBound<'py>(Bound<'py, PyList>); -impl PartialEq for FooRef<'_> { +impl PartialEq for FooBound<'_> { fn eq(&self, other: &Foo) -> bool { Python::with_gil(|py| { - #[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. - let len = other.0.as_ref(py).len(); + let len = other.0.bind(py).len(); self.0.len() == len }) } } ``` -use more efficient +use the more efficient ```rust # #![allow(dead_code)] # use pyo3::prelude::*; # use pyo3::types::PyList; # struct Foo(Py); -# struct FooRef<'a>(&'a PyList); +# struct FooBound<'py>(Bound<'py, PyList>); # -impl PartialEq for FooRef<'_> { +impl PartialEq for FooBound<'_> { fn eq(&self, other: &Foo) -> bool { - // Access to `&'a PyAny` implies access to `Python<'a>`. + // Access to `&Bound<'py, PyAny>` implies access to `Python<'py>`. let py = self.0.py(); - #[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. - let len = other.0.as_ref(py).len(); + let len = other.0.bind(py).len(); self.0.len() == len } } diff --git a/guide/src/python-from-rust.md b/guide/src/python-from-rust.md index 4a81d9a668f..ee618f3fa47 100644 --- a/guide/src/python-from-rust.md +++ b/guide/src/python-from-rust.md @@ -1,511 +1,46 @@ # Calling Python in Rust code -This chapter of the guide documents some ways to interact with Python code from Rust: - - How to call Python functions - - How to execute existing Python code - -## Calling Python functions - -Any Python-native object reference (such as `&PyAny`, `&PyList`, or `&PyCell`) can be used to call Python functions. - -PyO3 offers two APIs to make function calls: - -* [`call`]({{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.call) - call any callable Python object. -* [`call_method`]({{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.call_method) - call a method on the Python object. - -Both of these APIs take `args` and `kwargs` arguments (for positional and keyword arguments respectively). There are variants for less complex calls: - -* [`call1`]({{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.call1) and [`call_method1`]({{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.call_method1) to call only with positional `args`. -* [`call0`]({{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.call0) and [`call_method0`]({{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.call_method0) to call with no arguments. - -For convenience the [`Py`](types.md#pyt-and-pyobject) smart pointer also exposes these same six API methods, but needs a `Python` token as an additional first argument to prove the GIL is held. - -The example below calls a Python function behind a `PyObject` (aka `Py`) reference: - -```rust -use pyo3::prelude::*; -use pyo3::types::PyTuple; - -fn main() -> PyResult<()> { - let arg1 = "arg1"; - let arg2 = "arg2"; - let arg3 = "arg3"; - - Python::with_gil(|py| { - let fun: Py = PyModule::from_code_bound( - py, - "def example(*args, **kwargs): - if args != (): - print('called with args', args) - if kwargs != {}: - print('called with kwargs', kwargs) - if args == () and kwargs == {}: - print('called with no arguments')", - "", - "", - )? - .getattr("example")? - .into(); - - // call object without any arguments - fun.call0(py)?; - - // call object with PyTuple - let args = PyTuple::new_bound(py, &[arg1, arg2, arg3]); - fun.call1(py, args)?; - - // pass arguments as rust tuple - let args = (arg1, arg2, arg3); - fun.call1(py, args)?; - Ok(()) - }) -} -``` - -### Creating keyword arguments - -For the `call` and `call_method` APIs, `kwargs` can be `None` or `Some(&PyDict)`. You can use the [`IntoPyDict`]({{#PYO3_DOCS_URL}}/pyo3/types/trait.IntoPyDict.html) trait to convert other dict-like containers, e.g. `HashMap` or `BTreeMap`, as well as tuples with up to 10 elements and `Vec`s where each element is a two-element tuple. - -```rust -use pyo3::prelude::*; -use pyo3::types::IntoPyDict; -use std::collections::HashMap; - -fn main() -> PyResult<()> { - let key1 = "key1"; - let val1 = 1; - let key2 = "key2"; - let val2 = 2; - - Python::with_gil(|py| { - let fun: Py = PyModule::from_code_bound( - py, - "def example(*args, **kwargs): - if args != (): - print('called with args', args) - if kwargs != {}: - print('called with kwargs', kwargs) - if args == () and kwargs == {}: - print('called with no arguments')", - "", - "", - )? - .getattr("example")? - .into(); - - // call object with PyDict - let kwargs = [(key1, val1)].into_py_dict_bound(py); - fun.call_bound(py, (), Some(&kwargs))?; - - // pass arguments as Vec - let kwargs = vec![(key1, val1), (key2, val2)]; - fun.call_bound(py, (), Some(&kwargs.into_py_dict_bound(py)))?; - - // pass arguments as HashMap - let mut kwargs = HashMap::<&str, i32>::new(); - kwargs.insert(key1, 1); - fun.call_bound(py, (), Some(&kwargs.into_py_dict_bound(py)))?; - - Ok(()) - }) -} -``` - -
- -During PyO3's [migration from "GIL Refs" to the `Bound` smart pointer](./migration.md#migrating-from-the-gil-refs-api-to-boundt), [`Py::call`]({{#PYO3_DOCS_URL}}/pyo3/struct.Py.html#method.call) is temporarily named `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.) - -
- -## Executing existing Python code - -If you already have some existing Python code that you need to execute from Rust, the following FAQs can help you select the right PyO3 functionality for your situation: - -### Want to access Python APIs? Then use `PyModule::import`. - -[`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. - -```rust -use pyo3::prelude::*; - -fn main() -> PyResult<()> { - Python::with_gil(|py| { - let builtins = PyModule::import_bound(py, "builtins")?; - let total: i32 = builtins - .getattr("sum")? - .call1((vec![1, 2, 3],))? - .extract()?; - assert_eq!(total, 6); - Ok(()) - }) -} -``` - -### 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.7/reference/expressions.html) -and return the evaluated value as a `&PyAny` object. - -```rust -use pyo3::prelude::*; - -# fn main() -> Result<(), ()> { -Python::with_gil(|py| { - let result = py - .eval_bound("[i * 10 for i in range(5)]", None, None) - .map_err(|e| { - e.print_and_set_sys_last_vars(py); - })?; - let res: Vec = result.extract().unwrap(); - assert_eq!(res, vec![0, 10, 20, 30, 40]); - Ok(()) -}) -# } -``` - -### Want to run statements? Then use `run`. - -[`Python::run`] 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`]. -Since [`py_run!`] panics on exceptions, we recommend you use this macro only for -quickly testing your Python extensions. - -```rust -use pyo3::prelude::*; -use pyo3::py_run; - -# fn main() { -#[pyclass] -struct UserData { - id: u32, - name: String, -} - -#[pymethods] -impl UserData { - fn as_tuple(&self) -> (u32, String) { - (self.id, self.name.clone()) - } - - fn __repr__(&self) -> PyResult { - Ok(format!("User {}(id: {})", self.name, self.id)) - } -} +This chapter of the guide documents some ways to interact with Python code from Rust. -Python::with_gil(|py| { - let userdata = UserData { - id: 34, - name: "Yu".to_string(), - }; - let userdata = Py::new(py, userdata).unwrap(); - let userdata_as_tuple = (34, "Yu"); - py_run!(py, userdata userdata_as_tuple, r#" -assert repr(userdata) == "User Yu(id: 34)" -assert userdata.as_tuple() == userdata_as_tuple - "#); -}) -# } -``` +Below is an introduction to the `'py` lifetime and some general remarks about how PyO3's API reasons about Python code. -## You have a Python file or code snippet? Then use `PyModule::from_code`. - -[`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`. - -**Warning**: This will compile and execute code. **Never** pass untrusted code -to this function! - -```rust -use pyo3::{ - prelude::*, - types::IntoPyDict, -}; - -# fn main() -> PyResult<()> { -Python::with_gil(|py| { - let activators = PyModule::from_code_bound( - py, - 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", - )?; - - 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 lrelu_result: f64 = activators - .getattr("leaky_relu")? - .call((-1.0,), Some(&kwargs))? - .extract()?; - assert_eq!(lrelu_result, -0.2); -# Ok(()) -}) -# } -``` - -### Want to embed Python in Rust with additional modules? - -Python maintains the `sys.modules` dict as a cache of all imported modules. -An import in Python will first attempt to lookup the module from this dict, -and if not present will use various strategies to attempt to locate and load -the module. - -The [`append_to_inittab`]({{#PYO3_DOCS_URL}}/pyo3/macro.append_to_inittab.html) -macro can be used to add additional `#[pymodule]` modules to an embedded -Python interpreter. The macro **must** be invoked _before_ initializing Python. - -As an example, the below adds the module `foo` to the embedded interpreter: - -```rust -use pyo3::prelude::*; - -#[pyfunction] -fn add_one(x: i64) -> i64 { - x + 1 -} - -#[pymodule] -fn foo(foo_module: &Bound<'_, PyModule>) -> PyResult<()> { - foo_module.add_function(wrap_pyfunction!(add_one, foo_module)?)?; - Ok(()) -} - -fn main() -> PyResult<()> { - pyo3::append_to_inittab!(foo); - Python::with_gil(|py| Python::run_bound(py, "import foo; foo.add_one(6)", None, None)) -} -``` - -If `append_to_inittab` cannot be used due to constraints in the program, -an alternative is to create a module using [`PyModule::new`] -and insert it manually into `sys.modules`: - -```rust -use pyo3::prelude::*; -use pyo3::types::PyDict; - -#[pyfunction] -pub fn add_one(x: i64) -> i64 { - x + 1 -} - -fn main() -> PyResult<()> { - Python::with_gil(|py| { - // Create new module - let foo_module = PyModule::new_bound(py, "foo")?; - foo_module.add_function(wrap_pyfunction!(add_one, &foo_module)?)?; - - // Import and get sys.modules - let sys = PyModule::import_bound(py, "sys")?; - let py_modules: Bound<'_, PyDict> = sys.getattr("modules")?.downcast_into()?; - - // Insert foo into sys.modules - py_modules.set_item("foo", foo_module)?; - - // Now we can import + run our python code - Python::run_bound(py, "import foo; foo.add_one(6)", None, None) - }) -} -``` - -### Include multiple Python files - -You can include a file at compile time by using -[`std::include_str`](https://doc.rust-lang.org/std/macro.include_str.html) macro. - -Or you can load a file at runtime by using -[`std::fs::read_to_string`](https://doc.rust-lang.org/std/fs/fn.read_to_string.html) function. - -Many Python files can be included and loaded as modules. If one file depends on -another you must preserve correct order while declaring `PyModule`. - -Example directory structure: -```text -. -├── Cargo.lock -├── Cargo.toml -├── python_app -│ ├── app.py -│ └── utils -│ └── foo.py -└── src - └── main.rs -``` - -`python_app/app.py`: -```python -from utils.foo import bar - - -def run(): - return bar() -``` - -`python_app/utils/foo.py`: -```python -def bar(): - return "baz" -``` - -The example below shows: -* how to include content of `app.py` and `utils/foo.py` into your rust binary -* how to call function `run()` (declared in `app.py`) that needs function - imported from `utils/foo.py` - -`src/main.rs`: -```rust,ignore -use pyo3::prelude::*; - -fn main() -> PyResult<()> { - let py_foo = 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 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, "", "")? - .getattr("run")? - .into(); - app.call0(py) - }); - - println!("py: {}", from_python?); - Ok(()) -} -``` - -The example below shows: -* how to load content of `app.py` at runtime so that it sees its dependencies - automatically -* how to call function `run()` (declared in `app.py`) that needs function - imported from `utils/foo.py` - -It is recommended to use absolute paths because then your binary can be run -from anywhere as long as your `app.py` is in the expected directory (in this example -that directory is `/usr/share/python_app`). - -`src/main.rs`: -```rust,no_run -use pyo3::prelude::*; -use pyo3::types::PyList; -use std::fs; -use std::path::Path; - -fn main() -> PyResult<()> { - let path = Path::new("/usr/share/python_app"); - let py_app = fs::read_to_string(path.join("app.py"))?; - let from_python = Python::with_gil(|py| -> PyResult> { - let syspath = py.import_bound("sys")?.getattr("path")?.downcast_into::()?; - syspath.insert(0, &path)?; - let app: Py = PyModule::from_code_bound(py, &py_app, "", "")? - .getattr("run")? - .into(); - app.call0(py) - }); - - println!("py: {}", from_python?); - Ok(()) -} -``` - - -[`Python::run`]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.run -[`py_run!`]: {{#PYO3_DOCS_URL}}/pyo3/macro.py_run.html - -## Need to use a context manager from Rust? - -Use context managers by directly invoking `__enter__` and `__exit__`. +The subchapters also cover the following topics: + - Python object types available in PyO3's API + - How to work with Python exceptions + - How to call Python functions + - How to execute existing Python code -```rust -use pyo3::prelude::*; +## The `'py` lifetime -fn main() { - Python::with_gil(|py| { - let custom_manager = PyModule::from_code_bound( - py, - r#" -class House(object): - def __init__(self, address): - self.address = address - def __enter__(self): - print(f"Welcome to {self.address}!") - def __exit__(self, type, value, traceback): - if type: - print(f"Sorry you had {type} trouble at {self.address}") - else: - print(f"Thank you for visiting {self.address}, come again soon!") +To safely interact with the Python interpreter a Rust thread must have a corresponding Python thread state and hold the [Global Interpreter Lock (GIL)](#the-global-interpreter-lock). PyO3 has a `Python<'py>` token that is used to prove that these conditions +are met. Its lifetime `'py` is a central part of PyO3's API. - "#, - "house.py", - "house", - ) - .unwrap(); +The `Python<'py>` token serves three purposes: - let house_class = custom_manager.getattr("House").unwrap(); - let house = house_class.call1(("123 Main Street",)).unwrap(); +* It provides global APIs for the Python interpreter, such as [`py.eval_bound()`][eval] and [`py.import_bound()`][import]. +* It can be passed to functions that require a proof of holding the GIL, such as [`Py::clone_ref`][clone_ref]. +* Its lifetime `'py` is used to bind many of PyO3's types to the Python interpreter, such as [`Bound<'py, T>`][Bound]. - house.call_method0("__enter__").unwrap(); +PyO3's types that are bound to the `'py` lifetime, for example `Bound<'py, T>`, all contain a `Python<'py>` token. This means they have full access to the Python interpreter and offer a complete API for interacting with Python objects. - let result = py.eval_bound("undefined_variable + 1", None, None); +Consult [PyO3's API documentation][obtaining-py] to learn how to acquire one of these tokens. - // If the eval threw an exception we'll pass it through to the context manager. - // Otherwise, __exit__ is called with empty arguments (Python "None"). - match result { - Ok(_) => { - let none = py.None(); - house - .call_method1("__exit__", (&none, &none, &none)) - .unwrap(); - } - Err(e) => { - house - .call_method1("__exit__", (e.get_type_bound(py), e.value_bound(py), e.traceback_bound(py))) - .unwrap(); - } - } - }) -} -``` +### The Global Interpreter Lock -## Handling system signals/interrupts (Ctrl-C) +Concurrent programming in Python is aided by the Global Interpreter Lock (GIL), which ensures that only one Python thread can use the Python interpreter and its API at the same time. This allows it to be used to synchronize code. See the [`pyo3::sync`] module for synchronization tools PyO3 offers that are based on the GIL's guarantees. -The best way to handle system signals when running Rust code is to periodically call `Python::check_signals` to handle any signals captured by Python's signal handler. See also [the FAQ entry](./faq.md#ctrl-c-doesnt-do-anything-while-my-rust-code-is-executing). +Non-Python operations (system calls and native Rust code) can unlock the GIL. See [the section on parallelism](parallelism.md) for how to do that using PyO3's API. -Alternatively, set Python's `signal` module to take the default action for a signal: +## Python's memory model -```rust -use pyo3::prelude::*; +Python's memory model differs from Rust's memory model in two key ways: +- There is no concept of ownership; all Python objects are shared and usually implemented via reference counting +- There is no concept of exclusive (`&mut`) references; any reference can mutate a Python object -# fn main() -> PyResult<()> { -Python::with_gil(|py| -> PyResult<()> { - let signal = py.import_bound("signal")?; - // Set SIGINT to have the default action - signal - .getattr("signal")? - .call1((signal.getattr("SIGINT")?, signal.getattr("SIG_DFL")?))?; - Ok(()) -}) -# } -``` +PyO3's API reflects this by providing [smart pointer][smart-pointers] types, `Py`, `Bound<'py, T>`, and (the very rarely used) `Borrowed<'a, 'py, T>`. These smart pointers all use Python reference counting. See the [subchapter on types](./types.md) for more detail on these types. +Because of the lack of exclusive `&mut` references, PyO3's APIs for Python objects, for example [`PyListMethods::append`], use shared references. This is safe because Python objects have internal mechanisms to prevent data races (as of time of writing, the Python GIL). -[`PyModule::new`]: {{#PYO3_DOCS_URL}}/pyo3/types/struct.PyModule.html#method.new +[smart-pointers]: https://doc.rust-lang.org/book/ch15-00-smart-pointers.html +[obtaining-py]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#obtaining-a-python-token +[`pyo3::sync`]: {{#PYO3_DOCS_URL}}/pyo3/sync/index.html diff --git a/guide/src/python-from-rust/calling-existing-code.md b/guide/src/python-from-rust/calling-existing-code.md new file mode 100644 index 00000000000..53051d4ce51 --- /dev/null +++ b/guide/src/python-from-rust/calling-existing-code.md @@ -0,0 +1,397 @@ +# Executing existing Python code + +If you already have some existing Python code that you need to execute from Rust, the following FAQs can help you select the right PyO3 functionality for your situation: + +## Want to access Python APIs? Then use `PyModule::import`. + +[`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. + +```rust +use pyo3::prelude::*; + +fn main() -> PyResult<()> { + Python::with_gil(|py| { + let builtins = PyModule::import_bound(py, "builtins")?; + let total: i32 = builtins + .getattr("sum")? + .call1((vec![1, 2, 3],))? + .extract()?; + assert_eq!(total, 6); + Ok(()) + }) +} +``` + +## 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.7/reference/expressions.html) +and return the evaluated value as a `Bound<'py, PyAny>` object. + +```rust +use pyo3::prelude::*; + +# fn main() -> Result<(), ()> { +Python::with_gil(|py| { + let result = py + .eval_bound("[i * 10 for i in range(5)]", None, None) + .map_err(|e| { + e.print_and_set_sys_last_vars(py); + })?; + let res: Vec = result.extract().unwrap(); + assert_eq!(res, vec![0, 10, 20, 30, 40]); + Ok(()) +}) +# } +``` + +## Want to run statements? Then use `run`. + +[`Python::run`] 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`]. +Since [`py_run!`] panics on exceptions, we recommend you use this macro only for +quickly testing your Python extensions. + +```rust +use pyo3::prelude::*; +use pyo3::py_run; + +# fn main() { +#[pyclass] +struct UserData { + id: u32, + name: String, +} + +#[pymethods] +impl UserData { + fn as_tuple(&self) -> (u32, String) { + (self.id, self.name.clone()) + } + + fn __repr__(&self) -> PyResult { + Ok(format!("User {}(id: {})", self.name, self.id)) + } +} + +Python::with_gil(|py| { + let userdata = UserData { + id: 34, + name: "Yu".to_string(), + }; + let userdata = Py::new(py, userdata).unwrap(); + let userdata_as_tuple = (34, "Yu"); + py_run!(py, userdata userdata_as_tuple, r#" +assert repr(userdata) == "User Yu(id: 34)" +assert userdata.as_tuple() == userdata_as_tuple + "#); +}) +# } +``` + +## You have a Python file or code snippet? Then use `PyModule::from_code`. + +[`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`. + +**Warning**: This will compile and execute code. **Never** pass untrusted code +to this function! + +```rust +use pyo3::{prelude::*, types::IntoPyDict}; + +# fn main() -> PyResult<()> { +Python::with_gil(|py| { + let activators = PyModule::from_code_bound( + py, + 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", + )?; + + 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 lrelu_result: f64 = activators + .getattr("leaky_relu")? + .call((-1.0,), Some(&kwargs))? + .extract()?; + assert_eq!(lrelu_result, -0.2); +# Ok(()) +}) +# } +``` + +## Want to embed Python in Rust with additional modules? + +Python maintains the `sys.modules` dict as a cache of all imported modules. +An import in Python will first attempt to lookup the module from this dict, +and if not present will use various strategies to attempt to locate and load +the module. + +The [`append_to_inittab`]({{#PYO3_DOCS_URL}}/pyo3/macro.append_to_inittab.html) +macro can be used to add additional `#[pymodule]` modules to an embedded +Python interpreter. The macro **must** be invoked _before_ initializing Python. + +As an example, the below adds the module `foo` to the embedded interpreter: + +```rust +use pyo3::prelude::*; + +#[pyfunction] +fn add_one(x: i64) -> i64 { + x + 1 +} + +#[pymodule] +fn foo(foo_module: &Bound<'_, PyModule>) -> PyResult<()> { + foo_module.add_function(wrap_pyfunction!(add_one, foo_module)?)?; + Ok(()) +} + +fn main() -> PyResult<()> { + pyo3::append_to_inittab!(foo); + Python::with_gil(|py| Python::run_bound(py, "import foo; foo.add_one(6)", None, None)) +} +``` + +If `append_to_inittab` cannot be used due to constraints in the program, +an alternative is to create a module using [`PyModule::new`] +and insert it manually into `sys.modules`: + +```rust +use pyo3::prelude::*; +use pyo3::types::PyDict; + +#[pyfunction] +pub fn add_one(x: i64) -> i64 { + x + 1 +} + +fn main() -> PyResult<()> { + Python::with_gil(|py| { + // Create new module + let foo_module = PyModule::new_bound(py, "foo")?; + foo_module.add_function(wrap_pyfunction!(add_one, &foo_module)?)?; + + // Import and get sys.modules + let sys = PyModule::import_bound(py, "sys")?; + let py_modules: Bound<'_, PyDict> = sys.getattr("modules")?.downcast_into()?; + + // Insert foo into sys.modules + py_modules.set_item("foo", foo_module)?; + + // Now we can import + run our python code + Python::run_bound(py, "import foo; foo.add_one(6)", None, None) + }) +} +``` + +## Include multiple Python files + +You can include a file at compile time by using +[`std::include_str`](https://doc.rust-lang.org/std/macro.include_str.html) macro. + +Or you can load a file at runtime by using +[`std::fs::read_to_string`](https://doc.rust-lang.org/std/fs/fn.read_to_string.html) function. + +Many Python files can be included and loaded as modules. If one file depends on +another you must preserve correct order while declaring `PyModule`. + +Example directory structure: +```text +. +├── Cargo.lock +├── Cargo.toml +├── python_app +│ ├── app.py +│ └── utils +│ └── foo.py +└── src + └── main.rs +``` + +`python_app/app.py`: +```python +from utils.foo import bar + + +def run(): + return bar() +``` + +`python_app/utils/foo.py`: +```python +def bar(): + return "baz" +``` + +The example below shows: +* how to include content of `app.py` and `utils/foo.py` into your rust binary +* how to call function `run()` (declared in `app.py`) that needs function + imported from `utils/foo.py` + +`src/main.rs`: +```rust,ignore +use pyo3::prelude::*; + +fn main() -> PyResult<()> { + let py_foo = 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 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, "", "")? + .getattr("run")? + .into(); + app.call0(py) + }); + + println!("py: {}", from_python?); + Ok(()) +} +``` + +The example below shows: +* how to load content of `app.py` at runtime so that it sees its dependencies + automatically +* how to call function `run()` (declared in `app.py`) that needs function + imported from `utils/foo.py` + +It is recommended to use absolute paths because then your binary can be run +from anywhere as long as your `app.py` is in the expected directory (in this example +that directory is `/usr/share/python_app`). + +`src/main.rs`: +```rust,no_run +use pyo3::prelude::*; +use pyo3::types::PyList; +use std::fs; +use std::path::Path; + +fn main() -> PyResult<()> { + let path = Path::new("/usr/share/python_app"); + let py_app = fs::read_to_string(path.join("app.py"))?; + let from_python = Python::with_gil(|py| -> PyResult> { + let syspath = py + .import_bound("sys")? + .getattr("path")? + .downcast_into::()?; + syspath.insert(0, &path)?; + let app: Py = PyModule::from_code_bound(py, &py_app, "", "")? + .getattr("run")? + .into(); + app.call0(py) + }); + + println!("py: {}", from_python?); + Ok(()) +} +``` + + +[`Python::run`]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.run +[`py_run!`]: {{#PYO3_DOCS_URL}}/pyo3/macro.py_run.html + +## Need to use a context manager from Rust? + +Use context managers by directly invoking `__enter__` and `__exit__`. + +```rust +use pyo3::prelude::*; + +fn main() { + Python::with_gil(|py| { + let custom_manager = PyModule::from_code_bound( + py, + r#" +class House(object): + def __init__(self, address): + self.address = address + def __enter__(self): + print(f"Welcome to {self.address}!") + def __exit__(self, type, value, traceback): + if type: + print(f"Sorry you had {type} trouble at {self.address}") + else: + print(f"Thank you for visiting {self.address}, come again soon!") + + "#, + "house.py", + "house", + ) + .unwrap(); + + let house_class = custom_manager.getattr("House").unwrap(); + let house = house_class.call1(("123 Main Street",)).unwrap(); + + house.call_method0("__enter__").unwrap(); + + let result = py.eval_bound("undefined_variable + 1", None, None); + + // If the eval threw an exception we'll pass it through to the context manager. + // Otherwise, __exit__ is called with empty arguments (Python "None"). + match result { + Ok(_) => { + let none = py.None(); + house + .call_method1("__exit__", (&none, &none, &none)) + .unwrap(); + } + Err(e) => { + house + .call_method1( + "__exit__", + ( + e.get_type_bound(py), + e.value_bound(py), + e.traceback_bound(py), + ), + ) + .unwrap(); + } + } + }) +} +``` + +## Handling system signals/interrupts (Ctrl-C) + +The best way to handle system signals when running Rust code is to periodically call `Python::check_signals` to handle any signals captured by Python's signal handler. See also [the FAQ entry](../faq.md#ctrl-c-doesnt-do-anything-while-my-rust-code-is-executing). + +Alternatively, set Python's `signal` module to take the default action for a signal: + +```rust +use pyo3::prelude::*; + +# fn main() -> PyResult<()> { +Python::with_gil(|py| -> PyResult<()> { + let signal = py.import_bound("signal")?; + // Set SIGINT to have the default action + signal + .getattr("signal")? + .call1((signal.getattr("SIGINT")?, signal.getattr("SIG_DFL")?))?; + Ok(()) +}) +# } +``` + + +[`PyModule::new`]: {{#PYO3_DOCS_URL}}/pyo3/types/struct.PyModule.html#method.new diff --git a/guide/src/python-from-rust/function-calls.md b/guide/src/python-from-rust/function-calls.md new file mode 100644 index 00000000000..f97de1f24ce --- /dev/null +++ b/guide/src/python-from-rust/function-calls.md @@ -0,0 +1,114 @@ +# Calling Python functions + +The `Bound<'py, T>` smart pointer (such as `Bound<'py, PyAny>`, `Bound<'py, PyList>`, or `Bound<'py, MyClass>`) can be used to call Python functions. + +PyO3 offers two APIs to make function calls: + +* [`call`]({{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.call) - call any callable Python object. +* [`call_method`]({{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.call_method) - call a method on the Python object. + +Both of these APIs take `args` and `kwargs` arguments (for positional and keyword arguments respectively). There are variants for less complex calls: + +* [`call1`]({{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.call1) and [`call_method1`]({{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.call_method1) to call only with positional `args`. +* [`call0`]({{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.call0) and [`call_method0`]({{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.call_method0) to call with no arguments. + +For convenience the [`Py`](../types.md#pyt-and-pyobject) smart pointer also exposes these same six API methods, but needs a `Python` token as an additional first argument to prove the GIL is held. + +The example below calls a Python function behind a `PyObject` (aka `Py`) reference: + +```rust +use pyo3::prelude::*; +use pyo3::types::PyTuple; + +fn main() -> PyResult<()> { + let arg1 = "arg1"; + let arg2 = "arg2"; + let arg3 = "arg3"; + + Python::with_gil(|py| { + let fun: Py = PyModule::from_code_bound( + py, + "def example(*args, **kwargs): + if args != (): + print('called with args', args) + if kwargs != {}: + print('called with kwargs', kwargs) + if args == () and kwargs == {}: + print('called with no arguments')", + "", + "", + )? + .getattr("example")? + .into(); + + // call object without any arguments + fun.call0(py)?; + + // pass object with Rust tuple of positional arguments + let args = (arg1, arg2, arg3); + fun.call1(py, args)?; + + // call object with Python tuple of positional arguments + let args = PyTuple::new_bound(py, &[arg1, arg2, arg3]); + fun.call1(py, args)?; + Ok(()) + }) +} +``` + +## Creating keyword arguments + +For the `call` and `call_method` APIs, `kwargs` are `Option<&Bound<'py, PyDict>>`, so can either be `None` or `Some(&dict)`. You can use the [`IntoPyDict`]({{#PYO3_DOCS_URL}}/pyo3/types/trait.IntoPyDict.html) trait to convert other dict-like containers, e.g. `HashMap` or `BTreeMap`, as well as tuples with up to 10 elements and `Vec`s where each element is a two-element tuple. + +```rust +use pyo3::prelude::*; +use pyo3::types::IntoPyDict; +use std::collections::HashMap; + +fn main() -> PyResult<()> { + let key1 = "key1"; + let val1 = 1; + let key2 = "key2"; + let val2 = 2; + + Python::with_gil(|py| { + let fun: Py = PyModule::from_code_bound( + py, + "def example(*args, **kwargs): + if args != (): + print('called with args', args) + if kwargs != {}: + print('called with kwargs', kwargs) + if args == () and kwargs == {}: + print('called with no arguments')", + "", + "", + )? + .getattr("example")? + .into(); + + // call object with PyDict + let kwargs = [(key1, val1)].into_py_dict_bound(py); + fun.call_bound(py, (), Some(&kwargs))?; + + // pass arguments as Vec + let kwargs = vec![(key1, val1), (key2, val2)]; + fun.call_bound(py, (), Some(&kwargs.into_py_dict_bound(py)))?; + + // pass arguments as HashMap + let mut kwargs = HashMap::<&str, i32>::new(); + kwargs.insert(key1, 1); + fun.call_bound(py, (), Some(&kwargs.into_py_dict_bound(py)))?; + + Ok(()) + }) +} +``` + +
+ +During PyO3's [migration from "GIL Refs" to the `Bound` smart pointer](../migration.md#migrating-from-the-gil-refs-api-to-boundt), [`Py::call`]({{#PYO3_DOCS_URL}}/pyo3/struct.Py.html#method.call) is temporarily named `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/rust-from-python.md b/guide/src/rust-from-python.md new file mode 100644 index 00000000000..470d5719098 --- /dev/null +++ b/guide/src/rust-from-python.md @@ -0,0 +1,13 @@ +# Using Rust from Python + +This chapter of the guide is dedicated to explaining how to wrap Rust code into Python objects. + +PyO3 uses Rust's "procedural macros" to provide a powerful yet simple API to denote what Rust code should map into Python objects. + +The three types of Python objects which PyO3 can produce are: + +- Python modules, via the `#[pymodule]` macro +- Python functions, via the `#[pyfunction]` macro +- Python classes, via the `#[pyclass]` macro (plus `#[pymethods]` to define methods for those clases) + +The following subchapters go through each of these in turn. diff --git a/guide/src/trait-bounds.md b/guide/src/trait-bounds.md index b0eee80c80a..0644e679190 100644 --- a/guide/src/trait-bounds.md +++ b/guide/src/trait-bounds.md @@ -66,6 +66,7 @@ The following wrapper will call the Python model from Rust, using a struct to ho ```rust # #![allow(dead_code)] use pyo3::prelude::*; +use pyo3::types::PyList; # pub trait Model { # fn set_variables(&mut self, inputs: &Vec); @@ -81,12 +82,9 @@ impl Model for UserModel { fn set_variables(&mut self, var: &Vec) { println!("Rust calling Python to set the variables"); Python::with_gil(|py| { - let values: Vec = var.clone(); - let list: PyObject = values.into_py(py); - #[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. - let py_model = self.model.as_ref(py); - py_model - .call_method("set_variables", (list,), None) + self.model + .bind(py) + .call_method("set_variables", (PyList::new_bound(py, var),), None) .unwrap(); }) } @@ -94,9 +92,8 @@ impl Model for UserModel { fn get_results(&self) -> Vec { println!("Rust calling Python to get the results"); Python::with_gil(|py| { - #[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. self.model - .as_ref(py) + .bind(py) .call_method("get_results", (), None) .unwrap() .extract() @@ -107,9 +104,8 @@ impl Model for UserModel { fn compute(&mut self) { println!("Rust calling Python to perform the computation"); Python::with_gil(|py| { - #[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. self.model - .as_ref(py) + .bind(py) .call_method("compute", (), None) .unwrap(); }) @@ -168,6 +164,7 @@ This wrapper will also perform the type conversions between Python and Rust. ```rust # #![allow(dead_code)] # use pyo3::prelude::*; +# use pyo3::types::PyList; # # pub trait Model { # fn set_variables(&mut self, inputs: &Vec); @@ -184,12 +181,8 @@ This wrapper will also perform the type conversions between Python and Rust. # fn set_variables(&mut self, var: &Vec) { # println!("Rust calling Python to set the variables"); # Python::with_gil(|py| { -# let values: Vec = var.clone(); -# let list: PyObject = values.into_py(py); -# #[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. -# let py_model = self.model.as_ref(py); -# py_model -# .call_method("set_variables", (list,), None) +# self.model.bind(py) +# .call_method("set_variables", (PyList::new_bound(py, var),), None) # .unwrap(); # }) # } @@ -197,9 +190,8 @@ This wrapper will also perform the type conversions between Python and Rust. # fn get_results(&self) -> Vec { # println!("Rust calling Python to get the results"); # Python::with_gil(|py| { -# #[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. # self.model -# .as_ref(py) +# .bind(py) # .call_method("get_results", (), None) # .unwrap() # .extract() @@ -210,9 +202,8 @@ This wrapper will also perform the type conversions between Python and Rust. # fn compute(&mut self) { # println!("Rust calling Python to perform the computation"); # Python::with_gil(|py| { -# #[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. # self.model -# .as_ref(py) +# .bind(py) # .call_method("compute", (), None) # .unwrap(); # }) @@ -340,6 +331,7 @@ We used in our `get_results` method the following call that performs the type co ```rust # #![allow(dead_code)] # use pyo3::prelude::*; +# use pyo3::types::PyList; # # pub trait Model { # fn set_variables(&mut self, inputs: &Vec); @@ -355,10 +347,9 @@ We used in our `get_results` method the following call that performs the type co impl Model for UserModel { fn get_results(&self) -> Vec { println!("Rust calling Python to get the results"); - #[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. Python::with_gil(|py| { self.model - .as_ref(py) + .bind(py) .call_method("get_results", (), None) .unwrap() .extract() @@ -368,12 +359,8 @@ impl Model for UserModel { # fn set_variables(&mut self, var: &Vec) { # println!("Rust calling Python to set the variables"); # Python::with_gil(|py| { -# let values: Vec = var.clone(); -# let list: PyObject = values.into_py(py); -# #[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. -# let py_model = self.model.as_ref(py); -# py_model -# .call_method("set_variables", (list,), None) +# self.model.bind(py) +# .call_method("set_variables", (PyList::new_bound(py, var),), None) # .unwrap(); # }) # } @@ -381,9 +368,8 @@ impl Model for UserModel { # fn compute(&mut self) { # println!("Rust calling Python to perform the computation"); # Python::with_gil(|py| { -# #[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. # self.model -# .as_ref(py) +# .bind(py) # .call_method("compute", (), None) # .unwrap(); # }) @@ -396,6 +382,7 @@ Let's break it down in order to perform better error handling: ```rust # #![allow(dead_code)] # use pyo3::prelude::*; +# use pyo3::types::PyList; # # pub trait Model { # fn set_variables(&mut self, inputs: &Vec); @@ -412,10 +399,9 @@ impl Model for UserModel { fn get_results(&self) -> Vec { println!("Get results from Rust calling Python"); Python::with_gil(|py| { - #[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. - let py_result: &PyAny = self + let py_result: Bound<'_, PyAny> = self .model - .as_ref(py) + .bind(py) .call_method("get_results", (), None) .unwrap(); @@ -432,12 +418,8 @@ impl Model for UserModel { # fn set_variables(&mut self, var: &Vec) { # println!("Rust calling Python to set the variables"); # Python::with_gil(|py| { -# let values: Vec = var.clone(); -# let list: PyObject = values.into_py(py); -# #[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. -# let py_model = self.model.as_ref(py); -# py_model -# .call_method("set_variables", (list,), None) +# let py_model = self.model.bind(py) +# .call_method("set_variables", (PyList::new_bound(py, var),), None) # .unwrap(); # }) # } @@ -445,9 +427,8 @@ impl Model for UserModel { # fn compute(&mut self) { # println!("Rust calling Python to perform the computation"); # Python::with_gil(|py| { -# #[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. # self.model -# .as_ref(py) +# .bind(py) # .call_method("compute", (), None) # .unwrap(); # }) @@ -478,6 +459,7 @@ It is also required to make the struct public. ```rust # #![allow(dead_code)] use pyo3::prelude::*; +use pyo3::types::PyList; pub trait Model { fn set_variables(&mut self, var: &Vec); @@ -533,12 +515,9 @@ impl Model for UserModel { fn set_variables(&mut self, var: &Vec) { println!("Rust calling Python to set the variables"); Python::with_gil(|py| { - let values: Vec = var.clone(); - let list: PyObject = values.into_py(py); - #[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. - let py_model = self.model.as_ref(py); - py_model - .call_method("set_variables", (list,), None) + self.model + .bind(py) + .call_method("set_variables", (PyList::new_bound(py, var),), None) .unwrap(); }) } @@ -546,10 +525,9 @@ impl Model for UserModel { fn get_results(&self) -> Vec { println!("Get results from Rust calling Python"); Python::with_gil(|py| { - #[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. - let py_result: &PyAny = self + let py_result: Bound<'_, PyAny> = self .model - .as_ref(py) + .bind(py) .call_method("get_results", (), None) .unwrap(); @@ -567,9 +545,8 @@ impl Model for UserModel { fn compute(&mut self) { println!("Rust calling Python to perform the computation"); Python::with_gil(|py| { - #[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. self.model - .as_ref(py) + .bind(py) .call_method("compute", (), None) .unwrap(); }) diff --git a/guide/src/types.md b/guide/src/types.md index 372c6c8632f..0f1fa3d0af1 100644 --- a/guide/src/types.md +++ b/guide/src/types.md @@ -1,66 +1,289 @@ -# GIL lifetimes, mutability and Python object types +# Python object types -On first glance, PyO3 provides a huge number of different types that can be used -to wrap or refer to Python objects. This page delves into the details and gives -an overview of their intended meaning, with examples when each type is best -used. +PyO3 offers two main sets of types to interact with Python objects. This section of the guide expands into detail about these types and how to choose which to use. +The first set of types is are the [smart pointers][smart-pointers] which all Python objects are wrapped in. These are `Py`, `Bound<'py, T>`, and `Borrowed<'a, 'py, T>`. The [first section below](#pyo3s-smart-pointers) expands on each of these in detail and why there are three of them. -## The Python GIL, mutability, and Rust types +The second set of types are types which fill in the generic parameter `T` of the smart pointers. The most common is `PyAny`, which represents any Python object (similar to Python's `typing.Any`). There are also concrete types for many Python built-in types, such as `PyList`, `PyDict`, and `PyTuple`. User defined `#[pyclass]` types also fit this category. The [second section below](#concrete-python-types) expands on how to use these types. -Since Python has no concept of ownership, and works solely with boxed objects, -any Python object can be referenced any number of times, and mutation is allowed -from any reference. +Before PyO3 0.21, PyO3's main API to interact with Python objects was a deprecated API known as the "GIL Refs" API, containing reference types such as `&PyAny`, `&PyList`, and `&PyCell` for user-defined `#[pyclass]` types. The [third section below](#the-gil-refs-api) details this deprecated API. -The situation is helped a little by the Global Interpreter Lock (GIL), which -ensures that only one thread can use the Python interpreter and its API at the -same time, while non-Python operations (system calls and extension code) can -unlock the GIL. (See [the section on parallelism](parallelism.md) for how to do -that in PyO3.) +## PyO3's smart pointers -In PyO3, holding the GIL is modeled by acquiring a token of the type -`Python<'py>`, which serves three purposes: +PyO3's API offers three generic smart pointers: `Py`, `Bound<'py, T>` and `Borrowed<'a, 'py, T>`. For each of these the type parameter `T` will be filled by a [concrete Python type](#concrete-python-types). For example, a Python list object can be represented by `Py`, `Bound<'py, PyList>`, and `Borrowed<'a, 'py, PyList>`. -* It provides some global API for the Python interpreter, such as - [`eval`][eval]. -* It can be passed to functions that require a proof of holding the GIL, - such as [`Py::clone_ref`][clone_ref]. -* Its lifetime can be used to create Rust references that implicitly guarantee - holding the GIL, such as [`&'py PyAny`][PyAny]. +These smart pointers behave differently due to their lifetime parameters. `Py` has no lifetime parameters, `Bound<'py, T>` has [the `'py` lifetime](./python-from-rust.md#the-py-lifetime) as a parameter, and `Borrowed<'a, 'py, T>` has the `'py` lifetime plus an additional lifetime `'a` to denote the lifetime it is borrowing data for. (You can read more about these lifetimes in the subsections below). -The latter two points are the reason why some APIs in PyO3 require the `py: -Python` argument, while others don't. +Python objects are reference counted, like [`std::sync::Arc`](https://doc.rust-lang.org/stable/std/sync/struct.Arc.html). A major reason for these smart pointers is to bring Python's reference counting to a Rust API. -The PyO3 API for Python objects is written such that instead of requiring a -mutable Rust reference for mutating operations such as -[`PyList::append`][PyList_append], a shared reference (which, in turn, can only -be created through `Python<'_>` with a GIL lifetime) is sufficient. +The recommendation of when to use each of these smart pointers is as follows: -However, Rust structs wrapped as Python objects (called `pyclass` types) usually -*do* need `&mut` access. Due to the GIL, PyO3 *can* guarantee thread-safe access -to them, but it cannot statically guarantee uniqueness of `&mut` references once -an object's ownership has been passed to the Python interpreter, ensuring -references is done at runtime using `PyCell`, a scheme very similar to -`std::cell::RefCell`. +- Use `Bound<'py, T>` for as much as possible, as it offers the most efficient and complete API. +- Use `Py` mostly just for storage inside Rust `struct`s which do not want to or can't add a lifetime parameter for `Bound<'py, T>`. +- `Borrowed<'a, 'py, T>` is almost never used. It is occasionally present at the boundary between Rust and the Python interpreter, for example when borrowing data from Python tuples (which is safe because they are immutable). -### Accessing the Python GIL +The sections below also explain these smart pointers in a little more detail. -To get hold of a `Python<'py>` token to prove the GIL is held, consult [PyO3's documentation][obtaining-py]. +### `Py` (and `PyObject`) -## Object types +[`Py`][Py] is the foundational smart pointer in PyO3's API. The type parameter `T` denotes the type of the Python object. Very frequently this is `PyAny`, meaning any Python object. This is so common that `Py` has a type alias `PyObject`. -### [`PyAny`][PyAny] +Because `Py` is not bound to [the `'py` lifetime](./python-from-rust.md#the-py-lifetime), it is the type to use when storing a Python object inside a Rust `struct` or `enum` which do not want to have a lifetime parameter. In particular, [`#[pyclass]`][pyclass] types are not permitted to have a lifetime, so `Py` is the correct type to store Python objects inside them. + +The lack of binding to the `'py` lifetime also carries drawbacks: + - Almost all methods on `Py` require a `Python<'py>` token as the first argument + - Other functionality, such as [`Drop`][Drop], needs to check at runtime for attachment to the Python GIL, at a small performance cost + +Because of the drawbacks `Bound<'py, T>` is preferred for many of PyO3's APIs. In particular, `Bound<'py, T>` is the better for function arguments. + +To convert a `Py` into a `Bound<'py, T>`, the `Py::bind` and `Py::into_bound` methods are available. `Bound<'py, T>` can be converted back into `Py` using [`Bound::unbind`]. + +### `Bound<'py, T>` + +[`Bound<'py, T>`][Bound] is the counterpart to `Py` which is also bound to the `'py` lifetime. It can be thought of as equivalent to the Rust tuple `(Python<'py>, Py)`. + +By having the binding to the `'py` lifetime, `Bound<'py, T>` can offer the complete PyO3 API at maximum efficiency. This means that in almost all cases where `Py` is not necessary for lifetime reasons, `Bound<'py, T>` should be used. + +`Bound<'py, T>` engages in Python reference counting. This means that `Bound<'py, T>` owns a Python object. Rust code which just wants to borrow a Python object should use a shared reference `&Bound<'py, T>`. Just like `std::sync::Arc`, using `.clone()` and `drop()` will cheaply increment and decrement the reference count of the object (just in this case, the reference counting is implemented by the Python interpreter itself). + +To give an example of how `Bound<'py, T>` is PyO3's primary API type, consider the following Python code: + +```python +def example(): + x = list() # create a Python list + x.append(1) # append the integer 1 to it + y = x # create a second reference to the list + del x # delete the original reference +``` + +Using PyO3's API, and in particular `Bound<'py, PyList>`, this code translates into the following Rust code: + +```rust +use pyo3::prelude::*; +use pyo3::types::PyList; + +fn example<'py>(py: Python<'py>) -> PyResult<()> { + let x: Bound<'py, PyList> = PyList::empty_bound(py); + x.append(1)?; + let y: Bound<'py, PyList> = x.clone(); // y is a new reference to the same list + drop(x); // release the original reference x + Ok(()) +} +# Python::with_gil(example).unwrap(); +``` + +Or, without the type annotations: + +```rust +use pyo3::prelude::*; +use pyo3::types::PyList; + +# fn example(py: Python<'_>) -> PyResult<()> { + let x = PyList::empty_bound(py); + x.append(1)?; + let y = x.clone(); + drop(x); + Ok(()) +} +# Python::with_gil(example).unwrap(); +``` + +#### Function argument lifetimes + +Because the `'py` lifetime often appears in many function arguments as part of the `Bound<'py, T>` smart pointer, the Rust compiler will often require annotations of input and output lifetimes. This occurs when the function output has at least one lifetime, and there is more than one lifetime present on the inputs. + +To demonstrate, consider this function which takes accepts Python objects and applies the [Python `+` operation][PyAnyMethods::add] to them: + +```rust,compile_fail +# use pyo3::prelude::*; +fn add(left: &'_ Bound<'_, PyAny>, right: &'_ Bound<'_, PyAny>) -> PyResult> { + left.add(right) +} +``` + +Because the Python `+` operation might raise an exception, this function returns `PyResult>`. It doesn't need ownership of the inputs, so it takes `&Bound<'_, PyAny>` shared references. To demonstrate the point, all lifetimes have used the wildcard `'_` to allow the Rust compiler to attempt to infer them. Because there are four input lifetimes (two lifetimes of the shared references, and two `'py` lifetimes unnamed inside the `Bound<'_, PyAny>` pointers), the compiler cannot reason about which must be connected to the output. + +The correct way to solve this is to add the `'py` lifetime as a parameter for the function, and name all the `'py` lifetimes inside the `Bound<'py, PyAny>` smart pointers. For the shared references, it's also fine to reduce `&'_` to just `&`. The working end result is below: + +```rust +# use pyo3::prelude::*; +fn add<'py>( + left: &Bound<'py, PyAny>, + right: &Bound<'py, PyAny>, +) -> PyResult> { + left.add(right) +} +# Python::with_gil(|py| { +# let s = pyo3::types::PyString::new_bound(py, "s"); +# assert!(add(&s, &s).unwrap().eq("ss").unwrap()); +# }) +``` + +If naming the `'py` lifetime adds unwanted complexity to the function signature, it is also acceptable to return `PyObject` (aka `Py`), which has no lifetime. The cost is instead paid by a slight increase in implementation complexity, as seen by the introduction of a call to [`Bound::unbind`]: + +```rust +# use pyo3::prelude::*; +fn add(left: &Bound<'_, PyAny>, right: &Bound<'_, PyAny>) -> PyResult { + let output: Bound<'_, PyAny> = left.add(right)?; + Ok(output.unbind()) +} +# Python::with_gil(|py| { +# let s = pyo3::types::PyString::new_bound(py, "s"); +# assert!(add(&s, &s).unwrap().bind(py).eq("ss").unwrap()); +# }) +``` + +### `Borrowed<'a, 'py, T>` + +[`Borrowed<'a, 'py, T>`][Borrowed] is an advanced type used just occasionally at the edge of interaction with the Python interpreter. It can be thought of as analogous to the shared reference `&'a Bound<'py, T>`. The difference is that `Borrowed<'a, 'py, T>` is just a smart pointer rather than a reference-to-a-smart-pointer, which is a helpful reduction in indirection in specific interactions with the Python interpreter. + +`Borrowed<'a, 'py, T>` dereferences to `Bound<'py, T>`, so all methods on `Bound<'py, T>` are available on `Borrowed<'a, 'py, T>`. + +An example where `Borrowed<'a, 'py, T>` is used is in [`PyTupleMethods::get_borrowed_item`]({{#PYO3_DOCS_URL}}/pyo3/types/trait.PyTupleMethods.html#tymethod.get_item): + +```rust +use pyo3::prelude::*; +use pyo3::types::PyTuple; + +# fn example<'py>(py: Python<'py>) -> PyResult<()> { +// Create a new tuple with the elements (0, 1, 2) +let t = PyTuple::new_bound(py, [0, 1, 2]); +for i in 0..=2 { + let entry: Borrowed<'_, 'py, PyAny> = t.get_borrowed_item(i)?; + // `PyAnyMethods::extract` is available on `Borrowed` + // via the dereference to `Bound` + let value: usize = entry.extract()?; + assert_eq!(i, value); +} +# Ok(()) +# } +# Python::with_gil(example).unwrap(); +``` + +## Concrete Python types + +In all of `Py`, `Bound<'py, T>`, and `Borrowed<'a, 'py, T>`, the type parameter `T` denotes the type of the Python object referred to by the smart pointer. + +This parameter `T` can be filled by: + - [`PyAny`][PyAny], which represents any Python object, + - Native Python types such as `PyList`, `PyTuple`, and `PyDict`, and + - [`#[pyclass]`][pyclass] types defined from Rust + +The following subsections covers some further detail about how to work with these types: +- the APIs that are available for these concrete types, +- how to cast `Bound<'py, T>` to a specific concrete type, and +- how to get Rust data out of a `Bound<'py, T>`. + +### Using APIs for concrete Python types + +Each concrete Python type such as `PyAny`, `PyTuple` and `PyDict` exposes its API on the corresponding bound smart pointer `Bound<'py, PyAny>`, `Bound<'py, PyTuple>` and `Bound<'py, PyDict>`. + +Each type's API is exposed as a trait: [`PyAnyMethods`], [`PyTupleMethods`], [`PyDictMethods`], and so on for all concrete types. Using traits rather than associated methods on the `Bound` smart pointer is done for a couple of reasons: +- Clarity of documentation: each trait gets its own documentation page in the PyO3 API docs. If all methods were on the `Bound` smart pointer directly, the vast majority of PyO3's API would be on a single, extremely long, documentation page. +- Consistency: downstream code implementing Rust APIs for existing Python types can also follow this pattern of using a trait. Downstream code would not be allowed to add new associated methods directly on the `Bound` type. +- Future design: it is hoped that a future Rust with [arbitrary self types](https://github.com/rust-lang/rust/issues/44874) will remove the need for these traits in favour of placing the methods directly on `PyAny`, `PyTuple`, `PyDict`, and so on. + +These traits are all included in the `pyo3::prelude` module, so with the glob import `use pyo3::prelude::*` the full PyO3 API is made available to downstream code. + +The following function accesses the first item in the input Python list, using the `.get_item()` method from the `PyListMethods` trait: + +```rust +use pyo3::prelude::*; +use pyo3::types::PyList; + +fn get_first_item<'py>(list: &Bound<'py, PyList>) -> PyResult> { + list.get_item(0) +} +# Python::with_gil(|py| { +# let l = PyList::new_bound(py, ["hello world"]); +# assert!(get_first_item(&l).unwrap().eq("hello world").unwrap()); +# }) +``` + +### Casting between Python object types + +To cast `Bound<'py, T>` smart pointers to some other type, use the [`.downcast()`][PyAnyMethods::downcast] family of functions. This converts `&Bound<'py, T>` to a different `&Bound<'py, U>`, without transferring ownership. There is also [`.downcast_into()`][PyAnyMethods::downcast_into] to convert `Bound<'py, T>` to `Bound<'py, U>` with transfer of ownership. These methods are available for all types `T` which implement the [`PyTypeCheck`] trait. + +Casting to `Bound<'py, PyAny>` can be done with `.as_any()` or `.into_any()`. + +For example, the following snippet shows how to cast `Bound<'py, PyAny>` to `Bound<'py, PyTuple>`: + +```rust +# use pyo3::prelude::*; +# use pyo3::types::PyTuple; +# fn example<'py>(py: Python<'py>) -> PyResult<()> { +// create a new Python `tuple`, and use `.into_any()` to erase the type +let obj: Bound<'py, PyAny> = PyTuple::empty_bound(py).into_any(); + +// use `.downcast()` to cast to `PyTuple` without transferring ownership +let _: &Bound<'py, PyTuple> = obj.downcast()?; + +// use `.downcast_into()` to cast to `PyTuple` with transfer of ownership +let _: Bound<'py, PyTuple> = obj.downcast_into()?; +# Ok(()) +# } +# Python::with_gil(example).unwrap() +``` + +Custom [`#[pyclass]`][pyclass] types implement [`PyTypeCheck`], so `.downcast()` also works for these types. The snippet below is the same as the snippet above casting instead to a custom type `MyClass`: + +```rust +use pyo3::prelude::*; + +#[pyclass] +struct MyClass {} + +# fn example<'py>(py: Python<'py>) -> PyResult<()> { +// create a new Python `tuple`, and use `.into_any()` to erase the type +let obj: Bound<'py, PyAny> = Bound::new(py, MyClass {})?.into_any(); + +// use `.downcast()` to cast to `MyClass` without transferring ownership +let _: &Bound<'py, MyClass> = obj.downcast()?; + +// use `.downcast_into()` to cast to `MyClass` with transfer of ownership +let _: Bound<'py, MyClass> = obj.downcast_into()?; +# Ok(()) +# } +# Python::with_gil(example).unwrap() +``` + +### Extracting Rust data from Python objects + +To extract Rust data from Python objects, use [`.extract()`][PyAnyMethods::extract] instead of `.downcast()`. This method is available for all types which implement the [`FromPyObject`] trait. -**Represents:** a Python object of unspecified type, restricted to a GIL -lifetime. Currently, `PyAny` can only ever occur as a reference, `&PyAny`. +For example, the following snippet extracts a Rust tuple of integers from a Python tuple: -**Used:** Whenever you want to refer to some Python object and will have the -GIL for the whole duration you need to access that object. For example, -intermediate values and arguments to `pyfunction`s or `pymethod`s implemented -in Rust where any type is allowed. +```rust +# use pyo3::prelude::*; +# use pyo3::types::PyTuple; +# fn example<'py>(py: Python<'py>) -> PyResult<()> { +// create a new Python `tuple`, and use `.into_any()` to erase the type +let obj: Bound<'py, PyAny> = PyTuple::new_bound(py, [1, 2, 3]).into_any(); + +// extracting the Python `tuple` to a rust `(i32, i32, i32)` tuple +let (x, y, z) = obj.extract::<(i32, i32, i32)>()?; +assert_eq!((x, y, z), (1, 2, 3)); +# Ok(()) +# } +# Python::with_gil(example).unwrap() +``` + +To avoid copying data, [`#[pyclass]`][pyclass] types can directly reference Rust data stored within the Python objects without needing to `.extract()`. See the [corresponding documentation in the class section of the guide](./class. +md#bound-and-interior-mutability) for more detail. -Many general methods for interacting with Python objects are on the `PyAny` struct, -such as `getattr`, `setattr`, and `.call`. +## 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:** @@ -71,7 +294,7 @@ a list: # use pyo3::prelude::*; # use pyo3::types::PyList; # Python::with_gil(|py| -> PyResult<()> { -#[allow(deprecated)] // PyList::empty is part of the deprecated "GIL Refs" API. +#[allow(deprecated)] // PyList::empty is part of the deprecated "GIL Refs" API. let obj: &PyAny = PyList::empty(py); // To &PyList with PyAny::downcast @@ -92,11 +315,11 @@ For a `&PyAny` object reference `any` where the underlying object is a `#[pyclas # use pyo3::prelude::*; # #[pyclass] #[derive(Clone)] struct MyClass { } # Python::with_gil(|py| -> PyResult<()> { -# #[allow(deprecated)] +#[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)] +#[allow(deprecated)] // &PyCell is part of the deprecated GIL Refs API let _: &PyCell = obj.downcast()?; // To Py (aka PyObject) with .into() @@ -117,18 +340,13 @@ let _: PyRefMut<'_, MyClass> = obj.extract()?; ### `PyTuple`, `PyDict`, and many more -**Represents:** a native Python object of known type, restricted to a GIL -lifetime just like `PyAny`. +**Represented:** a native Python object of known type. In the GIL Refs API, they were only accessed as the GIL Refs `&'py PyTuple`, `&'py PyDict`. -**Used:** Whenever you want to operate with native Python types while holding -the GIL. Like `PyAny`, this is the most convenient form to use for function -arguments and intermediate values. +**Used:** `&'py PyTuple` and similar were used to operate with native Python types while holding the GIL. Like `PyAny`, this is the most convenient form to use for function arguments and intermediate values. -These types all implement `Deref`, so they all expose the same -methods which can be found on `PyAny`. +These GIL Refs implement `Deref`, so they all expose the same methods which can be found on `PyAny`. -To see all Python types exposed by `PyO3` you should consult the -[`pyo3::types`][pyo3::types] module. +To see all Python types exposed by `PyO3` consult the [`pyo3::types`][pyo3::types] module. **Conversions:** @@ -136,7 +354,7 @@ To see all Python types exposed by `PyO3` you should consult the # use pyo3::prelude::*; # use pyo3::types::PyList; # Python::with_gil(|py| -> PyResult<()> { -#[allow(deprecated)] // PyList::empty is part of the deprecated "GIL Refs" API. +#[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 @@ -146,7 +364,7 @@ let _ = list.repr()?; let _: &PyAny = list; // To &PyAny explicitly with .as_ref() -#[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. +#[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. let _: &PyAny = list.as_ref(); // To Py with .into() or Py::from() @@ -160,7 +378,7 @@ let _: PyObject = list.into(); ### `Py` and `PyObject` -**Represents:** a GIL-independent reference to a Python object. This can be a Python native type +**Represented:** a GIL-independent reference to a Python object. This can be a Python native type (like `PyTuple`), or a `pyclass` type implemented in Rust. The most commonly-used variant, `Py`, is also known as `PyObject`. @@ -170,86 +388,23 @@ Python-Rust FFI boundary, or returning objects from functions implemented in Rus Can be cloned using Python reference counts with `.clone()`. -**Conversions:** - -For a `Py`, the conversions are as below: - -```rust -# use pyo3::prelude::*; -# use pyo3::types::PyList; -# Python::with_gil(|py| { -let list: Py = PyList::empty_bound(py).unbind(); - -// To &PyList with Py::as_ref() (borrows from the Py) -#[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. -let _: &PyList = list.as_ref(py); - -# let list_clone = list.clone(); // Because `.into_ref()` will consume `list`. -// To &PyList with Py::into_ref() (moves the pointer into PyO3's object storage) -# #[allow(deprecated)] -let _: &PyList = list.into_ref(py); - -# let list = list_clone; -// To Py (aka PyObject) with .into() -let _: Py = list.into(); -# }) -``` - -For a `#[pyclass] struct MyClass`, the conversions for `Py` are below: - -```rust -# use pyo3::prelude::*; -# Python::with_gil(|py| { -# #[pyclass] struct MyClass { } -# Python::with_gil(|py| -> PyResult<()> { -let my_class: Py = Py::new(py, MyClass { })?; - -// To &PyCell with Py::as_ref() (borrows from the Py) -#[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. -let _: &PyCell = my_class.as_ref(py); - -# let my_class_clone = my_class.clone(); // Because `.into_ref()` will consume `my_class`. -// To &PyCell with Py::into_ref() (moves the pointer into PyO3's object storage) -# #[allow(deprecated)] -let _: &PyCell = my_class.into_ref(py); - -# let my_class = my_class_clone.clone(); -// To Py (aka PyObject) with .into_py(py) -let _: Py = my_class.into_py(py); - -# let my_class = my_class_clone; -// To PyRef<'_, MyClass> with Py::borrow or Py::try_borrow -let _: PyRef<'_, MyClass> = my_class.try_borrow(py)?; - -// To PyRefMut<'_, MyClass> with Py::borrow_mut or Py::try_borrow_mut -let _: PyRefMut<'_, MyClass> = my_class.try_borrow_mut(py)?; -# Ok(()) -# }).unwrap(); -# }); -``` - ### `PyCell` -**Represents:** a reference to a Rust object (instance of `PyClass`) which is -wrapped in a Python object. The cell part is an analog to stdlib's -[`RefCell`][RefCell] to allow access to `&mut` references. +**Represented:** a reference to a Rust object (instance of `PyClass`) wrapped in a Python object. The cell part is an analog to stdlib's [`RefCell`][RefCell] to allow access to `&mut` references. -**Used:** for accessing pure-Rust API of the instance (members and functions -taking `&SomeType` or `&mut SomeType`) while maintaining the aliasing rules of -Rust references. +**Used:** for accessing pure-Rust API of the instance (members and functions taking `&SomeType` or `&mut SomeType`) while maintaining the aliasing rules of Rust references. -Like PyO3's Python native types, `PyCell` implements `Deref`, -so it also exposes all of the methods on `PyAny`. +Like PyO3's Python native types, the GIL Ref `&PyCell` implements `Deref`, so it also exposed all of the methods on `PyAny`. **Conversions:** -`PyCell` can be used to access `&T` and `&mut T` via `PyRef` and `PyRefMut` respectively. +`PyCell` was used to access `&T` and `&mut T` via `PyRef` and `PyRefMut` respectively. ```rust # use pyo3::prelude::*; # #[pyclass] struct MyClass { } # Python::with_gil(|py| -> PyResult<()> { -# #[allow(deprecated)] +#[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() @@ -264,13 +419,13 @@ let _: &mut MyClass = &mut *py_ref_mut; # }).unwrap(); ``` -`PyCell` can also be accessed like a Python-native type. +`PyCell` was also accessed like a Python-native type. ```rust # use pyo3::prelude::*; # #[pyclass] struct MyClass { } # Python::with_gil(|py| -> PyResult<()> { -# #[allow(deprecated)] +#[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 @@ -280,37 +435,30 @@ let _ = cell.repr()?; let _: &PyAny = cell; // To &PyAny explicitly with .as_ref() -#[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. +#[allow(deprecated)] // as_ref is part of the deprecated "GIL Refs" API. let _: &PyAny = cell.as_ref(); # Ok(()) # }).unwrap(); ``` -### `PyRef` and `PyRefMut` - -**Represents:** reference wrapper types employed by `PyCell` to keep track of -borrows, analog to `Ref` and `RefMut` used by `RefCell`. - -**Used:** while borrowing a `PyCell`. They can also be used with `.extract()` -on types like `Py` and `PyAny` to get a reference quickly. - - -## Related traits and types - -### `PyClass` - -This trait marks structs defined in Rust that are also usable as Python classes, -usually defined using the `#[pyclass]` macro. - -### `PyNativeType` - -This trait marks structs that mirror native Python types, such as `PyList`. - - +[Bound]: {{#PYO3_DOCS_URL}}/pyo3/struct.Bound.html +[`Bound::unbind`]: {{#PYO3_DOCS_URL}}/pyo3/struct.Bound.html#method.unbind +[Py]: {{#PYO3_DOCS_URL}}/pyo3/struct.Py.html +[PyAnyMethods::add]: {{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.add +[PyAnyMethods::extract]: {{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.extract +[PyAnyMethods::downcast]: {{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.downcast +[PyAnyMethods::downcast_into]: {{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.downcast_into +[`PyTypeCheck`]: {{#PYO3_DOCS_URL}}/pyo3/type_object/trait.PyTypeCheck.html +[`PyAnyMethods`]: {{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html +[`PyDictMethods`]: {{#PYO3_DOCS_URL}}/pyo3/types/trait.PyDictMethods.html +[`PyTupleMethods`]: {{#PYO3_DOCS_URL}}/pyo3/types/trait.PyTupleMethods.html +[pyclass]: class.md +[Borrowed]: {{#PYO3_DOCS_URL}}/pyo3/struct.Borrowed.html +[Drop]: https://doc.rust-lang.org/std/ops/trait.Drop.html [eval]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.eval [clone_ref]: {{#PYO3_DOCS_URL}}/pyo3/struct.Py.html#method.clone_ref [pyo3::types]: {{#PYO3_DOCS_URL}}/pyo3/types/index.html [PyAny]: {{#PYO3_DOCS_URL}}/pyo3/types/struct.PyAny.html [PyList_append]: {{#PYO3_DOCS_URL}}/pyo3/types/struct.PyList.html#method.append [RefCell]: https://doc.rust-lang.org/std/cell/struct.RefCell.html -[obtaining-py]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#obtaining-a-python-token +[smart-pointers]: https://doc.rust-lang.org/book/ch15-00-smart-pointers.html diff --git a/src/lib.rs b/src/lib.rs index bc88838d4b1..a7c03d1b6cb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -30,29 +30,38 @@ //! //! PyO3 has several core types that you should familiarize yourself with: //! -//! ## The Python<'py> object -//! -//! Holding the [global interpreter lock] (GIL) is modeled with the [`Python<'py>`](crate::Python) -//! token. All APIs that require that the GIL is held require this token as proof that you really -//! are holding the GIL. It can be explicitly acquired and is also implicitly acquired by PyO3 as -//! it wraps Rust functions and structs into Python functions and objects. -//! -//! ## The GIL-dependent types -//! -//! For example `&`[`PyAny`]. These are only ever seen as references, with a lifetime that is only -//! valid for as long as the GIL is held, which is why using them doesn't require a -//! [`Python<'py>`](crate::Python) token. The underlying Python object, if mutable, can be mutated -//! through any reference. +//! ## The `Python<'py>` object, and the `'py` lifetime +//! +//! Holding the [global interpreter lock] (GIL) is modeled with the [`Python<'py>`](Python) token. Many +//! Python APIs require that the GIL is held, and PyO3 uses this token as proof that these APIs +//! can be called safely. It can be explicitly acquired and is also implicitly acquired by PyO3 +//! as it wraps Rust functions and structs into Python functions and objects. +//! +//! The [`Python<'py>`](Python) token's lifetime `'py` is common to many PyO3 APIs: +//! - Types that also have the `'py` lifetime, such as the [`Bound<'py, T>`](Bound) smart pointer, are +//! bound to the Python GIL and rely on this to offer their functionality. These types often +//! have a [`.py()`](Bound::py) method to get the associated [`Python<'py>`](Python) token. +//! - Functions which depend on the `'py` lifetime, such as [`PyList::new_bound`](types::PyList::new_bound), +//! require a [`Python<'py>`](Python) token as an input. Sometimes the token is passed implicitly by +//! taking a [`Bound<'py, T>`](Bound) or other type which is bound to the `'py` lifetime. +//! - Traits which depend on the `'py` lifetime, such as [`FromPyObject<'py>`](FromPyObject), usually have +//! inputs or outputs which depend on the lifetime. Adding the lifetime to the trait allows +//! these inputs and outputs to express their binding to the GIL in the Rust type system. +//! +//! ## Python object smart pointers +//! +//! PyO3 has two core smart pointers to refer to Python objects, [`Py`](Py) and its GIL-bound +//! form [`Bound<'py, T>`](Bound) which carries the `'py` lifetime. (There is also +//! [`Borrowed<'a, 'py, T>`](instance::Borrowed), but it is used much more rarely). +//! +//! The type parameter `T` in these smart pointers can be filled by: +//! - [`PyAny`], e.g. `Py` or `Bound<'py, PyAny>`, where the Python object type is not +//! known. `Py` is so common it has a type alias [`PyObject`]. +//! - Concrete Python types like [`PyList`](types::PyList) or [`PyTuple`](types::PyTuple). +//! - Rust types which are exposed to Python using the [`#[pyclass]`](macro@pyclass) macro. //! //! See the [guide][types] for an explanation of the different Python object types. //! -//! ## The GIL-independent types -//! -//! When wrapped in [`Py`]`<...>`, like with [`Py`]`<`[`PyAny`]`>` or [`Py`]``, Python -//! objects no longer have a limited lifetime which makes them easier to store in structs and pass -//! between functions. However, you cannot do much with them without a -//! [`Python<'py>`](crate::Python) token, for which you’d need to reacquire the GIL. -//! //! ## PyErr //! //! The vast majority of operations in this library will return [`PyResult<...>`](PyResult). @@ -512,7 +521,10 @@ pub mod doc_test { "guide/src/parallelism.md" => guide_parallelism_md, "guide/src/performance.md" => guide_performance_md, "guide/src/python-from-rust.md" => guide_python_from_rust_md, + "guide/src/python-from-rust/calling-existing-code.md" => guide_pfr_calling_existing_code_md, + "guide/src/python-from-rust/function-calls.md" => guide_pfr_function_calls_md, "guide/src/python-typing-hints.md" => guide_python_typing_hints_md, + "guide/src/rust-from-python.md" => guide_rust_from_python_md, "guide/src/trait-bounds.md" => guide_trait_bounds_md, "guide/src/types.md" => guide_types_md, } diff --git a/src/sync.rs b/src/sync.rs index f4ffe0409a7..e89a8edd853 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -1,4 +1,9 @@ //! Synchronization mechanisms based on the Python GIL. +//! +//! With the acceptance of [PEP 703] (aka a "freethreaded Python") for Python 3.13, these +//! are likely to undergo significant developments in the future. +//! +//! [PEP 703]: https://peps.python.org/pep-703/ use crate::{ types::{any::PyAnyMethods, PyString, PyType}, Bound, Py, PyResult, PyVisit, Python, From db0a98c0404cb9e7554779836d9f165078b86298 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sun, 10 Mar 2024 16:17:15 +0000 Subject: [PATCH 077/936] ci: pin pytest < 8.1 (#3946) --- pytests/pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pytests/pyproject.toml b/pytests/pyproject.toml index 126eaf77b09..43403a1241c 100644 --- a/pytests/pyproject.toml +++ b/pytests/pyproject.toml @@ -24,6 +24,7 @@ dev = [ "hypothesis>=3.55", "pytest-asyncio>=0.21", "pytest-benchmark>=3.4", - "pytest>=6.0", + # pinned < 8.1 because https://github.com/CodSpeedHQ/pytest-codspeed/issues/27 + "pytest>=8,<8.1", "typing_extensions>=4.0.0" ] From 67b1b3501300bd8842c75586b7a3c37381da701d Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sun, 10 Mar 2024 22:16:18 +0000 Subject: [PATCH 078/936] release: 0.21.0-beta.0 (#3944) --- CHANGELOG.md | 68 +++++++++++++++++++++++++++++++++- Cargo.toml | 8 ++-- newsfragments/3514.added.md | 1 - newsfragments/3532.changed.md | 1 - newsfragments/3540.added.md | 1 - newsfragments/3577.added.md | 1 - newsfragments/3577.changed.md | 1 - newsfragments/3582.added.md | 1 - newsfragments/3588.added.md | 1 - newsfragments/3599.added.md | 1 - newsfragments/3600.changed.md | 1 - newsfragments/3601.changed.md | 1 - newsfragments/3603.removed.md | 1 - newsfragments/3609.changed.md | 1 - newsfragments/3632.added.md | 1 - newsfragments/3638.changed.md | 1 - newsfragments/3653.changed.md | 1 - newsfragments/3657.changed.md | 1 - newsfragments/3660.changed.md | 1 - newsfragments/3661.changed.md | 1 - newsfragments/3663.changed.md | 1 - newsfragments/3664.changed.md | 1 - newsfragments/3670.added.md | 1 - newsfragments/3677.added.md | 1 - newsfragments/3679.changed.md | 1 - newsfragments/3686.added.md | 1 - newsfragments/3689.changed.md | 1 - newsfragments/3692.added.md | 1 - newsfragments/3692.changed.md | 1 - newsfragments/3706.added.md | 1 - newsfragments/3707.added.md | 1 - newsfragments/3712.added.md | 1 - newsfragments/3730.added.md | 1 - newsfragments/3734.added.md | 1 - newsfragments/3736.added.md | 1 - newsfragments/3742.changed.md | 1 - newsfragments/3757.fixed.md | 1 - newsfragments/3776.changed.md | 1 - newsfragments/3785.added.md | 1 - newsfragments/3801.added.md | 1 - newsfragments/3802.added.md | 1 - newsfragments/3815.added.md | 2 - newsfragments/3818.fixed.md | 1 - newsfragments/3849.added.md | 1 - newsfragments/3849.changed.md | 1 - newsfragments/3871.added.md | 1 - newsfragments/3901.fixed.md | 1 - newsfragments/3905.changed.md | 1 - newsfragments/3928.added.md | 1 - newsfragments/3928.changed.md | 1 - newsfragments/3931.added.md | 1 - newsfragments/3934.removed.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 +- 57 files changed, 79 insertions(+), 64 deletions(-) delete mode 100644 newsfragments/3514.added.md delete mode 100644 newsfragments/3532.changed.md delete mode 100644 newsfragments/3540.added.md delete mode 100644 newsfragments/3577.added.md delete mode 100644 newsfragments/3577.changed.md delete mode 100644 newsfragments/3582.added.md delete mode 100644 newsfragments/3588.added.md delete mode 100644 newsfragments/3599.added.md delete mode 100644 newsfragments/3600.changed.md delete mode 100644 newsfragments/3601.changed.md delete mode 100644 newsfragments/3603.removed.md delete mode 100644 newsfragments/3609.changed.md delete mode 100644 newsfragments/3632.added.md delete mode 100644 newsfragments/3638.changed.md delete mode 100644 newsfragments/3653.changed.md delete mode 100644 newsfragments/3657.changed.md delete mode 100644 newsfragments/3660.changed.md delete mode 100644 newsfragments/3661.changed.md delete mode 100644 newsfragments/3663.changed.md delete mode 100644 newsfragments/3664.changed.md delete mode 100644 newsfragments/3670.added.md delete mode 100644 newsfragments/3677.added.md delete mode 100644 newsfragments/3679.changed.md delete mode 100644 newsfragments/3686.added.md delete mode 100644 newsfragments/3689.changed.md delete mode 100644 newsfragments/3692.added.md delete mode 100644 newsfragments/3692.changed.md delete mode 100644 newsfragments/3706.added.md delete mode 100644 newsfragments/3707.added.md delete mode 100644 newsfragments/3712.added.md delete mode 100644 newsfragments/3730.added.md delete mode 100644 newsfragments/3734.added.md delete mode 100644 newsfragments/3736.added.md delete mode 100644 newsfragments/3742.changed.md delete mode 100644 newsfragments/3757.fixed.md delete mode 100644 newsfragments/3776.changed.md delete mode 100644 newsfragments/3785.added.md delete mode 100644 newsfragments/3801.added.md delete mode 100644 newsfragments/3802.added.md delete mode 100644 newsfragments/3815.added.md delete mode 100644 newsfragments/3818.fixed.md delete mode 100644 newsfragments/3849.added.md delete mode 100644 newsfragments/3849.changed.md delete mode 100644 newsfragments/3871.added.md delete mode 100644 newsfragments/3901.fixed.md delete mode 100644 newsfragments/3905.changed.md delete mode 100644 newsfragments/3928.added.md delete mode 100644 newsfragments/3928.changed.md delete mode 100644 newsfragments/3931.added.md delete mode 100644 newsfragments/3934.removed.md diff --git a/CHANGELOG.md b/CHANGELOG.md index fcd4ca4f495..36b1b552acc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,72 @@ To see unreleased changes, please see the [CHANGELOG on the main branch guide](h +## [0.21.0-beta.0] - 2024-03-10 + +### Added + +- Add `PyMemoryView` type. [#3514](https://github.com/PyO3/pyo3/pull/3514) +- Support `async fn` in macros with coroutine implementation [#3540](https://github.com/PyO3/pyo3/pull/3540) +- Implement `PyTypeInfo` for `PyEllipsis`, `PyNone` and `PyNotImplemented`. [#3577](https://github.com/PyO3/pyo3/pull/3577) +- Support `#[pyclass]` on enums that have non-unit variants. [#3582](https://github.com/PyO3/pyo3/pull/3582) +- Add `__name__`/`__qualname__` attributes to `Coroutine` [#3588](https://github.com/PyO3/pyo3/pull/3588) +- Add `coroutine::CancelHandle` to catch coroutine cancellation [#3599](https://github.com/PyO3/pyo3/pull/3599) +- Add support for extracting Rust set types from `frozenset`. [#3632](https://github.com/PyO3/pyo3/pull/3632) +- `FromPyObject`, `IntoPy` and `ToPyObject` are implemented on `std::duration::Duration` [#3670](https://github.com/PyO3/pyo3/pull/3670) +- Add `PyString::to_cow`. Add `Py::to_str`, `Py::to_cow`, and `Py::to_string_lossy`, as ways to access Python string data safely beyond the GIL lifetime. [#3677](https://github.com/PyO3/pyo3/pull/3677) +- Add `Bound` and `Borrowed` smart pointers as a new API for accessing Python objects. [#3686](https://github.com/PyO3/pyo3/pull/3686) +- Add `PyNativeType::as_bound` to convert "GIL refs" to the new `Bound` smart pointer. [#3692](https://github.com/PyO3/pyo3/pull/3692) +- Add `FromPyObject::extract_bound` method, which can be implemented to avoid using the GIL Ref API in `FromPyObject` implementations. [#3706](https://github.com/PyO3/pyo3/pull/3706) +- Add `gil-refs` feature to allow continued use of the deprecated GIL Refs APIs. [#3707](https://github.com/PyO3/pyo3/pull/3707) +- Added methods to `PyAnyMethods` for binary operators (`add`, `sub`, etc.) [#3712](https://github.com/PyO3/pyo3/pull/3712) +- `chrono-tz` feature allowing conversion between `chrono_tz::Tz` and `zoneinfo.ZoneInfo` [#3730](https://github.com/PyO3/pyo3/pull/3730) +- Add definition for `PyType_GetModuleByDef` to `pyo3_ffi`. [#3734](https://github.com/PyO3/pyo3/pull/3734) +- Conversion between `std::time::SystemTime` and `datetime.datetime` [#3736](https://github.com/PyO3/pyo3/pull/3736) +- Add `Py::as_any` and `Py::into_any`. [#3785](https://github.com/PyO3/pyo3/pull/3785) +- Add `PyStringMethods::encode_utf8`. [#3801](https://github.com/PyO3/pyo3/pull/3801) +- Add `PyBackedStr` and `PyBackedBytes`, as alternatives to `&str` and `&bytes` where a Python object owns the data. [#3802](https://github.com/PyO3/pyo3/pull/3802) +- The ability to create Python modules with a Rust `mod` block + behind the `experimental-declarative-modules` feature. [#3815](https://github.com/PyO3/pyo3/pull/3815) +- Implement `ExactSizeIterator` for `set` and `frozenset` iterators on `abi3` feature. [#3849](https://github.com/PyO3/pyo3/pull/3849) +- Add `Py::drop_ref` to explicitly drop a `Py`` and immediately decrease the Python reference count if the GIL is already held. [#3871](https://github.com/PyO3/pyo3/pull/3871) +- Implement `FromPyObject` for `Cow`. [#3928](https://github.com/PyO3/pyo3/pull/3928) +- Add `experimental-async` feature. [#3931](https://github.com/PyO3/pyo3/pull/3931) + +### Changed + +- - `PyDict::from_sequence` now takes a single argument of type `&PyAny` (previously took two arguments `Python` and `PyObject`). [#3532](https://github.com/PyO3/pyo3/pull/3532) +- Deprecate `Py::is_ellipsis` and `PyAny::is_ellipsis` in favour of `any.is(py.Ellipsis())`. [#3577](https://github.com/PyO3/pyo3/pull/3577) +- Split some `PyTypeInfo` functionality into new traits `HasPyGilRef` and `PyTypeCheck`. [#3600](https://github.com/PyO3/pyo3/pull/3600) +- Deprecate `PyTryFrom` and `PyTryInto` traits in favor of `any.downcast()` via the `PyTypeCheck` and `PyTypeInfo` traits. [#3601](https://github.com/PyO3/pyo3/pull/3601) +- Allow async methods to accept `&self`/`&mut self` [#3609](https://github.com/PyO3/pyo3/pull/3609) +- Values of type `bool` can now be extracted from NumPy's `bool_`. [#3638](https://github.com/PyO3/pyo3/pull/3638) +- Add `AsRefSource` to `PyNativeType`. [#3653](https://github.com/PyO3/pyo3/pull/3653) +- Changed `.is_true` to `.is_truthy` on `PyAny` and `Py` to clarify that the test is not based on identity with or equality to the True singleton. [#3657](https://github.com/PyO3/pyo3/pull/3657) +- `PyType::name` is now `PyType::qualname` whereas `PyType::name` efficiently accesses the full name which includes the module name. [#3660](https://github.com/PyO3/pyo3/pull/3660) +- The `Iter(A)NextOutput` types are now deprecated and `__(a)next__` can directly return anything which can be converted into Python objects, i.e. awaitables do not need to be wrapped into `IterANextOutput` or `Option` any more. `Option` can still be used as well and returning `None` will trigger the fast path for `__next__`, stopping iteration without having to raise a `StopIteration` exception. [#3661](https://github.com/PyO3/pyo3/pull/3661) +- Implements `FromPyObject` on `chrono::DateTime` for all `Tz` and not only `FixedOffset` and `Utc` [#3663](https://github.com/PyO3/pyo3/pull/3663) +- `chrono` conversions are compatible with `abi3` [#3664](https://github.com/PyO3/pyo3/pull/3664) +- Add lifetime parameter to `PyTzInfoAccess` trait. For the deprecated gil-ref API, the trait is now implemented for `&'py PyTime` and `&'py PyDateTime` instead of `PyTime` and `PyDate`. [#3679](https://github.com/PyO3/pyo3/pull/3679) +- Calls to `__traverse__` become no-ops for unsendable pyclasses if on the wrong thread, thereby avoiding hard aborts at the cost of potential leakage. [#3689](https://github.com/PyO3/pyo3/pull/3689) +- Include `PyNativeType` in `pyo3::prelude`. [#3692](https://github.com/PyO3/pyo3/pull/3692) +- Improve performance of `extract::` (and other integer types) by avoiding call to `__index__()` converting the value to an integer for 3.10+. Gives performance improvement of around 30% for successful extraction. [#3742](https://github.com/PyO3/pyo3/pull/3742) +- Relax bound of `FromPyObject` for `Py` to just `T: PyTypeCheck`. [#3776](https://github.com/PyO3/pyo3/pull/3776) +- `PySet` and `PyFrozenSet` iterators now always iterate the equivalent of `iter(set)`. (A "fast path" with no noticeable performance benefit was removed.) [#3849](https://github.com/PyO3/pyo3/pull/3849) +- The `#[pymodule]` macro now supports module functions that take a single argument as a `&Bound<'_, PyModule>`. [#3905](https://github.com/PyO3/pyo3/pull/3905) +- Move implementations of `FromPyObject` for `&str`, `Cow`, `&[u8]` and `Cow<[u8]>` onto a temporary trait `FromPyObjectBound` when `gil-refs` feature is deactivated. [#3928](https://github.com/PyO3/pyo3/pull/3928) + +### Removed + +- Remove all functionality deprecated in PyO3 0.19. [#3603](https://github.com/PyO3/pyo3/pull/3603) +- Remove `PyCode` and `PyCode_Type` on PyPy: `PyCode_Type` is not exposed by PyPy. [#3934](https://github.com/PyO3/pyo3/pull/3934) + +### Fixed + +- Match PyPy 7.3.14 in removing PyPy-only symbol `Py_MAX_NDIMS` in favour of `PyBUF_MAX_NDIM`. [#3757](https://github.com/PyO3/pyo3/pull/3757) +- Fix segmentation fault using `datetime` types when an invalid `datetime` module is on sys.path. [#3818](https://github.com/PyO3/pyo3/pull/3818) +- Fix `non_local_definitions` lint warning triggered by many PyO3 macros. [#3901](https://github.com/PyO3/pyo3/pull/3901) + + ## [0.20.3] - 2024-02-23 ### Packaging @@ -1640,7 +1706,7 @@ Yanked - Initial release -[Unreleased]: https://github.com/pyo3/pyo3/compare/v0.20.3...HEAD +[0.21.0-beta.0]: https://github.com/pyo3/pyo3/compare/v0.20.3...v0.21.0-beta.0 [0.20.3]: https://github.com/pyo3/pyo3/compare/v0.20.2...v0.20.3 [0.20.2]: https://github.com/pyo3/pyo3/compare/v0.20.1...v0.20.2 [0.20.1]: https://github.com/pyo3/pyo3/compare/v0.20.0...v0.20.1 diff --git a/Cargo.toml b/Cargo.toml index 32dc0ba75ef..ba5c3461180 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3" -version = "0.21.0-dev" +version = "0.21.0-beta.0" 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.0-dev" } +pyo3-ffi = { path = "pyo3-ffi", version = "=0.21.0-beta.0" } # support crates for macros feature -pyo3-macros = { path = "pyo3-macros", version = "=0.21.0-dev", optional = true } +pyo3-macros = { path = "pyo3-macros", version = "=0.21.0-beta.0", 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.0-dev", features = ["resolve-config"] } +pyo3-build-config = { path = "pyo3-build-config", version = "=0.21.0-beta.0", features = ["resolve-config"] } [features] default = ["macros"] diff --git a/newsfragments/3514.added.md b/newsfragments/3514.added.md deleted file mode 100644 index 7fbf662b2ec..00000000000 --- a/newsfragments/3514.added.md +++ /dev/null @@ -1 +0,0 @@ -Add `PyMemoryView` type. diff --git a/newsfragments/3532.changed.md b/newsfragments/3532.changed.md deleted file mode 100644 index b65f240931e..00000000000 --- a/newsfragments/3532.changed.md +++ /dev/null @@ -1 +0,0 @@ -- `PyDict::from_sequence` now takes a single argument of type `&PyAny` (previously took two arguments `Python` and `PyObject`). diff --git a/newsfragments/3540.added.md b/newsfragments/3540.added.md deleted file mode 100644 index 2b113193bef..00000000000 --- a/newsfragments/3540.added.md +++ /dev/null @@ -1 +0,0 @@ -Support `async fn` in macros with coroutine implementation \ No newline at end of file diff --git a/newsfragments/3577.added.md b/newsfragments/3577.added.md deleted file mode 100644 index 632274984ec..00000000000 --- a/newsfragments/3577.added.md +++ /dev/null @@ -1 +0,0 @@ -Implement `PyTypeInfo` for `PyEllipsis`, `PyNone` and `PyNotImplemented`. diff --git a/newsfragments/3577.changed.md b/newsfragments/3577.changed.md deleted file mode 100644 index a7e6629d6a5..00000000000 --- a/newsfragments/3577.changed.md +++ /dev/null @@ -1 +0,0 @@ -Deprecate `Py::is_ellipsis` and `PyAny::is_ellipsis` in favour of `any.is(py.Ellipsis())`. diff --git a/newsfragments/3582.added.md b/newsfragments/3582.added.md deleted file mode 100644 index 59659a8819d..00000000000 --- a/newsfragments/3582.added.md +++ /dev/null @@ -1 +0,0 @@ -Support `#[pyclass]` on enums that have non-unit variants. diff --git a/newsfragments/3588.added.md b/newsfragments/3588.added.md deleted file mode 100644 index acddf296a6f..00000000000 --- a/newsfragments/3588.added.md +++ /dev/null @@ -1 +0,0 @@ -Add `__name__`/`__qualname__` attributes to `Coroutine` \ No newline at end of file diff --git a/newsfragments/3599.added.md b/newsfragments/3599.added.md deleted file mode 100644 index 36078fbcdb6..00000000000 --- a/newsfragments/3599.added.md +++ /dev/null @@ -1 +0,0 @@ -Add `coroutine::CancelHandle` to catch coroutine cancellation \ No newline at end of file diff --git a/newsfragments/3600.changed.md b/newsfragments/3600.changed.md deleted file mode 100644 index c8701ef4b25..00000000000 --- a/newsfragments/3600.changed.md +++ /dev/null @@ -1 +0,0 @@ -Split some `PyTypeInfo` functionality into new traits `HasPyGilRef` and `PyTypeCheck`. diff --git a/newsfragments/3601.changed.md b/newsfragments/3601.changed.md deleted file mode 100644 index 413765ecad5..00000000000 --- a/newsfragments/3601.changed.md +++ /dev/null @@ -1 +0,0 @@ -Deprecate `PyTryFrom` and `PyTryInto` traits in favor of `any.downcast()` via the `PyTypeCheck` and `PyTypeInfo` traits. diff --git a/newsfragments/3603.removed.md b/newsfragments/3603.removed.md deleted file mode 100644 index e8f5004e3b9..00000000000 --- a/newsfragments/3603.removed.md +++ /dev/null @@ -1 +0,0 @@ -Remove all functionality deprecated in PyO3 0.19. diff --git a/newsfragments/3609.changed.md b/newsfragments/3609.changed.md deleted file mode 100644 index 7979ea71960..00000000000 --- a/newsfragments/3609.changed.md +++ /dev/null @@ -1 +0,0 @@ -Allow async methods to accept `&self`/`&mut self` \ No newline at end of file diff --git a/newsfragments/3632.added.md b/newsfragments/3632.added.md deleted file mode 100644 index d9c954fa0b4..00000000000 --- a/newsfragments/3632.added.md +++ /dev/null @@ -1 +0,0 @@ -Add support for extracting Rust set types from `frozenset`. diff --git a/newsfragments/3638.changed.md b/newsfragments/3638.changed.md deleted file mode 100644 index 6bdafde8422..00000000000 --- a/newsfragments/3638.changed.md +++ /dev/null @@ -1 +0,0 @@ -Values of type `bool` can now be extracted from NumPy's `bool_`. diff --git a/newsfragments/3653.changed.md b/newsfragments/3653.changed.md deleted file mode 100644 index 75fea03cb71..00000000000 --- a/newsfragments/3653.changed.md +++ /dev/null @@ -1 +0,0 @@ -Add `AsRefSource` to `PyNativeType`. diff --git a/newsfragments/3657.changed.md b/newsfragments/3657.changed.md deleted file mode 100644 index 0a519b09d62..00000000000 --- a/newsfragments/3657.changed.md +++ /dev/null @@ -1 +0,0 @@ -Changed `.is_true` to `.is_truthy` on `PyAny` and `Py` to clarify that the test is not based on identity with or equality to the True singleton. diff --git a/newsfragments/3660.changed.md b/newsfragments/3660.changed.md deleted file mode 100644 index 8b4a3f734e1..00000000000 --- a/newsfragments/3660.changed.md +++ /dev/null @@ -1 +0,0 @@ -`PyType::name` is now `PyType::qualname` whereas `PyType::name` efficiently accesses the full name which includes the module name. diff --git a/newsfragments/3661.changed.md b/newsfragments/3661.changed.md deleted file mode 100644 index 8245a6f1a80..00000000000 --- a/newsfragments/3661.changed.md +++ /dev/null @@ -1 +0,0 @@ -The `Iter(A)NextOutput` types are now deprecated and `__(a)next__` can directly return anything which can be converted into Python objects, i.e. awaitables do not need to be wrapped into `IterANextOutput` or `Option` any more. `Option` can still be used as well and returning `None` will trigger the fast path for `__next__`, stopping iteration without having to raise a `StopIteration` exception. diff --git a/newsfragments/3663.changed.md b/newsfragments/3663.changed.md deleted file mode 100644 index 13c07e01f2d..00000000000 --- a/newsfragments/3663.changed.md +++ /dev/null @@ -1 +0,0 @@ -Implements `FromPyObject` on `chrono::DateTime` for all `Tz` and not only `FixedOffset` and `Utc` \ No newline at end of file diff --git a/newsfragments/3664.changed.md b/newsfragments/3664.changed.md deleted file mode 100644 index 3a167d2f9d2..00000000000 --- a/newsfragments/3664.changed.md +++ /dev/null @@ -1 +0,0 @@ -`chrono` conversions are compatible with `abi3` \ No newline at end of file diff --git a/newsfragments/3670.added.md b/newsfragments/3670.added.md deleted file mode 100644 index a524261e9d9..00000000000 --- a/newsfragments/3670.added.md +++ /dev/null @@ -1 +0,0 @@ -`FromPyObject`, `IntoPy` and `ToPyObject` are implemented on `std::duration::Duration` \ No newline at end of file diff --git a/newsfragments/3677.added.md b/newsfragments/3677.added.md deleted file mode 100644 index 3e6bc56d582..00000000000 --- a/newsfragments/3677.added.md +++ /dev/null @@ -1 +0,0 @@ -Add `PyString::to_cow`. Add `Py::to_str`, `Py::to_cow`, and `Py::to_string_lossy`, as ways to access Python string data safely beyond the GIL lifetime. diff --git a/newsfragments/3679.changed.md b/newsfragments/3679.changed.md deleted file mode 100644 index ab46598ad65..00000000000 --- a/newsfragments/3679.changed.md +++ /dev/null @@ -1 +0,0 @@ -Add lifetime parameter to `PyTzInfoAccess` trait. For the deprecated gil-ref API, the trait is now implemented for `&'py PyTime` and `&'py PyDateTime` instead of `PyTime` and `PyDate`. diff --git a/newsfragments/3686.added.md b/newsfragments/3686.added.md deleted file mode 100644 index f808df3685a..00000000000 --- a/newsfragments/3686.added.md +++ /dev/null @@ -1 +0,0 @@ -Add `Bound` and `Borrowed` smart pointers as a new API for accessing Python objects. diff --git a/newsfragments/3689.changed.md b/newsfragments/3689.changed.md deleted file mode 100644 index 30928e82f64..00000000000 --- a/newsfragments/3689.changed.md +++ /dev/null @@ -1 +0,0 @@ -Calls to `__traverse__` become no-ops for unsendable pyclasses if on the wrong thread, thereby avoiding hard aborts at the cost of potential leakage. diff --git a/newsfragments/3692.added.md b/newsfragments/3692.added.md deleted file mode 100644 index 45cdd5aba28..00000000000 --- a/newsfragments/3692.added.md +++ /dev/null @@ -1 +0,0 @@ -Add `PyNativeType::as_bound` to convert "GIL refs" to the new `Bound` smart pointer. diff --git a/newsfragments/3692.changed.md b/newsfragments/3692.changed.md deleted file mode 100644 index 9535cbb23db..00000000000 --- a/newsfragments/3692.changed.md +++ /dev/null @@ -1 +0,0 @@ -Include `PyNativeType` in `pyo3::prelude`. diff --git a/newsfragments/3706.added.md b/newsfragments/3706.added.md deleted file mode 100644 index 31db8b96cef..00000000000 --- a/newsfragments/3706.added.md +++ /dev/null @@ -1 +0,0 @@ -Add `FromPyObject::extract_bound` method, which can be implemented to avoid using the GIL Ref API in `FromPyObject` implementations. diff --git a/newsfragments/3707.added.md b/newsfragments/3707.added.md deleted file mode 100644 index bc92e2c0f95..00000000000 --- a/newsfragments/3707.added.md +++ /dev/null @@ -1 +0,0 @@ -Add `gil-refs` feature to allow continued use of the deprecated GIL Refs APIs. diff --git a/newsfragments/3712.added.md b/newsfragments/3712.added.md deleted file mode 100644 index d7390f77c14..00000000000 --- a/newsfragments/3712.added.md +++ /dev/null @@ -1 +0,0 @@ -Added methods to `PyAnyMethods` for binary operators (`add`, `sub`, etc.) diff --git a/newsfragments/3730.added.md b/newsfragments/3730.added.md deleted file mode 100644 index 7e287245eb1..00000000000 --- a/newsfragments/3730.added.md +++ /dev/null @@ -1 +0,0 @@ -`chrono-tz` feature allowing conversion between `chrono_tz::Tz` and `zoneinfo.ZoneInfo` \ No newline at end of file diff --git a/newsfragments/3734.added.md b/newsfragments/3734.added.md deleted file mode 100644 index e58c2038e70..00000000000 --- a/newsfragments/3734.added.md +++ /dev/null @@ -1 +0,0 @@ -Add definition for `PyType_GetModuleByDef` to `pyo3_ffi`. diff --git a/newsfragments/3736.added.md b/newsfragments/3736.added.md deleted file mode 100644 index 0d3a4a08c1a..00000000000 --- a/newsfragments/3736.added.md +++ /dev/null @@ -1 +0,0 @@ -Conversion between `std::time::SystemTime` and `datetime.datetime` \ No newline at end of file diff --git a/newsfragments/3742.changed.md b/newsfragments/3742.changed.md deleted file mode 100644 index b8805abafda..00000000000 --- a/newsfragments/3742.changed.md +++ /dev/null @@ -1 +0,0 @@ -Improve performance of `extract::` (and other integer types) by avoiding call to `__index__()` converting the value to an integer for 3.10+. Gives performance improvement of around 30% for successful extraction. diff --git a/newsfragments/3757.fixed.md b/newsfragments/3757.fixed.md deleted file mode 100644 index 103a634af9f..00000000000 --- a/newsfragments/3757.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Match PyPy 7.3.14 in removing PyPy-only symbol `Py_MAX_NDIMS` in favour of `PyBUF_MAX_NDIM`. diff --git a/newsfragments/3776.changed.md b/newsfragments/3776.changed.md deleted file mode 100644 index 71ffd893a18..00000000000 --- a/newsfragments/3776.changed.md +++ /dev/null @@ -1 +0,0 @@ -Relax bound of `FromPyObject` for `Py` to just `T: PyTypeCheck`. diff --git a/newsfragments/3785.added.md b/newsfragments/3785.added.md deleted file mode 100644 index 6af3bb999f8..00000000000 --- a/newsfragments/3785.added.md +++ /dev/null @@ -1 +0,0 @@ -Add `Py::as_any` and `Py::into_any`. diff --git a/newsfragments/3801.added.md b/newsfragments/3801.added.md deleted file mode 100644 index 78f45032ba2..00000000000 --- a/newsfragments/3801.added.md +++ /dev/null @@ -1 +0,0 @@ -Add `PyStringMethods::encode_utf8`. diff --git a/newsfragments/3802.added.md b/newsfragments/3802.added.md deleted file mode 100644 index 86b98e9df97..00000000000 --- a/newsfragments/3802.added.md +++ /dev/null @@ -1 +0,0 @@ -Add `PyBackedStr` and `PyBackedBytes`, as alternatives to `&str` and `&bytes` where a Python object owns the data. diff --git a/newsfragments/3815.added.md b/newsfragments/3815.added.md deleted file mode 100644 index e4fd3e9315a..00000000000 --- a/newsfragments/3815.added.md +++ /dev/null @@ -1,2 +0,0 @@ -The ability to create Python modules with a Rust `mod` block -behind the `experimental-declarative-modules` feature. \ No newline at end of file diff --git a/newsfragments/3818.fixed.md b/newsfragments/3818.fixed.md deleted file mode 100644 index 76fe01a545c..00000000000 --- a/newsfragments/3818.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fix segmentation fault using `datetime` types when an invalid `datetime` module is on sys.path. diff --git a/newsfragments/3849.added.md b/newsfragments/3849.added.md deleted file mode 100644 index 8aa9a55df03..00000000000 --- a/newsfragments/3849.added.md +++ /dev/null @@ -1 +0,0 @@ -Implement `ExactSizeIterator` for `set` and `frozenset` iterators on `abi3` feature. diff --git a/newsfragments/3849.changed.md b/newsfragments/3849.changed.md deleted file mode 100644 index ee8dfbf5b2c..00000000000 --- a/newsfragments/3849.changed.md +++ /dev/null @@ -1 +0,0 @@ -`PySet` and `PyFrozenSet` iterators now always iterate the equivalent of `iter(set)`. (A "fast path" with no noticeable performance benefit was removed.) diff --git a/newsfragments/3871.added.md b/newsfragments/3871.added.md deleted file mode 100644 index f90e92fdfff..00000000000 --- a/newsfragments/3871.added.md +++ /dev/null @@ -1 +0,0 @@ -Add `Py::drop_ref` to explicitly drop a `Py`` and immediately decrease the Python reference count if the GIL is already held. diff --git a/newsfragments/3901.fixed.md b/newsfragments/3901.fixed.md deleted file mode 100644 index 0845c2bbcf5..00000000000 --- a/newsfragments/3901.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fix `non_local_definitions` lint warning triggered by many PyO3 macros. diff --git a/newsfragments/3905.changed.md b/newsfragments/3905.changed.md deleted file mode 100644 index 917584eb72a..00000000000 --- a/newsfragments/3905.changed.md +++ /dev/null @@ -1 +0,0 @@ -The `#[pymodule]` macro now supports module functions that take a single argument as a `&Bound<'_, PyModule>`. diff --git a/newsfragments/3928.added.md b/newsfragments/3928.added.md deleted file mode 100644 index e768e6b0163..00000000000 --- a/newsfragments/3928.added.md +++ /dev/null @@ -1 +0,0 @@ -Implement `FromPyObject` for `Cow`. diff --git a/newsfragments/3928.changed.md b/newsfragments/3928.changed.md deleted file mode 100644 index a4734c41ed3..00000000000 --- a/newsfragments/3928.changed.md +++ /dev/null @@ -1 +0,0 @@ -Move implementations of `FromPyObject` for `&str`, `Cow`, `&[u8]` and `Cow<[u8]>` onto a temporary trait `FromPyObjectBound` when `gil-refs` feature is deactivated. diff --git a/newsfragments/3931.added.md b/newsfragments/3931.added.md deleted file mode 100644 index b532adeeae5..00000000000 --- a/newsfragments/3931.added.md +++ /dev/null @@ -1 +0,0 @@ -Add `experimental-async` feature. diff --git a/newsfragments/3934.removed.md b/newsfragments/3934.removed.md deleted file mode 100644 index 66741d3f6b3..00000000000 --- a/newsfragments/3934.removed.md +++ /dev/null @@ -1 +0,0 @@ -Remove `PyCode` and `PyCode_Type` on PyPy: `PyCode_Type` is not exposed by PyPy. \ No newline at end of file diff --git a/pyo3-build-config/Cargo.toml b/pyo3-build-config/Cargo.toml index 702e99a4aac..c00427eb18e 100644 --- a/pyo3-build-config/Cargo.toml +++ b/pyo3-build-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-build-config" -version = "0.21.0-dev" +version = "0.21.0-beta.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 8021dc72b69..8dea584e304 100644 --- a/pyo3-ffi/Cargo.toml +++ b/pyo3-ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-ffi" -version = "0.21.0-dev" +version = "0.21.0-beta.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.21.0-dev", features = ["resolve-config"] } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.21.0-beta.0", features = ["resolve-config"] } [lints] workspace = true diff --git a/pyo3-macros-backend/Cargo.toml b/pyo3-macros-backend/Cargo.toml index 665c8c3510d..abf71902e20 100644 --- a/pyo3-macros-backend/Cargo.toml +++ b/pyo3-macros-backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-macros-backend" -version = "0.21.0-dev" +version = "0.21.0-beta.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.4" proc-macro2 = { version = "1", default-features = false } -pyo3-build-config = { path = "../pyo3-build-config", version = "=0.21.0-dev", features = ["resolve-config"] } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.21.0-beta.0", features = ["resolve-config"] } quote = { version = "1", default-features = false } [dependencies.syn] diff --git a/pyo3-macros/Cargo.toml b/pyo3-macros/Cargo.toml index 97d2de07cba..008224e78ef 100644 --- a/pyo3-macros/Cargo.toml +++ b/pyo3-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-macros" -version = "0.21.0-dev" +version = "0.21.0-beta.0" 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.0-dev" } +pyo3-macros-backend = { path = "../pyo3-macros-backend", version = "=0.21.0-beta.0" } [lints] workspace = true diff --git a/pyproject.toml b/pyproject.toml index 866645d2ffc..5d5ef42b1db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ [tool.towncrier] filename = "CHANGELOG.md" -version = "0.21.0-dev" +version = "0.21.0-beta.0" start_string = "\n" template = ".towncrier.template.md" title_format = "## [{version}] - {project_date}" From 93323bc9221ef5fabfd8cef68c1995a4faab67f0 Mon Sep 17 00:00:00 2001 From: acceptacross <150119116+acceptacross@users.noreply.github.com> Date: Mon, 11 Mar 2024 18:15:03 +0800 Subject: [PATCH 079/936] chore: remove repetitive words (#3950) Signed-off-by: acceptacross --- CHANGELOG.md | 2 +- guide/src/class.md | 2 +- guide/src/class/protocols.md | 2 +- guide/src/faq.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 36b1b552acc..3581d789ff3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -678,7 +678,7 @@ To see unreleased changes, please see the [CHANGELOG on the main branch guide](h - Respect Rust privacy rules for items wrapped with `wrap_pyfunction` and `wrap_pymodule`. [#2081](https://github.com/PyO3/pyo3/pull/2081) - Add modulo argument to `__ipow__` magic method. [#2083](https://github.com/PyO3/pyo3/pull/2083) - Fix FFI definition for `_PyCFunctionFast`. [#2126](https://github.com/PyO3/pyo3/pull/2126) -- `PyDateTimeAPI` and `PyDateTime_TimeZone_UTC` are are now unsafe functions instead of statics. [#2126](https://github.com/PyO3/pyo3/pull/2126) +- `PyDateTimeAPI` and `PyDateTime_TimeZone_UTC` are now unsafe functions instead of statics. [#2126](https://github.com/PyO3/pyo3/pull/2126) - `PyDateTimeAPI` does not implicitly call `PyDateTime_IMPORT` anymore to reflect the original Python API more closely. Before the first call to `PyDateTime_IMPORT` a null pointer is returned. Therefore before calling any of the following FFI functions `PyDateTime_IMPORT` must be called to avoid undefined behavior: [#2126](https://github.com/PyO3/pyo3/pull/2126) - `PyDateTime_TimeZone_UTC` - `PyDate_Check` diff --git a/guide/src/class.md b/guide/src/class.md index 2c6d854ff08..f353cc4787e 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -191,7 +191,7 @@ fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> { ## Bound and interior mutability -Often is 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. +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. 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. diff --git a/guide/src/class/protocols.md b/guide/src/class/protocols.md index 0a77cd7f2a9..3b12fd531c3 100644 --- a/guide/src/class/protocols.md +++ b/guide/src/class/protocols.md @@ -6,7 +6,7 @@ In the Python C-API which PyO3 is implemented upon, many of these magic methods If a function name in `#[pymethods]` is a recognised magic method, it will be automatically placed into the correct slot in the Python type object. The function name is taken from the usual rules for naming `#[pymethods]`: the `#[pyo3(name = "...")]` attribute is used if present, otherwise the Rust function name is used. -The magic methods handled by PyO3 are very similar to the standard Python ones on [this page](https://docs.python.org/3/reference/datamodel.html#special-method-names) - in particular they are the the subset which have slots as [defined here](https://docs.python.org/3/c-api/typeobj.html). Some of the slots do not have a magic method in Python, which leads to a few additional magic methods defined only in PyO3: +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 diff --git a/guide/src/faq.md b/guide/src/faq.md index 1034e9ccc2a..19f9b5d50ab 100644 --- a/guide/src/faq.md +++ b/guide/src/faq.md @@ -163,7 +163,7 @@ b: ``` The downside to this approach is that any Rust code working on the `Outer` struct now has to acquire the GIL to do anything with its field. -## I want to use the `pyo3` crate re-exported from from dependency but the proc-macros fail! +## I want to use the `pyo3` crate re-exported from dependency but the proc-macros fail! All PyO3 proc-macros (`#[pyclass]`, `#[pyfunction]`, `#[derive(FromPyObject)]` and so on) expect the `pyo3` crate to be available under that name in your crate From a7fa1bdf22c67e93543c9c4aa1927c52a1124e24 Mon Sep 17 00:00:00 2001 From: Georg Brandl Date: Mon, 11 Mar 2024 13:40:26 +0100 Subject: [PATCH 080/936] fix: remove "track_caller" cfg check (#3951) This "cfg" value does not seem to exist (any more?), and #[track_caller] is used a lot elsewhere without cfg_attr. Found using cargo check -Zcheck-cfg. --- src/err/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/err/mod.rs b/src/err/mod.rs index 5e054449bc9..cc4de79909b 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -485,7 +485,7 @@ impl PyErr { /// /// Use this function when the error is expected to have been set, for example from /// [PyErr::occurred] or by an error return value from a C FFI function. - #[cfg_attr(all(debug_assertions, track_caller), track_caller)] + #[cfg_attr(debug_assertions, track_caller)] #[inline] pub fn fetch(py: Python<'_>) -> PyErr { const FAILED_TO_FETCH: &str = "attempted to fetch exception but none was set"; From ee89b2e8e2015d8fdfa28dd69e459391809c601b Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Tue, 12 Mar 2024 23:57:03 +0100 Subject: [PATCH 081/936] deprecate `wrap_pyfunction` with `py` argument (#3954) * deprecate `wrap_pyfunction` with `py` argument The Python token in `wrap_pyfunction` is not handled automatically by `WrapPyFunctionArg`, for backwards compatibility. This uses deref specialization to deprecate this variant. * merge `Extractor`s * add deprecation ui test, revert closure variant due to test failure * fix nightly --- guide/src/conversions/traits.md | 2 +- guide/src/migration.md | 7 +++--- pytests/src/enums.rs | 8 ++++--- src/conversion.rs | 1 + src/conversions/anyhow.rs | 3 +-- src/conversions/eyre.rs | 3 +-- src/coroutine/waker.rs | 7 +++--- src/err/mod.rs | 4 ++-- src/exceptions.rs | 4 ++-- src/impl_/pymethods.rs | 9 ++++++++ src/macros.rs | 4 +++- src/marker.rs | 2 +- src/pycell.rs | 6 ++--- src/sync.rs | 6 ++--- src/tests/hygiene/pyfunction.rs | 1 + src/types/bytearray.rs | 4 ++-- tests/test_anyhow.rs | 10 +++++---- tests/test_bytes.rs | 6 ++--- tests/test_coroutine.rs | 19 +++++++++------- tests/test_enum.rs | 6 ++--- tests/test_exceptions.rs | 4 ++-- tests/test_macros.rs | 2 +- tests/test_methods.rs | 2 +- tests/test_pyfunction.rs | 30 ++++++++++++------------- tests/test_string.rs | 2 +- tests/test_text_signature.rs | 18 +++++++-------- tests/test_various.rs | 4 ++-- tests/test_wrap_pyfunction_deduction.rs | 1 + tests/ui/deprecations.rs | 16 +++++++++++++ tests/ui/deprecations.stderr | 8 +++++++ tests/ui/invalid_result_conversion.rs | 8 ++++--- 31 files changed, 127 insertions(+), 80 deletions(-) diff --git a/guide/src/conversions/traits.md b/guide/src/conversions/traits.md index 3a00a160c65..65a5d150e79 100644 --- a/guide/src/conversions/traits.md +++ b/guide/src/conversions/traits.md @@ -499,7 +499,7 @@ _without_ having a unique python type. ```rust use pyo3::prelude::*; - +# #[allow(dead_code)] struct MyPyObjectWrapper(PyObject); impl IntoPy for MyPyObjectWrapper { diff --git a/guide/src/migration.md b/guide/src/migration.md index 5af6eb2336c..1c5ca59714a 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -436,7 +436,7 @@ impl SomeClass { When converting from `anyhow::Error` or `eyre::Report` to `PyErr`, if the inner error is a "simple" `PyErr` (with no source error), then the inner error will be used directly as the `PyErr` instead of wrapping it in a new `PyRuntimeError` with the original information converted into a string. -```rust +```rust,ignore # #[cfg(feature = "anyhow")] # #[allow(dead_code)] # mod anyhow_only { @@ -597,9 +597,9 @@ fn function_with_defaults(a: i32, b: i32, c: i32) {} # fn main() { # Python::with_gil(|py| { -# let simple = wrap_pyfunction!(simple_function, py).unwrap(); +# let simple = wrap_pyfunction_bound!(simple_function, py).unwrap(); # assert_eq!(simple.getattr("__text_signature__").unwrap().to_string(), "(a, b, c)"); -# let defaulted = wrap_pyfunction!(function_with_defaults, py).unwrap(); +# let defaulted = wrap_pyfunction_bound!(function_with_defaults, py).unwrap(); # assert_eq!(defaulted.getattr("__text_signature__").unwrap().to_string(), "(a, b=1, c=2)"); # }) # } @@ -1090,6 +1090,7 @@ impl FromPy for PyObject { After ```rust # use pyo3::prelude::*; +# #[allow(dead_code)] struct MyPyObjectWrapper(PyObject); impl IntoPy for MyPyObjectWrapper { diff --git a/pytests/src/enums.rs b/pytests/src/enums.rs index 32478cbefc2..4bb269fbbd2 100644 --- a/pytests/src/enums.rs +++ b/pytests/src/enums.rs @@ -1,11 +1,13 @@ -use pyo3::{pyclass, pyfunction, pymodule, types::PyModule, wrap_pyfunction, Bound, PyResult}; +use pyo3::{ + pyclass, pyfunction, pymodule, types::PyModule, wrap_pyfunction_bound, Bound, PyResult, +}; #[pymodule] pub fn enums(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; - m.add_wrapped(wrap_pyfunction!(do_simple_stuff))?; - m.add_wrapped(wrap_pyfunction!(do_complex_stuff))?; + m.add_wrapped(wrap_pyfunction_bound!(do_simple_stuff))?; + m.add_wrapped(wrap_pyfunction_bound!(do_complex_stuff))?; Ok(()) } diff --git a/src/conversion.rs b/src/conversion.rs index efb09b6b341..8d4ad776837 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -96,6 +96,7 @@ pub trait ToPyObject { /// ```rust /// use pyo3::prelude::*; /// +/// # #[allow(dead_code)] /// struct Number { /// value: i32, /// } diff --git a/src/conversions/anyhow.rs b/src/conversions/anyhow.rs index fba8816d23a..623ee7d548c 100644 --- a/src/conversions/anyhow.rs +++ b/src/conversions/anyhow.rs @@ -35,7 +35,6 @@ //! //! ```rust //! use pyo3::prelude::*; -//! use pyo3::wrap_pyfunction; //! use std::path::PathBuf; //! //! // A wrapper around a Rust function. @@ -48,7 +47,7 @@ //! //! fn main() { //! let error = Python::with_gil(|py| -> PyResult> { -//! let fun = wrap_pyfunction!(py_open, py)?; +//! let fun = wrap_pyfunction_bound!(py_open, py)?; //! let text = fun.call1(("foo.txt",))?.extract::>()?; //! Ok(text) //! }).unwrap_err(); diff --git a/src/conversions/eyre.rs b/src/conversions/eyre.rs index 236e2c8bc92..d4704e411c5 100644 --- a/src/conversions/eyre.rs +++ b/src/conversions/eyre.rs @@ -34,7 +34,6 @@ //! //! ```rust //! use pyo3::prelude::*; -//! use pyo3::wrap_pyfunction; //! use std::path::PathBuf; //! //! // A wrapper around a Rust function. @@ -47,7 +46,7 @@ //! //! fn main() { //! let error = Python::with_gil(|py| -> PyResult> { -//! let fun = wrap_pyfunction!(py_open, py)?; +//! let fun = wrap_pyfunction_bound!(py_open, py)?; //! let text = fun.call1(("foo.txt",))?.extract::>()?; //! Ok(text) //! }).unwrap_err(); diff --git a/src/coroutine/waker.rs b/src/coroutine/waker.rs index 096146f8292..b524b6d7298 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, Py, PyAny, PyObject, PyResult, Python}; +use crate::{intern, wrap_pyfunction_bound, Bound, Py, PyAny, PyObject, PyResult, Python}; use pyo3_macros::pyfunction; use std::sync::Arc; use std::task::Wake; @@ -70,8 +70,9 @@ impl LoopAndFuture { fn set_result(&self, py: Python<'_>) -> PyResult<()> { static RELEASE_WAITER: GILOnceCell> = GILOnceCell::new(); - let release_waiter = RELEASE_WAITER - .get_or_try_init(py, || wrap_pyfunction!(release_waiter, py).map(Into::into))?; + let release_waiter = RELEASE_WAITER.get_or_try_init(py, || { + wrap_pyfunction_bound!(release_waiter, py).map(Bound::unbind) + })?; // `Future.set_result` must be called in event loop thread, // so it requires `call_soon_threadsafe` let call_soon_threadsafe = self.event_loop.call_method1( diff --git a/src/err/mod.rs b/src/err/mod.rs index cc4de79909b..7610f49951c 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -145,7 +145,7 @@ impl PyErr { /// } /// # /// # Python::with_gil(|py| { - /// # let fun = pyo3::wrap_pyfunction!(always_throws, py).unwrap(); + /// # let fun = pyo3::wrap_pyfunction_bound!(always_throws, py).unwrap(); /// # let err = fun.call0().expect_err("called a function that should always return an error but the return value was Ok"); /// # assert!(err.is_instance_of::(py)) /// # }); @@ -163,7 +163,7 @@ impl PyErr { /// } /// # /// # Python::with_gil(|py| { - /// # let fun = pyo3::wrap_pyfunction!(always_throws, py).unwrap(); + /// # let fun = pyo3::wrap_pyfunction_bound!(always_throws, py).unwrap(); /// # let err = fun.call0().expect_err("called a function that should always return an error but the return value was Ok"); /// # assert!(err.is_instance_of::(py)) /// # }); diff --git a/src/exceptions.rs b/src/exceptions.rs index 3401679b25e..bd9c89c425f 100644 --- a/src/exceptions.rs +++ b/src/exceptions.rs @@ -171,7 +171,7 @@ macro_rules! import_exception { /// } /// # fn main() -> PyResult<()> { /// # Python::with_gil(|py| -> PyResult<()> { -/// # let fun = wrap_pyfunction!(raise_myerror, py)?; +/// # let fun = wrap_pyfunction_bound!(raise_myerror, py)?; /// # let locals = pyo3::types::PyDict::new_bound(py); /// # locals.set_item("MyError", py.get_type_bound::())?; /// # locals.set_item("raise_myerror", fun)?; @@ -322,7 +322,7 @@ fn always_throws() -> PyResult<()> { } # # Python::with_gil(|py| { -# let fun = pyo3::wrap_pyfunction!(always_throws, py).unwrap(); +# let fun = pyo3::wrap_pyfunction_bound!(always_throws, py).unwrap(); # let err = fun.call0().expect_err(\"called a function that should always return an error but the return value was Ok\"); # assert!(err.is_instance_of::(py)) # }); diff --git a/src/impl_/pymethods.rs b/src/impl_/pymethods.rs index a7df90b572d..bc1125f97fd 100644 --- a/src/impl_/pymethods.rs +++ b/src/impl_/pymethods.rs @@ -599,6 +599,14 @@ impl Extractor { } } +impl Extractor> { + #[cfg_attr( + not(feature = "gil-refs"), + deprecated(since = "0.21.0", note = "use `wrap_pyfunction_bound!` instead") + )] + pub fn is_python(&self) {} +} + impl Extractor { #[cfg_attr( not(feature = "gil-refs"), @@ -612,6 +620,7 @@ impl Extractor { impl NotAGilRef { pub fn extract_gil_ref(&self) {} + pub fn is_python(&self) {} } impl std::ops::Deref for Extractor { diff --git a/src/macros.rs b/src/macros.rs index 33f378e7b83..bd0bd81421c 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -144,8 +144,10 @@ macro_rules! wrap_pyfunction { }; ($function:path, $py_or_module:expr) => {{ use $function as wrapped_pyfunction; + let (py_or_module, e) = $crate::impl_::pymethods::inspect_type($py_or_module); + e.is_python(); $crate::impl_::pyfunction::WrapPyFunctionArg::wrap_pyfunction( - $py_or_module, + py_or_module, &wrapped_pyfunction::DEF, ) }}; diff --git a/src/marker.rs b/src/marker.rs index 9b3d7329316..2a38b83cba5 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -509,7 +509,7 @@ impl<'py> Python<'py> { /// # /// # fn main() -> PyResult<()> { /// # Python::with_gil(|py| -> PyResult<()> { - /// # let fun = pyo3::wrap_pyfunction!(sum_numbers, py)?; + /// # let fun = pyo3::wrap_pyfunction_bound!(sum_numbers, py)?; /// # let res = fun.call1((vec![1_u32, 2, 3],))?; /// # assert_eq!(res.extract::()?, 6_u32); /// # Ok(()) diff --git a/src/pycell.rs b/src/pycell.rs index 397e7b6a22a..636fb90cef6 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!(swap_numbers, py).unwrap(); +//! # let fun = pyo3::wrap_pyfunction_bound!(swap_numbers, py).unwrap(); //! # fun.call1((n, n2)).expect_err("Managed to create overlapping mutable references. Note: this is undefined behaviour."); //! # }); //! # } @@ -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!(swap_numbers, py).unwrap(); +//! # let fun = pyo3::wrap_pyfunction_bound!(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!(swap_numbers, py).unwrap(); +//! # let fun = pyo3::wrap_pyfunction_bound!(swap_numbers, py).unwrap(); //! # fun.call1((&n, &n2)).unwrap(); //! # let n: u32 = n.borrow(py).inner; //! # let n2: u32 = n2.borrow(py).inner; diff --git a/src/sync.rs b/src/sync.rs index e89a8edd853..38471fb7ca3 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -220,7 +220,7 @@ impl GILOnceCell> { /// /// ``` /// use pyo3::intern; -/// # use pyo3::{pyfunction, types::PyDict, wrap_pyfunction, PyResult, Python, prelude::PyDictMethods, Bound}; +/// # use pyo3::{prelude::*, types::PyDict}; /// /// #[pyfunction] /// fn create_dict(py: Python<'_>) -> PyResult> { @@ -241,10 +241,10 @@ impl GILOnceCell> { /// } /// # /// # Python::with_gil(|py| { -/// # let fun_slow = wrap_pyfunction!(create_dict, py).unwrap(); +/// # let fun_slow = wrap_pyfunction_bound!(create_dict, py).unwrap(); /// # let dict = fun_slow.call0().unwrap(); /// # assert!(dict.contains("foo").unwrap()); -/// # let fun = wrap_pyfunction!(create_dict_faster, py).unwrap(); +/// # let fun = wrap_pyfunction_bound!(create_dict_faster, py).unwrap(); /// # let dict = fun.call0().unwrap(); /// # assert!(dict.contains("foo").unwrap()); /// # }); diff --git a/src/tests/hygiene/pyfunction.rs b/src/tests/hygiene/pyfunction.rs index 9cfad0db6c6..edc8b6e35d3 100644 --- a/src/tests/hygiene/pyfunction.rs +++ b/src/tests/hygiene/pyfunction.rs @@ -10,6 +10,7 @@ fn do_something(x: i32) -> crate::PyResult { #[test] 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)"#); }); diff --git a/src/types/bytearray.rs b/src/types/bytearray.rs index a860b4d4cca..55cda1a355a 100644 --- a/src/types/bytearray.rs +++ b/src/types/bytearray.rs @@ -193,7 +193,7 @@ impl PyByteArray { /// } /// # fn main() -> PyResult<()> { /// # Python::with_gil(|py| -> PyResult<()> { - /// # let fun = wrap_pyfunction!(a_valid_function, py)?; + /// # let fun = wrap_pyfunction_bound!(a_valid_function, py)?; /// # let locals = pyo3::types::PyDict::new_bound(py); /// # locals.set_item("a_valid_function", fun)?; /// # @@ -355,7 +355,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 fun = wrap_pyfunction_bound!(a_valid_function, py)?; /// # let locals = pyo3::types::PyDict::new_bound(py); /// # locals.set_item("a_valid_function", fun)?; /// # diff --git a/tests/test_anyhow.rs b/tests/test_anyhow.rs index 1807cfe9708..d6f5c036b74 100644 --- a/tests/test_anyhow.rs +++ b/tests/test_anyhow.rs @@ -1,8 +1,10 @@ #![cfg(feature = "anyhow")] +use pyo3::wrap_pyfunction_bound; + #[test] fn test_anyhow_py_function_ok_result() { - use pyo3::{py_run, pyfunction, wrap_pyfunction, Python}; + use pyo3::{py_run, pyfunction, Python}; #[pyfunction] #[allow(clippy::unnecessary_wraps)] @@ -11,7 +13,7 @@ fn test_anyhow_py_function_ok_result() { } Python::with_gil(|py| { - let func = wrap_pyfunction!(produce_ok_result)(py).unwrap(); + let func = wrap_pyfunction_bound!(produce_ok_result)(py).unwrap(); py_run!( py, @@ -26,7 +28,7 @@ fn test_anyhow_py_function_ok_result() { #[test] fn test_anyhow_py_function_err_result() { use pyo3::prelude::PyDictMethods; - use pyo3::{pyfunction, types::PyDict, wrap_pyfunction, Python}; + use pyo3::{pyfunction, types::PyDict, Python}; #[pyfunction] fn produce_err_result() -> anyhow::Result { @@ -34,7 +36,7 @@ fn test_anyhow_py_function_err_result() { } Python::with_gil(|py| { - let func = wrap_pyfunction!(produce_err_result)(py).unwrap(); + let func = wrap_pyfunction_bound!(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 cdb4ec15750..a3f1e2fcafe 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!(bytes_pybytes_conversion)(py).unwrap(); + let f = wrap_pyfunction_bound!(bytes_pybytes_conversion)(py).unwrap(); py_assert!(py, f, "f(b'Hello World') == b'Hello World'"); }); } @@ -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!(bytes_vec_conversion)(py).unwrap(); + let f = wrap_pyfunction_bound!(bytes_vec_conversion)(py).unwrap(); py_assert!(py, f, "f(b'Hello World') == b'Hello World'"); }); } @@ -35,7 +35,7 @@ fn test_pybytes_vec_conversion() { #[test] fn test_bytearray_vec_conversion() { Python::with_gil(|py| { - let f = wrap_pyfunction!(bytes_vec_conversion)(py).unwrap(); + let f = wrap_pyfunction_bound!(bytes_vec_conversion)(py).unwrap(); py_assert!(py, f, "f(bytearray(b'Hello World')) == b'Hello World'"); }); } diff --git a/tests/test_coroutine.rs b/tests/test_coroutine.rs index 17539fa113e..23f6a6722d4 100644 --- a/tests/test_coroutine.rs +++ b/tests/test_coroutine.rs @@ -30,7 +30,7 @@ fn noop_coroutine() { 42 } Python::with_gil(|gil| { - let noop = wrap_pyfunction!(noop, gil).unwrap(); + let noop = wrap_pyfunction_bound!(noop, gil).unwrap(); let test = "import asyncio; assert asyncio.run(noop()) == 42"; py_run!(gil, noop, &handle_windows(test)); }) @@ -68,7 +68,10 @@ fn test_coroutine_qualname() { let locals = [ ( "my_fn", - wrap_pyfunction!(my_fn, gil).unwrap().as_borrowed().as_any(), + wrap_pyfunction_bound!(my_fn, gil) + .unwrap() + .as_borrowed() + .as_any(), ), ("MyClass", gil.get_type_bound::().as_any()), ] @@ -93,7 +96,7 @@ fn sleep_0_like_coroutine() { .await } Python::with_gil(|gil| { - let sleep_0 = wrap_pyfunction!(sleep_0, gil).unwrap(); + let sleep_0 = wrap_pyfunction_bound!(sleep_0, gil).unwrap(); let test = "import asyncio; assert asyncio.run(sleep_0()) == 42"; py_run!(gil, sleep_0, &handle_windows(test)); }) @@ -112,7 +115,7 @@ async fn sleep(seconds: f64) -> usize { #[test] fn sleep_coroutine() { Python::with_gil(|gil| { - let sleep = wrap_pyfunction!(sleep, gil).unwrap(); + let sleep = wrap_pyfunction_bound!(sleep, gil).unwrap(); let test = r#"import asyncio; assert asyncio.run(sleep(0.1)) == 42"#; py_run!(gil, sleep, &handle_windows(test)); }) @@ -121,7 +124,7 @@ fn sleep_coroutine() { #[test] fn cancelled_coroutine() { Python::with_gil(|gil| { - let sleep = wrap_pyfunction!(sleep, gil).unwrap(); + let sleep = wrap_pyfunction_bound!(sleep, gil).unwrap(); let test = r#" import asyncio async def main(): @@ -160,7 +163,7 @@ fn coroutine_cancel_handle() { } } Python::with_gil(|gil| { - let cancellable_sleep = wrap_pyfunction!(cancellable_sleep, gil).unwrap(); + let cancellable_sleep = wrap_pyfunction_bound!(cancellable_sleep, gil).unwrap(); let test = r#" import asyncio; async def main(): @@ -192,7 +195,7 @@ fn coroutine_is_cancelled() { } } Python::with_gil(|gil| { - let sleep_loop = wrap_pyfunction!(sleep_loop, gil).unwrap(); + let sleep_loop = wrap_pyfunction_bound!(sleep_loop, gil).unwrap(); let test = r#" import asyncio; async def main(): @@ -220,7 +223,7 @@ fn coroutine_panic() { panic!("test panic"); } Python::with_gil(|gil| { - let panic = wrap_pyfunction!(panic, gil).unwrap(); + let panic = wrap_pyfunction_bound!(panic, gil).unwrap(); let test = r#" import asyncio coro = panic() diff --git a/tests/test_enum.rs b/tests/test_enum.rs index d73316e5512..63492b8d3cd 100644 --- a/tests/test_enum.rs +++ b/tests/test_enum.rs @@ -1,7 +1,7 @@ #![cfg(feature = "macros")] use pyo3::prelude::*; -use pyo3::{py_run, wrap_pyfunction}; +use pyo3::py_run; #[path = "../src/tests/common.rs"] mod common; @@ -30,7 +30,7 @@ fn return_enum() -> MyEnum { #[test] fn test_return_enum() { Python::with_gil(|py| { - let f = wrap_pyfunction!(return_enum)(py).unwrap(); + let f = wrap_pyfunction_bound!(return_enum)(py).unwrap(); let mynum = py.get_type_bound::(); py_run!(py, f mynum, "assert f() == mynum.Variant") @@ -45,7 +45,7 @@ fn enum_arg(e: MyEnum) { #[test] fn test_enum_arg() { Python::with_gil(|py| { - let f = wrap_pyfunction!(enum_arg)(py).unwrap(); + let f = wrap_pyfunction_bound!(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 3603586033e..ec2fe156b29 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!(fail_to_open_file)(py).unwrap(); + let fail_to_open_file = wrap_pyfunction_bound!(fail_to_open_file)(py).unwrap(); py_run!( py, @@ -68,7 +68,7 @@ fn call_fail_with_custom_error() -> PyResult<()> { fn test_custom_error() { Python::with_gil(|py| { let call_fail_with_custom_error = - wrap_pyfunction!(call_fail_with_custom_error)(py).unwrap(); + wrap_pyfunction_bound!(call_fail_with_custom_error)(py).unwrap(); py_run!( py, diff --git a/tests/test_macros.rs b/tests/test_macros.rs index 0d2b125b870..6a50e5b36e4 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!(my_function_in_macro, py).unwrap(); + let my_func = wrap_pyfunction_bound!(my_function_in_macro, py).unwrap(); py_assert!( py, my_func, diff --git a/tests/test_methods.rs b/tests/test_methods.rs index a9e7d37d64a..e7eacf26b40 100644 --- a/tests/test_methods.rs +++ b/tests/test_methods.rs @@ -1124,7 +1124,7 @@ fn test_option_pyclass_arg() { } Python::with_gil(|py| { - let f = wrap_pyfunction!(option_class_arg, py).unwrap(); + let f = wrap_pyfunction_bound!(option_class_arg, py).unwrap(); assert!(f.call0().unwrap().is_none()); let obj = Py::new(py, SomePyClass {}).unwrap(); assert!(f diff --git a/tests/test_pyfunction.rs b/tests/test_pyfunction.rs index 32c3ede6309..5c4350467c4 100644 --- a/tests/test_pyfunction.rs +++ b/tests/test_pyfunction.rs @@ -23,7 +23,7 @@ fn optional_bool(arg: Option) -> String { fn test_optional_bool() { // Regression test for issue #932 Python::with_gil(|py| { - let f = wrap_pyfunction!(optional_bool)(py).unwrap(); + let f = wrap_pyfunction_bound!(optional_bool)(py).unwrap(); py_assert!(py, f, "f() == 'Some(true)'"); py_assert!(py, f, "f(True) == 'Some(true)'"); @@ -47,7 +47,7 @@ fn buffer_inplace_add(py: Python<'_>, x: PyBuffer, y: PyBuffer) { #[test] fn test_buffer_add() { Python::with_gil(|py| { - let f = wrap_pyfunction!(buffer_inplace_add)(py).unwrap(); + let f = wrap_pyfunction_bound!(buffer_inplace_add)(py).unwrap(); py_expect_exception!( py, @@ -89,8 +89,8 @@ fn function_with_pycfunction_arg(fun: &PyCFunction) -> PyResult<&PyAny> { #[test] fn test_functions_with_function_args() { Python::with_gil(|py| { - let py_cfunc_arg = wrap_pyfunction!(function_with_pycfunction_arg)(py).unwrap(); - let bool_to_string = wrap_pyfunction!(optional_bool)(py).unwrap(); + let py_cfunc_arg = wrap_pyfunction_bound!(function_with_pycfunction_arg)(py).unwrap(); + let bool_to_string = wrap_pyfunction_bound!(optional_bool)(py).unwrap(); pyo3::py_run!( py, @@ -103,7 +103,7 @@ fn test_functions_with_function_args() { #[cfg(not(any(Py_LIMITED_API, PyPy)))] { - let py_func_arg = wrap_pyfunction!(function_with_pyfunction_arg)(py).unwrap(); + let py_func_arg = wrap_pyfunction_bound!(function_with_pyfunction_arg)(py).unwrap(); pyo3::py_run!( py, @@ -137,7 +137,7 @@ fn function_with_custom_conversion( #[test] fn test_function_with_custom_conversion() { Python::with_gil(|py| { - let custom_conv_func = wrap_pyfunction!(function_with_custom_conversion)(py).unwrap(); + let custom_conv_func = wrap_pyfunction_bound!(function_with_custom_conversion)(py).unwrap(); pyo3::py_run!( py, @@ -156,7 +156,7 @@ fn test_function_with_custom_conversion() { #[test] fn test_function_with_custom_conversion_error() { Python::with_gil(|py| { - let custom_conv_func = wrap_pyfunction!(function_with_custom_conversion)(py).unwrap(); + let custom_conv_func = wrap_pyfunction_bound!(function_with_custom_conversion)(py).unwrap(); py_expect_exception!( py, @@ -190,13 +190,13 @@ fn test_from_py_with_defaults() { } Python::with_gil(|py| { - let f = wrap_pyfunction!(from_py_with_option)(py).unwrap(); + let f = wrap_pyfunction_bound!(from_py_with_option)(py).unwrap(); assert_eq!(f.call0().unwrap().extract::().unwrap(), 0); assert_eq!(f.call1((123,)).unwrap().extract::().unwrap(), 123); assert_eq!(f.call1((999,)).unwrap().extract::().unwrap(), 999); - let f2 = wrap_pyfunction!(from_py_with_default)(py).unwrap(); + let f2 = wrap_pyfunction_bound!(from_py_with_default)(py).unwrap(); assert_eq!(f2.call0().unwrap().extract::().unwrap(), 0); assert_eq!(f2.call1(("123",)).unwrap().extract::().unwrap(), 3); @@ -228,7 +228,7 @@ fn conversion_error( #[test] fn test_conversion_error() { Python::with_gil(|py| { - let conversion_error = wrap_pyfunction!(conversion_error)(py).unwrap(); + let conversion_error = wrap_pyfunction_bound!(conversion_error)(py).unwrap(); py_expect_exception!( py, conversion_error, @@ -473,12 +473,12 @@ fn use_pyfunction() { use function_in_module::foo; // check imported name can be wrapped - let f = wrap_pyfunction!(foo, py).unwrap(); + let f = wrap_pyfunction_bound!(foo, py).unwrap(); assert_eq!(f.call1((5,)).unwrap().extract::().unwrap(), 5); assert_eq!(f.call1((42,)).unwrap().extract::().unwrap(), 42); // check path import can be wrapped - let f2 = wrap_pyfunction!(function_in_module::foo, py).unwrap(); + let f2 = wrap_pyfunction_bound!(function_in_module::foo, py).unwrap(); assert_eq!(f2.call1((5,)).unwrap().extract::().unwrap(), 5); assert_eq!(f2.call1((42,)).unwrap().extract::().unwrap(), 42); }) @@ -506,7 +506,7 @@ fn return_value_borrows_from_arguments<'py>( #[test] fn test_return_value_borrows_from_arguments() { Python::with_gil(|py| { - let function = wrap_pyfunction!(return_value_borrows_from_arguments, py).unwrap(); + let function = wrap_pyfunction_bound!(return_value_borrows_from_arguments, py).unwrap(); let key = Py::new(py, Key("key".to_owned())).unwrap(); let value = Py::new(py, Value(42)).unwrap(); @@ -530,7 +530,7 @@ fn test_some_wrap_arguments() { } Python::with_gil(|py| { - let function = wrap_pyfunction!(some_wrap_arguments, py).unwrap(); + let function = wrap_pyfunction_bound!(some_wrap_arguments, py).unwrap(); py_assert!(py, function, "function() == [1, 2, None, None]"); }) } @@ -546,7 +546,7 @@ fn test_reference_to_bound_arguments() { } Python::with_gil(|py| { - let function = wrap_pyfunction!(reference_args, py).unwrap(); + let function = wrap_pyfunction_bound!(reference_args, py).unwrap(); py_assert!(py, function, "function(1) == 1"); py_assert!(py, function, "function(1, 2) == 3"); }) diff --git a/tests/test_string.rs b/tests/test_string.rs index 02bf2ecd4df..d90c5a81b83 100644 --- a/tests/test_string.rs +++ b/tests/test_string.rs @@ -11,7 +11,7 @@ fn take_str(_s: &str) {} #[test] fn test_unicode_encode_error() { Python::with_gil(|py| { - let take_str = wrap_pyfunction!(take_str)(py).unwrap(); + let take_str = wrap_pyfunction_bound!(take_str)(py).unwrap(); py_expect_exception!( py, take_str, diff --git a/tests/test_text_signature.rs b/tests/test_text_signature.rs index 0b93500db7e..32a78346e9a 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!(my_function)(py).unwrap(); + let f = wrap_pyfunction_bound!(my_function)(py).unwrap(); py_assert!(py, f, "f.__text_signature__ == '(a, b=None, *, c=42)'"); }); @@ -147,42 +147,42 @@ fn test_auto_test_signature_function() { } Python::with_gil(|py| { - let f = wrap_pyfunction!(my_function)(py).unwrap(); + let f = wrap_pyfunction_bound!(my_function)(py).unwrap(); py_assert!( py, f, "f.__text_signature__ == '(a, b, c)', f.__text_signature__" ); - let f = wrap_pyfunction!(my_function_2)(py).unwrap(); + let f = wrap_pyfunction_bound!(my_function_2)(py).unwrap(); py_assert!( py, f, "f.__text_signature__ == '($module, a, b, c)', f.__text_signature__" ); - let f = wrap_pyfunction!(my_function_3)(py).unwrap(); + let f = wrap_pyfunction_bound!(my_function_3)(py).unwrap(); py_assert!( py, f, "f.__text_signature__ == '(a, /, b=None, *, c=5)', f.__text_signature__" ); - let f = wrap_pyfunction!(my_function_4)(py).unwrap(); + let f = wrap_pyfunction_bound!(my_function_4)(py).unwrap(); py_assert!( py, f, "f.__text_signature__ == '(a, /, b=None, *args, c, d=5, **kwargs)', f.__text_signature__" ); - let f = wrap_pyfunction!(my_function_5)(py).unwrap(); + let f = wrap_pyfunction_bound!(my_function_5)(py).unwrap(); py_assert!( py, f, "f.__text_signature__ == '(a=1, /, b=None, c=1.5, d=5, e=\"pyo3\", f=\\'f\\', h=True)', f.__text_signature__" ); - let f = wrap_pyfunction!(my_function_6)(py).unwrap(); + let f = wrap_pyfunction_bound!(my_function_6)(py).unwrap(); py_assert!( py, f, @@ -317,10 +317,10 @@ fn test_auto_test_signature_opt_out() { } Python::with_gil(|py| { - let f = wrap_pyfunction!(my_function)(py).unwrap(); + let f = wrap_pyfunction_bound!(my_function)(py).unwrap(); py_assert!(py, f, "f.__text_signature__ == None"); - let f = wrap_pyfunction!(my_function_2)(py).unwrap(); + let f = wrap_pyfunction_bound!(my_function_2)(py).unwrap(); py_assert!(py, f, "f.__text_signature__ == None"); let cls = py.get_type_bound::(); diff --git a/tests/test_various.rs b/tests/test_various.rs index 250f39834d1..0e619f49a28 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!(get_zero)(py).unwrap(); + let get_zero = wrap_pyfunction_bound!(get_zero)(py).unwrap(); py_assert!(py, get_zero, "get_zero().value == 0"); }); } @@ -201,6 +201,6 @@ fn result_conversion_function() -> Result<(), MyError> { #[test] fn test_result_conversion() { Python::with_gil(|py| { - wrap_pyfunction!(result_conversion_function)(py).unwrap(); + wrap_pyfunction_bound!(result_conversion_function)(py).unwrap(); }); } diff --git a/tests/test_wrap_pyfunction_deduction.rs b/tests/test_wrap_pyfunction_deduction.rs index 52c9adcb6d7..845cf2a39d7 100644 --- a/tests/test_wrap_pyfunction_deduction.rs +++ b/tests/test_wrap_pyfunction_deduction.rs @@ -11,6 +11,7 @@ pub fn add_wrapped(wrapper: &impl Fn(Python<'_>) -> PyResult<&PyCFunction>) { #[test] fn wrap_pyfunction_deduction() { + #[allow(deprecated)] add_wrapped(wrap_pyfunction!(f)); } diff --git a/tests/ui/deprecations.rs b/tests/ui/deprecations.rs index c062cead3ad..1f3cc302c61 100644 --- a/tests/ui/deprecations.rs +++ b/tests/ui/deprecations.rs @@ -72,3 +72,19 @@ fn module_bound_by_value(m: Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(double, &m)?)?; Ok(()) } + +fn test_wrap_pyfunction(py: Python<'_>, m: &Bound<'_, PyModule>) { + // should lint + let _ = wrap_pyfunction!(double, py); + + // should lint but currently does not + let _ = wrap_pyfunction!(double)(py); + + // should not lint + let _ = wrap_pyfunction!(double, m); + let _ = wrap_pyfunction!(double)(m); + let _ = wrap_pyfunction!(double, m.as_gil_ref()); + let _ = wrap_pyfunction!(double)(m.as_gil_ref()); + let _ = wrap_pyfunction_bound!(double, py); + let _ = wrap_pyfunction_bound!(double)(py); +} diff --git a/tests/ui/deprecations.stderr b/tests/ui/deprecations.stderr index edb74f97b74..d4a3e01f99f 100644 --- a/tests/ui/deprecations.stderr +++ b/tests/ui/deprecations.stderr @@ -45,3 +45,11 @@ error: use of deprecated method `pyo3::methods::Extractor::::extract_gil_ref` | 53 | fn module_gil_ref_with_explicit_py_arg(_py: Python<'_>, m: &PyModule) -> PyResult<()> { | ^ + +error: use of deprecated method `pyo3::methods::Extractor::>::is_python`: use `wrap_pyfunction_bound!` instead + --> tests/ui/deprecations.rs:78:13 + | +78 | let _ = wrap_pyfunction!(double, py); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the macro `wrap_pyfunction` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/invalid_result_conversion.rs b/tests/ui/invalid_result_conversion.rs index 4cf3e0bd8fc..373d3cacd9d 100644 --- a/tests/ui/invalid_result_conversion.rs +++ b/tests/ui/invalid_result_conversion.rs @@ -20,11 +20,13 @@ impl fmt::Display for MyError { #[pyfunction] fn should_not_work() -> Result<(), MyError> { - Err(MyError { descr: "something went wrong" }) + Err(MyError { + descr: "something went wrong", + }) } fn main() { - Python::with_gil(|py|{ - wrap_pyfunction!(should_not_work)(py); + Python::with_gil(|py| { + wrap_pyfunction_bound!(should_not_work)(py); }); } From 7cde95bba4f672b4840dcd8543e3da0076bd49ec Mon Sep 17 00:00:00 2001 From: Thomas Tanon Date: Tue, 12 Mar 2024 23:57:31 +0100 Subject: [PATCH 082/936] Documents experimental-declarative-modules feature (#3953) * Documents experimental-declarative-modules feature * More details on experimental-declarative-modules progress --- guide/src/features.md | 6 +++++ guide/src/module.md | 52 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/guide/src/features.md b/guide/src/features.md index 6e4e5ab70b1..0816770a781 100644 --- a/guide/src/features.md +++ b/guide/src/features.md @@ -57,6 +57,12 @@ 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 8cac9a5be4c..c9c7f78aaf5 100644 --- a/guide/src/module.md +++ b/guide/src/module.md @@ -105,3 +105,55 @@ submodules by using `from parent_module import child_module`. For more informati [#1517](https://github.com/PyO3/pyo3/issues/1517#issuecomment-808664021). It is not necessary to add `#[pymodule]` on nested modules, which is only required on the top-level module. + +## Declarative modules (experimental) + +Another syntax based on Rust inline modules is also available to declare modules. +The `experimental-declarative-modules` feature must be enabled to use it. + +For example: +```rust +# #[cfg(feature = "experimental-declarative-modules")] +# mod declarative_module_test { +use pyo3::prelude::*; + +#[pyfunction] +fn double(x: usize) -> usize { + x * 2 +} + +#[pymodule] +mod my_extension { + use super::*; + + #[pymodule_export] + use super::double; // Exports the double function as part of the module + + #[pyfunction] // This will be part of the module + fn triple(x: usize) -> usize { + x * 3 + } + + #[pyclass] // This will be part of the module + struct Unit; + + #[pymodule] + mod submodule { + // This is a submodule + } + + #[pymodule_init] + fn init(m: &Bound<'_, PyModule>) -> PyResult<()> { + // Arbitrary code to run at the module initialization + m.add("double2", m.getattr("double")?)?; + Ok(()) + } +} +# } +``` + +Some changes are planned to this feature before stabilization, like automatically +filling submodules into `sys.modules` to allow easier imports (see [issue #759](https://github.com/PyO3/pyo3/issues/759)) +and filling the `module` argument of inlined `#[pyclass]` automatically with the proper module name. +Macro names might also change. +See [issue #3900](https://github.com/PyO3/pyo3/issues/3900) to track this feature progress. From 989d2c53ab2e2c0cfb36c1ab86e943e7486ef470 Mon Sep 17 00:00:00 2001 From: Bruno Kolenbrander <59372212+mejrs@users.noreply.github.com> Date: Tue, 12 Mar 2024 23:59:33 +0100 Subject: [PATCH 083/936] Fix non_local_definitions lint triggers (#3955) --- pyo3-macros-backend/src/frompyobject.rs | 34 +++++++--------- pyo3-macros-backend/src/module.rs | 35 +++++++--------- pyo3-macros-backend/src/pyclass.rs | 54 ++++++++++--------------- pyo3-macros-backend/src/pyfunction.rs | 14 +++---- pyo3-macros-backend/src/pyimpl.rs | 18 ++++----- src/tests/hygiene/misc.rs | 1 + 6 files changed, 63 insertions(+), 93 deletions(-) diff --git a/pyo3-macros-backend/src/frompyobject.rs b/pyo3-macros-backend/src/frompyobject.rs index 24471c1aae8..68ef72ea157 100644 --- a/pyo3-macros-backend/src/frompyobject.rs +++ b/pyo3-macros-backend/src/frompyobject.rs @@ -310,7 +310,7 @@ impl<'a> Container<'a> { } }); quote!( - match obj.extract() { + 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), } @@ -327,27 +327,29 @@ impl<'a> Container<'a> { let field_name = ident.to_string(); let getter = match field.getter.as_ref().unwrap_or(&FieldGetter::GetAttr(None)) { FieldGetter::GetAttr(Some(name)) => { - quote!(getattr(#pyo3_path::intern!(obj.py(), #name))) + quote!(#pyo3_path::types::PyAnyMethods::getattr(obj, #pyo3_path::intern!(obj.py(), #name))) } FieldGetter::GetAttr(None) => { - quote!(getattr(#pyo3_path::intern!(obj.py(), #field_name))) + quote!(#pyo3_path::types::PyAnyMethods::getattr(obj, #pyo3_path::intern!(obj.py(), #field_name))) } FieldGetter::GetItem(Some(syn::Lit::Str(key))) => { - quote!(get_item(#pyo3_path::intern!(obj.py(), #key))) + quote!(#pyo3_path::types::PyAnyMethods::get_item(obj, #pyo3_path::intern!(obj.py(), #key))) + } + FieldGetter::GetItem(Some(key)) => { + quote!(#pyo3_path::types::PyAnyMethods::get_item(obj, #key)) } - FieldGetter::GetItem(Some(key)) => quote!(get_item(#key)), FieldGetter::GetItem(None) => { - quote!(get_item(#pyo3_path::intern!(obj.py(), #field_name))) + quote!(#pyo3_path::types::PyAnyMethods::get_item(obj, #pyo3_path::intern!(obj.py(), #field_name))) } }; let extractor = match &field.from_py_with { None => { - quote!(#pyo3_path::impl_::frompyobject::extract_struct_field(&obj.#getter?, #struct_name, #field_name)?) + quote!(#pyo3_path::impl_::frompyobject::extract_struct_field(&#getter?, #struct_name, #field_name)?) } Some(FromPyWithAttribute { value: expr_path, .. }) => { - quote! (#pyo3_path::impl_::frompyobject::extract_struct_field_with(#expr_path as fn(_) -> _, &obj.#getter?, #struct_name, #field_name)?) + quote! (#pyo3_path::impl_::frompyobject::extract_struct_field_with(#expr_path as fn(_) -> _, &#getter?, #struct_name, #field_name)?) } }; @@ -609,17 +611,11 @@ pub fn build_derive_from_pyobject(tokens: &DeriveInput) -> Result { let ident = &tokens.ident; Ok(quote!( - // FIXME https://github.com/PyO3/pyo3/issues/3903 - #[allow(unknown_lints, non_local_definitions)] - const _: () = { - use #pyo3_path::prelude::PyAnyMethods; - - #[automatically_derived] - impl #trait_generics #pyo3_path::FromPyObject<#lt_param> for #ident #generics #where_clause { - fn extract_bound(obj: &#pyo3_path::Bound<#lt_param, #pyo3_path::PyAny>) -> #pyo3_path::PyResult { - #derives - } + #[automatically_derived] + impl #trait_generics #pyo3_path::FromPyObject<#lt_param> for #ident #generics #where_clause { + fn extract_bound(obj: &#pyo3_path::Bound<#lt_param, #pyo3_path::PyAny>) -> #pyo3_path::PyResult { + #derives } - }; + } )) } diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index fb02c074996..1cc3e836404 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -295,7 +295,7 @@ pub fn pymodule_function_impl(mut function: syn::ItemFn) -> Result if function.sig.inputs.len() == 2 { module_args.push(quote!(module.py())); } - module_args.push(quote!(::std::convert::Into::into(BoundRef(module)))); + module_args.push(quote!(::std::convert::Into::into(#pyo3_path::methods::BoundRef(module)))); let extractors = function .sig @@ -330,29 +330,22 @@ 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) - // FIXME https://github.com/PyO3/pyo3/issues/3903 - #[allow(unknown_lints, non_local_definitions)] - const _: () = { - use #pyo3_path::impl_::pymodule as impl_; - use #pyo3_path::impl_::pymethods::BoundRef; - - fn __pyo3_pymodule(module: &#pyo3_path::Bound<'_, #pyo3_path::types::PyModule>) -> #pyo3_path::PyResult<()> { - #ident(#(#module_args),*) - } + impl #ident::MakeDef { + const fn make_def() -> #pyo3_path::impl_::pymodule::ModuleDef { + fn __pyo3_pymodule(module: &#pyo3_path::Bound<'_, #pyo3_path::types::PyModule>) -> #pyo3_path::PyResult<()> { + #ident(#(#module_args),*) + } - impl #ident::MakeDef { - const fn make_def() -> impl_::ModuleDef { - const INITIALIZER: impl_::ModuleInitializer = impl_::ModuleInitializer(__pyo3_pymodule); - unsafe { - impl_::ModuleDef::new( - #ident::__PYO3_NAME, - #doc, - INITIALIZER - ) - } + const INITIALIZER: #pyo3_path::impl_::pymodule::ModuleInitializer = #pyo3_path::impl_::pymodule::ModuleInitializer(__pyo3_pymodule); + unsafe { + #pyo3_path::impl_::pymodule::ModuleDef::new( + #ident::__PYO3_NAME, + #doc, + INITIALIZER + ) } } - }; + } }) } diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 3eca80861b7..27a7ee969d5 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -366,15 +366,11 @@ fn impl_class( .impl_all(ctx)?; Ok(quote! { - // FIXME https://github.com/PyO3/pyo3/issues/3903 - #[allow(unknown_lints, non_local_definitions)] - const _: () = { - impl #pyo3_path::types::DerefToPyAny for #cls {} + impl #pyo3_path::types::DerefToPyAny for #cls {} - #pytypeinfo_impl + #pytypeinfo_impl - #py_class_impl - }; + #py_class_impl }) } @@ -794,21 +790,17 @@ fn impl_simple_enum( .impl_all(ctx)?; Ok(quote! { - // FIXME https://github.com/PyO3/pyo3/issues/3903 - #[allow(unknown_lints, non_local_definitions)] - const _: () = { - #pytypeinfo + #pytypeinfo - #pyclass_impls + #pyclass_impls - #[doc(hidden)] - #[allow(non_snake_case)] - impl #cls { - #default_repr - #default_int - #default_richcmp - } - }; + #[doc(hidden)] + #[allow(non_snake_case)] + impl #cls { + #default_repr + #default_int + #default_richcmp + } }) } @@ -933,25 +925,21 @@ fn impl_complex_enum( } Ok(quote! { - // FIXME https://github.com/PyO3/pyo3/issues/3903 - #[allow(unknown_lints, non_local_definitions)] - const _: () = { - #pytypeinfo + #pytypeinfo - #pyclass_impls + #pyclass_impls - #[doc(hidden)] - #[allow(non_snake_case)] - impl #cls {} + #[doc(hidden)] + #[allow(non_snake_case)] + impl #cls {} - #(#variant_cls_zsts)* + #(#variant_cls_zsts)* - #(#variant_cls_pytypeinfos)* + #(#variant_cls_pytypeinfos)* - #(#variant_cls_pyclass_impls)* + #(#variant_cls_pyclass_impls)* - #(#variant_cls_impls)* - }; + #(#variant_cls_impls)* }) } diff --git a/pyo3-macros-backend/src/pyfunction.rs b/pyo3-macros-backend/src/pyfunction.rs index 4b1e0eadeba..cce9f74824c 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -282,16 +282,12 @@ 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) - // FIXME https://github.com/PyO3/pyo3/issues/3903 - #[allow(unknown_lints, non_local_definitions)] - const _: () = { - impl #name::MakeDef { - const DEF: #pyo3_path::impl_::pymethods::PyMethodDef = #methoddef; - } + impl #name::MakeDef { + const DEF: #pyo3_path::impl_::pymethods::PyMethodDef = #methoddef; + } - #[allow(non_snake_case)] - #wrapper - }; + #[allow(non_snake_case)] + #wrapper }; Ok(wrapped_pyfunction) } diff --git a/pyo3-macros-backend/src/pyimpl.rs b/pyo3-macros-backend/src/pyimpl.rs index 30a6d6dd17e..cf27cf37066 100644 --- a/pyo3-macros-backend/src/pyimpl.rs +++ b/pyo3-macros-backend/src/pyimpl.rs @@ -170,19 +170,15 @@ pub fn impl_methods( }; Ok(quote! { - // FIXME https://github.com/PyO3/pyo3/issues/3903 - #[allow(unknown_lints, non_local_definitions)] - const _: () = { - #(#trait_impls)* + #(#trait_impls)* - #items + #items - #[doc(hidden)] - #[allow(non_snake_case)] - impl #ty { - #(#associated_methods)* - } - }; + #[doc(hidden)] + #[allow(non_snake_case)] + impl #ty { + #(#associated_methods)* + } }) } diff --git a/src/tests/hygiene/misc.rs b/src/tests/hygiene/misc.rs index 66db7f3a28a..24dad7ec196 100644 --- a/src/tests/hygiene/misc.rs +++ b/src/tests/hygiene/misc.rs @@ -14,6 +14,7 @@ struct Derive2(i32, i32); // tuple case #[allow(dead_code)] struct Derive3 { f: i32, + #[pyo3(item(42))] g: i32, } // struct case From 5c86dc35c1108591e553ff3829bca88678d3ccf8 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Fri, 15 Mar 2024 08:53:52 +0100 Subject: [PATCH 084/936] allow borrowed extracts with `gil-refs` disabled (#3959) --- src/conversion.rs | 10 ++++++---- src/conversions/std/slice.rs | 13 ++++++------- src/conversions/std/string.rs | 4 ++-- src/err/mod.rs | 23 ++++++++++++++++------- src/instance.rs | 14 ++++++++++++++ src/types/any.rs | 14 ++++++++------ src/types/bytes.rs | 2 +- src/types/string.rs | 2 +- 8 files changed, 54 insertions(+), 28 deletions(-) diff --git a/src/conversion.rs b/src/conversion.rs index 8d4ad776837..dfa53eac83e 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -6,7 +6,9 @@ use crate::pyclass::boolean_struct::False; use crate::type_object::PyTypeInfo; use crate::types::any::PyAnyMethods; use crate::types::PyTuple; -use crate::{ffi, gil, Bound, Py, PyAny, PyClass, PyNativeType, PyObject, PyRef, PyRefMut, Python}; +use crate::{ + ffi, gil, Borrowed, Bound, Py, PyAny, PyClass, PyNativeType, PyObject, PyRef, PyRefMut, Python, +}; use std::ptr::NonNull; /// Returns a borrowed pointer to a Python object. @@ -288,7 +290,7 @@ pub trait FromPyObjectBound<'a, 'py>: Sized + from_py_object_bound_sealed::Seale /// /// Users are advised against calling this method directly: instead, use this via /// [`Bound<'_, PyAny>::extract`] or [`Py::extract`]. - fn from_py_object_bound(ob: &'a Bound<'py, PyAny>) -> PyResult; + fn from_py_object_bound(ob: Borrowed<'a, 'py, PyAny>) -> PyResult; /// Extracts the type hint information for this type when it appears as an argument. /// @@ -307,8 +309,8 @@ impl<'py, T> FromPyObjectBound<'_, 'py> for T where T: FromPyObject<'py>, { - fn from_py_object_bound(ob: &Bound<'py, PyAny>) -> PyResult { - Self::extract_bound(ob) + fn from_py_object_bound(ob: Borrowed<'_, 'py, PyAny>) -> PyResult { + Self::extract_bound(&ob) } #[cfg(feature = "experimental-inspect")] diff --git a/src/conversions/std/slice.rs b/src/conversions/std/slice.rs index 18bb52cdd1a..b3932302ef3 100644 --- a/src/conversions/std/slice.rs +++ b/src/conversions/std/slice.rs @@ -2,11 +2,9 @@ use std::borrow::Cow; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; -#[cfg(not(feature = "gil-refs"))] -use crate::types::PyBytesMethods; use crate::{ - types::{PyAnyMethods, PyByteArray, PyByteArrayMethods, PyBytes}, - Bound, IntoPy, Py, PyAny, PyObject, PyResult, Python, ToPyObject, + types::{PyByteArray, PyByteArrayMethods, PyBytes}, + IntoPy, Py, PyAny, PyObject, PyResult, Python, ToPyObject, }; impl<'a> IntoPy for &'a [u8] { @@ -34,7 +32,7 @@ impl<'py> crate::FromPyObject<'py> for &'py [u8] { #[cfg(not(feature = "gil-refs"))] impl<'a> crate::conversion::FromPyObjectBound<'a, '_> for &'a [u8] { - fn from_py_object_bound(obj: &'a Bound<'_, PyAny>) -> PyResult { + fn from_py_object_bound(obj: crate::Borrowed<'a, '_, PyAny>) -> PyResult { Ok(obj.downcast::()?.as_bytes()) } @@ -51,7 +49,8 @@ impl<'a> crate::conversion::FromPyObjectBound<'a, '_> for &'a [u8] { /// 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: &Bound<'py, PyAny>) -> PyResult { + 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())); } @@ -63,7 +62,7 @@ impl<'py> crate::FromPyObject<'py> for Cow<'py, [u8]> { #[cfg(not(feature = "gil-refs"))] impl<'a> crate::conversion::FromPyObjectBound<'a, '_> for Cow<'a, [u8]> { - fn from_py_object_bound(ob: &'a Bound<'_, PyAny>) -> PyResult { + fn from_py_object_bound(ob: crate::Borrowed<'a, '_, PyAny>) -> PyResult { if let Ok(bytes) = ob.downcast::() { return Ok(Cow::Borrowed(bytes.as_bytes())); } diff --git a/src/conversions/std/string.rs b/src/conversions/std/string.rs index d013890a77e..9c276d1d3d9 100644 --- a/src/conversions/std/string.rs +++ b/src/conversions/std/string.rs @@ -128,7 +128,7 @@ impl<'py> FromPyObject<'py> for &'py str { #[cfg(all(not(feature = "gil-refs"), any(Py_3_10, not(Py_LIMITED_API))))] impl<'a> crate::conversion::FromPyObjectBound<'a, '_> for &'a str { - fn from_py_object_bound(ob: &'a Bound<'_, PyAny>) -> PyResult { + fn from_py_object_bound(ob: crate::Borrowed<'a, '_, PyAny>) -> PyResult { ob.downcast::()?.to_str() } @@ -152,7 +152,7 @@ impl<'py> FromPyObject<'py> for Cow<'py, str> { #[cfg(not(feature = "gil-refs"))] impl<'a> crate::conversion::FromPyObjectBound<'a, '_> for Cow<'a, str> { - fn from_py_object_bound(ob: &'a Bound<'_, PyAny>) -> PyResult { + 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 7610f49951c..de1e621fc13 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -7,7 +7,7 @@ use crate::{ exceptions::{self, PyBaseException}, ffi, }; -use crate::{IntoPy, Py, PyAny, PyNativeType, PyObject, Python, ToPyObject}; +use crate::{Borrowed, IntoPy, Py, PyAny, PyNativeType, PyObject, Python, ToPyObject}; use std::borrow::Cow; use std::cell::UnsafeCell; use std::ffi::CString; @@ -65,17 +65,16 @@ impl<'a> PyDowncastError<'a> { /// 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 { - Self { - from: from.as_gil_ref(), - to, - } + #[allow(deprecated)] + let from = unsafe { from.py().from_borrowed_ptr(from.as_ptr()) }; + Self { from, to } } } /// Error that indicates a failure to convert a PyAny to a more specific Python type. #[derive(Debug)] pub struct DowncastError<'a, 'py> { - from: &'a Bound<'py, PyAny>, + from: Borrowed<'a, 'py, PyAny>, to: Cow<'static, str>, } @@ -83,6 +82,16 @@ impl<'a, 'py> DowncastError<'a, 'py> { /// Create a new `PyDowncastError` representing a failure to convert the object /// `from` into the type named in `to`. pub fn new(from: &'a Bound<'py, PyAny>, to: impl Into>) -> Self { + DowncastError { + from: from.as_borrowed(), + to: to.into(), + } + } + #[cfg(not(feature = "gil-refs"))] + pub(crate) fn new_from_borrowed( + from: Borrowed<'a, 'py, PyAny>, + to: impl Into>, + ) -> Self { DowncastError { from, to: to.into(), @@ -1036,7 +1045,7 @@ impl std::error::Error for DowncastError<'_, '_> {} impl std::fmt::Display for DowncastError<'_, '_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { - display_downcast_error(f, self.from, &self.to) + display_downcast_error(f, &self.from, &self.to) } } diff --git a/src/instance.rs b/src/instance.rs index 069bea69f8c..83775708e1a 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -579,6 +579,20 @@ impl<'a, 'py> Borrowed<'a, 'py, PyAny> { Self(NonNull::new_unchecked(ptr), PhantomData, py) } + #[inline] + #[cfg(not(feature = "gil-refs"))] + pub(crate) fn downcast(self) -> Result, DowncastError<'a, 'py>> + where + T: PyTypeCheck, + { + if T::type_check(&self) { + // Safety: type_check is responsible for ensuring that the type is correct + Ok(unsafe { self.downcast_unchecked() }) + } else { + Err(DowncastError::new_from_borrowed(self, T::NAME)) + } + } + /// Converts this `PyAny` to a concrete Python type without checking validity. /// /// # Safety diff --git a/src/types/any.rs b/src/types/any.rs index e4dbaf80200..19855abbb9a 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -1,5 +1,5 @@ use crate::class::basic::CompareOp; -use crate::conversion::{AsPyPointer, FromPyObject, FromPyObjectBound, IntoPy, ToPyObject}; +use crate::conversion::{AsPyPointer, FromPyObjectBound, IntoPy, ToPyObject}; use crate::err::{DowncastError, DowncastIntoError, PyDowncastError, PyErr, PyResult}; use crate::exceptions::{PyAttributeError, PyTypeError}; use crate::ffi_ptr_ext::FfiPtrExt; @@ -799,13 +799,14 @@ impl PyAny { /// Extracts some type from the Python object. /// - /// This is a wrapper function around [`FromPyObject::extract()`]. + /// This is a wrapper function around + /// [`FromPyObject::extract()`](crate::FromPyObject::extract). #[inline] pub fn extract<'py, D>(&'py self) -> PyResult where - D: FromPyObject<'py>, + D: FromPyObjectBound<'py, 'py>, { - FromPyObject::extract(self) + FromPyObjectBound::from_py_object_bound(self.as_borrowed()) } /// Returns the reference count for the Python object. @@ -1641,7 +1642,8 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// Extracts some type from the Python object. /// - /// This is a wrapper function around [`FromPyObject::extract()`]. + /// This is a wrapper function around + /// [`FromPyObject::extract()`](crate::FromPyObject::extract). fn extract<'a, T>(&'a self) -> PyResult where T: FromPyObjectBound<'a, 'py>; @@ -2182,7 +2184,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { where T: FromPyObjectBound<'a, 'py>, { - FromPyObjectBound::from_py_object_bound(self) + FromPyObjectBound::from_py_object_bound(self.as_borrowed()) } fn get_refcnt(&self) -> isize { diff --git a/src/types/bytes.rs b/src/types/bytes.rs index 55c295c416f..44c87560514 100644 --- a/src/types/bytes.rs +++ b/src/types/bytes.rs @@ -158,7 +158,7 @@ impl<'py> PyBytesMethods<'py> for Bound<'py, PyBytes> { impl<'a> Borrowed<'a, '_, PyBytes> { /// Gets the Python string as a byte slice. #[allow(clippy::wrong_self_convention)] - fn as_bytes(self) -> &'a [u8] { + pub(crate) fn as_bytes(self) -> &'a [u8] { unsafe { let buffer = ffi::PyBytes_AsString(self.as_ptr()) as *const u8; let length = ffi::PyBytes_Size(self.as_ptr()) as usize; diff --git a/src/types/string.rs b/src/types/string.rs index 93f3682c3a4..81c41adb545 100644 --- a/src/types/string.rs +++ b/src/types/string.rs @@ -369,7 +369,7 @@ impl<'a> Borrowed<'a, '_, PyString> { } #[allow(clippy::wrong_self_convention)] - fn to_cow(self) -> PyResult> { + pub(crate) fn to_cow(self) -> PyResult> { // TODO: this method can probably be deprecated once Python 3.9 support is dropped, // because all versions then support the more efficient `to_str`. #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] From dcba984b51be253cb5d385e72008ee9578807504 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 15 Mar 2024 10:25:27 +0000 Subject: [PATCH 085/936] deprecate `GILPool` (#3947) * deprecate `GILPool` * review: adamreichold * fix deprecation warnings in tests --- guide/src/memory.md | 14 +++++++++++++- guide/src/migration.md | 2 ++ newsfragments/3947.changed.md | 1 + src/gil.rs | 30 +++++++++++++++++++++--------- src/impl_/trampoline.rs | 8 +++++++- src/lib.rs | 1 + src/marker.rs | 32 ++++++++++++++++++++++++++------ tests/ui/deprecations.rs | 1 + tests/ui/deprecations.stderr | 32 ++++++++++++++++---------------- 9 files changed, 88 insertions(+), 33 deletions(-) create mode 100644 newsfragments/3947.changed.md diff --git a/guide/src/memory.md b/guide/src/memory.md index fe98184e3e1..78f9f348f40 100644 --- a/guide/src/memory.md +++ b/guide/src/memory.md @@ -83,6 +83,13 @@ 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. @@ -114,6 +121,7 @@ this is unsafe. # fn main() -> PyResult<()> { 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 @@ -146,7 +154,11 @@ function call, releasing objects when the function returns. Most functions only a few objects, meaning this doesn't have a significant impact. Occasionally functions with long complex loops may need to use `Python::new_pool` as shown above. -This behavior may change in future, see [issue #1056](https://github.com/PyO3/pyo3/issues/1056). +
+⚠️ Warning: `GILPool` is no longer the preferred way to manage memory with PyO3 🛠️ + +PyO3 0.21 has introduced a new API known as the Bound API, which doesn't have the same surprising results. Instead, each `Bound` smart pointer releases the Python reference immediately on drop. See [the smart pointer types](./types.md#pyo3s-smart-pointers) for more details. +
## GIL-independent memory diff --git a/guide/src/migration.md b/guide/src/migration.md index 1c5ca59714a..8ea3f0730c0 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -280,6 +280,8 @@ The expectation is that in 0.22 `extract_bound` will have the default implementa As a final step of migration, deactivating the `gil-refs` feature will set up code for best performance and is intended to set up a forward-compatible API for PyO3 0.22. +At this point code which needed to manage GIL Ref memory can safely remove uses of `GILPool` (which are constructed by calls to `Python::new_pool` and `Python::with_pool`). Deprecation warnings will highlight these cases. + There is one notable API removed when this feature is disabled. `FromPyObject` trait implementations for types which borrow directly from the input data cannot be implemented by PyO3 without GIL Refs (while the migration is ongoing). These types are `&str`, `Cow<'_, str>`, `&[u8]`, `Cow<'_, u8>`. To ease pain during migration, these types instead implement a new temporary trait `FromPyObjectBound` which is the expected future form of `FromPyObject`. The new temporary trait ensures is that `obj.extract::<&str>()` continues to work (with the new constraint that the extracted value now depends on the input `obj` lifetime), as well for these types in `#[pyfunction]` arguments. diff --git a/newsfragments/3947.changed.md b/newsfragments/3947.changed.md new file mode 100644 index 00000000000..4618e9378fb --- /dev/null +++ b/newsfragments/3947.changed.md @@ -0,0 +1 @@ +Deprecate `GILPool`, `Python::with_pool`, and `Python::new_pool`. diff --git a/src/gil.rs b/src/gil.rs index 91c3d1cdd27..5ca3167e66c 100644 --- a/src/gil.rs +++ b/src/gil.rs @@ -139,6 +139,7 @@ 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" @@ -160,6 +161,7 @@ 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, } @@ -222,6 +224,7 @@ impl GILGuard { } let gstate = unsafe { ffi::PyGILState_Ensure() }; // acquire GIL + #[allow(deprecated)] let pool = unsafe { mem::ManuallyDrop::new(GILPool::new()) }; Some(GILGuard { gstate, pool }) @@ -358,6 +361,13 @@ 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" + ) +)] pub struct GILPool { /// Initial length of owned objects and anys. /// `Option` is used since TSL can be broken when `new` is called from `atexit`. @@ -365,6 +375,7 @@ pub struct GILPool { _not_send: NotSend, } +#[allow(deprecated)] impl GILPool { /// Creates a new [`GILPool`]. This function should only ever be called with the GIL held. /// @@ -401,6 +412,7 @@ impl GILPool { } } +#[allow(deprecated)] impl Drop for GILPool { fn drop(&mut self) { if let Some(start) = self.start { @@ -506,21 +518,17 @@ fn decrement_gil_count() { #[cfg(test)] mod tests { - use super::{gil_is_acquired, GILPool, GIL_COUNT, OWNED_OBJECTS, POOL}; + #[allow(deprecated)] + use super::GILPool; + use super::{gil_is_acquired, GIL_COUNT, OWNED_OBJECTS, POOL}; use crate::types::any::PyAnyMethods; - use crate::{ffi, gil, PyObject, Python, ToPyObject}; + use crate::{ffi, gil, PyObject, Python}; #[cfg(not(target_arch = "wasm32"))] use parking_lot::{const_mutex, Condvar, Mutex}; use std::ptr::NonNull; fn get_object(py: Python<'_>) -> PyObject { - // Convenience function for getting a single unique object, using `new_pool` so as to leave - // the original pool state unchanged. - let pool = unsafe { py.new_pool() }; - let py = pool.python(); - - let obj = py.eval_bound("object()", None, None).unwrap(); - obj.to_object(py) + py.eval_bound("object()", None, None).unwrap().unbind() } fn owned_object_count() -> usize { @@ -556,6 +564,7 @@ mod tests { } #[test] + #[allow(deprecated)] fn test_owned() { Python::with_gil(|py| { let obj = get_object(py); @@ -581,6 +590,7 @@ mod tests { } #[test] + #[allow(deprecated)] fn test_owned_nested() { Python::with_gil(|py| { let obj = get_object(py); @@ -666,6 +676,7 @@ mod tests { } #[test] + #[allow(deprecated)] fn test_gil_counts() { // Check with_gil and GILPool both increase counts correctly let get_gil_count = || GIL_COUNT.with(|c| c.get()); @@ -906,6 +917,7 @@ mod tests { unsafe extern "C" fn capsule_drop(capsule: *mut ffi::PyObject) { // This line will implicitly call update_counts // -> and so cause deadlock if update_counts is not handling recursion correctly. + #[allow(deprecated)] let pool = GILPool::new(); // Rebuild obj so that it can be dropped diff --git a/src/impl_/trampoline.rs b/src/impl_/trampoline.rs index 4b4eac17a15..4d77f329125 100644 --- a/src/impl_/trampoline.rs +++ b/src/impl_/trampoline.rs @@ -9,9 +9,11 @@ use std::{ panic::{self, UnwindSafe}, }; +#[allow(deprecated)] +use crate::gil::GILPool; use crate::{ callback::PyCallbackOutput, ffi, ffi_ptr_ext::FfiPtrExt, impl_::panic::PanicTrap, - methods::IPowModulo, panic::PanicException, types::PyModule, GILPool, Py, PyResult, Python, + methods::IPowModulo, panic::PanicException, types::PyModule, Py, PyResult, Python, }; #[inline] @@ -174,6 +176,8 @@ where 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(); let out = panic_result_into_callback_output( @@ -219,6 +223,8 @@ 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(); if let Err(py_err) = panic::catch_unwind(move || body(py)) diff --git a/src/lib.rs b/src/lib.rs index a7c03d1b6cb..dab48fbe01b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -319,6 +319,7 @@ pub use crate::conversion::{FromPyPointer, PyTryFrom, PyTryInto}; pub use crate::err::{ DowncastError, DowncastIntoError, PyDowncastError, PyErr, PyErrArguments, PyResult, ToPyErr, }; +#[allow(deprecated)] pub use crate::gil::GILPool; #[cfg(not(PyPy))] pub use crate::gil::{prepare_freethreaded_python, with_embedded_python_interpreter}; diff --git a/src/marker.rs b/src/marker.rs index 2a38b83cba5..08b9042491b 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -118,7 +118,7 @@ //! [`Py`]: crate::Py use crate::err::{self, PyDowncastError, PyErr, PyResult}; use crate::ffi_ptr_ext::FfiPtrExt; -use crate::gil::{GILGuard, GILPool, SuspendGIL}; +use crate::gil::{GILGuard, SuspendGIL}; use crate::impl_::not_send::NotSend; use crate::py_result_ext::PyResultExt; use crate::type_object::HasPyGilRef; @@ -127,9 +127,9 @@ use crate::types::{ PyAny, PyDict, PyEllipsis, PyModule, PyNone, PyNotImplemented, PyString, PyType, }; use crate::version::PythonVersionInfo; -#[allow(deprecated)] -use crate::FromPyPointer; use crate::{ffi, Bound, IntoPy, Py, PyNativeType, PyObject, PyTypeCheck, PyTypeInfo}; +#[allow(deprecated)] +use crate::{gil::GILPool, FromPyPointer}; use std::ffi::{CStr, CString}; use std::marker::PhantomData; use std::os::raw::c_int; @@ -1053,9 +1053,10 @@ impl<'py> Python<'py> { err::error_on_minusone(self, unsafe { ffi::PyErr_CheckSignals() }) } - /// Create a new pool for managing PyO3's owned references. + /// Create a new pool for managing PyO3's GIL Refs. This has no functional + /// use for code which does not use the deprecated GIL Refs API. /// - /// When this `GILPool` is dropped, all PyO3 owned references created after this `GILPool` will + /// When this `GILPool` is dropped, all GIL Refs created after this `GILPool` will /// all have their Python reference counts decremented, potentially allowing Python to drop /// the corresponding Python objects. /// @@ -1074,6 +1075,7 @@ impl<'py> Python<'py> { /// // Some long-running process like a webserver, which never releases the GIL. /// loop { /// // Create a new pool, so that PyO3 can clear memory at the end of the loop. + /// #[allow(deprecated)] // `new_pool` is not needed in code not using the GIL Refs API /// let pool = unsafe { py.new_pool() }; /// /// // It is recommended to *always* immediately set py to the pool's Python, to help @@ -1108,13 +1110,22 @@ 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`" + ) + )] + #[allow(deprecated)] pub unsafe fn new_pool(self) -> GILPool { GILPool::new() } } impl Python<'_> { - /// Creates a scope using a new pool for managing PyO3's owned references. + /// 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 @@ -1131,6 +1142,7 @@ impl Python<'_> { /// // 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... /// }); @@ -1167,6 +1179,14 @@ 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`" + ) + )] + #[allow(deprecated)] pub fn with_pool(&self, f: F) -> R where F: for<'py> FnOnce(Python<'py>) -> R + Ungil, diff --git a/tests/ui/deprecations.rs b/tests/ui/deprecations.rs index 1f3cc302c61..d82c406f9c6 100644 --- a/tests/ui/deprecations.rs +++ b/tests/ui/deprecations.rs @@ -1,4 +1,5 @@ #![deny(deprecated)] +#![allow(dead_code)] use pyo3::prelude::*; use pyo3::types::{PyString, PyType}; diff --git a/tests/ui/deprecations.stderr b/tests/ui/deprecations.stderr index d4a3e01f99f..e54e1a4e266 100644 --- a/tests/ui/deprecations.stderr +++ b/tests/ui/deprecations.stderr @@ -1,7 +1,7 @@ error: use of deprecated constant `pyo3::impl_::deprecations::PYMETHODS_NEW_DEPRECATED_FORM`: use `#[new]` instead of `#[__new__]` - --> tests/ui/deprecations.rs:11:7 + --> tests/ui/deprecations.rs:12:7 | -11 | #[__new__] +12 | #[__new__] | ^^^^^^^ | note: the lint level is defined here @@ -11,45 +11,45 @@ note: the lint level is defined here | ^^^^^^^^^^ 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:22:30 + --> tests/ui/deprecations.rs:23:30 | -22 | fn method_gil_ref(_slf: &PyCell) {} +23 | fn method_gil_ref(_slf: &PyCell) {} | ^^^^^^ error: use of deprecated method `pyo3::methods::Extractor::::extract_gil_ref`: use `&Bound<'_, T>` instead for this function argument - --> tests/ui/deprecations.rs:17:33 + --> tests/ui/deprecations.rs:18:33 | -17 | fn cls_method_gil_ref(_cls: &PyType) {} +18 | fn cls_method_gil_ref(_cls: &PyType) {} | ^ error: use of deprecated method `pyo3::methods::Extractor::::extract_gil_ref`: use `&Bound<'_, T>` instead for this function argument - --> tests/ui/deprecations.rs:22:29 + --> tests/ui/deprecations.rs:23:29 | -22 | fn method_gil_ref(_slf: &PyCell) {} +23 | fn method_gil_ref(_slf: &PyCell) {} | ^ error: use of deprecated method `pyo3::methods::Extractor::::extract_gil_ref`: use `&Bound<'_, T>` instead for this function argument - --> tests/ui/deprecations.rs:37:43 + --> tests/ui/deprecations.rs:38:43 | -37 | fn pyfunction_with_module_gil_ref(module: &PyModule) -> PyResult<&str> { +38 | fn pyfunction_with_module_gil_ref(module: &PyModule) -> PyResult<&str> { | ^ error: use of deprecated method `pyo3::methods::Extractor::::extract_gil_ref`: use `&Bound<'_, T>` instead for this function argument - --> tests/ui/deprecations.rs:47:19 + --> tests/ui/deprecations.rs:48:19 | -47 | fn module_gil_ref(m: &PyModule) -> PyResult<()> { +48 | fn module_gil_ref(m: &PyModule) -> PyResult<()> { | ^ error: use of deprecated method `pyo3::methods::Extractor::::extract_gil_ref`: use `&Bound<'_, T>` instead for this function argument - --> tests/ui/deprecations.rs:53:57 + --> tests/ui/deprecations.rs:54:57 | -53 | fn module_gil_ref_with_explicit_py_arg(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +54 | fn module_gil_ref_with_explicit_py_arg(_py: Python<'_>, m: &PyModule) -> PyResult<()> { | ^ error: use of deprecated method `pyo3::methods::Extractor::>::is_python`: use `wrap_pyfunction_bound!` instead - --> tests/ui/deprecations.rs:78:13 + --> tests/ui/deprecations.rs:79:13 | -78 | let _ = wrap_pyfunction!(double, py); +79 | 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 da24f0cf937c833763dd94c341a0d4188def40dd Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sun, 17 Mar 2024 10:17:09 +0100 Subject: [PATCH 086/936] exposes `Borrowed::to_owned` as public API (#3963) * exposes `Borrowed::to_owned` as public API * add newsfragment --- newsfragments/3963.added.md | 1 + src/instance.rs | 27 +++++++++++++++++++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 newsfragments/3963.added.md diff --git a/newsfragments/3963.added.md b/newsfragments/3963.added.md new file mode 100644 index 00000000000..86da17acc89 --- /dev/null +++ b/newsfragments/3963.added.md @@ -0,0 +1 @@ +added `Borrowed::to_owned` \ No newline at end of file diff --git a/src/instance.rs b/src/instance.rs index 83775708e1a..024a9fe5b51 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -511,8 +511,31 @@ unsafe impl AsPyPointer for Bound<'_, T> { pub struct Borrowed<'a, 'py, T>(NonNull, PhantomData<&'a Py>, Python<'py>); impl<'py, T> Borrowed<'_, 'py, T> { - /// Creates a new owned `Bound` from this borrowed reference by increasing the reference count. - pub(crate) fn to_owned(self) -> Bound<'py, T> { + /// Creates a new owned [`Bound`] from this borrowed reference by + /// increasing the reference count. + /// + /// # Example + /// ``` + /// use pyo3::{prelude::*, types::PyTuple}; + /// + /// # fn main() -> PyResult<()> { + /// Python::with_gil(|py| -> PyResult<()> { + /// let tuple = PyTuple::new_bound(py, [1, 2, 3]); + /// + /// // borrows from `tuple`, so can only be + /// // used while `tuple` stays alive + /// let borrowed = tuple.get_borrowed_item(0)?; + /// + /// // creates a new owned reference, which + /// // can be used indendently of `tuple` + /// let bound = borrowed.to_owned(); + /// drop(tuple); + /// + /// assert_eq!(bound.extract::().unwrap(), 1); + /// Ok(()) + /// }) + /// # } + pub fn to_owned(self) -> Bound<'py, T> { (*self).clone() } } From ebeea943fea21b93e9ab89e0ea8d009bb5f704dc Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Tue, 19 Mar 2024 08:14:42 +0000 Subject: [PATCH 087/936] ci: fixes to actions caches (#3970) --- .github/workflows/cache-cleanup.yml | 4 ++-- .github/workflows/ci.yml | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cache-cleanup.yml b/.github/workflows/cache-cleanup.yml index 2b81a69ce88..02e8a6ab3cb 100644 --- a/.github/workflows/cache-cleanup.yml +++ b/.github/workflows/cache-cleanup.yml @@ -1,6 +1,6 @@ name: CI Cache Cleanup on: - pull_request: + pull_request_target: types: - closed @@ -22,7 +22,7 @@ jobs: echo "Deleting caches..." for cacheKey in $cacheKeysForPR do - gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm + gh actions-cache delete -R $REPO -B $BRANCH --confirm -- $cacheKey done echo "Done" env: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5f4347aa6f1..148938fe016 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -526,6 +526,7 @@ jobs: workspaces: examples/maturin-starter save-if: ${{ github.event_name != 'merge_group' }} + key: ${{ matrix.target }} - name: Setup cross-compiler if: ${{ matrix.target == 'x86_64-pc-windows-gnu' }} run: sudo apt-get install -y mingw-w64 llvm From b06e95727b1bda03a5352f162bb90fa483cc1a8b Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Tue, 19 Mar 2024 09:58:41 +0100 Subject: [PATCH 088/936] deprecate gil-refs in `from_py_with` (#3967) * deprecate gil-refs in `from_py_with` * review feedback davidhewitt --- pyo3-macros-backend/src/params.rs | 39 +++++++++++++++++++++++++++---- src/impl_/pymethods.rs | 13 +++++++++++ tests/test_methods.rs | 2 +- tests/test_pyfunction.rs | 10 ++++---- tests/ui/deprecations.rs | 15 ++++++++++++ tests/ui/deprecations.stderr | 10 ++++++-- 6 files changed, 77 insertions(+), 12 deletions(-) diff --git a/pyo3-macros-backend/src/params.rs b/pyo3-macros-backend/src/params.rs index 7260362fa43..74b3eba411f 100644 --- a/pyo3-macros-backend/src/params.rs +++ b/pyo3-macros-backend/src/params.rs @@ -36,6 +36,23 @@ pub fn impl_arg_params( let args_array = syn::Ident::new("output", Span::call_site()); let Ctx { pyo3_path } = ctx; + let from_py_with = spec + .signature + .arguments + .iter() + .enumerate() + .filter_map(|(i, arg)| { + let from_py_with = &arg.attrs.from_py_with.as_ref()?.value; + let from_py_with_holder = + syn::Ident::new(&format!("from_py_with_{}", i), Span::call_site()); + Some(quote_spanned! { from_py_with.span() => + let e = #pyo3_path::impl_::pymethods::Extractor::new(); + let #from_py_with_holder = #pyo3_path::impl_::pymethods::inspect_fn(#from_py_with, &e); + e.extract_from_py_with(); + }) + }) + .collect::(); + if !fastcall && is_forwarded_args(&spec.signature) { // In the varargs convention, we can just pass though if the signature // is (*args, **kwds). @@ -43,12 +60,14 @@ pub fn impl_arg_params( .signature .arguments .iter() - .map(|arg| impl_arg_param(arg, &mut 0, &args_array, holders, ctx)) + .enumerate() + .map(|(i, arg)| impl_arg_param(arg, i, &mut 0, &args_array, holders, ctx)) .collect::>()?; return Ok(( 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, )); @@ -81,7 +100,8 @@ pub fn impl_arg_params( .signature .arguments .iter() - .map(|arg| impl_arg_param(arg, &mut option_pos, &args_array, holders, ctx)) + .enumerate() + .map(|(i, arg)| impl_arg_param(arg, i, &mut option_pos, &args_array, holders, ctx)) .collect::>()?; let args_handler = if spec.signature.python_signature.varargs.is_some() { @@ -136,6 +156,7 @@ pub fn impl_arg_params( }; let mut #args_array = [::std::option::Option::None; #num_params]; let (_args, _kwargs) = #extract_expression; + #from_py_with }, param_conversion, )) @@ -145,6 +166,7 @@ pub fn impl_arg_params( /// index and the index in option diverge when using py: Python fn impl_arg_param( arg: &FnArg<'_>, + pos: usize, option_pos: &mut usize, args_array: &syn::Ident, holders: &mut Vec, @@ -222,14 +244,21 @@ fn impl_arg_param( )); } - let tokens = if let Some(expr_path) = arg.attrs.from_py_with.as_ref().map(|attr| &attr.value) { + let tokens = if arg + .attrs + .from_py_with + .as_ref() + .map(|attr| &attr.value) + .is_some() + { + let from_py_with = syn::Ident::new(&format!("from_py_with_{}", pos), Span::call_site()); if let Some(default) = default { quote_arg_span! { #[allow(clippy::redundant_closure)] #pyo3_path::impl_::extract_argument::from_py_with_with_default( #arg_value.as_deref(), #name_str, - #expr_path as fn(_) -> _, + #from_py_with as fn(_) -> _, || #default )? } @@ -238,7 +267,7 @@ fn impl_arg_param( #pyo3_path::impl_::extract_argument::from_py_with( &#pyo3_path::impl_::extract_argument::unwrap_required_argument(#arg_value), #name_str, - #expr_path as fn(_) -> _, + #from_py_with as fn(_) -> _, )? } } diff --git a/src/impl_/pymethods.rs b/src/impl_/pymethods.rs index bc1125f97fd..ca3f0a06bf3 100644 --- a/src/impl_/pymethods.rs +++ b/src/impl_/pymethods.rs @@ -585,6 +585,10 @@ pub fn inspect_type(t: T) -> (T, Extractor) { (t, Extractor::new()) } +pub fn inspect_fn(f: fn(A) -> PyResult, _: &Extractor) -> fn(A) -> PyResult { + f +} + pub struct Extractor(NotAGilRef); pub struct NotAGilRef(std::marker::PhantomData); @@ -616,10 +620,19 @@ impl Extractor { ) )] pub fn extract_gil_ref(&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" + ) + )] + pub fn extract_from_py_with(&self) {} } impl NotAGilRef { pub fn extract_gil_ref(&self) {} + pub fn extract_from_py_with(&self) {} pub fn is_python(&self) {} } diff --git a/tests/test_methods.rs b/tests/test_methods.rs index e7eacf26b40..7ca06b27c05 100644 --- a/tests/test_methods.rs +++ b/tests/test_methods.rs @@ -1146,7 +1146,7 @@ fn test_issue_2988() { _data: Vec, // The from_py_with here looks a little odd, we just need some way // to encourage the macro to expand the from_py_with default path too - #[pyo3(from_py_with = "PyAny::extract")] _data2: Vec, + #[pyo3(from_py_with = " as PyAnyMethods>::extract")] _data2: Vec, ) { } } diff --git a/tests/test_pyfunction.rs b/tests/test_pyfunction.rs index 5c4350467c4..5221b0f85a5 100644 --- a/tests/test_pyfunction.rs +++ b/tests/test_pyfunction.rs @@ -118,8 +118,8 @@ fn test_functions_with_function_args() { } #[cfg(not(Py_LIMITED_API))] -fn datetime_to_timestamp(dt: &PyAny) -> PyResult { - let dt: &PyDateTime = dt.extract()?; +fn datetime_to_timestamp(dt: &Bound<'_, PyAny>) -> PyResult { + let dt = dt.downcast::()?; let ts: f64 = dt.call_method0("timestamp")?.extract()?; Ok(ts as i64) @@ -170,7 +170,7 @@ fn test_function_with_custom_conversion_error() { #[test] fn test_from_py_with_defaults() { - fn optional_int(x: &PyAny) -> PyResult> { + fn optional_int(x: &Bound<'_, PyAny>) -> PyResult> { if x.is_none() { Ok(None) } else { @@ -185,7 +185,9 @@ fn test_from_py_with_defaults() { } #[pyfunction(signature = (len=0))] - fn from_py_with_default(#[pyo3(from_py_with = "PyAny::len")] len: usize) -> usize { + fn from_py_with_default( + #[pyo3(from_py_with = " as PyAnyMethods>::len")] len: usize, + ) -> usize { len } diff --git a/tests/ui/deprecations.rs b/tests/ui/deprecations.rs index d82c406f9c6..1dcc673a33a 100644 --- a/tests/ui/deprecations.rs +++ b/tests/ui/deprecations.rs @@ -74,6 +74,21 @@ fn module_bound_by_value(m: Bound<'_, PyModule>) -> PyResult<()> { Ok(()) } +fn extract_gil_ref(obj: &PyAny) -> PyResult { + obj.extract() +} + +fn extract_bound(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, +) { +} + fn test_wrap_pyfunction(py: Python<'_>, m: &Bound<'_, PyModule>) { // should lint let _ = wrap_pyfunction!(double, py); diff --git a/tests/ui/deprecations.stderr b/tests/ui/deprecations.stderr index e54e1a4e266..10a21d3a5e8 100644 --- a/tests/ui/deprecations.stderr +++ b/tests/ui/deprecations.stderr @@ -46,10 +46,16 @@ error: use of deprecated method `pyo3::methods::Extractor::::extract_gil_ref` 54 | fn module_gil_ref_with_explicit_py_arg(_py: Python<'_>, m: &PyModule) -> PyResult<()> { | ^ +error: use of deprecated method `pyo3::methods::Extractor::::extract_from_py_with`: use `&Bound<'_, PyAny>` as the argument for this `from_py_with` extractor + --> tests/ui/deprecations.rs:87:27 + | +87 | #[pyo3(from_py_with = "extract_gil_ref")] _gil_ref: i32, + | ^^^^^^^^^^^^^^^^^ + error: use of deprecated method `pyo3::methods::Extractor::>::is_python`: use `wrap_pyfunction_bound!` instead - --> tests/ui/deprecations.rs:79:13 + --> tests/ui/deprecations.rs:94:13 | -79 | let _ = wrap_pyfunction!(double, py); +94 | 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 e29fac9c463eb3c1ae9a72d918e886e5f0e21515 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Tue, 19 Mar 2024 08:59:05 +0000 Subject: [PATCH 089/936] docs: use details to condense migration guide (#3961) --- guide/src/migration.md | 172 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) diff --git a/guide/src/migration.md b/guide/src/migration.md index 8ea3f0730c0..16f9616e683 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -291,15 +291,21 @@ An unfortunate final point here is that PyO3 cannot offer this new implementatio ## from 0.19.* to 0.20 ### Drop support for older technologies +
+Click to expand PyO3 0.20 has increased minimum Rust version to 1.56. This enables use of newer language features and simplifies maintenance of the project. +
### `PyDict::get_item` now returns a `Result` +
+Click to expand `PyDict::get_item` in PyO3 0.19 and older was implemented using a Python API which would suppress all exceptions and return `None` in those cases. This included errors in `__hash__` and `__eq__` implementations of the key being looked up. Newer recommendations by the Python core developers advise against using these APIs which suppress exceptions, instead allowing exceptions to bubble upwards. `PyDict::get_item_with_error` already implemented this recommended behavior, so that API has been renamed to `PyDict::get_item`. + Before: ```rust,ignore @@ -349,8 +355,11 @@ Python::with_gil(|py| -> PyResult<()> { }); # } ``` +
### Required arguments are no longer accepted after optional arguments +
+Click to expand [Trailing `Option` arguments](./function/signature.md#trailing-optional-arguments) have an automatic default of `None`. To avoid unwanted changes when modifying function signatures, in PyO3 0.18 it was deprecated to have a required argument after an `Option` argument without using `#[pyo3(signature = (...))]` to specify the intended defaults. In PyO3 0.20, this becomes a hard error. @@ -375,8 +384,11 @@ fn x_or_y(x: Option, y: u64) -> u64 { x.unwrap_or(y) } ``` +
### Remove deprecated function forms +
+Click to expand In PyO3 0.18 the `#[args]` attribute for `#[pymethods]`, and directly specifying the function signature in `#[pyfunction]`, was deprecated. This functionality has been removed in PyO3 0.20. @@ -403,17 +415,27 @@ fn add(a: u64, b: u64) -> u64 { } ``` +
+ ### `IntoPyPointer` trait removed +
+Click to expand The trait `IntoPyPointer`, which provided the `into_ptr` method on many types, has been removed. `into_ptr` is now available as an inherent method on all types that previously implemented this trait. +
### `AsPyPointer` now `unsafe` trait +
+Click to expand The trait `AsPyPointer` is now `unsafe trait`, meaning any external implementation of it must be marked as `unsafe impl`, and ensure that they uphold the invariant of returning valid pointers. +
## from 0.18.* to 0.19 ### Access to `Python` inside `__traverse__` implementations are now forbidden +
+Click to expand During `__traverse__` implementations for Python's Garbage Collection it is forbidden to do anything other than visit the members of the `#[pyclass]` being traversed. This means making Python function calls or other API calls are forbidden. @@ -433,8 +455,11 @@ impl SomeClass { } } ``` +
### Smarter `anyhow::Error` / `eyre::Report` conversion when inner error is "simple" `PyErr` +
+Click to expand When converting from `anyhow::Error` or `eyre::Report` to `PyErr`, if the inner error is a "simple" `PyErr` (with no source error), then the inner error will be used directly as the `PyErr` instead of wrapping it in a new `PyRuntimeError` with the original information converted into a string. @@ -472,8 +497,11 @@ Before, the above code would have printed `RuntimeError('ValueError: original er After, the same code will print `ValueError: original error message`, which is more straightforward. However, if the `anyhow::Error` or `eyre::Report` has a source, then the original exception will still be wrapped in a `PyRuntimeError`. +
### The deprecated `Python::acquire_gil` was removed and `Python::with_gil` must be used instead +
+Click to expand While the API provided by [`Python::acquire_gil`](https://docs.rs/pyo3/0.18.3/pyo3/marker/struct.Python.html#method.acquire_gil) seems convenient, it is somewhat brittle as the design of the GIL token [`Python`](https://docs.rs/pyo3/0.18.3/pyo3/marker/struct.Python.html) relies on proper nesting and panics if not used correctly, e.g. @@ -542,10 +570,13 @@ Python::with_gil(|py| { ``` Furthermore, `Python::acquire_gil` provides ownership of a `GILGuard` which can be freely stored and passed around. This is usually not helpful as it may keep the lock held for a long time thereby blocking progress in other parts of the program. Due to the generative lifetime attached to the GIL token supplied by `Python::with_gil`, the problem is avoided as the GIL token can only be passed down the call chain. Often, this issue can also be avoided entirely as any GIL-bound reference `&'py PyAny` implies access to a GIL token `Python<'py>` via the [`PyAny::py`](https://docs.rs/pyo3/latest/pyo3/types/struct.PyAny.html#method.py) method. +
## from 0.17.* to 0.18 ### Required arguments after `Option<_>` arguments will no longer be automatically inferred +
+Click to expand In `#[pyfunction]` and `#[pymethods]`, if a "required" function input such as `i32` came after an `Option<_>` input, then the `Option<_>` would be implicitly treated as required. (All trailing `Option<_>` arguments were treated as optional with a default value of `None`). @@ -575,8 +606,11 @@ fn required_argument_after_option_a(x: Option, y: i32) {} #[pyfunction(signature = (x=None, y=0))] fn required_argument_after_option_b(x: Option, y: i32) {} ``` +
### `__text_signature__` is now automatically generated for `#[pyfunction]` and `#[pymethods]` +
+Click to expand The [`#[pyo3(text_signature = "...")]` option](./function/signature.md#making-the-function-signature-available-to-python) was previously the only supported way to set the `__text_signature__` attribute on generated Python functions. @@ -606,10 +640,13 @@ fn function_with_defaults(a: i32, b: i32, c: i32) {} # }) # } ``` +
## from 0.16.* to 0.17 ### Type checks have been changed for `PyMapping` and `PySequence` types +
+Click to expand Previously the type checks for `PyMapping` and `PySequence` (implemented in `PyTryFrom`) used the Python C-API functions `PyMapping_Check` and `PySequence_Check`. @@ -659,13 +696,19 @@ assert!(m.as_ref(py).downcast::().is_ok()); ``` Note that this requirement may go away in the future when a pyclass is able to inherit from the abstract base class directly (see [pyo3/pyo3#991](https://github.com/PyO3/pyo3/issues/991)). +
### The `multiple-pymethods` feature now requires Rust 1.62 +
+Click to expand Due to limitations in the `inventory` crate which the `multiple-pymethods` feature depends on, this feature now requires Rust 1.62. For more information see [dtolnay/inventory#32](https://github.com/dtolnay/inventory/issues/32). +
### Added `impl IntoPy> for &str` +
+Click to expand This may cause inference errors. @@ -692,12 +735,18 @@ Python::with_gil(|py| { }); # } ``` +
### The `pyproto` feature is now disabled by default +
+Click to expand In preparation for removing the deprecated `#[pyproto]` attribute macro in a future PyO3 version, it is now gated behind an opt-in feature flag. This also gives a slight saving to compile times for code which does not use the deprecated macro. +
### `PyTypeObject` trait has been deprecated +
+Click to expand The `PyTypeObject` trait already was near-useless; almost all functionality was already on the `PyTypeInfo` trait, which `PyTypeObject` had a blanket implementation based upon. In PyO3 0.17 the final method, `PyTypeObject::type_object` was moved to `PyTypeInfo::type_object`. @@ -727,22 +776,34 @@ fn get_type_object(py: Python<'_>) -> &PyType { # Python::with_gil(|py| { get_type_object::(py); }); ``` +
### `impl IntoPy for [T; N]` now requires `T: IntoPy` rather than `T: ToPyObject` +
+Click to expand If this leads to errors, simply implement `IntoPy`. Because pyclasses already implement `IntoPy`, you probably don't need to worry about this. +
### Each `#[pymodule]` can now only be initialized once per process +
+Click to expand To make PyO3 modules sound in the presence of Python sub-interpreters, for now it has been necessary to explicitly disable the ability to initialize a `#[pymodule]` more than once in the same process. Attempting to do this will now raise an `ImportError`. +
## from 0.15.* to 0.16 ### Drop support for older technologies +
+Click to expand PyO3 0.16 has increased minimum Rust version to 1.48 and minimum Python version to 3.7. This enables use of newer language features (enabling some of the other additions in 0.16) and simplifies maintenance of the project. +
### `#[pyproto]` has been deprecated +
+Click to expand In PyO3 0.15, the `#[pymethods]` attribute macro gained support for implementing "magic methods" such as `__str__` (aka "dunder" methods). This implementation was not quite finalized at the time, with a few edge cases to be decided upon. The existing `#[pyproto]` attribute macro was left untouched, because it covered these edge cases. @@ -795,8 +856,11 @@ impl MyClass { } } ``` +
### Removed `PartialEq` for object wrappers +
+Click to expand The Python object wrappers `Py` and `PyAny` had implementations of `PartialEq` so that `object_a == object_b` would compare the Python objects for pointer @@ -808,8 +872,11 @@ wrapper type for `object_a` and `object_b`; you can now directly compare a To check for Python object equality (the Python `==` operator), use the new method `eq()`. +
### Container magic methods now match Python behavior +
+Click to expand In PyO3 0.15, `__getitem__`, `__setitem__` and `__delitem__` in `#[pymethods]` would generate only the _mapping_ implementation for a `#[pyclass]`. To match the Python behavior, these methods now generate both the _mapping_ **and** _sequence_ implementations. @@ -838,8 +905,11 @@ The `__len__` and `__getitem__` methods are also used to implement a Python [map Because there is no such distinction from Python, implementing these methods will fill the mapping and sequence slots simultaneously. A Python class with `__len__` implemented, for example, will have both the `sq_length` and `mp_length` slots filled. The PyO3 behavior in 0.16 has been changed to be closer to this Python behavior by default. +
### `wrap_pymodule!` and `wrap_pyfunction!` now respect privacy correctly +
+Click to expand Prior to PyO3 0.16 the `wrap_pymodule!` and `wrap_pyfunction!` macros could use modules and functions whose defining `fn` was not reachable according Rust privacy rules. @@ -887,10 +957,13 @@ fn my_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { Ok(()) } ``` +
## from 0.14.* to 0.15 ### Changes in sequence indexing +
+Click to expand For all types that take sequence indices (`PyList`, `PyTuple` and `PySequence`), the API has been made consistent to only take `usize` indices, for consistency @@ -919,20 +992,29 @@ Python::with_gil(|py| { assert_eq!(list[0..2].to_string(), "[1, 2]"); }); ``` +
## from 0.13.* to 0.14 ### `auto-initialize` feature is now opt-in +
+Click to expand For projects embedding Python in Rust, PyO3 no longer automatically initializes a Python interpreter on the first call to `Python::with_gil` (or `Python::acquire_gil`) unless the [`auto-initialize` feature](features.md#auto-initialize) is enabled. +
### New `multiple-pymethods` feature +
+Click to expand `#[pymethods]` have been reworked with a simpler default implementation which removes the dependency on the `inventory` crate. This reduces dependencies and compile times for the majority of users. The limitation of the new default implementation is that it cannot support multiple `#[pymethods]` blocks for the same `#[pyclass]`. If you need this functionality, you must enable the `multiple-pymethods` feature which will switch `#[pymethods]` to the inventory-based implementation. +
### Deprecated `#[pyproto]` methods +
+Click to expand Some protocol (aka `__dunder__`) methods such as `__bytes__` and `__format__` have been possible to implement two ways in PyO3 for some time: via a `#[pyproto]` (e.g. `PyObjectProtocol` for the methods listed here), or by writing them directly in `#[pymethods]`. This is only true for a handful of the `#[pyproto]` methods (for technical reasons to do with the way PyO3 currently interacts with the Python C-API). @@ -972,14 +1054,20 @@ impl MyClass { } } ``` +
## from 0.12.* to 0.13 ### Minimum Rust version increased to Rust 1.45 +
+Click to expand PyO3 `0.13` makes use of new Rust language features stabilized between Rust 1.40 and Rust 1.45. If you are using a Rust compiler older than Rust 1.45, you will need to update your toolchain to be able to continue using PyO3. +
### Runtime changes to support the CPython limited API +
+Click to expand In PyO3 `0.13` support was added for compiling against the CPython limited API. This had a number of implications for _all_ PyO3 users, described here. @@ -988,10 +1076,13 @@ The largest of these is that all types created from PyO3 are what CPython calls - If you wish to subclass one of these types _from Rust_ you must mark it `#[pyclass(subclass)]`, as you would if you wished to allow subclassing it from Python code. - Type objects are now mutable - Python code can set attributes on them. - `__module__` on types without `#[pyclass(module="mymodule")]` no longer returns `builtins`, it now raises `AttributeError`. +
## from 0.11.* to 0.12 ### `PyErr` has been reworked +
+Click to expand In PyO3 `0.12` the `PyErr` type has been re-implemented to be significantly more compatible with the standard Rust error handling ecosystem. Specifically `PyErr` now implements @@ -1000,28 +1091,40 @@ the standard Rust error handling ecosystem. Specifically `PyErr` now implements While this has necessitated the removal of a number of APIs, the resulting `PyErr` type should now be much more easier to work with. The following sections list the changes in detail and how to migrate to the new APIs. +
#### `PyErr::new` and `PyErr::from_type` now require `Send + Sync` for their argument +
+Click to expand For most uses no change will be needed. If you are trying to construct `PyErr` from a value that is not `Send + Sync`, you will need to first create the Python object and then use `PyErr::from_instance`. Similarly, any types which implemented `PyErrArguments` will now need to be `Send + Sync`. +
#### `PyErr`'s contents are now private +
+Click to expand It is no longer possible to access the fields `.ptype`, `.pvalue` and `.ptraceback` of a `PyErr`. You should instead now use the new methods `PyErr::ptype`, `PyErr::pvalue` and `PyErr::ptraceback`. +
#### `PyErrValue` and `PyErr::from_value` have been removed +
+Click to expand As these were part the internals of `PyErr` which have been reworked, these APIs no longer exist. If you used this API, it is recommended to use `PyException::new_err` (see [the section on Exception types](#exception-types-have-been-reworked)). +
#### `Into>` for `PyErr` has been removed +
+Click to expand This implementation was redundant. Just construct the `Result::Err` variant directly. @@ -1035,8 +1138,11 @@ After (also using the new reworked exception types; see the following section): # use pyo3::{PyResult, exceptions::PyTypeError}; let result: PyResult<()> = Err(PyTypeError::new_err("error message")); ``` +
### Exception types have been reworked +
+Click to expand Previously exception types were zero-sized marker types purely used to construct `PyErr`. In PyO3 0.12, these types have been replaced with full definitions and are usable in the same way as `PyAny`, `PyDict` etc. This @@ -1068,8 +1174,12 @@ assert_eq!( # Ok(()) # }).unwrap(); ``` +
### `FromPy` has been removed +
+Click to expand + To simplify the PyO3 conversion traits, the `FromPy` trait has been removed. Previously there were two ways to define the to-Python conversion for a type: `FromPy for PyObject` and `IntoPy for T`. @@ -1119,12 +1229,20 @@ After: let obj: PyObject = 1.234.into_py(py); # }) ``` +
### `PyObject` is now a type alias of `Py` +
+Click to expand + This should change very little from a usage perspective. If you implemented traits for both `PyObject` and `Py`, you may find you can just remove the `PyObject` implementation. +
### `AsPyRef` has been removed +
+Click to expand + As `PyObject` has been changed to be just a type alias, the only remaining implementor of `AsPyRef` was `Py`. This removed the need for a trait, so the `AsPyRef::as_ref` method has been moved to `Py::as_ref`. @@ -1149,13 +1267,21 @@ let list_py: Py = PyList::empty(py).into(); let list_ref: &PyList = list_py.as_ref(py); # }) ``` +
## from 0.10.* to 0.11 ### Stable Rust +
+Click to expand + PyO3 now supports the stable Rust toolchain. The minimum required version is 1.39.0. +
### `#[pyclass]` structs must now be `Send` or `unsendable` +
+Click to expand + Because `#[pyclass]` structs can be sent between threads by the Python interpreter, they must implement `Send` or declared as `unsendable` (by `#[pyclass(unsendable)]`). Note that `unsendable` is added in PyO3 `0.11.1` and `Send` is always required in PyO3 `0.11.0`. @@ -1222,8 +1348,12 @@ There can be two fixes: pointers: Vec<*mut std::os::raw::c_char>, } ``` +
### All `PyObject` and `Py` methods now take `Python` as an argument +
+Click to expand + Previously, a few methods such as `Object::get_refcnt` did not take `Python` as an argument (to ensure that the Python GIL was held by the current thread). Technically, this was not sound. To migrate, just pass a `py` argument to any calls to these methods. @@ -1241,10 +1371,14 @@ After: py.None().get_refcnt(py); # }) ``` +
## from 0.9.* to 0.10 ### `ObjectProtocol` is removed +
+Click to expand + All methods are moved to [`PyAny`]. And since now all native types (e.g., `PyList`) implements `Deref`, all you need to do is remove `ObjectProtocol` from your code. @@ -1269,14 +1403,22 @@ let hi: &pyo3::types::PyString = obj.call0().unwrap().downcast().unwrap(); assert_eq!(hi.len().unwrap(), 5); # }) ``` +
### No `#![feature(specialization)]` in user code +
+Click to expand + While PyO3 itself still requires specialization and nightly Rust, now you don't have to use `#![feature(specialization)]` in your crate. +
## from 0.8.* to 0.9 ### `#[new]` interface +
+Click to expand + [`PyRawObject`](https://docs.rs/pyo3/0.8.5/pyo3/type_object/struct.PyRawObject.html) is now removed and our syntax for constructors has changed. @@ -1311,8 +1453,12 @@ impl MyClass { Basically you can return `Self` or `Result` directly. For more, see [the constructor section](class.md#constructor) of this guide. +
### PyCell +
+Click to expand + PyO3 0.9 introduces [`PyCell`], which is a [`RefCell`]-like object wrapper for ensuring Rust's rules regarding aliasing of references are upheld. For more detail, see the @@ -1463,6 +1609,32 @@ impl PySequenceProtocol for ByteSequence { } } ``` +
+ + [`FromPyObject`]: {{#PYO3_DOCS_URL}}/pyo3/conversion/trait.FromPyObject.html [`PyAny`]: {{#PYO3_DOCS_URL}}/pyo3/types/struct.PyAny.html From cbed7c11b639107462b938c1025508a60d82efae Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Tue, 19 Mar 2024 19:36:22 +0000 Subject: [PATCH 090/936] ci: tidy ups for 3.7 (#3969) --- pytests/pyproject.toml | 2 +- src/tests/hygiene/pymethods.rs | 396 --------------------------------- 2 files changed, 1 insertion(+), 397 deletions(-) diff --git a/pytests/pyproject.toml b/pytests/pyproject.toml index 43403a1241c..aace57dd4d4 100644 --- a/pytests/pyproject.toml +++ b/pytests/pyproject.toml @@ -25,6 +25,6 @@ dev = [ "pytest-asyncio>=0.21", "pytest-benchmark>=3.4", # pinned < 8.1 because https://github.com/CodSpeedHQ/pytest-codspeed/issues/27 - "pytest>=8,<8.1", + "pytest>=7,<8.1", "typing_extensions>=4.0.0" ] diff --git a/src/tests/hygiene/pymethods.rs b/src/tests/hygiene/pymethods.rs index a00e67d9a85..2ffaf68690f 100644 --- a/src/tests/hygiene/pymethods.rs +++ b/src/tests/hygiene/pymethods.rs @@ -9,7 +9,6 @@ pub struct Dummy; #[pyo3(crate = "crate")] pub struct DummyIter; -#[cfg(Py_3_8)] #[crate::pymethods] #[pyo3(crate = "crate")] impl Dummy { @@ -405,401 +404,6 @@ impl Dummy { // Buffer protocol? } -#[cfg(not(Py_3_8))] -#[crate::pymethods] -#[pyo3(crate = "crate")] -impl Dummy { - ////////////////////// - // Basic customization - ////////////////////// - fn __repr__(&self) -> &'static str { - "Dummy" - } - - fn __str__(&self) -> &'static str { - "Dummy" - } - - fn __bytes__<'py>(&self, py: crate::Python<'py>) -> crate::Bound<'py, crate::types::PyBytes> { - crate::types::PyBytes::new_bound(py, &[0]) - } - - fn __format__(&self, format_spec: ::std::string::String) -> ::std::string::String { - ::std::panic!("unimplemented isn't hygienic before 1.50") - } - - fn __lt__(&self, other: &Self) -> bool { - false - } - - fn __le__(&self, other: &Self) -> bool { - false - } - fn __eq__(&self, other: &Self) -> bool { - false - } - fn __ne__(&self, other: &Self) -> bool { - false - } - fn __gt__(&self, other: &Self) -> bool { - false - } - fn __ge__(&self, other: &Self) -> bool { - false - } - - fn __hash__(&self) -> u64 { - 42 - } - - fn __bool__(&self) -> bool { - true - } - - ////////////////////// - // Customizing attribute access - ////////////////////// - - fn __getattr__(&self, name: ::std::string::String) -> &crate::PyAny { - ::std::panic!("unimplemented isn't hygienic before 1.50") - } - - fn __getattribute__(&self, name: ::std::string::String) -> &crate::PyAny { - ::std::panic!("unimplemented isn't hygienic before 1.50") - } - - fn __setattr__(&mut self, name: ::std::string::String, value: ::std::string::String) {} - - fn __delattr__(&mut self, name: ::std::string::String) {} - - fn __dir__<'py>(&self, py: crate::Python<'py>) -> crate::Bound<'py, crate::types::PyList> { - crate::types::PyList::new_bound(py, ::std::vec![0_u8]) - } - - ////////////////////// - // Implementing Descriptors - ////////////////////// - - fn __get__( - &self, - instance: &crate::PyAny, - owner: &crate::PyAny, - ) -> crate::PyResult<&crate::PyAny> { - ::std::panic!("unimplemented isn't hygienic before 1.50") - } - - fn __set__(&self, instance: &crate::PyAny, owner: &crate::PyAny) {} - - fn __delete__(&self, instance: &crate::PyAny) {} - - fn __set_name__(&self, owner: &crate::PyAny, name: &crate::PyAny) {} - - ////////////////////// - // Implementing Descriptors - ////////////////////// - - fn __len__(&self) -> usize { - 0 - } - - fn __getitem__(&self, key: u32) -> crate::PyResult { - ::std::result::Result::Err(crate::exceptions::PyKeyError::new_err("boo")) - } - - fn __setitem__(&self, key: u32, value: u32) {} - - fn __delitem__(&self, key: u32) {} - - fn __iter__(_: crate::pycell::PyRef<'_, Self>, py: crate::Python<'_>) -> crate::Py { - crate::Py::new(py, DummyIter {}).unwrap() - } - - fn __next__(&mut self) -> ::std::option::Option<()> { - ::std::option::Option::None - } - - fn __reversed__( - slf: crate::pycell::PyRef<'_, Self>, - py: crate::Python<'_>, - ) -> crate::Py { - crate::Py::new(py, DummyIter {}).unwrap() - } - - fn __contains__(&self, item: u32) -> bool { - false - } - - ////////////////////// - // Emulating numeric types - ////////////////////// - - fn __add__(&self, other: &Self) -> Dummy { - Dummy {} - } - - fn __sub__(&self, other: &Self) -> Dummy { - Dummy {} - } - - fn __mul__(&self, other: &Self) -> Dummy { - Dummy {} - } - - fn __truediv__(&self, _other: &Self) -> crate::PyResult<()> { - ::std::result::Result::Err(crate::exceptions::PyZeroDivisionError::new_err("boo")) - } - - fn __floordiv__(&self, _other: &Self) -> crate::PyResult<()> { - ::std::result::Result::Err(crate::exceptions::PyZeroDivisionError::new_err("boo")) - } - - fn __mod__(&self, _other: &Self) -> u32 { - 0 - } - - fn __divmod__(&self, _other: &Self) -> (u32, u32) { - (0, 0) - } - - fn __pow__(&self, _other: &Self, modulo: ::std::option::Option) -> Dummy { - Dummy {} - } - - fn __lshift__(&self, other: &Self) -> Dummy { - Dummy {} - } - - fn __rshift__(&self, other: &Self) -> Dummy { - Dummy {} - } - - fn __and__(&self, other: &Self) -> Dummy { - Dummy {} - } - - fn __xor__(&self, other: &Self) -> Dummy { - Dummy {} - } - - fn __or__(&self, other: &Self) -> Dummy { - Dummy {} - } - - fn __radd__(&self, other: &Self) -> Dummy { - Dummy {} - } - - fn __rrsub__(&self, other: &Self) -> Dummy { - Dummy {} - } - - fn __rmul__(&self, other: &Self) -> Dummy { - Dummy {} - } - - fn __rtruediv__(&self, _other: &Self) -> crate::PyResult<()> { - ::std::result::Result::Err(crate::exceptions::PyZeroDivisionError::new_err("boo")) - } - - fn __rfloordiv__(&self, _other: &Self) -> crate::PyResult<()> { - ::std::result::Result::Err(crate::exceptions::PyZeroDivisionError::new_err("boo")) - } - - fn __rmod__(&self, _other: &Self) -> u32 { - 0 - } - - fn __rdivmod__(&self, _other: &Self) -> (u32, u32) { - (0, 0) - } - - fn __rpow__(&self, _other: &Self, modulo: ::std::option::Option) -> Dummy { - Dummy {} - } - - fn __rlshift__(&self, other: &Self) -> Dummy { - Dummy {} - } - - fn __rrshift__(&self, other: &Self) -> Dummy { - Dummy {} - } - - fn __rand__(&self, other: &Self) -> Dummy { - Dummy {} - } - - fn __rxor__(&self, other: &Self) -> Dummy { - Dummy {} - } - - fn __ror__(&self, other: &Self) -> Dummy { - Dummy {} - } - - fn __iadd__(&mut self, other: &Self) {} - - fn __irsub__(&mut self, other: &Self) {} - - fn __imul__(&mut self, other: &Self) {} - - fn __itruediv__(&mut self, _other: &Self) {} - - fn __ifloordiv__(&mut self, _other: &Self) {} - - fn __imod__(&mut self, _other: &Self) {} - - fn __ipow__(&mut self, _other: &Self, _modulo: ::std::option::Option) {} - fn __ilshift__(&mut self, other: &Self) {} - - fn __irshift__(&mut self, other: &Self) {} - - fn __iand__(&mut self, other: &Self) {} - - fn __ixor__(&mut self, other: &Self) {} - - fn __ior__(&mut self, other: &Self) {} - - fn __neg__(slf: crate::pycell::PyRef<'_, Self>) -> crate::pycell::PyRef<'_, Self> { - slf - } - - fn __pos__(slf: crate::pycell::PyRef<'_, Self>) -> crate::pycell::PyRef<'_, Self> { - slf - } - - fn __abs__(slf: crate::pycell::PyRef<'_, Self>) -> crate::pycell::PyRef<'_, Self> { - slf - } - - fn __invert__(slf: crate::pycell::PyRef<'_, Self>) -> crate::pycell::PyRef<'_, Self> { - slf - } - - fn __complex__<'py>( - &self, - py: crate::Python<'py>, - ) -> crate::Bound<'py, crate::types::PyComplex> { - crate::types::PyComplex::from_doubles_bound(py, 0.0, 0.0) - } - - fn __int__(&self) -> u32 { - 0 - } - - fn __float__(&self) -> f64 { - 0.0 - } - - fn __index__(&self) -> u32 { - 0 - } - - fn __round__(&self, ndigits: ::std::option::Option) -> u32 { - 0 - } - - fn __trunc__(&self) -> u32 { - 0 - } - - fn __floor__(&self) -> u32 { - 0 - } - - fn __ceil__(&self) -> u32 { - 0 - } - - ////////////////////// - // With Statement Context Managers - ////////////////////// - - fn __enter__(&mut self) {} - - fn __exit__( - &mut self, - exc_type: &crate::PyAny, - exc_value: &crate::PyAny, - traceback: &crate::PyAny, - ) { - } - - ////////////////////// - // Awaitable Objects - ////////////////////// - - fn __await__(slf: crate::pycell::PyRef<'_, Self>) -> crate::pycell::PyRef<'_, Self> { - slf - } - - ////////////////////// - - // Asynchronous Iterators - ////////////////////// - - fn __aiter__( - slf: crate::pycell::PyRef<'_, Self>, - py: crate::Python<'_>, - ) -> crate::Py { - crate::Py::new(py, DummyIter {}).unwrap() - } - - fn __anext__(&mut self) -> ::std::option::Option<()> { - ::std::option::Option::None - } - - ////////////////////// - // Asynchronous Context Managers - ////////////////////// - - fn __aenter__(&mut self) {} - - fn __aexit__( - &mut self, - exc_type: &crate::PyAny, - exc_value: &crate::PyAny, - traceback: &crate::PyAny, - ) { - } - - // Things with attributes - - #[pyo3(signature = (_y, *, _z=2))] - fn test(&self, _y: &Dummy, _z: i32) {} - #[staticmethod] - fn staticmethod() {} - #[classmethod] - fn clsmethod(_: &crate::Bound<'_, crate::types::PyType>) {} - #[pyo3(signature = (*_args, **_kwds))] - fn __call__( - &self, - _args: &crate::types::PyTuple, - _kwds: ::std::option::Option<&crate::types::PyDict>, - ) -> crate::PyResult { - ::std::panic!("unimplemented isn't hygienic before 1.50") - } - #[new] - fn new(a: u8) -> Self { - Dummy {} - } - #[getter] - fn get(&self) -> i32 { - 0 - } - #[setter] - fn set(&mut self, _v: i32) {} - #[classattr] - fn class_attr() -> i32 { - 0 - } - - // Dunder methods invented for protocols - - // PyGcProtocol - // Buffer protocol? -} - // Ensure that crate argument is also accepted inline #[crate::pyclass(crate = "crate")] From 02e188e4b4151afb42c52866bd7e3a5f77dfb87d Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Tue, 19 Mar 2024 21:08:20 +0000 Subject: [PATCH 091/936] adjust path for GIL Refs deprecation warnings (#3968) --- pyo3-macros-backend/src/method.rs | 27 ++++++------- pyo3-macros-backend/src/module.rs | 7 ++-- pyo3-macros-backend/src/params.rs | 6 +-- src/impl_/deprecations.rs | 64 +++++++++++++++++++++++++++++++ src/impl_/pymethods.rs | 62 ------------------------------ src/lib.rs | 23 +++++++++-- src/macros.rs | 2 +- src/types/function.rs | 3 +- tests/ui/deprecations.stderr | 16 ++++---- 9 files changed, 113 insertions(+), 97 deletions(-) diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index 3cc2a96e899..93a54395c94 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -540,7 +540,7 @@ impl<'a> FnSpec<'a> { holders.pop().unwrap(); // does not actually use holder created by `self_arg` quote! {{ - #self_e = #pyo3_path::impl_::pymethods::Extractor::<()>::new(); + #self_e = #pyo3_path::impl_::deprecations::GilRefs::<()>::new(); let __guard = #pyo3_path::impl_::coroutine::RefGuard::<#cls>::new(&#pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(py, &_slf))?; async move { function(&__guard, #(#args),*).await } }} @@ -549,7 +549,7 @@ impl<'a> FnSpec<'a> { holders.pop().unwrap(); // does not actually use holder created by `self_arg` quote! {{ - #self_e = #pyo3_path::impl_::pymethods::Extractor::<()>::new(); + #self_e = #pyo3_path::impl_::deprecations::GilRefs::<()>::new(); 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, #(#args),*).await } }} @@ -557,12 +557,12 @@ impl<'a> FnSpec<'a> { _ => { if self_arg.is_empty() { quote! {{ - #self_e = #pyo3_path::impl_::pymethods::Extractor::<()>::new(); + #self_e = #pyo3_path::impl_::deprecations::GilRefs::<()>::new(); function(#(#args),*) }} } else { quote! { function({ - let (self_arg, e) = #pyo3_path::impl_::pymethods::inspect_type(#self_arg); + let (self_arg, e) = #pyo3_path::impl_::deprecations::inspect_type(#self_arg); #self_e = e; self_arg }, #(#args),*) } @@ -588,13 +588,13 @@ impl<'a> FnSpec<'a> { call } else if self_arg.is_empty() { quote! {{ - #self_e = #pyo3_path::impl_::pymethods::Extractor::<()>::new(); + #self_e = #pyo3_path::impl_::deprecations::GilRefs::<()>::new(); function(#(#args),*) }} } else { quote! { function({ - let (self_arg, e) = #pyo3_path::impl_::pymethods::inspect_type(#self_arg); + let (self_arg, e) = #pyo3_path::impl_::deprecations::inspect_type(#self_arg); #self_e = e; self_arg }, #(#args),*) @@ -632,8 +632,7 @@ impl<'a> FnSpec<'a> { }) .collect(); let (call, self_arg_span) = rust_call(args, &self_e, &mut holders); - let extract_gil_ref = - quote_spanned! { self_arg_span => #self_e.extract_gil_ref(); }; + let function_arg = quote_spanned! { self_arg_span => #self_e.function_arg(); }; quote! { unsafe fn #ident<'py>( @@ -645,7 +644,7 @@ impl<'a> FnSpec<'a> { let #self_e; #( #holders )* let result = #call; - #extract_gil_ref + #function_arg result } } @@ -654,8 +653,7 @@ impl<'a> FnSpec<'a> { let mut holders = Vec::new(); let (arg_convert, args) = impl_arg_params(self, cls, true, &mut holders, ctx)?; let (call, self_arg_span) = rust_call(args, &self_e, &mut holders); - let extract_gil_ref = - quote_spanned! { self_arg_span => #self_e.extract_gil_ref(); }; + let function_arg = quote_spanned! { self_arg_span => #self_e.function_arg(); }; quote! { unsafe fn #ident<'py>( @@ -671,7 +669,7 @@ impl<'a> FnSpec<'a> { #arg_convert #( #holders )* let result = #call; - #extract_gil_ref + #function_arg result } } @@ -680,8 +678,7 @@ impl<'a> FnSpec<'a> { let mut holders = Vec::new(); let (arg_convert, args) = impl_arg_params(self, cls, false, &mut holders, ctx)?; let (call, self_arg_span) = rust_call(args, &self_e, &mut holders); - let extract_gil_ref = - quote_spanned! { self_arg_span => #self_e.extract_gil_ref(); }; + let function_arg = quote_spanned! { self_arg_span => #self_e.function_arg(); }; quote! { unsafe fn #ident<'py>( @@ -696,7 +693,7 @@ impl<'a> FnSpec<'a> { #arg_convert #( #holders )* let result = #call; - #extract_gil_ref + #function_arg result } } diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index 1cc3e836404..2515d519287 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -295,7 +295,8 @@ pub fn pymodule_function_impl(mut function: syn::ItemFn) -> Result if function.sig.inputs.len() == 2 { module_args.push(quote!(module.py())); } - module_args.push(quote!(::std::convert::Into::into(#pyo3_path::methods::BoundRef(module)))); + module_args + .push(quote!(::std::convert::Into::into(#pyo3_path::impl_::pymethods::BoundRef(module)))); let extractors = function .sig @@ -306,8 +307,8 @@ pub fn pymodule_function_impl(mut function: syn::ItemFn) -> Result if let syn::Pat::Ident(pat_ident) = &*pat_type.pat { let ident = &pat_ident.ident; return Some([ - parse_quote! { let (#ident, e) = #pyo3_path::impl_::pymethods::inspect_type(#ident); }, - parse_quote_spanned! { pat_type.span() => e.extract_gil_ref(); }, + parse_quote! { let (#ident, e) = #pyo3_path::impl_::deprecations::inspect_type(#ident); }, + parse_quote_spanned! { pat_type.span() => e.function_arg(); }, ]); } } diff --git a/pyo3-macros-backend/src/params.rs b/pyo3-macros-backend/src/params.rs index 74b3eba411f..ae1661fe2c1 100644 --- a/pyo3-macros-backend/src/params.rs +++ b/pyo3-macros-backend/src/params.rs @@ -46,9 +46,9 @@ pub fn impl_arg_params( let from_py_with_holder = syn::Ident::new(&format!("from_py_with_{}", i), Span::call_site()); Some(quote_spanned! { from_py_with.span() => - let e = #pyo3_path::impl_::pymethods::Extractor::new(); - let #from_py_with_holder = #pyo3_path::impl_::pymethods::inspect_fn(#from_py_with, &e); - e.extract_from_py_with(); + let e = #pyo3_path::impl_::deprecations::GilRefs::new(); + let #from_py_with_holder = #pyo3_path::impl_::deprecations::inspect_fn(#from_py_with, &e); + e.from_py_with_arg(); }) }) .collect::(); diff --git a/src/impl_/deprecations.rs b/src/impl_/deprecations.rs index 6b9930ac69b..0c749ff985b 100644 --- a/src/impl_/deprecations.rs +++ b/src/impl_/deprecations.rs @@ -1,4 +1,68 @@ //! 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) -> (T, GilRefs) { + (t, GilRefs::new()) +} + +pub fn inspect_fn(f: fn(A) -> PyResult, _: &GilRefs
) -> fn(A) -> PyResult { + f +} + +pub struct GilRefs(NotAGilRef); +pub struct NotAGilRef(std::marker::PhantomData); + +pub trait IsGilRef {} + +impl IsGilRef for &'_ T {} + +impl GilRefs { + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + GilRefs(NotAGilRef(std::marker::PhantomData)) + } +} + +impl GilRefs> { + #[cfg_attr( + not(feature = "gil-refs"), + 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" + ) + )] + 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" + ) + )] + pub fn from_py_with_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 = NotAGilRef; + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/src/impl_/pymethods.rs b/src/impl_/pymethods.rs index ca3f0a06bf3..df89dba7dbd 100644 --- a/src/impl_/pymethods.rs +++ b/src/impl_/pymethods.rs @@ -580,65 +580,3 @@ pub unsafe fn tp_new_impl( .create_class_object_of_type(py, target_type) .map(Bound::into_ptr) } - -pub fn inspect_type(t: T) -> (T, Extractor) { - (t, Extractor::new()) -} - -pub fn inspect_fn(f: fn(A) -> PyResult, _: &Extractor) -> fn(A) -> PyResult { - f -} - -pub struct Extractor(NotAGilRef); -pub struct NotAGilRef(std::marker::PhantomData); - -pub trait IsGilRef {} - -impl IsGilRef for &'_ T {} - -impl Extractor { - #[allow(clippy::new_without_default)] - pub fn new() -> Self { - Extractor(NotAGilRef(std::marker::PhantomData)) - } -} - -impl Extractor> { - #[cfg_attr( - not(feature = "gil-refs"), - deprecated(since = "0.21.0", note = "use `wrap_pyfunction_bound!` instead") - )] - pub fn is_python(&self) {} -} - -impl Extractor { - #[cfg_attr( - not(feature = "gil-refs"), - deprecated( - since = "0.21.0", - note = "use `&Bound<'_, T>` instead for this function argument" - ) - )] - pub fn extract_gil_ref(&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" - ) - )] - pub fn extract_from_py_with(&self) {} -} - -impl NotAGilRef { - pub fn extract_gil_ref(&self) {} - pub fn extract_from_py_with(&self) {} - pub fn is_python(&self) {} -} - -impl std::ops::Deref for Extractor { - type Target = NotAGilRef; - fn deref(&self) -> &Self::Target { - &self.0 - } -} diff --git a/src/lib.rs b/src/lib.rs index dab48fbe01b..fd5a520fdb5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -346,9 +346,6 @@ pub(crate) mod sealed; /// For compatibility reasons this has not yet been removed, however will be done so /// once is resolved. pub mod class { - #[doc(hidden)] - pub use crate::impl_::pymethods as methods; - pub use self::gc::{PyTraverseError, PyVisit}; #[doc(hidden)] @@ -356,6 +353,16 @@ pub mod class { PyClassAttributeDef, PyGetterDef, PyMethodDef, PyMethodDefType, PyMethodType, PySetterDef, }; + #[doc(hidden)] + pub mod methods { + // frozen with the contents of the `impl_::pymethods` module in 0.20, + // this should probably all be replaced with deprecated type aliases and removed. + pub use crate::impl_::pymethods::{ + IPowModulo, PyClassAttributeDef, PyGetterDef, PyMethodDef, PyMethodDefType, + PyMethodType, PySetterDef, + }; + } + /// Old module which contained some implementation details of the `#[pyproto]` module. /// /// Prefer using the same content from `pyo3::pyclass`, e.g. `use pyo3::pyclass::CompareOp` instead @@ -479,6 +486,16 @@ mod macros; #[cfg(feature = "experimental-inspect")] pub mod inspect; +/// Ths module only contains re-exports of pyo3 deprecation warnings and exists +/// purely to make compiler error messages nicer. +/// +/// (The compiler uses this module in error messages, probably because it's a public +/// re-export at a shorter path than `pyo3::impl_::deprecations`.) +#[doc(hidden)] +pub mod deprecations { + pub use crate::impl_::deprecations::*; +} + /// Test readme and user guide #[cfg(doctest)] pub mod doc_test { diff --git a/src/macros.rs b/src/macros.rs index bd0bd81421c..db409515085 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -144,7 +144,7 @@ macro_rules! wrap_pyfunction { }; ($function:path, $py_or_module:expr) => {{ use $function as wrapped_pyfunction; - let (py_or_module, e) = $crate::impl_::pymethods::inspect_type($py_or_module); + let (py_or_module, e) = $crate::impl_::deprecations::inspect_type($py_or_module); e.is_python(); $crate::impl_::pyfunction::WrapPyFunctionArg::wrap_pyfunction( py_or_module, diff --git a/src/types/function.rs b/src/types/function.rs index ea8201fb131..f5305e31886 100644 --- a/src/types/function.rs +++ b/src/types/function.rs @@ -1,12 +1,11 @@ use crate::derive_utils::PyFunctionArguments; use crate::ffi_ptr_ext::FfiPtrExt; -use crate::methods::PyMethodDefDestructor; use crate::py_result_ext::PyResultExt; use crate::types::capsule::PyCapsuleMethods; use crate::types::module::PyModuleMethods; use crate::{ ffi, - impl_::pymethods::{self, PyMethodDef}, + impl_::pymethods::{self, PyMethodDef, PyMethodDefDestructor}, types::{PyCapsule, PyDict, PyModule, PyString, PyTuple}, }; use crate::{Bound, IntoPy, Py, PyAny, PyNativeType, PyResult, Python}; diff --git a/tests/ui/deprecations.stderr b/tests/ui/deprecations.stderr index 10a21d3a5e8..8bd1450874f 100644 --- a/tests/ui/deprecations.stderr +++ b/tests/ui/deprecations.stderr @@ -1,4 +1,4 @@ -error: use of deprecated constant `pyo3::impl_::deprecations::PYMETHODS_NEW_DEPRECATED_FORM`: use `#[new]` instead of `#[__new__]` +error: use of deprecated constant `pyo3::deprecations::PYMETHODS_NEW_DEPRECATED_FORM`: use `#[new]` instead of `#[__new__]` --> tests/ui/deprecations.rs:12:7 | 12 | #[__new__] @@ -16,43 +16,43 @@ 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::methods::Extractor::::extract_gil_ref`: use `&Bound<'_, T>` instead for this function argument +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::methods::Extractor::::extract_gil_ref`: use `&Bound<'_, T>` instead for this function argument +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::methods::Extractor::::extract_gil_ref`: use `&Bound<'_, T>` instead for this function argument +error: use of deprecated method `pyo3::deprecations::GilRefs::::function_arg`: use `&Bound<'_, T>` instead for this function argument --> tests/ui/deprecations.rs:38:43 | 38 | fn pyfunction_with_module_gil_ref(module: &PyModule) -> PyResult<&str> { | ^ -error: use of deprecated method `pyo3::methods::Extractor::::extract_gil_ref`: use `&Bound<'_, T>` instead for this function argument +error: use of deprecated method `pyo3::deprecations::GilRefs::::function_arg`: use `&Bound<'_, T>` instead for this function argument --> tests/ui/deprecations.rs:48:19 | 48 | fn module_gil_ref(m: &PyModule) -> PyResult<()> { | ^ -error: use of deprecated method `pyo3::methods::Extractor::::extract_gil_ref`: use `&Bound<'_, T>` instead for this function argument +error: use of deprecated method `pyo3::deprecations::GilRefs::::function_arg`: use `&Bound<'_, T>` instead for this function argument --> tests/ui/deprecations.rs:54:57 | 54 | fn module_gil_ref_with_explicit_py_arg(_py: Python<'_>, m: &PyModule) -> PyResult<()> { | ^ -error: use of deprecated method `pyo3::methods::Extractor::::extract_from_py_with`: use `&Bound<'_, PyAny>` as the argument for this `from_py_with` extractor +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:87:27 | 87 | #[pyo3(from_py_with = "extract_gil_ref")] _gil_ref: i32, | ^^^^^^^^^^^^^^^^^ -error: use of deprecated method `pyo3::methods::Extractor::>::is_python`: use `wrap_pyfunction_bound!` instead +error: use of deprecated method `pyo3::deprecations::GilRefs::>::is_python`: use `wrap_pyfunction_bound!` instead --> tests/ui/deprecations.rs:94:13 | 94 | let _ = wrap_pyfunction!(double, py); From caf80eca6607db6dfffa1f305c3118423320e9de Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Tue, 19 Mar 2024 21:41:27 +0000 Subject: [PATCH 092/936] handle clippy `new_without_default` warnings (#3971) * handle clippy `new_without_default` warnings * add newsfragment --- newsfragments/3971.added.md | 1 + pyo3-ffi/src/cpython/object.rs | 1 + pyo3-ffi/src/pybuffer.rs | 1 + src/impl_/pyclass/lazy_type_object.rs | 1 + src/sync.rs | 1 + 5 files changed, 5 insertions(+) create mode 100644 newsfragments/3971.added.md diff --git a/newsfragments/3971.added.md b/newsfragments/3971.added.md new file mode 100644 index 00000000000..12c0d2265bc --- /dev/null +++ b/newsfragments/3971.added.md @@ -0,0 +1 @@ +Implement `Default` for `GILOnceCell`. diff --git a/pyo3-ffi/src/cpython/object.rs b/pyo3-ffi/src/cpython/object.rs index 161fb50cf24..d0c1634082b 100644 --- a/pyo3-ffi/src/cpython/object.rs +++ b/pyo3-ffi/src/cpython/object.rs @@ -46,6 +46,7 @@ mod bufferinfo { } impl Py_buffer { + #[allow(clippy::new_without_default)] pub const fn new() -> Self { Py_buffer { buf: ptr::null_mut(), diff --git a/pyo3-ffi/src/pybuffer.rs b/pyo3-ffi/src/pybuffer.rs index a414f333ce6..50bf4e6109c 100644 --- a/pyo3-ffi/src/pybuffer.rs +++ b/pyo3-ffi/src/pybuffer.rs @@ -27,6 +27,7 @@ pub struct Py_buffer { } impl Py_buffer { + #[allow(clippy::new_without_default)] pub const fn new() -> Self { Py_buffer { buf: ptr::null_mut(), diff --git a/src/impl_/pyclass/lazy_type_object.rs b/src/impl_/pyclass/lazy_type_object.rs index 1318e1abbbc..efb6ecf37e6 100644 --- a/src/impl_/pyclass/lazy_type_object.rs +++ b/src/impl_/pyclass/lazy_type_object.rs @@ -32,6 +32,7 @@ struct LazyTypeObjectInner { impl LazyTypeObject { /// Creates an uninitialized `LazyTypeObject`. + #[allow(clippy::new_without_default)] pub const fn new() -> Self { LazyTypeObject( LazyTypeObjectInner { diff --git a/src/sync.rs b/src/sync.rs index 38471fb7ca3..5af4940461d 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -90,6 +90,7 @@ 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>); // T: Send is needed for Sync because the thread which drops the GILOnceCell can be different From 2736cf670c7d7a0bc38a96f6c8833fd16dc8548d Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Wed, 20 Mar 2024 10:27:38 +0100 Subject: [PATCH 093/936] deprecate gil-refs in `from_py_with` (Part 2) (#3972) * deprecate `from_py_with` in `#[derive(FromPyObject)]` (NewType) * deprecate `from_py_with` in `#[derive(FromPyObject)]` (Enum, Struct) --- pyo3-macros-backend/src/frompyobject.rs | 168 ++++++++++++++++++------ tests/ui/deprecations.rs | 39 ++++++ tests/ui/deprecations.stderr | 36 ++++- 3 files changed, 197 insertions(+), 46 deletions(-) diff --git a/pyo3-macros-backend/src/frompyobject.rs b/pyo3-macros-backend/src/frompyobject.rs index 68ef72ea157..7f26e5b14fc 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}; +use quote::{format_ident, quote, quote_spanned}; use syn::{ parenthesized, parse::{Parse, ParseStream}, @@ -44,13 +44,16 @@ impl<'a> Enum<'a> { } /// Build derivation body for enums. - fn build(&self, ctx: &Ctx) -> TokenStream { + fn build(&self, ctx: &Ctx) -> (TokenStream, TokenStream) { let Ctx { pyo3_path } = ctx; let mut var_extracts = Vec::new(); let mut variant_names = Vec::new(); let mut error_names = Vec::new(); + + let mut deprecations = TokenStream::new(); for var in &self.variants { - let struct_derive = var.build(ctx); + let (struct_derive, dep) = var.build(ctx); + deprecations.extend(dep); let ext = quote!({ let maybe_ret = || -> #pyo3_path::PyResult { #struct_derive @@ -67,19 +70,22 @@ impl<'a> Enum<'a> { error_names.push(&var.err_name); } let ty_name = self.enum_ident.to_string(); - quote!( - let errors = [ - #(#var_extracts),* - ]; - ::std::result::Result::Err( - #pyo3_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, ) } } @@ -238,7 +244,7 @@ impl<'a> Container<'a> { } /// Build derivation body for a struct. - fn build(&self, ctx: &Ctx) -> TokenStream { + fn build(&self, ctx: &Ctx) -> (TokenStream, TokenStream) { match &self.ty { ContainerType::StructNewtype(ident, from_py_with) => { self.build_newtype_struct(Some(ident), from_py_with, ctx) @@ -256,41 +262,73 @@ 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)? - }) - }, + None => ( + quote! { + Ok(#self_ty { + #ident: #pyo3_path::impl_::frompyobject::extract_struct_field(obj, #struct_name, #field_name)? + }) + }, + TokenStream::new(), + ), Some(FromPyWithAttribute { value: expr_path, .. - }) => quote! { - Ok(#self_ty { - #ident: #pyo3_path::impl_::frompyobject::extract_struct_field_with(#expr_path as fn(_) -> _, obj, #struct_name, #field_name)? - }) - }, + }) => ( + quote! { + Ok(#self_ty { + #ident: #pyo3_path::impl_::frompyobject::extract_struct_field_with(#expr_path as fn(_) -> _, obj, #struct_name, #field_name)? + }) + }, + quote_spanned! { expr_path.span() => + const _: () = { + fn check_from_py_with() { + let e = #pyo3_path::impl_::deprecations::GilRefs::new(); + #pyo3_path::impl_::deprecations::inspect_fn(#expr_path, &e); + e.from_py_with_arg(); + } + }; + }, + ), } } else { match from_py_with { - None => quote!( - #pyo3_path::impl_::frompyobject::extract_tuple_struct_field(obj, #struct_name, 0).map(#self_ty) + None => ( + quote!( + #pyo3_path::impl_::frompyobject::extract_tuple_struct_field(obj, #struct_name, 0).map(#self_ty) + ), + TokenStream::new(), ), Some(FromPyWithAttribute { value: expr_path, .. - }) => quote! ( - #pyo3_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_with(#expr_path as fn(_) -> _, obj, #struct_name, 0).map(#self_ty) + ), + quote_spanned! { expr_path.span() => + const _: () = { + fn check_from_py_with() { + let e = #pyo3_path::impl_::deprecations::GilRefs::new(); + #pyo3_path::impl_::deprecations::inspect_fn(#expr_path, &e); + e.from_py_with_arg(); + } + }; + }, ), } } } - fn build_tuple_struct(&self, struct_fields: &[TupleStructField], ctx: &Ctx) -> TokenStream { + fn build_tuple_struct( + &self, + struct_fields: &[TupleStructField], + ctx: &Ctx, + ) -> (TokenStream, TokenStream) { let Ctx { pyo3_path } = ctx; let self_ty = &self.path; let struct_name = &self.name(); @@ -309,15 +347,41 @@ impl<'a> Container<'a> { ), } }); - 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), - } + + let deprecations = struct_fields + .iter() + .filter_map(|field| { + let FromPyWithAttribute { + value: expr_path, .. + } = field.from_py_with.as_ref()?; + Some(quote_spanned! { expr_path.span() => + const _: () = { + fn check_from_py_with() { + let e = #pyo3_path::impl_::deprecations::GilRefs::new(); + #pyo3_path::impl_::deprecations::inspect_fn(#expr_path, &e); + e.from_py_with_arg(); + } + }; + }) + }) + .collect::(); + + ( + quote!( + match #pyo3_path::types::PyAnyMethods::extract(obj) { + ::std::result::Result::Ok((#(#field_idents),*)) => ::std::result::Result::Ok(#self_ty(#(#fields),*)), + ::std::result::Result::Err(err) => ::std::result::Result::Err(err), + } + ), + deprecations, ) } - fn build_struct(&self, struct_fields: &[NamedStructField<'_>], ctx: &Ctx) -> TokenStream { + fn build_struct( + &self, + struct_fields: &[NamedStructField<'_>], + ctx: &Ctx, + ) -> (TokenStream, TokenStream) { let Ctx { pyo3_path } = ctx; let self_ty = &self.path; let struct_name = &self.name(); @@ -355,7 +419,29 @@ impl<'a> Container<'a> { fields.push(quote!(#ident: #extractor)); } - quote!(::std::result::Result::Ok(#self_ty{#fields})) + + let deprecations = struct_fields + .iter() + .filter_map(|field| { + let FromPyWithAttribute { + value: expr_path, .. + } = field.from_py_with.as_ref()?; + Some(quote_spanned! { expr_path.span() => + const _: () = { + fn check_from_py_with() { + let e = #pyo3_path::impl_::deprecations::GilRefs::new(); + #pyo3_path::impl_::deprecations::inspect_fn(#expr_path, &e); + e.from_py_with_arg(); + } + }; + }) + }) + .collect::(); + + ( + quote!(::std::result::Result::Ok(#self_ty{#fields})), + deprecations, + ) } } @@ -587,7 +673,7 @@ pub fn build_derive_from_pyobject(tokens: &DeriveInput) -> Result { let ctx = &Ctx::new(&options.krate); let Ctx { pyo3_path } = &ctx; - let derives = match &tokens.data { + let (derives, from_py_with_deprecations) = match &tokens.data { syn::Data::Enum(en) => { if options.transparent || options.annotation.is_some() { bail_spanned!(tokens.span() => "`transparent` or `annotation` is not supported \ @@ -617,5 +703,7 @@ pub fn build_derive_from_pyobject(tokens: &DeriveInput) -> Result { #derives } } + + #from_py_with_deprecations )) } diff --git a/tests/ui/deprecations.rs b/tests/ui/deprecations.rs index 1dcc673a33a..cbaba2fa4ff 100644 --- a/tests/ui/deprecations.rs +++ b/tests/ui/deprecations.rs @@ -89,6 +89,45 @@ fn pyfunction_from_py_with( ) { } +#[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); diff --git a/tests/ui/deprecations.stderr b/tests/ui/deprecations.stderr index 8bd1450874f..d5da8572d02 100644 --- a/tests/ui/deprecations.stderr +++ b/tests/ui/deprecations.stderr @@ -52,10 +52,34 @@ error: use of deprecated method `pyo3::deprecations::GilRefs::::from_py_with_ 87 | #[pyo3(from_py_with = "extract_gil_ref")] _gil_ref: i32, | ^^^^^^^^^^^^^^^^^ -error: use of deprecated method `pyo3::deprecations::GilRefs::>::is_python`: use `wrap_pyfunction_bound!` instead - --> tests/ui/deprecations.rs:94:13 - | -94 | let _ = wrap_pyfunction!(double, py); - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +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:97:27 | - = note: this error originates in the macro `wrap_pyfunction` (in Nightly builds, run with -Z macro-backtrace for more info) +97 | #[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:107:27 + | +107 | #[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:113:31 + | +113 | 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:120:27 + | +120 | #[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:133:13 + | +133 | 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 cedac43dbb90b191a6233d52493610ae521d8a14 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Wed, 20 Mar 2024 12:52:09 +0000 Subject: [PATCH 094/936] add Bound::as_unbound (#3973) * add Bound::as_unbound * Update src/instance.rs --- src/instance.rs | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/src/instance.rs b/src/instance.rs index 024a9fe5b51..5f054d0d2d4 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -465,6 +465,13 @@ impl<'py, T> Bound<'py, T> { unsafe { Py::from_non_null(non_null) } } + /// Removes the connection for this `Bound` from the GIL, allowing + /// it to cross thread boundaries, without transferring ownership. + #[inline] + pub fn as_unbound(&self) -> &Py { + &self.1 + } + /// Casts this `Bound` as the corresponding "GIL Ref" type. /// /// This is a helper to be used for migration from the deprecated "GIL Refs" API. @@ -521,11 +528,11 @@ impl<'py, T> Borrowed<'_, 'py, T> { /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| -> PyResult<()> { /// let tuple = PyTuple::new_bound(py, [1, 2, 3]); - /// + /// /// // borrows from `tuple`, so can only be /// // used while `tuple` stays alive /// let borrowed = tuple.get_borrowed_item(0)?; - /// + /// /// // creates a new owned reference, which /// // can be used indendently of `tuple` /// let bound = borrowed.to_owned(); @@ -1960,8 +1967,8 @@ impl PyObject { mod tests { use super::{Bound, Py, PyObject}; use crate::types::any::PyAnyMethods; - use crate::types::PyCapsule; use crate::types::{dict::IntoPyDict, PyDict, PyString}; + use crate::types::{PyCapsule, PyStringMethods}; use crate::{ffi, Borrowed, PyAny, PyNativeType, PyResult, Python, ToPyObject}; #[test] @@ -2161,6 +2168,20 @@ a = A() }); } + #[test] + fn test_bound_py_conversions() { + Python::with_gil(|py| { + let obj: Bound<'_, PyString> = PyString::new_bound(py, "hello world"); + let obj_unbound: &Py = obj.as_unbound(); + let _: &Bound<'_, PyString> = obj_unbound.bind(py); + + let obj_unbound: Py = obj.unbind(); + let obj: Bound<'_, PyString> = obj_unbound.into_bound(py); + + assert_eq!(obj.to_cow().unwrap(), "hello world"); + }); + } + #[test] fn bound_from_borrowed_ptr_constructors() { // More detailed tests of the underlying semantics in pycell.rs From 870a4bb20dad883a50439b0e1b6cf64a7f15e049 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Wed, 20 Mar 2024 22:35:08 +0000 Subject: [PATCH 095/936] deprecate GIL refs in function argument (#3847) * deprecate GIL Refs in function arguments * fix deprecated gil refs in function arguments * add notes on deprecations limitations to migration guide * Apply suggestions from code review Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> * review: Icxolu * fix proto method extract failure for option * fix gil refs in examples --------- Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> --- examples/getitem/src/lib.rs | 9 +- guide/src/migration.md | 43 ++++++- pyo3-macros-backend/src/method.rs | 118 ++++++++---------- pyo3-macros-backend/src/module.rs | 7 +- pyo3-macros-backend/src/params.rs | 116 +++++++++++++---- pyo3-macros-backend/src/pyclass.rs | 3 +- pyo3-macros-backend/src/pymethod.rs | 94 +++++++++----- pyo3-macros-backend/src/quotes.rs | 4 +- pytests/src/buf_and_str.rs | 6 +- pytests/src/datetime.rs | 34 +++-- pytests/src/dict_iter.rs | 2 +- pytests/src/misc.rs | 4 +- pytests/src/objstore.rs | 2 +- pytests/src/pyclasses.rs | 35 +++--- src/coroutine.rs | 4 +- src/coroutine/waker.rs | 2 +- src/impl_/deprecations.rs | 4 +- src/impl_/extract_argument.rs | 25 +++- src/macros.rs | 6 +- src/pycell.rs | 2 +- src/tests/hygiene/pymethods.rs | 52 ++++---- src/types/bytearray.rs | 8 +- tests/test_arithmetics.rs | 86 ++++++------- tests/test_inheritance.rs | 4 +- tests/test_methods.rs | 81 ++++++------ tests/test_no_imports.rs | 2 +- tests/test_proto_methods.rs | 12 +- tests/test_pyfunction.rs | 6 +- tests/test_sequence.rs | 4 +- tests/test_text_signature.rs | 8 +- tests/test_variable_arguments.rs | 4 +- tests/ui/deprecations.rs | 6 + tests/ui/deprecations.stderr | 52 +++++--- tests/ui/invalid_cancel_handle.stderr | 1 + tests/ui/invalid_frozen_pyclass_borrow.stderr | 13 ++ tests/ui/invalid_pyfunctions.rs | 6 +- tests/ui/invalid_pyfunctions.stderr | 30 ++--- tests/ui/invalid_pymethod_enum.stderr | 13 ++ tests/ui/static_ref.rs | 7 +- tests/ui/static_ref.stderr | 34 +++++ 40 files changed, 597 insertions(+), 352 deletions(-) diff --git a/examples/getitem/src/lib.rs b/examples/getitem/src/lib.rs index eed60076bb8..c3c662ab92f 100644 --- a/examples/getitem/src/lib.rs +++ b/examples/getitem/src/lib.rs @@ -7,7 +7,7 @@ use std::os::raw::c_long; #[derive(FromPyObject)] enum IntOrSlice<'py> { Int(i32), - Slice(&'py PySlice), + Slice(Bound<'py, PySlice>), } #[pyclass] @@ -23,7 +23,7 @@ impl ExampleContainer { ExampleContainer { max_length: 100 } } - fn __getitem__(&self, key: &PyAny) -> PyResult { + fn __getitem__(&self, key: &Bound<'_, PyAny>) -> PyResult { if let Ok(position) = key.extract::() { return Ok(position); } else if let Ok(slice) = key.downcast::() { @@ -63,7 +63,10 @@ impl ExampleContainer { match idx { IntOrSlice::Slice(slice) => { let index = slice.indices(self.max_length as c_long).unwrap(); - println!("Got a slice! {}-{}, step: {}, value: {}", index.start, index.stop, index.step, value); + println!( + "Got a slice! {}-{}, step: {}, value: {}", + index.start, index.stop, index.step, value + ); } IntOrSlice::Int(index) => { println!("Got an index! {} : value: {}", index, value); diff --git a/guide/src/migration.md b/guide/src/migration.md index 16f9616e683..5a362ec448d 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -4,6 +4,8 @@ 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.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. @@ -17,8 +19,11 @@ The recommended steps to update to PyO3 0.21 is as follows: 3. Disable the `gil-refs` feature and migrate off the deprecated APIs The following sections are laid out in this order. +
### Enable the `gil-refs` feature +
+Click to expand To make the transition for the PyO3 ecosystem away from the GIL Refs API as smooth as possible, in PyO3 0.21 no APIs consuming or producing GIL Refs have been altered. Instead, variants using `Bound` smart pointers have been introduced, for example `PyTuple::new_bound` which returns `Bound` is the replacement form of `PyTuple::new`. The GIL Ref APIs have been deprecated, but to make migration easier it is possible to disable these deprecation warnings by enabling the `gil-refs` feature. @@ -41,8 +46,11 @@ After: [dependencies] pyo3 = { version = "0.21", features = ["gil-refs"] } ``` +
### `PyTypeInfo` and `PyTryFrom` have been adjusted +
+Click to expand The `PyTryFrom` trait has aged poorly, its `try_from` method now conflicts with `TryFrom::try_from` in the 2021 edition prelude. A lot of its functionality was also duplicated with `PyTypeInfo`. @@ -81,8 +89,11 @@ 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. @@ -200,20 +211,29 @@ 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` +### Migrating from the GIL Refs API to `Bound` +
+Click to expand -To minimise breakage of code using the GIL-Refs API, the `Bound` smart pointer has been introduced by adding complements to all functions which accept or return GIL Refs. This allows code to migrate by replacing the deprecated APIs with the new ones. +To minimise breakage of code using the GIL Refs API, the `Bound` smart pointer has been introduced by adding complements to all functions which accept or return GIL Refs. This allows code to migrate by replacing the deprecated APIs with the new ones. -To identify what to migrate, temporarily switch off the `gil-refs` feature to see deprecation warnings on all uses of APIs accepting and producing GIL Refs. Over one or more PRs it should be possible to follow the deprecation hints to update code. Depending on your development environment, switching off the `gil-refs` feature may introduce [some very targeted breakages](#deactivating-the-gil-refs-feature), so you may need to fixup those first. +To identify what to migrate, temporarily switch off the `gil-refs` feature to see deprecation warnings on [almost](#cases-where-pyo3-cannot-emit-gil-ref-deprecation-warnings) all uses of APIs accepting and producing GIL Refs . Over one or more PRs it should be possible to follow the deprecation hints to update code. Depending on your development environment, switching off the `gil-refs` feature may introduce [some very targeted breakages](#deactivating-the-gil-refs-feature), so you may need to fixup those first. For example, the following APIs have gained updated variants: - `PyList::new`, `PyTyple::new` and similar constructors have replacements `PyList::new_bound`, `PyTuple::new_bound` etc. @@ -276,7 +296,19 @@ impl<'py> FromPyObject<'py> for MyType { The expectation is that in 0.22 `extract_bound` will have the default implementation removed and in 0.23 `extract` will be removed. +#### Cases where PyO3 cannot emit GIL Ref deprecation warnings + +Despite a large amount of deprecations warnings produced by PyO3 to aid with the transition from GIL Refs to the Bound API, there are a few cases where PyO3 cannot automatically warn on uses of GIL Refs. It is worth checking for these cases manually after the deprecation warnings have all been addressed: + +- Individual implementations of the `FromPyObject` trait cannot be deprecated, so PyO3 cannot warn about uses of code patterns like `.extract<&PyAny>()` which produce a GIL Ref. +- GIL Refs in `#[pyfunction]` arguments emit a warning, but if the GIL Ref is wrapped inside another container such as `Option<&PyAny>` or `Vec<&PyAny>` then PyO3 cannot warn against this. +- The `wrap_pyfunction!(function)(py)` deferred argument form of the `wrap_pyfunction` macro taking `py: Python<'py>` produces a GIL Ref, and due to limitations in type inference PyO3 cannot warn against this specific case. + +
+ ### Deactivating the `gil-refs` feature +
+Click to expand As a final step of migration, deactivating the `gil-refs` feature will set up code for best performance and is intended to set up a forward-compatible API for PyO3 0.22. @@ -284,9 +316,10 @@ At this point code which needed to manage GIL Ref memory can safely remove uses There is one notable API removed when this feature is disabled. `FromPyObject` trait implementations for types which borrow directly from the input data cannot be implemented by PyO3 without GIL Refs (while the migration is ongoing). These types are `&str`, `Cow<'_, str>`, `&[u8]`, `Cow<'_, u8>`. -To ease pain during migration, these types instead implement a new temporary trait `FromPyObjectBound` which is the expected future form of `FromPyObject`. The new temporary trait ensures is that `obj.extract::<&str>()` continues to work (with the new constraint that the extracted value now depends on the input `obj` lifetime), as well for these types in `#[pyfunction]` arguments. +To ease pain during migration, these types instead implement a new temporary trait `FromPyObjectBound` which is the expected future form of `FromPyObject`. The new temporary trait ensures is that `obj.extract::<&str>()` continues to work, as well for these types in `#[pyfunction]` arguments. -An unfortunate final point here is that PyO3 cannot offer this new implementation for `&str` on `abi3` builds for Python older than 3.10. On code which needs `abi3` builds for these older Python versions, many cases of `.extract::<&str>()` may need to be replaced with `.extract::()`, which is string data which borrows from the Python `str` object. Alternatively, use `.extract::>()`, `.extract::()` to copy the data into Rust for these versions. +An unfortunate final point here is that PyO3 cannot offer this new implementation for `&str` on `abi3` builds for Python older than 3.10. On code which needs `abi3` builds for these older Python versions, many cases of `.extract::<&str>()` may need to be replaced with `.extract::()`, which is string data which borrows from the Python `str` object. Alternatively, use `.extract::>()` ro `.extract::()` to copy the data into Rust for these versions. +
## from 0.19.* to 0.20 diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index 93a54395c94..1a46d333c3b 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -8,7 +8,7 @@ use crate::utils::Ctx; use crate::{ attributes::{TextSignatureAttribute, TextSignatureAttributeValue}, deprecations::{Deprecation, Deprecations}, - params::impl_arg_params, + params::{impl_arg_params, Holders}, pyfunction::{ FunctionSignature, PyFunctionArgPyO3Attributes, PyFunctionOptions, SignatureAttribute, }, @@ -108,7 +108,7 @@ impl FnType { &self, cls: Option<&syn::Type>, error_mode: ExtractErrorMode, - holders: &mut Vec, + holders: &mut Holders, ctx: &Ctx, ) -> TokenStream { let Ctx { pyo3_path } = ctx; @@ -186,7 +186,7 @@ impl SelfType { &self, cls: &syn::Type, error_mode: ExtractErrorMode, - holders: &mut Vec, + holders: &mut Holders, ctx: &Ctx, ) -> TokenStream { // Due to use of quote_spanned in this function, need to bind these idents to the @@ -201,17 +201,12 @@ impl SelfType { } else { syn::Ident::new("extract_pyclass_ref", *span) }; - let holder = syn::Ident::new(&format!("holder_{}", holders.len()), *span); + let holder = holders.push_holder(*span); let pyo3_path = pyo3_path.to_tokens_spanned(*span); - holders.push(quote_spanned! { *span => - #[allow(clippy::let_unit_value)] - let mut #holder = #pyo3_path::impl_::extract_argument::FunctionArgumentHolder::INIT; - let mut #slf = #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(#py, &#slf); - }); error_mode.handle_error( quote_spanned! { *span => #pyo3_path::impl_::extract_argument::#method::<#cls>( - &#slf, + #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(#py, &#slf).0, &mut #holder, ) }, @@ -519,10 +514,8 @@ impl<'a> FnSpec<'a> { ); } - let rust_call = |args: Vec, - self_e: &syn::Ident, - holders: &mut Vec| { - let self_arg = self.tp.self_arg(cls, ExtractErrorMode::Raise, holders, ctx); + let rust_call = |args: Vec, holders: &mut Holders| { + let mut self_arg = || self.tp.self_arg(cls, ExtractErrorMode::Raise, holders, ctx); let call = if self.asyncness.is_some() { let throw_callback = if cancel_handle.is_some() { @@ -537,35 +530,30 @@ impl<'a> FnSpec<'a> { }; let future = match self.tp { FnType::Fn(SelfType::Receiver { mutable: false, .. }) => { - holders.pop().unwrap(); // does not actually use holder created by `self_arg` - quote! {{ - #self_e = #pyo3_path::impl_::deprecations::GilRefs::<()>::new(); let __guard = #pyo3_path::impl_::coroutine::RefGuard::<#cls>::new(&#pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(py, &_slf))?; async move { function(&__guard, #(#args),*).await } }} } FnType::Fn(SelfType::Receiver { mutable: true, .. }) => { - holders.pop().unwrap(); // does not actually use holder created by `self_arg` - quote! {{ - #self_e = #pyo3_path::impl_::deprecations::GilRefs::<()>::new(); 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, #(#args),*).await } }} } _ => { + let self_arg = self_arg(); if self_arg.is_empty() { - quote! {{ - #self_e = #pyo3_path::impl_::deprecations::GilRefs::<()>::new(); - function(#(#args),*) - }} + quote! { function(#(#args),*) } } else { - quote! { function({ - let (self_arg, e) = #pyo3_path::impl_::deprecations::inspect_type(#self_arg); - #self_e = e; - self_arg - }, #(#args),*) } + 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),* + ) + } } } }; @@ -586,24 +574,22 @@ impl<'a> FnSpec<'a> { }}; } call - } else if self_arg.is_empty() { - quote! {{ - #self_e = #pyo3_path::impl_::deprecations::GilRefs::<()>::new(); - function(#(#args),*) - }} } else { - quote! { - function({ - let (self_arg, e) = #pyo3_path::impl_::deprecations::inspect_type(#self_arg); - #self_e = e; - self_arg - }, #(#args),*) + let self_arg = self_arg(); + if self_arg.is_empty() { + quote! { function(#(#args),*) } + } else { + let self_checker = holders.push_gil_refs_checker(self_arg.span()); + quote! { + function( + // NB #self_arg includes a comma, so none inserted here + #pyo3_path::impl_::deprecations::inspect_type(#self_arg &#self_checker), + #(#args),* + ) + } } }; - ( - quotes::map_result_into_ptr(quotes::ok_wrap(call, ctx), ctx), - self_arg.span(), - ) + quotes::map_result_into_ptr(quotes::ok_wrap(call, ctx), ctx) }; let func_name = &self.name; @@ -613,10 +599,9 @@ impl<'a> FnSpec<'a> { quote!(#func_name) }; - let self_e = syn::Ident::new("self_e", Span::call_site()); Ok(match self.convention { CallingConvention::Noargs => { - let mut holders = Vec::new(); + let mut holders = Holders::new(); let args = self .signature .arguments @@ -631,8 +616,9 @@ impl<'a> FnSpec<'a> { } }) .collect(); - let (call, self_arg_span) = rust_call(args, &self_e, &mut holders); - let function_arg = quote_spanned! { self_arg_span => #self_e.function_arg(); }; + 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>( @@ -641,19 +627,19 @@ impl<'a> FnSpec<'a> { ) -> #pyo3_path::PyResult<*mut #pyo3_path::ffi::PyObject> { let _slf_ref = &_slf; let function = #rust_name; // Shadow the function name to avoid #3017 - let #self_e; - #( #holders )* + #init_holders let result = #call; - #function_arg + #check_gil_refs result } } } CallingConvention::Fastcall => { - let mut holders = Vec::new(); + let mut holders = Holders::new(); let (arg_convert, args) = impl_arg_params(self, cls, true, &mut holders, ctx)?; - let (call, self_arg_span) = rust_call(args, &self_e, &mut holders); - let function_arg = quote_spanned! { self_arg_span => #self_e.function_arg(); }; + 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>( @@ -665,20 +651,20 @@ impl<'a> FnSpec<'a> { ) -> #pyo3_path::PyResult<*mut #pyo3_path::ffi::PyObject> { let _slf_ref = &_slf; let function = #rust_name; // Shadow the function name to avoid #3017 - let #self_e; #arg_convert - #( #holders )* + #init_holders let result = #call; - #function_arg + #check_gil_refs result } } } CallingConvention::Varargs => { - let mut holders = Vec::new(); + let mut holders = Holders::new(); let (arg_convert, args) = impl_arg_params(self, cls, false, &mut holders, ctx)?; - let (call, self_arg_span) = rust_call(args, &self_e, &mut holders); - let function_arg = quote_spanned! { self_arg_span => #self_e.function_arg(); }; + 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>( @@ -689,22 +675,23 @@ impl<'a> FnSpec<'a> { ) -> #pyo3_path::PyResult<*mut #pyo3_path::ffi::PyObject> { let _slf_ref = &_slf; let function = #rust_name; // Shadow the function name to avoid #3017 - let #self_e; #arg_convert - #( #holders )* + #init_holders let result = #call; - #function_arg + #check_gil_refs result } } } CallingConvention::TpNew => { - let mut holders = Vec::new(); + let mut holders = Holders::new(); let (arg_convert, args) = impl_arg_params(self, cls, false, &mut holders, ctx)?; let self_arg = self .tp .self_arg(cls, ExtractErrorMode::Raise, &mut holders, ctx); let call = quote! { #rust_name(#self_arg #(#args),*) }; + let init_holders = holders.init_holders(ctx); + let check_gil_refs = holders.check_gil_refs(); quote! { unsafe fn #ident( py: #pyo3_path::Python<'_>, @@ -716,9 +703,10 @@ impl<'a> FnSpec<'a> { let _slf_ref = &_slf; let function = #rust_name; // Shadow the function name to avoid #3017 #arg_convert - #( #holders )* + #init_holders let result = #call; let initializer: #pyo3_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 2515d519287..dc2b3bfc86b 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -305,10 +305,11 @@ pub fn pymodule_function_impl(mut function: syn::ItemFn) -> Result .filter_map(|param| { if let syn::FnArg::Typed(pat_type) = param { if let syn::Pat::Ident(pat_ident) = &*pat_type.pat { - let ident = &pat_ident.ident; + let ident: &syn::Ident = &pat_ident.ident; return Some([ - parse_quote! { let (#ident, e) = #pyo3_path::impl_::deprecations::inspect_type(#ident); }, - parse_quote_spanned! { pat_type.span() => e.function_arg(); }, + 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(); }, ]); } } diff --git a/pyo3-macros-backend/src/params.rs b/pyo3-macros-backend/src/params.rs index ae1661fe2c1..cab28698b71 100644 --- a/pyo3-macros-backend/src/params.rs +++ b/pyo3-macros-backend/src/params.rs @@ -9,6 +9,53 @@ use quote::{quote, quote_spanned}; use syn::spanned::Spanned; use syn::Result; +pub struct Holders { + holders: Vec, + gil_refs_checkers: Vec, +} + +impl Holders { + pub fn new() -> Self { + Holders { + holders: Vec::new(), + gil_refs_checkers: Vec::new(), + } + } + + pub fn push_holder(&mut self, span: Span) -> syn::Ident { + let holder = syn::Ident::new(&format!("holder_{}", self.holders.len()), span); + self.holders.push(holder.clone()); + holder + } + + pub fn push_gil_refs_checker(&mut self, span: Span) -> syn::Ident { + let gil_refs_checker = syn::Ident::new( + &format!("gil_refs_checker_{}", self.gil_refs_checkers.len()), + span, + ); + self.gil_refs_checkers.push(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; + 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(|e| quote_spanned! { e.span() => #e.function_arg(); }) + .collect() + } +} + /// Return true if the argument list is simply (*args, **kwds). pub fn is_forwarded_args(signature: &FunctionSignature<'_>) -> bool { matches!( @@ -26,11 +73,22 @@ pub fn is_forwarded_args(signature: &FunctionSignature<'_>) -> bool { ) } +fn check_arg_for_gil_refs( + tokens: TokenStream, + gil_refs_checker: syn::Ident, + ctx: &Ctx, +) -> TokenStream { + let Ctx { pyo3_path } = ctx; + quote! { + #pyo3_path::impl_::deprecations::inspect_type(#tokens, &#gil_refs_checker) + } +} + pub fn impl_arg_params( spec: &FnSpec<'_>, self_: Option<&syn::Type>, fastcall: bool, - holders: &mut Vec, + holders: &mut Holders, ctx: &Ctx, ) -> Result<(TokenStream, Vec)> { let args_array = syn::Ident::new("output", Span::call_site()); @@ -61,7 +119,15 @@ pub fn impl_arg_params( .arguments .iter() .enumerate() - .map(|(i, arg)| impl_arg_param(arg, i, &mut 0, &args_array, holders, ctx)) + .map(|(i, arg)| { + impl_arg_param(arg, i, &mut 0, &args_array, holders, ctx).map(|tokens| { + check_arg_for_gil_refs( + tokens, + holders.push_gil_refs_checker(arg.ty.span()), + ctx, + ) + }) + }) .collect::>()?; return Ok(( quote! { @@ -101,7 +167,11 @@ pub fn impl_arg_params( .arguments .iter() .enumerate() - .map(|(i, arg)| impl_arg_param(arg, i, &mut option_pos, &args_array, holders, ctx)) + .map(|(i, arg)| { + impl_arg_param(arg, i, &mut option_pos, &args_array, holders, ctx).map(|tokens| { + check_arg_for_gil_refs(tokens, holders.push_gil_refs_checker(arg.ty.span()), ctx) + }) + }) .collect::>()?; let args_handler = if spec.signature.python_signature.varargs.is_some() { @@ -169,7 +239,7 @@ fn impl_arg_param( pos: usize, option_pos: &mut usize, args_array: &syn::Ident, - holders: &mut Vec, + holders: &mut Holders, ctx: &Ctx, ) -> Result { let Ctx { pyo3_path } = ctx; @@ -192,21 +262,12 @@ fn impl_arg_param( let name = arg.name; let name_str = name.to_string(); - let mut push_holder = || { - let holder = syn::Ident::new(&format!("holder_{}", holders.len()), arg.ty.span()); - holders.push(quote_arg_span! { - #[allow(clippy::let_unit_value)] - let mut #holder = #pyo3_path::impl_::extract_argument::FunctionArgumentHolder::INIT; - }); - holder - }; - if arg.is_varargs { ensure_spanned!( arg.optional.is_none(), arg.name.span() => "args cannot be optional" ); - let holder = push_holder(); + let holder = holders.push_holder(arg.ty.span()); return Ok(quote_arg_span! { #pyo3_path::impl_::extract_argument::extract_argument( &_args, @@ -219,7 +280,7 @@ fn impl_arg_param( arg.optional.is_some(), arg.name.span() => "kwargs must be Option<_>" ); - let holder = push_holder(); + let holder = holders.push_holder(arg.name.span()); return Ok(quote_arg_span! { #pyo3_path::impl_::extract_argument::extract_optional_argument( _kwargs.as_deref(), @@ -254,12 +315,14 @@ fn impl_arg_param( let from_py_with = syn::Ident::new(&format!("from_py_with_{}", pos), Span::call_site()); if let Some(default) = default { quote_arg_span! { - #[allow(clippy::redundant_closure)] #pyo3_path::impl_::extract_argument::from_py_with_with_default( #arg_value.as_deref(), #name_str, #from_py_with as fn(_) -> _, - || #default + #[allow(clippy::redundant_closure)] + { + || #default + } )? } } else { @@ -272,29 +335,33 @@ fn impl_arg_param( } } } else if arg.optional.is_some() { - let holder = push_holder(); + let holder = holders.push_holder(arg.name.span()); quote_arg_span! { - #[allow(clippy::redundant_closure)] #pyo3_path::impl_::extract_argument::extract_optional_argument( #arg_value.as_deref(), &mut #holder, #name_str, - || #default + #[allow(clippy::redundant_closure)] + { + || #default + } )? } } else if let Some(default) = default { - let holder = push_holder(); + let holder = holders.push_holder(arg.name.span()); quote_arg_span! { - #[allow(clippy::redundant_closure)] #pyo3_path::impl_::extract_argument::extract_argument_with_default( #arg_value.as_deref(), &mut #holder, #name_str, - || #default + #[allow(clippy::redundant_closure)] + { + || #default + } )? } } else { - let holder = push_holder(); + let holder = holders.push_holder(arg.name.span()); quote_arg_span! { #pyo3_path::impl_::extract_argument::extract_argument( &#pyo3_path::impl_::extract_argument::unwrap_required_argument(#arg_value), @@ -303,5 +370,6 @@ fn impl_arg_param( )? } }; + Ok(tokens) } diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 27a7ee969d5..5122d205640 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -736,10 +736,11 @@ fn impl_simple_enum( fn __pyo3__richcmp__( &self, py: #pyo3_path::Python, - other: &#pyo3_path::PyAny, + 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 => { diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index 1c19112d183..4e6f46d96d5 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -2,6 +2,7 @@ use std::borrow::Cow; use crate::attributes::{NameAttribute, RenamingRule}; use crate::method::{CallingConvention, ExtractErrorMode}; +use crate::params::Holders; use crate::utils::Ctx; use crate::utils::PythonDoc; use crate::{ @@ -510,7 +511,7 @@ fn impl_call_setter( cls: &syn::Type, spec: &FnSpec<'_>, self_type: &SelfType, - holders: &mut Vec, + holders: &mut Holders, ctx: &Ctx, ) -> syn::Result { let (py_arg, args) = split_off_python_arg(&spec.signature.arguments); @@ -544,7 +545,7 @@ pub fn impl_py_setter_def( let Ctx { pyo3_path } = ctx; let python_name = property_type.null_terminated_python_name()?; let doc = property_type.doc(); - let mut holders = Vec::new(); + let mut holders = Holders::new(); let setter_impl = match property_type { PropertyType::Descriptor { field_index, field, .. @@ -596,6 +597,8 @@ 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( @@ -609,8 +612,10 @@ pub fn impl_py_setter_def( #pyo3_path::exceptions::PyAttributeError::new_err("can't delete attribute") })?; let _val = #pyo3_path::FromPyObject::extract_bound(_value.into())?; - #( #holders )* - #pyo3_path::callback::convert(py, #setter_impl) + #init_holders + let result = #setter_impl; + #check_gil_refs + #pyo3_path::callback::convert(py, result) } }; @@ -635,7 +640,7 @@ fn impl_call_getter( cls: &syn::Type, spec: &FnSpec<'_>, self_type: &SelfType, - holders: &mut Vec, + holders: &mut Holders, ctx: &Ctx, ) -> syn::Result { let (py_arg, args) = split_off_python_arg(&spec.signature.arguments); @@ -665,7 +670,7 @@ pub fn impl_py_getter_def( let python_name = property_type.null_terminated_python_name()?; let doc = property_type.doc(); - let mut holders = Vec::new(); + let mut holders = Holders::new(); let body = match property_type { PropertyType::Descriptor { field_index, field, .. @@ -731,14 +736,17 @@ pub fn impl_py_getter_def( } } + let init_holders = holders.init_holders(ctx); + let check_gil_refs = holders.check_gil_refs(); let associated_method = quote! { #cfg_attrs unsafe fn #wrapper_ident( py: #pyo3_path::Python<'_>, _slf: *mut #pyo3_path::ffi::PyObject ) -> #pyo3_path::PyResult<*mut #pyo3_path::ffi::PyObject> { - #( #holders )* + #init_holders let result = #body; + #check_gil_refs result } }; @@ -966,7 +974,7 @@ impl Ty { ident: &syn::Ident, arg: &FnArg<'_>, extract_error_mode: ExtractErrorMode, - holders: &mut Vec, + holders: &mut Holders, ctx: &Ctx, ) -> TokenStream { let Ctx { pyo3_path } = ctx; @@ -976,7 +984,9 @@ impl Ty { extract_error_mode, holders, &name_str, - quote! { #ident },ctx + quote! { #ident }, + arg.ty.span(), + ctx ), Ty::MaybeNullObject => extract_object( extract_error_mode, @@ -988,32 +998,40 @@ impl Ty { } else { #ident } - },ctx + }, + arg.ty.span(), + ctx ), Ty::NonNullObject => extract_object( extract_error_mode, holders, &name_str, - quote! { #ident.as_ptr() },ctx + quote! { #ident.as_ptr() }, + arg.ty.span(), + ctx ), Ty::IPowModulo => extract_object( extract_error_mode, holders, &name_str, - quote! { #ident.as_ptr() },ctx + quote! { #ident.as_ptr() }, + arg.ty.span(), + ctx ), Ty::CompareOp => extract_error_mode.handle_error( quote! { #pyo3_path::class::basic::CompareOp::from_raw(#ident) .ok_or_else(|| #pyo3_path::exceptions::PyValueError::new_err("invalid comparison operator")) - },ctx + }, + ctx ), Ty::PySsizeT => { 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())) - },ctx + }, + ctx ) } // Just pass other types through unmodified @@ -1024,27 +1042,28 @@ impl Ty { fn extract_object( extract_error_mode: ExtractErrorMode, - holders: &mut Vec, + holders: &mut Holders, name: &str, source_ptr: TokenStream, + span: Span, ctx: &Ctx, ) -> TokenStream { let Ctx { pyo3_path } = ctx; - let holder = syn::Ident::new(&format!("holder_{}", holders.len()), Span::call_site()); - holders.push(quote! { - #[allow(clippy::let_unit_value)] - let mut #holder = #pyo3_path::impl_::extract_argument::FunctionArgumentHolder::INIT; - }); - extract_error_mode.handle_error( + 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( quote! { #pyo3_path::impl_::extract_argument::extract_argument( - &#pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(py, &#source_ptr), + #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(py, &#source_ptr).0, &mut #holder, #name ) }, ctx, - ) + ); + quote! { + #pyo3_path::impl_::deprecations::inspect_type(#extracted, &#gil_refs_checker) + } } enum ReturnMode { @@ -1054,13 +1073,15 @@ enum ReturnMode { } impl ReturnMode { - fn return_call_output(&self, call: TokenStream, ctx: &Ctx) -> TokenStream { + fn return_call_output(&self, call: TokenStream, ctx: &Ctx, holders: &Holders) -> TokenStream { let Ctx { pyo3_path } = ctx; + let check_gil_refs = holders.check_gil_refs(); match self { ReturnMode::Conversion(conversion) => { 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) } } @@ -1070,12 +1091,14 @@ 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) }, @@ -1176,7 +1199,7 @@ impl SlotDef { .collect(); let wrapper_ident = format_ident!("__pymethod_{}__", method_name); let ret_ty = ret_ty.ffi_type(ctx); - let mut holders = Vec::new(); + let mut holders = Holders::new(); let body = generate_method_body( cls, spec, @@ -1187,6 +1210,7 @@ impl SlotDef { ctx, )?; let name = spec.name; + let holders = holders.init_holders(ctx); let associated_method = quote! { unsafe fn #wrapper_ident( py: #pyo3_path::Python<'_>, @@ -1195,7 +1219,7 @@ impl SlotDef { ) -> #pyo3_path::PyResult<#ret_ty> { let function = #cls::#name; // Shadow the method name to avoid #3017 let _slf = _raw_slf; - #( #holders )* + #holders #body } }; @@ -1229,7 +1253,7 @@ fn generate_method_body( spec: &FnSpec<'_>, arguments: &[Ty], extract_error_mode: ExtractErrorMode, - holders: &mut Vec, + holders: &mut Holders, return_mode: Option<&ReturnMode>, ctx: &Ctx, ) -> Result { @@ -1241,9 +1265,14 @@ 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) + return_mode.return_call_output(call, ctx, holders) } else { - quote! { #pyo3_path::callback::convert(py, #call) } + let check_gil_refs = holders.check_gil_refs(); + quote! { + let result = #call; + #check_gil_refs; + #pyo3_path::callback::convert(py, result) + } }) } @@ -1294,7 +1323,7 @@ impl SlotFragmentDef { let arg_idents: &Vec<_> = &(0..arguments.len()) .map(|i| format_ident!("arg{}", i)) .collect(); - let mut holders = Vec::new(); + let mut holders = Holders::new(); let body = generate_method_body( cls, spec, @@ -1305,6 +1334,7 @@ impl SlotFragmentDef { ctx, )?; let ret_ty = ret_ty.ffi_type(ctx); + let holders = holders.init_holders(ctx); Ok(quote! { impl #cls { unsafe fn #wrapper_ident( @@ -1313,7 +1343,7 @@ impl SlotFragmentDef { #(#arg_idents: #arg_types),* ) -> #pyo3_path::PyResult<#ret_ty> { let _slf = _raw_slf; - #( #holders )* + #holders #body } } @@ -1412,7 +1442,7 @@ fn extract_proto_arguments( spec: &FnSpec<'_>, proto_args: &[Ty], extract_error_mode: ExtractErrorMode, - holders: &mut Vec, + holders: &mut Holders, ctx: &Ctx, ) -> Result> { let mut args = Vec::with_capacity(spec.signature.arguments.len()); diff --git a/pyo3-macros-backend/src/quotes.rs b/pyo3-macros-backend/src/quotes.rs index 0219cb9ae7f..ceef23fb034 100644 --- a/pyo3-macros-backend/src/quotes.rs +++ b/pyo3-macros-backend/src/quotes.rs @@ -19,7 +19,5 @@ pub(crate) fn ok_wrap(obj: TokenStream, ctx: &Ctx) -> TokenStream { 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) - } + quote! { #pyo3_path::impl_::wrap::map_result_into_ptr(py, #result) } } diff --git a/pytests/src/buf_and_str.rs b/pytests/src/buf_and_str.rs index e9651e0cfd9..879d76af883 100644 --- a/pytests/src/buf_and_str.rs +++ b/pytests/src/buf_and_str.rs @@ -17,19 +17,19 @@ impl BytesExtractor { } #[staticmethod] - pub fn from_bytes(bytes: &PyBytes) -> PyResult { + pub fn from_bytes(bytes: &Bound<'_, PyBytes>) -> PyResult { let byte_vec: Vec = bytes.extract()?; Ok(byte_vec.len()) } #[staticmethod] - pub fn from_str(string: &PyString) -> PyResult { + pub fn from_str(string: &Bound<'_, PyString>) -> PyResult { let rust_string: String = string.extract()?; Ok(rust_string.len()) } #[staticmethod] - pub fn from_str_lossy(string: &PyString) -> usize { + pub fn from_str_lossy(string: &Bound<'_, PyString>) -> usize { let rust_string_lossy: String = string.to_string_lossy().to_string(); rust_string_lossy.len() } diff --git a/pytests/src/datetime.rs b/pytests/src/datetime.rs index aeb57240c15..d0de99ae406 100644 --- a/pytests/src/datetime.rs +++ b/pytests/src/datetime.rs @@ -12,8 +12,11 @@ fn make_date(py: Python<'_>, year: i32, month: u8, day: u8) -> PyResult(py: Python<'p>, d: &PyDate) -> Bound<'p, PyTuple> { - PyTuple::new_bound(py, [d.get_year(), d.get_month() as i32, d.get_day() as i32]) +fn get_date_tuple<'py>(d: &Bound<'py, PyDate>) -> Bound<'py, PyTuple> { + PyTuple::new_bound( + d.py(), + [d.get_year(), d.get_month() as i32, d.get_day() as i32], + ) } #[pyfunction] @@ -48,9 +51,9 @@ fn time_with_fold<'py>( } #[pyfunction] -fn get_time_tuple<'p>(py: Python<'p>, dt: &PyTime) -> Bound<'p, PyTuple> { +fn get_time_tuple<'py>(dt: &Bound<'py, PyTime>) -> Bound<'py, PyTuple> { PyTuple::new_bound( - py, + dt.py(), [ dt.get_hour() as u32, dt.get_minute() as u32, @@ -61,9 +64,9 @@ fn get_time_tuple<'p>(py: Python<'p>, dt: &PyTime) -> Bound<'p, PyTuple> { } #[pyfunction] -fn get_time_tuple_fold<'p>(py: Python<'p>, dt: &PyTime) -> Bound<'p, PyTuple> { +fn get_time_tuple_fold<'py>(dt: &Bound<'py, PyTime>) -> Bound<'py, PyTuple> { PyTuple::new_bound( - py, + dt.py(), [ dt.get_hour() as u32, dt.get_minute() as u32, @@ -123,9 +126,9 @@ fn make_datetime<'py>( } #[pyfunction] -fn get_datetime_tuple<'py>(py: Python<'py>, dt: &Bound<'py, PyDateTime>) -> Bound<'py, PyTuple> { +fn get_datetime_tuple<'py>(dt: &Bound<'py, PyDateTime>) -> Bound<'py, PyTuple> { PyTuple::new_bound( - py, + dt.py(), [ dt.get_year(), dt.get_month() as i32, @@ -139,12 +142,9 @@ fn get_datetime_tuple<'py>(py: Python<'py>, dt: &Bound<'py, PyDateTime>) -> Boun } #[pyfunction] -fn get_datetime_tuple_fold<'py>( - py: Python<'py>, - dt: &Bound<'py, PyDateTime>, -) -> Bound<'py, PyTuple> { +fn get_datetime_tuple_fold<'py>(dt: &Bound<'py, PyDateTime>) -> Bound<'py, PyTuple> { PyTuple::new_bound( - py, + dt.py(), [ dt.get_year(), dt.get_month() as i32, @@ -187,12 +187,8 @@ impl TzClass { TzClass {} } - fn utcoffset<'py>( - &self, - py: Python<'py>, - _dt: &Bound<'py, PyDateTime>, - ) -> PyResult> { - PyDelta::new_bound(py, 0, 3600, 0, true) + fn utcoffset<'py>(&self, dt: &Bound<'py, PyDateTime>) -> PyResult> { + PyDelta::new_bound(dt.py(), 0, 3600, 0, true) } fn tzname(&self, _dt: &Bound<'_, PyDateTime>) -> String { diff --git a/pytests/src/dict_iter.rs b/pytests/src/dict_iter.rs index 985c929792f..c312fbb5f83 100644 --- a/pytests/src/dict_iter.rs +++ b/pytests/src/dict_iter.rs @@ -20,7 +20,7 @@ impl DictSize { DictSize { expected } } - fn iter_dict(&mut self, _py: Python<'_>, dict: &PyDict) -> PyResult { + fn iter_dict(&mut self, _py: Python<'_>, dict: &Bound<'_, PyDict>) -> PyResult { let mut seen = 0u32; for (sym, values) in dict { seen += 1; diff --git a/pytests/src/misc.rs b/pytests/src/misc.rs index 3b893ccd1f6..7704098bd5b 100644 --- a/pytests/src/misc.rs +++ b/pytests/src/misc.rs @@ -8,8 +8,8 @@ fn issue_219() { } #[pyfunction] -fn get_type_full_name(obj: &PyAny) -> PyResult> { - obj.get_type().name() +fn get_type_full_name(obj: &Bound<'_, PyAny>) -> PyResult { + obj.get_type().name().map(Cow::into_owned) } #[pyfunction] diff --git a/pytests/src/objstore.rs b/pytests/src/objstore.rs index 440f29fad63..9a005c0ec97 100644 --- a/pytests/src/objstore.rs +++ b/pytests/src/objstore.rs @@ -13,7 +13,7 @@ impl ObjStore { ObjStore::default() } - fn push(&mut self, py: Python<'_>, obj: &PyAny) { + fn push(&mut self, py: Python<'_>, obj: &Bound<'_, PyAny>) { self.obj.push(obj.to_object(py)); } } diff --git a/pytests/src/pyclasses.rs b/pytests/src/pyclasses.rs index 1f3baec2755..8e957c77955 100644 --- a/pytests/src/pyclasses.rs +++ b/pytests/src/pyclasses.rs @@ -57,22 +57,27 @@ impl AssertingBaseClass { } } -#[pyclass(subclass)] -#[derive(Clone, Debug)] -struct AssertingBaseClassGilRef; +#[allow(deprecated)] +mod deprecated { + use super::*; -#[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 - ))); + #[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) } - Ok(Self) } } @@ -84,7 +89,7 @@ pub fn pyclasses(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; - m.add_class::()?; + m.add_class::()?; m.add_class::()?; Ok(()) } diff --git a/src/coroutine.rs b/src/coroutine.rs index b24197ad593..f2feab4af16 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}, - IntoPy, Py, PyAny, PyErr, PyObject, PyResult, Python, + Bound, IntoPy, Py, PyAny, PyErr, PyObject, PyResult, Python, }; pub(crate) mod cancel; @@ -143,7 +143,7 @@ impl Coroutine { } } - fn send(&mut self, py: Python<'_>, _value: &PyAny) -> PyResult { + fn send(&mut self, py: Python<'_>, _value: &Bound<'_, PyAny>) -> PyResult { self.poll(py, None) } diff --git a/src/coroutine/waker.rs b/src/coroutine/waker.rs index b524b6d7298..fc7c54e1f5a 100644 --- a/src/coroutine/waker.rs +++ b/src/coroutine/waker.rs @@ -97,7 +97,7 @@ impl LoopAndFuture { /// Future can be cancelled by the event loop before being waken. /// See #[pyfunction(crate = "crate")] -fn release_waiter(future: &PyAny) -> PyResult<()> { +fn release_waiter(future: &Bound<'_, PyAny>) -> PyResult<()> { let done = future.call_method0(intern!(future.py(), "done"))?; if !done.extract::()? { future.call_method1(intern!(future.py(), "set_result"), (future.py().None(),))?; diff --git a/src/impl_/deprecations.rs b/src/impl_/deprecations.rs index 0c749ff985b..459eba913d3 100644 --- a/src/impl_/deprecations.rs +++ b/src/impl_/deprecations.rs @@ -5,8 +5,8 @@ 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) -> (T, GilRefs) { - (t, GilRefs::new()) +pub fn inspect_type(t: T, _: &GilRefs) -> T { + t } pub fn inspect_fn(f: fn(A) -> PyResult, _: &GilRefs
) -> fn(A) -> PyResult { diff --git a/src/impl_/extract_argument.rs b/src/impl_/extract_argument.rs index 82285b3d50c..4dcef02c5e6 100644 --- a/src/impl_/extract_argument.rs +++ b/src/impl_/extract_argument.rs @@ -41,14 +41,27 @@ impl<'a, 'py, T: 'py> PyFunctionArgument<'a, 'py> for &'a Bound<'py, T> where T: PyTypeCheck, { - type Holder = Option<&'a Bound<'py, T>>; + type Holder = Option<()>; #[inline] - fn extract( - obj: &'a Bound<'py, PyAny>, - holder: &'a mut Option<&'a Bound<'py, T>>, - ) -> PyResult { - Ok(holder.insert(obj.downcast()?)) + fn extract(obj: &'a Bound<'py, PyAny>, _: &'a mut Option<()>) -> PyResult { + obj.downcast().map_err(Into::into) + } +} + +impl<'a, 'py, T: 'py> PyFunctionArgument<'a, 'py> for Option<&'a Bound<'py, T>> +where + T: PyTypeCheck, +{ + type Holder = (); + + #[inline] + fn extract(obj: &'a Bound<'py, PyAny>, _: &'a mut ()) -> PyResult { + if obj.is_none() { + Ok(None) + } else { + Ok(Some(obj.downcast()?)) + } } } diff --git a/src/macros.rs b/src/macros.rs index db409515085..648bac180ff 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -144,8 +144,10 @@ macro_rules! wrap_pyfunction { }; ($function:path, $py_or_module:expr) => {{ use $function as wrapped_pyfunction; - let (py_or_module, e) = $crate::impl_::deprecations::inspect_type($py_or_module); - e.is_python(); + let check_gil_refs = $crate::impl_::deprecations::GilRefs::new(); + let py_or_module = + $crate::impl_::deprecations::inspect_type($py_or_module, &check_gil_refs); + check_gil_refs.is_python(); $crate::impl_::pyfunction::WrapPyFunctionArg::wrap_pyfunction( py_or_module, &wrapped_pyfunction::DEF, diff --git a/src/pycell.rs b/src/pycell.rs index 636fb90cef6..ccc55a756d6 100644 --- a/src/pycell.rs +++ b/src/pycell.rs @@ -149,12 +149,12 @@ //! //! It is better to write that function like this: //! ```rust +//! # #![allow(deprecated)] //! # use pyo3::prelude::*; //! # #[pyclass] //! # pub struct Number { //! # inner: u32, //! # } -//! # #[allow(deprecated)] //! #[pyfunction] //! fn swap_numbers(a: &PyCell, b: &PyCell) { //! // Check that the pointers are unequal diff --git a/src/tests/hygiene/pymethods.rs b/src/tests/hygiene/pymethods.rs index 2ffaf68690f..020f983be31 100644 --- a/src/tests/hygiene/pymethods.rs +++ b/src/tests/hygiene/pymethods.rs @@ -28,7 +28,7 @@ impl Dummy { } fn __format__(&self, format_spec: ::std::string::String) -> ::std::string::String { - ::std::panic!("unimplemented isn't hygienic before 1.50") + ::std::unimplemented!() } fn __lt__(&self, other: &Self) -> bool { @@ -63,12 +63,12 @@ impl Dummy { // Customizing attribute access ////////////////////// - fn __getattr__(&self, name: ::std::string::String) -> &crate::PyAny { - ::std::panic!("unimplemented isn't hygienic before 1.50") + fn __getattr__(&self, name: ::std::string::String) -> &crate::Bound<'_, crate::PyAny> { + ::std::unimplemented!() } - fn __getattribute__(&self, name: ::std::string::String) -> &crate::PyAny { - ::std::panic!("unimplemented isn't hygienic before 1.50") + fn __getattribute__(&self, name: ::std::string::String) -> &crate::Bound<'_, crate::PyAny> { + ::std::unimplemented!() } fn __setattr__(&mut self, name: ::std::string::String, value: ::std::string::String) {} @@ -85,17 +85,27 @@ impl Dummy { fn __get__( &self, - instance: &crate::PyAny, - owner: &crate::PyAny, - ) -> crate::PyResult<&crate::PyAny> { - ::std::panic!("unimplemented isn't hygienic before 1.50") + instance: &crate::Bound<'_, crate::PyAny>, + owner: &crate::Bound<'_, crate::PyAny>, + ) -> crate::PyResult<&crate::Bound<'_, crate::PyAny>> { + ::std::unimplemented!() } - fn __set__(&self, instance: &crate::PyAny, owner: &crate::PyAny) {} + fn __set__( + &self, + instance: &crate::Bound<'_, crate::PyAny>, + owner: &crate::Bound<'_, crate::PyAny>, + ) { + } - fn __delete__(&self, instance: &crate::PyAny) {} + fn __delete__(&self, instance: &crate::Bound<'_, crate::PyAny>) {} - fn __set_name__(&self, owner: &crate::PyAny, name: &crate::PyAny) {} + fn __set_name__( + &self, + owner: &crate::Bound<'_, crate::PyAny>, + name: &crate::Bound<'_, crate::PyAny>, + ) { + } ////////////////////// // Implementing Descriptors @@ -323,9 +333,9 @@ impl Dummy { fn __exit__( &mut self, - exc_type: &crate::PyAny, - exc_value: &crate::PyAny, - traceback: &crate::PyAny, + exc_type: &crate::Bound<'_, crate::PyAny>, + exc_value: &crate::Bound<'_, crate::PyAny>, + traceback: &crate::Bound<'_, crate::PyAny>, ) { } @@ -361,9 +371,9 @@ impl Dummy { fn __aexit__( &mut self, - exc_type: &crate::PyAny, - exc_value: &crate::PyAny, - traceback: &crate::PyAny, + exc_type: &crate::Bound<'_, crate::PyAny>, + exc_value: &crate::Bound<'_, crate::PyAny>, + traceback: &crate::Bound<'_, crate::PyAny>, ) { } @@ -378,10 +388,10 @@ impl Dummy { #[pyo3(signature = (*_args, **_kwds))] fn __call__( &self, - _args: &crate::types::PyTuple, - _kwds: ::std::option::Option<&crate::types::PyDict>, + _args: &crate::Bound<'_, crate::types::PyTuple>, + _kwds: ::std::option::Option<&crate::Bound<'_, crate::types::PyDict>>, ) -> crate::PyResult { - ::std::panic!("unimplemented isn't hygienic before 1.50") + ::std::unimplemented!() } #[new] fn new(a: u8) -> Self { diff --git a/src/types/bytearray.rs b/src/types/bytearray.rs index 55cda1a355a..7227519975b 100644 --- a/src/types/bytearray.rs +++ b/src/types/bytearray.rs @@ -171,7 +171,7 @@ impl PyByteArray { /// use pyo3::types::PyByteArray; /// /// #[pyfunction] - /// fn a_valid_function(bytes: &PyByteArray) -> PyResult<()> { + /// fn a_valid_function(bytes: &Bound<'_, PyByteArray>) -> PyResult<()> { /// let section = { /// // SAFETY: We promise to not let the interpreter regain control /// // or invoke any PyO3 APIs while using the slice. @@ -224,7 +224,7 @@ impl PyByteArray { /// /// # #[allow(dead_code)] /// #[pyfunction] - /// fn bug(py: Python<'_>, bytes: &PyByteArray) { + /// fn bug(py: Python<'_>, bytes: &Bound<'_, PyByteArray>) { /// let slice = unsafe { bytes.as_bytes() }; /// /// // This explicitly yields control back to the Python interpreter... @@ -333,7 +333,7 @@ pub trait PyByteArrayMethods<'py>: crate::sealed::Sealed { /// use pyo3::types::PyByteArray; /// /// #[pyfunction] - /// fn a_valid_function(bytes: &PyByteArray) -> PyResult<()> { + /// fn a_valid_function(bytes: &Bound<'_, PyByteArray>) -> PyResult<()> { /// let section = { /// // SAFETY: We promise to not let the interpreter regain control /// // or invoke any PyO3 APIs while using the slice. @@ -386,7 +386,7 @@ pub trait PyByteArrayMethods<'py>: crate::sealed::Sealed { /// /// # #[allow(dead_code)] /// #[pyfunction] - /// fn bug(py: Python<'_>, bytes: &PyByteArray) { + /// fn bug(py: Python<'_>, bytes: &Bound<'_, PyByteArray>) { /// let slice = unsafe { bytes.as_bytes() }; /// /// // This explicitly yields control back to the Python interpreter... diff --git a/tests/test_arithmetics.rs b/tests/test_arithmetics.rs index 88f9ca44c28..da8d72ea9cc 100644 --- a/tests/test_arithmetics.rs +++ b/tests/test_arithmetics.rs @@ -166,43 +166,43 @@ impl BinaryArithmetic { "BA" } - fn __add__(&self, rhs: &PyAny) -> String { + fn __add__(&self, rhs: &Bound<'_, PyAny>) -> String { format!("BA + {:?}", rhs) } - fn __sub__(&self, rhs: &PyAny) -> String { + fn __sub__(&self, rhs: &Bound<'_, PyAny>) -> String { format!("BA - {:?}", rhs) } - fn __mul__(&self, rhs: &PyAny) -> String { + fn __mul__(&self, rhs: &Bound<'_, PyAny>) -> String { format!("BA * {:?}", rhs) } - fn __truediv__(&self, rhs: &PyAny) -> String { + fn __truediv__(&self, rhs: &Bound<'_, PyAny>) -> String { format!("BA / {:?}", rhs) } - fn __lshift__(&self, rhs: &PyAny) -> String { + fn __lshift__(&self, rhs: &Bound<'_, PyAny>) -> String { format!("BA << {:?}", rhs) } - fn __rshift__(&self, rhs: &PyAny) -> String { + fn __rshift__(&self, rhs: &Bound<'_, PyAny>) -> String { format!("BA >> {:?}", rhs) } - fn __and__(&self, rhs: &PyAny) -> String { + fn __and__(&self, rhs: &Bound<'_, PyAny>) -> String { format!("BA & {:?}", rhs) } - fn __xor__(&self, rhs: &PyAny) -> String { + fn __xor__(&self, rhs: &Bound<'_, PyAny>) -> String { format!("BA ^ {:?}", rhs) } - fn __or__(&self, rhs: &PyAny) -> String { + fn __or__(&self, rhs: &Bound<'_, PyAny>) -> String { format!("BA | {:?}", rhs) } - fn __pow__(&self, rhs: &PyAny, mod_: Option) -> String { + fn __pow__(&self, rhs: &Bound<'_, PyAny>, mod_: Option) -> String { format!("BA ** {:?} (mod: {:?})", rhs, mod_) } } @@ -257,39 +257,39 @@ struct RhsArithmetic {} #[pymethods] impl RhsArithmetic { - fn __radd__(&self, other: &PyAny) -> String { + fn __radd__(&self, other: &Bound<'_, PyAny>) -> String { format!("{:?} + RA", other) } - fn __rsub__(&self, other: &PyAny) -> String { + fn __rsub__(&self, other: &Bound<'_, PyAny>) -> String { format!("{:?} - RA", other) } - fn __rmul__(&self, other: &PyAny) -> String { + fn __rmul__(&self, other: &Bound<'_, PyAny>) -> String { format!("{:?} * RA", other) } - fn __rlshift__(&self, other: &PyAny) -> String { + fn __rlshift__(&self, other: &Bound<'_, PyAny>) -> String { format!("{:?} << RA", other) } - fn __rrshift__(&self, other: &PyAny) -> String { + fn __rrshift__(&self, other: &Bound<'_, PyAny>) -> String { format!("{:?} >> RA", other) } - fn __rand__(&self, other: &PyAny) -> String { + fn __rand__(&self, other: &Bound<'_, PyAny>) -> String { format!("{:?} & RA", other) } - fn __rxor__(&self, other: &PyAny) -> String { + fn __rxor__(&self, other: &Bound<'_, PyAny>) -> String { format!("{:?} ^ RA", other) } - fn __ror__(&self, other: &PyAny) -> String { + fn __ror__(&self, other: &Bound<'_, PyAny>) -> String { format!("{:?} | RA", other) } - fn __rpow__(&self, other: &PyAny, _mod: Option<&PyAny>) -> String { + fn __rpow__(&self, other: &Bound<'_, PyAny>, _mod: Option<&PyAny>) -> String { format!("{:?} ** RA", other) } } @@ -334,91 +334,91 @@ impl LhsAndRhs { // "BA" // } - fn __add__(lhs: PyRef<'_, Self>, rhs: &PyAny) -> String { + fn __add__(lhs: PyRef<'_, Self>, rhs: &Bound<'_, PyAny>) -> String { format!("{:?} + {:?}", lhs, rhs) } - fn __sub__(lhs: PyRef<'_, Self>, rhs: &PyAny) -> String { + fn __sub__(lhs: PyRef<'_, Self>, rhs: &Bound<'_, PyAny>) -> String { format!("{:?} - {:?}", lhs, rhs) } - fn __mul__(lhs: PyRef<'_, Self>, rhs: &PyAny) -> String { + fn __mul__(lhs: PyRef<'_, Self>, rhs: &Bound<'_, PyAny>) -> String { format!("{:?} * {:?}", lhs, rhs) } - fn __lshift__(lhs: PyRef<'_, Self>, rhs: &PyAny) -> String { + fn __lshift__(lhs: PyRef<'_, Self>, rhs: &Bound<'_, PyAny>) -> String { format!("{:?} << {:?}", lhs, rhs) } - fn __rshift__(lhs: PyRef<'_, Self>, rhs: &PyAny) -> String { + fn __rshift__(lhs: PyRef<'_, Self>, rhs: &Bound<'_, PyAny>) -> String { format!("{:?} >> {:?}", lhs, rhs) } - fn __and__(lhs: PyRef<'_, Self>, rhs: &PyAny) -> String { + fn __and__(lhs: PyRef<'_, Self>, rhs: &Bound<'_, PyAny>) -> String { format!("{:?} & {:?}", lhs, rhs) } - fn __xor__(lhs: PyRef<'_, Self>, rhs: &PyAny) -> String { + fn __xor__(lhs: PyRef<'_, Self>, rhs: &Bound<'_, PyAny>) -> String { format!("{:?} ^ {:?}", lhs, rhs) } - fn __or__(lhs: PyRef<'_, Self>, rhs: &PyAny) -> String { + fn __or__(lhs: PyRef<'_, Self>, rhs: &Bound<'_, PyAny>) -> String { format!("{:?} | {:?}", lhs, rhs) } - fn __pow__(lhs: PyRef<'_, Self>, rhs: &PyAny, _mod: Option) -> String { + fn __pow__(lhs: PyRef<'_, Self>, rhs: &Bound<'_, PyAny>, _mod: Option) -> String { format!("{:?} ** {:?}", lhs, rhs) } - fn __matmul__(lhs: PyRef<'_, Self>, rhs: &PyAny) -> String { + fn __matmul__(lhs: PyRef<'_, Self>, rhs: &Bound<'_, PyAny>) -> String { format!("{:?} @ {:?}", lhs, rhs) } - fn __radd__(&self, other: &PyAny) -> String { + fn __radd__(&self, other: &Bound<'_, PyAny>) -> String { format!("{:?} + RA", other) } - fn __rsub__(&self, other: &PyAny) -> String { + fn __rsub__(&self, other: &Bound<'_, PyAny>) -> String { format!("{:?} - RA", other) } - fn __rmul__(&self, other: &PyAny) -> String { + fn __rmul__(&self, other: &Bound<'_, PyAny>) -> String { format!("{:?} * RA", other) } - fn __rlshift__(&self, other: &PyAny) -> String { + fn __rlshift__(&self, other: &Bound<'_, PyAny>) -> String { format!("{:?} << RA", other) } - fn __rrshift__(&self, other: &PyAny) -> String { + fn __rrshift__(&self, other: &Bound<'_, PyAny>) -> String { format!("{:?} >> RA", other) } - fn __rand__(&self, other: &PyAny) -> String { + fn __rand__(&self, other: &Bound<'_, PyAny>) -> String { format!("{:?} & RA", other) } - fn __rxor__(&self, other: &PyAny) -> String { + fn __rxor__(&self, other: &Bound<'_, PyAny>) -> String { format!("{:?} ^ RA", other) } - fn __ror__(&self, other: &PyAny) -> String { + fn __ror__(&self, other: &Bound<'_, PyAny>) -> String { format!("{:?} | RA", other) } - fn __rpow__(&self, other: &PyAny, _mod: Option<&PyAny>) -> String { + fn __rpow__(&self, other: &Bound<'_, PyAny>, _mod: Option<&PyAny>) -> String { format!("{:?} ** RA", other) } - fn __rmatmul__(&self, other: &PyAny) -> String { + fn __rmatmul__(&self, other: &Bound<'_, PyAny>) -> String { format!("{:?} @ RA", other) } - fn __rtruediv__(&self, other: &PyAny) -> String { + fn __rtruediv__(&self, other: &Bound<'_, PyAny>) -> String { format!("{:?} / RA", other) } - fn __rfloordiv__(&self, other: &PyAny) -> String { + fn __rfloordiv__(&self, other: &Bound<'_, PyAny>) -> String { format!("{:?} // RA", other) } } @@ -461,7 +461,7 @@ impl RichComparisons { "RC" } - fn __richcmp__(&self, other: &PyAny, op: CompareOp) -> String { + fn __richcmp__(&self, other: &Bound<'_, PyAny>, op: CompareOp) -> String { match op { CompareOp::Lt => format!("{} < {:?}", self.__repr__(), other), CompareOp::Le => format!("{} <= {:?}", self.__repr__(), other), @@ -482,7 +482,7 @@ impl RichComparisons2 { "RC2" } - fn __richcmp__(&self, other: &PyAny, op: CompareOp) -> PyObject { + fn __richcmp__(&self, other: &Bound<'_, PyAny>, op: CompareOp) -> PyObject { match op { CompareOp::Eq => true.into_py(other.py()), CompareOp::Ne => false.into_py(other.py()), diff --git a/tests/test_inheritance.rs b/tests/test_inheritance.rs index 1209713e487..fc9a7699c1b 100644 --- a/tests/test_inheritance.rs +++ b/tests/test_inheritance.rs @@ -41,7 +41,7 @@ impl BaseClass { fn base_method(&self, x: usize) -> usize { x * self.val1 } - fn base_set(&mut self, fn_: &pyo3::PyAny) -> PyResult<()> { + fn base_set(&mut self, fn_: &Bound<'_, PyAny>) -> PyResult<()> { let value: usize = fn_.call0()?.extract()?; self.val1 = value; Ok(()) @@ -264,7 +264,7 @@ mod inheriting_native_type { #[pymethods] impl CustomException { #[new] - fn new(_exc_arg: &PyAny) -> Self { + fn new(_exc_arg: &Bound<'_, PyAny>) -> Self { CustomException { context: "Hello :)", } diff --git a/tests/test_methods.rs b/tests/test_methods.rs index 7ca06b27c05..fa6813379bf 100644 --- a/tests/test_methods.rs +++ b/tests/test_methods.rs @@ -112,12 +112,8 @@ struct ClassMethodWithArgs {} #[pymethods] impl ClassMethodWithArgs { #[classmethod] - fn method(cls: &Bound<'_, PyType>, input: &PyString) -> PyResult { - Ok(format!( - "{}.method({})", - cls.as_gil_ref().qualname()?, - input - )) + fn method(cls: &Bound<'_, PyType>, input: &Bound<'_, PyString>) -> PyResult { + Ok(format!("{}.method({})", cls.qualname()?, input)) } } @@ -215,8 +211,13 @@ impl MethSignature { test } #[pyo3(signature = (*args, **kwargs))] - fn get_kwargs(&self, py: Python<'_>, args: &PyTuple, kwargs: Option<&PyDict>) -> PyObject { - [args.into(), kwargs.to_object(py)].to_object(py) + fn get_kwargs( + &self, + py: Python<'_>, + args: &Bound<'_, PyTuple>, + kwargs: Option<&Bound<'_, PyDict>>, + ) -> PyObject { + [args.to_object(py), kwargs.to_object(py)].to_object(py) } #[pyo3(signature = (a, *args, **kwargs))] @@ -224,10 +225,10 @@ impl MethSignature { &self, py: Python<'_>, a: i32, - args: &PyTuple, - kwargs: Option<&PyDict>, + args: &Bound<'_, PyTuple>, + kwargs: Option<&Bound<'_, PyDict>>, ) -> PyObject { - [a.to_object(py), args.into(), kwargs.to_object(py)].to_object(py) + [a.to_object(py), args.to_object(py), kwargs.to_object(py)].to_object(py) } #[pyo3(signature = (a, b, /))] @@ -270,7 +271,7 @@ impl MethSignature { &self, py: Python<'_>, a: i32, - kwargs: Option<&PyDict>, + kwargs: Option<&Bound<'_, PyDict>>, ) -> PyObject { [a.to_object(py), kwargs.to_object(py)].to_object(py) } @@ -280,7 +281,7 @@ impl MethSignature { &self, py: Python<'_>, a: i32, - kwargs: Option<&PyDict>, + kwargs: Option<&Bound<'_, PyDict>>, ) -> PyObject { [a.to_object(py), kwargs.to_object(py)].to_object(py) } @@ -301,7 +302,12 @@ impl MethSignature { } #[pyo3(signature = (*args, a))] - fn get_args_and_required_keyword(&self, py: Python<'_>, args: &PyTuple, a: i32) -> PyObject { + fn get_args_and_required_keyword( + &self, + py: Python<'_>, + args: &Bound<'_, PyTuple>, + a: i32, + ) -> PyObject { (args, a).to_object(py) } @@ -316,7 +322,7 @@ impl MethSignature { } #[pyo3(signature = (a, **kwargs))] - fn get_pos_kw(&self, py: Python<'_>, a: i32, kwargs: Option<&PyDict>) -> PyObject { + fn get_pos_kw(&self, py: Python<'_>, a: i32, kwargs: Option<&Bound<'_, PyDict>>) -> PyObject { [a.to_object(py), kwargs.to_object(py)].to_object(py) } @@ -697,7 +703,8 @@ struct MethodWithLifeTime {} #[pymethods] impl MethodWithLifeTime { - fn set_to_list<'py>(&self, py: Python<'py>, set: &'py PySet) -> PyResult> { + fn set_to_list<'py>(&self, set: &Bound<'py, PySet>) -> PyResult> { + let py = set.py(); let mut items = vec![]; for _ in 0..set.len() { items.push(set.pop().unwrap()); @@ -1028,45 +1035,45 @@ issue_1506!( fn issue_1506( &self, _py: Python<'_>, - _arg: &PyAny, - _args: &PyTuple, - _kwargs: Option<&PyDict>, + _arg: &Bound<'_, PyAny>, + _args: &Bound<'_, PyTuple>, + _kwargs: Option<&Bound<'_, PyDict>>, ) { } fn issue_1506_mut( &mut self, _py: Python<'_>, - _arg: &PyAny, - _args: &PyTuple, - _kwargs: Option<&PyDict>, + _arg: &Bound<'_, PyAny>, + _args: &Bound<'_, PyTuple>, + _kwargs: Option<&Bound<'_, PyDict>>, ) { } fn issue_1506_custom_receiver( _slf: Py, _py: Python<'_>, - _arg: &PyAny, - _args: &PyTuple, - _kwargs: Option<&PyDict>, + _arg: &Bound<'_, PyAny>, + _args: &Bound<'_, PyTuple>, + _kwargs: Option<&Bound<'_, PyDict>>, ) { } fn issue_1506_custom_receiver_explicit( _slf: Py, _py: Python<'_>, - _arg: &PyAny, - _args: &PyTuple, - _kwargs: Option<&PyDict>, + _arg: &Bound<'_, PyAny>, + _args: &Bound<'_, PyTuple>, + _kwargs: Option<&Bound<'_, PyDict>>, ) { } #[new] fn issue_1506_new( _py: Python<'_>, - _arg: &PyAny, - _args: &PyTuple, - _kwargs: Option<&PyDict>, + _arg: &Bound<'_, PyAny>, + _args: &Bound<'_, PyTuple>, + _kwargs: Option<&Bound<'_, PyDict>>, ) -> Self { Issue1506 {} } @@ -1082,9 +1089,9 @@ issue_1506!( #[staticmethod] fn issue_1506_static( _py: Python<'_>, - _arg: &PyAny, - _args: &PyTuple, - _kwargs: Option<&PyDict>, + _arg: &Bound<'_, PyAny>, + _args: &Bound<'_, PyTuple>, + _kwargs: Option<&Bound<'_, PyDict>>, ) { } @@ -1092,9 +1099,9 @@ issue_1506!( fn issue_1506_class( _cls: &Bound<'_, PyType>, _py: Python<'_>, - _arg: &PyAny, - _args: &PyTuple, - _kwargs: Option<&PyDict>, + _arg: &Bound<'_, PyAny>, + _args: &Bound<'_, PyTuple>, + _kwargs: Option<&Bound<'_, PyDict>>, ) { } } diff --git a/tests/test_no_imports.rs b/tests/test_no_imports.rs index 4c77cc8e2a0..35c978b0da5 100644 --- a/tests/test_no_imports.rs +++ b/tests/test_no_imports.rs @@ -49,7 +49,7 @@ impl BasicClass { const OKAY: bool = true; #[new] - fn new(arg: &pyo3::PyAny) -> pyo3::PyResult { + fn new(arg: &pyo3::Bound<'_, pyo3::PyAny>) -> pyo3::PyResult { if let Ok(v) = arg.extract::() { Ok(Self { v, diff --git a/tests/test_proto_methods.rs b/tests/test_proto_methods.rs index a1cfde2badf..dd707990c0b 100644 --- a/tests/test_proto_methods.rs +++ b/tests/test_proto_methods.rs @@ -28,7 +28,7 @@ impl ExampleClass { } } - fn __setattr__(&mut self, attr: &str, value: &PyAny) -> PyResult<()> { + fn __setattr__(&mut self, attr: &str, value: &Bound<'_, PyAny>) -> PyResult<()> { if attr == "special_custom_attr" { self.custom_attr = Some(value.extract()?); Ok(()) @@ -528,7 +528,7 @@ struct GetItem {} #[pymethods] impl GetItem { - fn __getitem__(&self, idx: &PyAny) -> PyResult<&'static str> { + fn __getitem__(&self, idx: &Bound<'_, PyAny>) -> PyResult<&'static str> { if let Ok(slice) = idx.downcast::() { let indices = slice.indices(1000)?; if indices.start == 100 && indices.stop == 200 && indices.step == 1 { @@ -767,18 +767,18 @@ impl DescrCounter { /// Each access will increase the count fn __get__<'a>( mut slf: PyRefMut<'a, Self>, - _instance: &PyAny, - _owner: Option<&PyType>, + _instance: &Bound<'_, PyAny>, + _owner: Option<&Bound<'_, PyType>>, ) -> PyRefMut<'a, Self> { slf.count += 1; slf } /// Allow assigning a new counter to the descriptor, copying the count across - fn __set__(&self, _instance: &PyAny, new_value: &mut Self) { + fn __set__(&self, _instance: &Bound<'_, PyAny>, new_value: &mut Self) { new_value.count = self.count; } /// Delete to reset the counter - fn __delete__(&mut self, _instance: &PyAny) { + fn __delete__(&mut self, _instance: &Bound<'_, PyAny>) { self.count = 0; } } diff --git a/tests/test_pyfunction.rs b/tests/test_pyfunction.rs index 5221b0f85a5..8a57e2707c9 100644 --- a/tests/test_pyfunction.rs +++ b/tests/test_pyfunction.rs @@ -77,12 +77,14 @@ assert a, array.array("i", [2, 4, 6, 8]) #[cfg(not(any(Py_LIMITED_API, PyPy)))] #[pyfunction] -fn function_with_pyfunction_arg(fun: &PyFunction) -> PyResult<&PyAny> { +fn function_with_pyfunction_arg<'py>(fun: &Bound<'py, PyFunction>) -> PyResult> { fun.call((), None) } #[pyfunction] -fn function_with_pycfunction_arg(fun: &PyCFunction) -> PyResult<&PyAny> { +fn function_with_pycfunction_arg<'py>( + fun: &Bound<'py, PyCFunction>, +) -> PyResult> { fun.call((), None) } diff --git a/tests/test_sequence.rs b/tests/test_sequence.rs index 363c27f5eb5..4e128a0bbff 100644 --- a/tests/test_sequence.rs +++ b/tests/test_sequence.rs @@ -60,8 +60,8 @@ impl ByteSequence { } } - fn __contains__(&self, other: &PyAny) -> bool { - match u8::extract(other) { + fn __contains__(&self, other: &Bound<'_, PyAny>) -> bool { + match other.extract::() { Ok(x) => self.elements.contains(&x), Err(_) => false, } diff --git a/tests/test_text_signature.rs b/tests/test_text_signature.rs index 32a78346e9a..a9f5a041596 100644 --- a/tests/test_text_signature.rs +++ b/tests/test_text_signature.rs @@ -128,10 +128,10 @@ fn test_auto_test_signature_function() { fn my_function_4( a: i32, b: Option, - args: &PyTuple, + args: &Bound<'_, PyTuple>, c: i32, d: i32, - kwargs: Option<&PyDict>, + kwargs: Option<&Bound<'_, PyDict>>, ) { let _ = (a, b, args, c, d, kwargs); } @@ -218,10 +218,10 @@ fn test_auto_test_signature_method() { &self, a: i32, b: Option, - args: &PyTuple, + args: &Bound<'_, PyTuple>, c: i32, d: i32, - kwargs: Option<&PyDict>, + kwargs: Option<&Bound<'_, PyDict>>, ) { let _ = (a, b, args, c, d, kwargs); } diff --git a/tests/test_variable_arguments.rs b/tests/test_variable_arguments.rs index 8d3cd5d3935..3724689d836 100644 --- a/tests/test_variable_arguments.rs +++ b/tests/test_variable_arguments.rs @@ -13,13 +13,13 @@ struct MyClass {} impl MyClass { #[staticmethod] #[pyo3(signature = (*args))] - fn test_args(args: &PyTuple) -> &PyTuple { + fn test_args(args: Bound<'_, PyTuple>) -> Bound<'_, PyTuple> { args } #[staticmethod] #[pyo3(signature = (**kwargs))] - fn test_kwargs(kwargs: Option<&PyDict>) -> Option<&PyDict> { + fn test_kwargs(kwargs: Option>) -> Option> { kwargs } } diff --git a/tests/ui/deprecations.rs b/tests/ui/deprecations.rs index cbaba2fa4ff..f623215f531 100644 --- a/tests/ui/deprecations.rs +++ b/tests/ui/deprecations.rs @@ -23,6 +23,9 @@ impl MyClass { fn method_gil_ref(_slf: &PyCell) {} fn method_bound(_slf: &Bound<'_, Self>) {} + + #[staticmethod] + fn static_method_gil_ref(_any: &PyAny) {} } fn main() {} @@ -89,6 +92,9 @@ fn pyfunction_from_py_with( ) { } +#[pyfunction] +fn pyfunction_gil_ref(_any: &PyAny) {} + #[derive(Debug, FromPyObject)] pub struct Zap { #[pyo3(item)] diff --git a/tests/ui/deprecations.stderr b/tests/ui/deprecations.stderr index d5da8572d02..b133b21486c 100644 --- a/tests/ui/deprecations.stderr +++ b/tests/ui/deprecations.stderr @@ -29,57 +29,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:38:43 + --> tests/ui/deprecations.rs:28:36 | -38 | fn pyfunction_with_module_gil_ref(module: &PyModule) -> PyResult<&str> { +28 | fn static_method_gil_ref(_any: &PyAny) {} + | ^ + +error: use of deprecated method `pyo3::deprecations::GilRefs::::function_arg`: use `&Bound<'_, T>` instead for this function argument + --> tests/ui/deprecations.rs:41:43 + | +41 | 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:48:19 + --> tests/ui/deprecations.rs:51:19 | -48 | fn module_gil_ref(m: &PyModule) -> PyResult<()> { +51 | 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:54:57 + --> tests/ui/deprecations.rs:57:57 | -54 | fn module_gil_ref_with_explicit_py_arg(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +57 | 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:87:27 + --> tests/ui/deprecations.rs:90:27 | -87 | #[pyo3(from_py_with = "extract_gil_ref")] _gil_ref: i32, +90 | #[pyo3(from_py_with = "extract_gil_ref")] _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:97:27 +error: use of deprecated method `pyo3::deprecations::GilRefs::::function_arg`: use `&Bound<'_, T>` instead for this function argument + --> tests/ui/deprecations.rs:96:29 | -97 | #[pyo3(from_py_with = "PyAny::len", item("my_object"))] - | ^^^^^^^^^^^^ +96 | fn pyfunction_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:103:27 + | +103 | #[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:107:27 + --> tests/ui/deprecations.rs:113:27 | -107 | #[pyo3(from_py_with = "PyAny::len")] usize, +113 | #[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:113:31 + --> tests/ui/deprecations.rs:119:31 | -113 | Zip(#[pyo3(from_py_with = "extract_gil_ref")] i32), +119 | 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:120:27 + --> tests/ui/deprecations.rs:126:27 | -120 | #[pyo3(from_py_with = "extract_gil_ref")] +126 | #[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:133:13 + --> tests/ui/deprecations.rs:139:13 | -133 | let _ = wrap_pyfunction!(double, py); +139 | let _ = wrap_pyfunction!(double, py); | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | = note: this error originates in the macro `wrap_pyfunction` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/invalid_cancel_handle.stderr b/tests/ui/invalid_cancel_handle.stderr index 9e617bca988..04f55ede4a5 100644 --- a/tests/ui/invalid_cancel_handle.stderr +++ b/tests/ui/invalid_cancel_handle.stderr @@ -58,6 +58,7 @@ error[E0277]: the trait bound `CancelHandle: Clone` is not satisfied | ^^^^ the trait `Clone` is not implemented for `CancelHandle` | = 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 diff --git a/tests/ui/invalid_frozen_pyclass_borrow.stderr b/tests/ui/invalid_frozen_pyclass_borrow.stderr index 3acfbeb1823..52a0623f282 100644 --- a/tests/ui/invalid_frozen_pyclass_borrow.stderr +++ b/tests/ui/invalid_frozen_pyclass_borrow.stderr @@ -16,6 +16,19 @@ note: required by a bound in `extract_pyclass_ref_mut` | 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_frozen_pyclass_borrow.rs:9:1 + | +9 | #[pymethods] + | ^^^^^^^^^^^^ expected `False`, found `True` + | +note: required by a bound in `PyRefMut` + --> src/pycell.rs + | + | pub struct PyRefMut<'p, T: PyClass> { + | ^^^^^^^^^^^^^^ required by this bound in `PyRefMut` + = note: this error originates in the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) + error[E0271]: type mismatch resolving `::Frozen == False` --> tests/ui/invalid_frozen_pyclass_borrow.rs:15:31 | diff --git a/tests/ui/invalid_pyfunctions.rs b/tests/ui/invalid_pyfunctions.rs index fba7d8b5016..eaa241c074b 100644 --- a/tests/ui/invalid_pyfunctions.rs +++ b/tests/ui/invalid_pyfunctions.rs @@ -1,4 +1,5 @@ use pyo3::prelude::*; +use pyo3::types::PyString; #[pyfunction] fn generic_function(value: T) {} @@ -19,7 +20,10 @@ fn function_with_required_after_option(_opt: Option, _x: i32) {} fn pass_module_but_no_arguments<'py>() {} #[pyfunction(pass_module)] -fn first_argument_not_module<'py>(string: &str, module: &'py PyModule) -> PyResult<&'py str> { +fn first_argument_not_module<'a, 'py>( + string: &str, + module: &'a Bound<'_, PyModule>, +) -> PyResult> { module.name() } diff --git a/tests/ui/invalid_pyfunctions.stderr b/tests/ui/invalid_pyfunctions.stderr index 6576997a3eb..8ae40243ba6 100644 --- a/tests/ui/invalid_pyfunctions.stderr +++ b/tests/ui/invalid_pyfunctions.stderr @@ -1,45 +1,45 @@ error: Python functions cannot have generic type parameters - --> tests/ui/invalid_pyfunctions.rs:4:21 + --> tests/ui/invalid_pyfunctions.rs:5:21 | -4 | fn generic_function(value: T) {} +5 | fn generic_function(value: T) {} | ^ error: Python functions cannot have `impl Trait` arguments - --> tests/ui/invalid_pyfunctions.rs:7:36 + --> tests/ui/invalid_pyfunctions.rs:8:36 | -7 | fn impl_trait_function(impl_trait: impl AsRef) {} +8 | fn impl_trait_function(impl_trait: impl AsRef) {} | ^^^^ error: wildcard argument names are not supported - --> tests/ui/invalid_pyfunctions.rs:10:22 + --> tests/ui/invalid_pyfunctions.rs:11:22 | -10 | fn wildcard_argument(_: i32) {} +11 | fn wildcard_argument(_: i32) {} | ^ error: destructuring in arguments is not supported - --> tests/ui/invalid_pyfunctions.rs:13:26 + --> tests/ui/invalid_pyfunctions.rs:14:26 | -13 | fn destructured_argument((a, b): (i32, i32)) {} +14 | fn destructured_argument((a, b): (i32, i32)) {} | ^^^^^^ error: required arguments after an `Option<_>` argument are ambiguous = help: add a `#[pyo3(signature)]` annotation on this function to unambiguously specify the default values for all optional parameters - --> tests/ui/invalid_pyfunctions.rs:16:63 + --> tests/ui/invalid_pyfunctions.rs:17:63 | -16 | fn function_with_required_after_option(_opt: Option, _x: i32) {} +17 | fn function_with_required_after_option(_opt: Option, _x: i32) {} | ^^^ error: expected `&PyModule` or `Py` as first argument with `pass_module` - --> tests/ui/invalid_pyfunctions.rs:19:37 + --> tests/ui/invalid_pyfunctions.rs:20:37 | -19 | fn pass_module_but_no_arguments<'py>() {} +20 | fn pass_module_but_no_arguments<'py>() {} | ^^ error[E0277]: the trait bound `&str: From>` is not satisfied - --> tests/ui/invalid_pyfunctions.rs:22:43 + --> tests/ui/invalid_pyfunctions.rs:24:13 | -22 | fn first_argument_not_module<'py>(string: &str, module: &'py PyModule) -> PyResult<&'py str> { - | ^ the trait `From>` is not implemented for `&str` +24 | string: &str, + | ^ the trait `From>` is not implemented for `&str` | = help: the following other types implement trait `From`: > diff --git a/tests/ui/invalid_pymethod_enum.stderr b/tests/ui/invalid_pymethod_enum.stderr index bb327dccb5d..6cf6fe89bdf 100644 --- a/tests/ui/invalid_pymethod_enum.stderr +++ b/tests/ui/invalid_pymethod_enum.stderr @@ -9,3 +9,16 @@ note: required by a bound in `extract_pyclass_ref_mut` | | pub fn extract_pyclass_ref_mut<'a, 'py: 'a, T: PyClass>( | ^^^^^^^^^^^^^^ required by this bound in `extract_pyclass_ref_mut` + +error[E0271]: type mismatch resolving `::Frozen == False` + --> tests/ui/invalid_pymethod_enum.rs:9:1 + | +9 | #[pymethods] + | ^^^^^^^^^^^^ expected `False`, found `True` + | +note: required by a bound in `PyRefMut` + --> src/pycell.rs + | + | pub struct PyRefMut<'p, T: PyClass> { + | ^^^^^^^^^^^^^^ required by this bound in `PyRefMut` + = note: this error originates in the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/static_ref.rs b/tests/ui/static_ref.rs index a426536d499..e8015db2f64 100644 --- a/tests/ui/static_ref.rs +++ b/tests/ui/static_ref.rs @@ -2,7 +2,12 @@ use pyo3::prelude::*; use pyo3::types::PyList; #[pyfunction] -fn static_ref(list: &'static PyList) -> usize { +fn static_ref(list: &'static Bound<'_, PyList>) -> usize { + list.len() +} + +#[pyfunction] +fn static_py(list: &Bound<'static, PyList>) -> usize { list.len() } diff --git a/tests/ui/static_ref.stderr b/tests/ui/static_ref.stderr index 2dd3342e5ba..50b054f6245 100644 --- a/tests/ui/static_ref.stderr +++ b/tests/ui/static_ref.stderr @@ -8,3 +8,37 @@ error: lifetime may not live long enough | cast requires that `'py` must outlive `'static` | = note: this error originates in the attribute macro `pyfunction` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0597]: `holder_0` does not live long enough + --> tests/ui/static_ref.rs:5:15 + | +4 | #[pyfunction] + | ------------- + | | | + | | `holder_0` dropped here while still borrowed + | binding `holder_0` declared here + | argument requires that `holder_0` is borrowed for `'static` +5 | fn static_ref(list: &'static Bound<'_, PyList>) -> usize { + | ^^^^^^^ borrowed value does not live long enough + +error[E0716]: temporary value dropped while borrowed + --> tests/ui/static_ref.rs:5:21 + | +4 | #[pyfunction] + | ------------- + | | | + | | temporary value is freed at the end of this statement + | argument requires that borrow lasts for `'static` +5 | fn static_ref(list: &'static Bound<'_, PyList>) -> usize { + | ^ creates a temporary value which is freed while still in use + +error: lifetime may not live long enough + --> tests/ui/static_ref.rs:9:1 + | +9 | #[pyfunction] + | ^^^^^^^^^^^^^ + | | + | lifetime `'py` defined here + | cast requires that `'py` must outlive `'static` + | + = note: this error originates in the attribute macro `pyfunction` (in Nightly builds, run with -Z macro-backtrace for more info) From 351c6a0a49d67b21837487e12e0375ed1ab75144 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Thu, 21 Mar 2024 07:24:40 +0000 Subject: [PATCH 096/936] deprecate optional GIL Ref in function argument (#3975) --- guide/src/migration.md | 2 +- src/conversions/chrono.rs | 2 +- src/impl_/deprecations.rs | 23 +++++++++++++++++++++-- tests/test_arithmetics.rs | 4 ++-- tests/test_mapping.rs | 4 ++-- tests/test_methods.rs | 3 ++- tests/test_sequence.rs | 4 ++-- tests/ui/deprecations.rs | 3 +++ tests/ui/deprecations.stderr | 26 ++++++++++++++++---------- 9 files changed, 50 insertions(+), 21 deletions(-) diff --git a/guide/src/migration.md b/guide/src/migration.md index 5a362ec448d..ac20c775d00 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -301,7 +301,7 @@ The expectation is that in 0.22 `extract_bound` will have the default implementa Despite a large amount of deprecations warnings produced by PyO3 to aid with the transition from GIL Refs to the Bound API, there are a few cases where PyO3 cannot automatically warn on uses of GIL Refs. It is worth checking for these cases manually after the deprecation warnings have all been addressed: - Individual implementations of the `FromPyObject` trait cannot be deprecated, so PyO3 cannot warn about uses of code patterns like `.extract<&PyAny>()` which produce a GIL Ref. -- GIL Refs in `#[pyfunction]` arguments emit a warning, but if the GIL Ref is wrapped inside another container such as `Option<&PyAny>` or `Vec<&PyAny>` then PyO3 cannot warn against this. +- GIL Refs in `#[pyfunction]` arguments emit a warning, but if the GIL Ref is wrapped inside another container such as `Vec<&PyAny>` then PyO3 cannot warn against this. - The `wrap_pyfunction!(function)(py)` deferred argument form of the `wrap_pyfunction` macro taking `py: Python<'py>` produces a GIL Ref, and due to limitations in type inference PyO3 cannot warn against this specific case. diff --git a/src/conversions/chrono.rs b/src/conversions/chrono.rs index 2e46a9e54ff..544d1cf2663 100644 --- a/src/conversions/chrono.rs +++ b/src/conversions/chrono.rs @@ -292,7 +292,7 @@ impl FromPyObject<'py>> FromPyObject<'_> for DateTime = dt.getattr(intern!(dt.py(), "tzinfo"))?.extract()?; + let tzinfo: Option> = dt.getattr(intern!(dt.py(), "tzinfo"))?.extract()?; let tz = if let Some(tzinfo) = tzinfo { tzinfo.extract()? diff --git a/src/impl_/deprecations.rs b/src/impl_/deprecations.rs index 459eba913d3..9eb1da05c92 100644 --- a/src/impl_/deprecations.rs +++ b/src/impl_/deprecations.rs @@ -13,7 +13,8 @@ pub fn inspect_fn(f: fn(A) -> PyResult, _: &GilRefs) -> fn(A) -> PyR f } -pub struct GilRefs(NotAGilRef); +pub struct GilRefs(OptionGilRefs); +pub struct OptionGilRefs(NotAGilRef); pub struct NotAGilRef(std::marker::PhantomData); pub trait IsGilRef {} @@ -23,7 +24,7 @@ impl IsGilRef for &'_ T {} impl GilRefs { #[allow(clippy::new_without_default)] pub fn new() -> Self { - GilRefs(NotAGilRef(std::marker::PhantomData)) + GilRefs(OptionGilRefs(NotAGilRef(std::marker::PhantomData))) } } @@ -54,6 +55,17 @@ impl GilRefs { 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" + ) + )] + pub fn function_arg(&self) {} +} + impl NotAGilRef { pub fn function_arg(&self) {} pub fn from_py_with_arg(&self) {} @@ -61,6 +73,13 @@ impl NotAGilRef { } 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/tests/test_arithmetics.rs b/tests/test_arithmetics.rs index da8d72ea9cc..b1efcd0ac55 100644 --- a/tests/test_arithmetics.rs +++ b/tests/test_arithmetics.rs @@ -289,7 +289,7 @@ impl RhsArithmetic { format!("{:?} | RA", other) } - fn __rpow__(&self, other: &Bound<'_, PyAny>, _mod: Option<&PyAny>) -> String { + fn __rpow__(&self, other: &Bound<'_, PyAny>, _mod: Option<&Bound<'_, PyAny>>) -> String { format!("{:?} ** RA", other) } } @@ -406,7 +406,7 @@ impl LhsAndRhs { format!("{:?} | RA", other) } - fn __rpow__(&self, other: &Bound<'_, PyAny>, _mod: Option<&PyAny>) -> String { + fn __rpow__(&self, other: &Bound<'_, PyAny>, _mod: Option<&Bound<'_, PyAny>>) -> String { format!("{:?} ** RA", other) } diff --git a/tests/test_mapping.rs b/tests/test_mapping.rs index 4e24db97793..675dd14d63f 100644 --- a/tests/test_mapping.rs +++ b/tests/test_mapping.rs @@ -21,11 +21,11 @@ struct Mapping { #[pymethods] impl Mapping { #[new] - fn new(elements: Option<&PyList>) -> PyResult { + fn new(elements: Option<&Bound<'_, PyList>>) -> PyResult { if let Some(pylist) = elements { let mut elems = HashMap::with_capacity(pylist.len()); for (i, pyelem) in pylist.into_iter().enumerate() { - let elem = String::extract(pyelem)?; + let elem = pyelem.extract()?; elems.insert(elem, i); } Ok(Self { index: elems }) diff --git a/tests/test_methods.rs b/tests/test_methods.rs index fa6813379bf..2b5396e9ee4 100644 --- a/tests/test_methods.rs +++ b/tests/test_methods.rs @@ -2,6 +2,7 @@ use pyo3::prelude::*; use pyo3::py_run; +use pyo3::types::PySequence; use pyo3::types::{IntoPyDict, PyDict, PyList, PySet, PyString, PyTuple, PyType}; #[path = "../src/tests/common.rs"] @@ -857,7 +858,7 @@ struct FromSequence { #[pymethods] impl FromSequence { #[new] - fn new(seq: Option<&pyo3::types::PySequence>) -> PyResult { + fn new(seq: Option<&Bound<'_, PySequence>>) -> PyResult { if let Some(seq) = seq { Ok(FromSequence { numbers: seq.as_ref().extract::>()?, diff --git a/tests/test_sequence.rs b/tests/test_sequence.rs index 4e128a0bbff..3715affc05b 100644 --- a/tests/test_sequence.rs +++ b/tests/test_sequence.rs @@ -17,11 +17,11 @@ struct ByteSequence { #[pymethods] impl ByteSequence { #[new] - fn new(elements: Option<&PyList>) -> PyResult { + fn new(elements: Option<&Bound<'_, PyList>>) -> PyResult { if let Some(pylist) = elements { let mut elems = Vec::with_capacity(pylist.len()); for pyelem in pylist { - let elem = u8::extract(pyelem)?; + let elem = pyelem.extract()?; elems.push(elem); } Ok(Self { elements: elems }) diff --git a/tests/ui/deprecations.rs b/tests/ui/deprecations.rs index f623215f531..6f3a1feda50 100644 --- a/tests/ui/deprecations.rs +++ b/tests/ui/deprecations.rs @@ -95,6 +95,9 @@ fn pyfunction_from_py_with( #[pyfunction] fn pyfunction_gil_ref(_any: &PyAny) {} +#[pyfunction] +fn pyfunction_option_gil_ref(_any: Option<&PyAny>) {} + #[derive(Debug, FromPyObject)] pub struct Zap { #[pyo3(item)] diff --git a/tests/ui/deprecations.stderr b/tests/ui/deprecations.stderr index b133b21486c..1d87f31c3f8 100644 --- a/tests/ui/deprecations.stderr +++ b/tests/ui/deprecations.stderr @@ -64,34 +64,40 @@ error: use of deprecated method `pyo3::deprecations::GilRefs::::function_arg` 96 | 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:99:36 + | +99 | 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:103:27 + --> tests/ui/deprecations.rs:106:27 | -103 | #[pyo3(from_py_with = "PyAny::len", item("my_object"))] +106 | #[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:113:27 + --> tests/ui/deprecations.rs:116:27 | -113 | #[pyo3(from_py_with = "PyAny::len")] usize, +116 | #[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:119:31 + --> tests/ui/deprecations.rs:122:31 | -119 | Zip(#[pyo3(from_py_with = "extract_gil_ref")] i32), +122 | 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:126:27 + --> tests/ui/deprecations.rs:129:27 | -126 | #[pyo3(from_py_with = "extract_gil_ref")] +129 | #[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:139:13 + --> tests/ui/deprecations.rs:142:13 | -139 | let _ = wrap_pyfunction!(double, py); +142 | 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 d0f5b6af465374e125cae56f12e6d798a83180ef Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 22 Mar 2024 20:43:23 +0000 Subject: [PATCH 097/936] ci: updates for Rust 1.77 (#3978) * ci: updates for Rust 1.77 * move `SendablePtr` inside of test which uses it --- tests/test_gc.rs | 10 +++++----- tests/ui/abi3_nativetype_inheritance.stderr | 20 ++++++++++---------- tests/ui/invalid_cancel_handle.stderr | 4 ++-- tests/ui/invalid_pyfunctions.stderr | 2 +- tests/ui/invalid_pymethod_receiver.stderr | 4 ++-- tests/ui/invalid_pymethods.stderr | 4 ++-- tests/ui/invalid_result_conversion.stderr | 2 +- tests/ui/missing_intopy.stderr | 2 +- tests/ui/not_send.stderr | 2 +- tests/ui/not_send2.stderr | 2 +- tests/ui/pyclass_send.stderr | 20 ++++++++++---------- 11 files changed, 36 insertions(+), 36 deletions(-) diff --git a/tests/test_gc.rs b/tests/test_gc.rs index 7a8bc9ded2d..11d9221c7ad 100644 --- a/tests/test_gc.rs +++ b/tests/test_gc.rs @@ -528,6 +528,11 @@ impl UnsendableTraversal { #[test] #[cfg(not(target_arch = "wasm32"))] // We are building wasm Python with pthreads disabled fn unsendable_are_not_traversed_on_foreign_thread() { + #[derive(Clone, Copy)] + struct SendablePtr(*mut pyo3::ffi::PyObject); + + unsafe impl Send for SendablePtr {} + Python::with_gil(|py| unsafe { let ty = py.get_type_bound::(); let traverse = get_type_traverse(ty.as_type_ptr()).unwrap(); @@ -579,8 +584,3 @@ extern "C" fn visit_error( ) -> std::os::raw::c_int { -1 } - -#[derive(Clone, Copy)] -struct SendablePtr(*mut pyo3::ffi::PyObject); - -unsafe impl Send for SendablePtr {} diff --git a/tests/ui/abi3_nativetype_inheritance.stderr b/tests/ui/abi3_nativetype_inheritance.stderr index f0672fbf0a7..784da4f55ba 100644 --- a/tests/ui/abi3_nativetype_inheritance.stderr +++ b/tests/ui/abi3_nativetype_inheritance.stderr @@ -1,27 +1,27 @@ error[E0277]: the trait bound `PyDict: PyClass` is not satisfied - --> tests/ui/abi3_nativetype_inheritance.rs:5:1 + --> tests/ui/abi3_nativetype_inheritance.rs:5:19 | 5 | #[pyclass(extends=PyDict)] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `PyClass` is not implemented for `PyDict` + | ^^^^^^ the trait `PyClass` is not implemented for `PyDict`, which is required by `PyDict: PyClassBaseType` | = help: the following other types implement trait `PyClass`: TestClass pyo3::coroutine::Coroutine = note: required for `PyDict` to implement `PyClassBaseType` - = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) +note: required by a bound in `PyClassImpl::BaseType` + --> src/impl_/pyclass.rs + | + | type BaseType: PyTypeInfo + PyClassBaseType; + | ^^^^^^^^^^^^^^^ required by this bound in `PyClassImpl::BaseType` error[E0277]: the trait bound `PyDict: PyClass` is not satisfied - --> tests/ui/abi3_nativetype_inheritance.rs:5:19 + --> tests/ui/abi3_nativetype_inheritance.rs:5:1 | 5 | #[pyclass(extends=PyDict)] - | ^^^^^^ the trait `PyClass` is not implemented for `PyDict` + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `PyClass` is not implemented for `PyDict`, which is required by `PyDict: PyClassBaseType` | = help: the following other types implement trait `PyClass`: TestClass pyo3::coroutine::Coroutine = 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` + = 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_cancel_handle.stderr b/tests/ui/invalid_cancel_handle.stderr index 04f55ede4a5..ffd0b3fd0da 100644 --- a/tests/ui/invalid_cancel_handle.stderr +++ b/tests/ui/invalid_cancel_handle.stderr @@ -36,7 +36,7 @@ error[E0277]: the trait bound `CancelHandle: PyClass` 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` + | ^^^^ the trait `PyClass` is not implemented for `CancelHandle`, which is required by `CancelHandle: PyFunctionArgument<'_, '_>` | = help: the trait `PyClass` is implemented for `pyo3::coroutine::Coroutine` = note: required for `CancelHandle` to implement `FromPyObject<'_>` @@ -55,7 +55,7 @@ error[E0277]: the trait bound `CancelHandle: Clone` 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` + | ^^^^ 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>> diff --git a/tests/ui/invalid_pyfunctions.stderr b/tests/ui/invalid_pyfunctions.stderr index 8ae40243ba6..299b20687cb 100644 --- a/tests/ui/invalid_pyfunctions.stderr +++ b/tests/ui/invalid_pyfunctions.stderr @@ -39,7 +39,7 @@ error[E0277]: the trait bound `&str: From tests/ui/invalid_pyfunctions.rs:24:13 | 24 | string: &str, - | ^ the trait `From>` is not implemented for `&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.stderr b/tests/ui/invalid_pymethod_receiver.stderr index b6dd44bd9bf..a79e289716a 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: From>` is not sati --> tests/ui/invalid_pymethod_receiver.rs:8:43 | 8 | fn method_with_invalid_self_type(slf: i32, py: Python<'_>, index: u32) {} - | ^^^ the trait `From>` is not implemented for `i32` + | ^^^ 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,6 @@ 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.stderr b/tests/ui/invalid_pymethods.stderr index 6a8d6ecab78..5bcb6aa1be6 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` + | ^^^ the trait `From>` is not implemented for `i32`, which is required by `BoundRef<'_, '_, PyType>: Into<_>` | = help: the following other types implement trait `From`: > @@ -191,5 +191,5 @@ error[E0277]: the trait bound `i32: From>` is not satis > > > - > + >> = 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 73ed2cba1aa..9695c5e6a19 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:21:1 | 21 | #[pyfunction] - | ^^^^^^^^^^^^^ the trait `From` is not implemented for `PyErr` + | ^^^^^^^^^^^^^ the trait `From` is not implemented for `PyErr`, which is required by `MyError: Into` | = help: the following other types implement trait `From`: >> diff --git a/tests/ui/missing_intopy.stderr b/tests/ui/missing_intopy.stderr index 26b32b3e742..1d233b28b6c 100644 --- a/tests/ui/missing_intopy.stderr +++ b/tests/ui/missing_intopy.stderr @@ -2,7 +2,7 @@ error[E0277]: the trait bound `Blah: IntoPy>` is not satisfied --> tests/ui/missing_intopy.rs:3:1 | 3 | #[pyo3::pyfunction] - | ^^^^^^^^^^^^^^^^^^^ the trait `IntoPy>` is not implemented for `Blah` + | ^^^^^^^^^^^^^^^^^^^ the trait `IntoPy>` is not implemented for `Blah`, which is required by `Blah: OkWrap<_>` | = help: the following other types implement trait `IntoPy`: >> diff --git a/tests/ui/not_send.stderr b/tests/ui/not_send.stderr index 5d05b3de059..8007c2f4e54 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>` + = 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` 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 eec2b644e9a..52a4e6a7208 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>` + = 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` 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 7a80989b63f..0db9106ab42 100644 --- a/tests/ui/pyclass_send.stderr +++ b/tests/ui/pyclass_send.stderr @@ -4,17 +4,19 @@ error[E0277]: `Rc` cannot be sent between threads safely 4 | #[pyclass] | ^^^^^^^^^^ `Rc` cannot be sent between threads safely | - = help: within `NotThreadSafe`, the trait `Send` is not implemented for `Rc` + = help: within `NotThreadSafe`, the trait `Send` is not implemented for `Rc`, which is required by `SendablePyClass: pyo3::impl_::pyclass::PyClassThreadChecker` + = help: the trait `pyo3::impl_::pyclass::PyClassThreadChecker` is implemented for `SendablePyClass` note: required because it appears within the type `NotThreadSafe` --> tests/ui/pyclass_send.rs:5:8 | 5 | struct NotThreadSafe { | ^^^^^^^^^^^^^ -note: required by a bound in `SendablePyClass` + = note: required for `SendablePyClass` to implement `pyo3::impl_::pyclass::PyClassThreadChecker` +note: required by a bound in `PyClassImpl::ThreadChecker` --> src/impl_/pyclass.rs | - | pub struct SendablePyClass(PhantomData); - | ^^^^ required by this bound in `SendablePyClass` + | type ThreadChecker: PyClassThreadChecker; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `PyClassImpl::ThreadChecker` = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0277]: `Rc` cannot be sent between threads safely @@ -23,17 +25,15 @@ error[E0277]: `Rc` cannot be sent between threads safely 4 | #[pyclass] | ^^^^^^^^^^ `Rc` cannot be sent between threads safely | - = help: within `NotThreadSafe`, the trait `Send` is not implemented for `Rc` - = help: the trait `pyo3::impl_::pyclass::PyClassThreadChecker` is implemented for `SendablePyClass` + = help: within `NotThreadSafe`, the trait `Send` is not implemented for `Rc`, which is required by `NotThreadSafe: Send` note: required because it appears within the type `NotThreadSafe` --> tests/ui/pyclass_send.rs:5:8 | 5 | struct NotThreadSafe { | ^^^^^^^^^^^^^ - = note: required for `SendablePyClass` to implement `pyo3::impl_::pyclass::PyClassThreadChecker` -note: required by a bound in `PyClassImpl::ThreadChecker` +note: required by a bound in `SendablePyClass` --> src/impl_/pyclass.rs | - | type ThreadChecker: PyClassThreadChecker; - | ^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `PyClassImpl::ThreadChecker` + | 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 990886efdad1bad0e772ea5f9439fb95c7335961 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 22 Mar 2024 22:16:28 +0000 Subject: [PATCH 098/936] docs: better document `FromPyObject` for `&str` changes in migration guide (#3974) * docs: better document `FromPyObject` for `&str` changes in migration guide * review: LilyFoote --- guide/src/index.md | 1 + guide/src/memory.md | 5 ++- guide/src/migration.md | 71 ++++++++++++++++++++++++++++++++++++++---- 3 files changed, 70 insertions(+), 7 deletions(-) diff --git a/guide/src/index.md b/guide/src/index.md index 87914975afe..fe8b76b69c9 100644 --- a/guide/src/index.md +++ b/guide/src/index.md @@ -11,6 +11,7 @@ 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.
+ ⚠️ Warning: API update in progress 🛠️ PyO3 0.21 has introduced a significant new API, termed the "Bound" API after the new smart pointer `Bound`. diff --git a/guide/src/memory.md b/guide/src/memory.md index 78f9f348f40..cd4af4f8f13 100644 --- a/guide/src/memory.md +++ b/guide/src/memory.md @@ -1,6 +1,7 @@ # 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`. @@ -84,6 +85,7 @@ 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. @@ -121,7 +123,7 @@ this is unsafe. # fn main() -> PyResult<()> { Python::with_gil(|py| -> PyResult<()> { for _ in 0..10 { - #[allow(deprecated)] // `new_pool` is not needed in code not using the GIL Refs API + #[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 @@ -155,6 +157,7 @@ a few objects, meaning this doesn't have a significant impact. Occasionally func 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. diff --git a/guide/src/migration.md b/guide/src/migration.md index ac20c775d00..69cf8922255 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -245,13 +245,19 @@ Because the new `Bound` API brings ownership out of the PyO3 framework and in - `Bound` and `Bound` cannot support indexing with `list[0]`, you should use `list.get_item(0)` instead. - `Bound::iter_borrowed` is slightly more efficient than `Bound::iter`. The default iteration of `Bound` cannot return borrowed references because Rust does not (yet) have "lending iterators". Similarly `Bound::get_borrowed_item` is more efficient than `Bound::get_item` for the same reason. - `&Bound` does not implement `FromPyObject` (although it might be possible to do this in the future once the GIL Refs API is completely removed). Use `bound_any.downcast::()` instead of `bound_any.extract::<&Bound>()`. -- To convert between `&PyAny` and `&Bound` you can use the `as_borrowed()` method: +- `Bound::to_str` now borrows from the `Bound` rather than from the `'py` lifetime, so code will need to store the smart pointer as a value in some cases where previously `&PyString` was just used as a temporary. (There are some more details relating to this in [the section below](#deactivating-the-gil-refs-feature).) + +To convert between `&PyAny` and `&Bound` you can use the `as_borrowed()` method: ```rust,ignore let gil_ref: &PyAny = ...; let bound: &Bound = &gil_ref.as_borrowed(); ``` +
+ +⚠️ Warning: dangling pointer trap 💣 + > Because of the ownership changes, code which uses `.as_ptr()` to convert `&PyAny` and other GIL Refs to a `*mut pyo3_ffi::PyObject` should take care to avoid creating dangling pointers now that `Bound` carries ownership. > > For example, the following pattern with `Option<&PyAny>` can easily create a dangling pointer when migrating to the `Bound` smart pointer: @@ -267,6 +273,7 @@ let bound: &Bound = &gil_ref.as_borrowed(); > let opt: Option> = ...; > let p: *mut ffi::PyObject = opt.as_ref().map_or(std::ptr::null_mut(), Bound::as_ptr); > ``` +
#### Migrating `FromPyObject` implementations @@ -312,14 +319,66 @@ Despite a large amount of deprecations warnings produced by PyO3 to aid with the As a final step of migration, deactivating the `gil-refs` feature will set up code for best performance and is intended to set up a forward-compatible API for PyO3 0.22. -At this point code which needed to manage GIL Ref memory can safely remove uses of `GILPool` (which are constructed by calls to `Python::new_pool` and `Python::with_pool`). Deprecation warnings will highlight these cases. +At this point code that needed to manage GIL Ref memory can safely remove uses of `GILPool` (which are constructed by calls to `Python::new_pool` and `Python::with_pool`). Deprecation warnings will highlight these cases. -There is one notable API removed when this feature is disabled. `FromPyObject` trait implementations for types which borrow directly from the input data cannot be implemented by PyO3 without GIL Refs (while the migration is ongoing). These types are `&str`, `Cow<'_, str>`, `&[u8]`, `Cow<'_, u8>`. +There is just one case of code that changes upon disabling these features: `FromPyObject` trait implementations for types that borrow directly from the input data cannot be implemented by PyO3 without GIL Refs (while the GIL Refs API is in the process of being removed). The main types affected are `&str`, `Cow<'_, str>`, `&[u8]`, `Cow<'_, u8>`. -To ease pain during migration, these types instead implement a new temporary trait `FromPyObjectBound` which is the expected future form of `FromPyObject`. The new temporary trait ensures is that `obj.extract::<&str>()` continues to work, as well for these types in `#[pyfunction]` arguments. +To make PyO3's core functionality continue to work while the GIL Refs API is in the process of being removed, disabling the `gil-refs` feature moves the implementations of `FromPyObject` for `&str`, `Cow<'_, str>`, `&[u8]`, `Cow<'_, u8>` to a new temporary trait `FromPyObjectBound`. This trait is the expected future form of `FromPyObject` and has an additional lifetime `'a` to enable these types to borrow data from Python objects. -An unfortunate final point here is that PyO3 cannot offer this new implementation for `&str` on `abi3` builds for Python older than 3.10. On code which needs `abi3` builds for these older Python versions, many cases of `.extract::<&str>()` may need to be replaced with `.extract::()`, which is string data which borrows from the Python `str` object. Alternatively, use `.extract::>()` ro `.extract::()` to copy the data into Rust for these versions. - +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. + +Before: + +```rust +# #[cfg(feature = "gil-refs-migration")] { +# use pyo3::prelude::*; +# use pyo3::types::{PyList, PyType}; +# fn example<'py>(py: Python<'py>) -> PyResult<()> { +#[allow(deprecated)] // GIL Ref API +let obj: &'py PyType = py.get_type::(); +let name: &'py str = obj.getattr("__name__")?.extract()?; +assert_eq!(name, "list"); +# Ok(()) +# } +# Python::with_gil(example).unwrap(); +# } +``` + +After: + +```rust +# #[cfg(any(not(Py_LIMITED_API), Py_3_10))] { +# use pyo3::prelude::*; +# use pyo3::types::{PyList, PyType}; +# fn example<'py>(py: Python<'py>) -> PyResult<()> { +let obj: Bound<'py, PyType> = py.get_type_bound::(); +let name_obj: Bound<'py, PyAny> = obj.getattr("__name__")?; +// the lifetime of the data is no longer `'py` but the much shorter +// lifetime of the `name_obj` smart pointer above +let name: &'_ str = name_obj.extract()?; +assert_eq!(name, "list"); +# Ok(()) +# } +# Python::with_gil(example).unwrap(); +# } +``` + +To avoid needing to worry about lifetimes at all, it is also possible to use the new `PyBackedStr` type, which stores a reference to the Python `str` without a lifetime attachment. In particular, `PyBackedStr` helps for `abi3` builds for Python older than 3.10. Due to limitations in the `abi3` CPython API for those older versions, PyO3 cannot offer a `FromPyObjectBound` implementation for `&str` on those versions. The easiest way to migrate for older `abi3` builds is to replace any cases of `.extract::<&str>()` with `.extract::()`. Alternatively, use `.extract::>()`, `.extract::()` to copy the data into Rust. + +The following example uses the same snippet as those just above, but this time the final extracted type is `PyBackedStr`: + +```rust +# use pyo3::prelude::*; +# use pyo3::types::{PyList, PyType}; +# fn example<'py>(py: Python<'py>) -> PyResult<()> { +use pyo3::pybacked::PyBackedStr; +let obj: Bound<'py, PyType> = py.get_type_bound::(); +let name: PyBackedStr = obj.getattr("__name__")?.extract()?; +assert_eq!(&*name, "list"); +# Ok(()) +# } +# Python::with_gil(example).unwrap(); +``` ## from 0.19.* to 0.20 From 20e477a7cd19c5bfeec1ebe813d2df2cea0f8a2b Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 22 Mar 2024 22:43:05 +0000 Subject: [PATCH 099/936] avoid reference count cycle in tuple extraction (#3976) --- src/types/tuple.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types/tuple.rs b/src/types/tuple.rs index d8053e1441a..d89830daa97 100644 --- a/src/types/tuple.rs +++ b/src/types/tuple.rs @@ -690,10 +690,10 @@ fn type_output() -> TypeInfo { let t = obj.downcast::()?; if t.len() == $length { #[cfg(any(Py_LIMITED_API, PyPy))] - return Ok(($(t.get_item($n)?.extract::<$T>()?,)+)); + return Ok(($(t.get_borrowed_item($n)?.extract::<$T>()?,)+)); #[cfg(not(any(Py_LIMITED_API, PyPy)))] - unsafe {return Ok(($(t.get_item_unchecked($n).extract::<$T>()?,)+));} + unsafe {return Ok(($(t.get_borrowed_item_unchecked($n).extract::<$T>()?,)+));} } else { Err(wrong_tuple_length(t, $length)) } From 9808f7111c5ec11725cb02a963475f6cf352eed8 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 22 Mar 2024 22:43:08 +0000 Subject: [PATCH 100/936] ci: add `update-ui-tests` nox session (#3979) * ci: add `update-ui-tests` nox session * defer error messages after the group closes * fix syntax warnings --- Contributing.md | 15 +++++++++++---- noxfile.py | 31 ++++++++++++++++++++++++++----- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/Contributing.md b/Contributing.md index 2abdd80d47f..29d1bb758a7 100644 --- a/Contributing.md +++ b/Contributing.md @@ -101,10 +101,17 @@ 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``` -and -```nox -l``` -lists further commands you can run. +`nox`. Use `nox -l` to list the full set of subcommands you can run. + +#### UI Tests + +PyO3 uses [`trybuild`][trybuild] to develop UI tests to capture error messages from the Rust compiler for some of the macro functionality. + +Because there are several feature combinations for these UI tests, when updating them all (e.g. for a new Rust compiler version) it may be helpful to use the `update-ui-tests` nox session: + +```bash +nox -s update-ui-tests +``` ### Documenting changes diff --git a/noxfile.py b/noxfile.py index f70fced76d6..3ee76da5f08 100644 --- a/noxfile.py +++ b/noxfile.py @@ -395,8 +395,8 @@ def check_guide(session: nox.Session): session.posargs.extend(posargs) remaps = { - f"file://{PYO3_GUIDE_SRC}/([^/]*/)*?%7B%7B#PYO3_DOCS_URL\}}\}}": f"file://{PYO3_DOCS_TARGET}", - "%7B%7B#PYO3_DOCS_VERSION\}\}": "latest", + f"file://{PYO3_GUIDE_SRC}/([^/]*/)*?%7B%7B#PYO3_DOCS_URL}}}}": f"file://{PYO3_DOCS_TARGET}", + "%7B%7B#PYO3_DOCS_VERSION}}": "latest", } remap_args = [] for key, value in remaps.items(): @@ -732,6 +732,16 @@ def check_feature_powerset(session: nox.Session): ) +@nox.session(name="update-ui-tests", venv_backend="none") +def update_ui_tests(session: nox.Session): + env = os.environ.copy() + env["TRYBUILD"] = "overwrite" + command = ["test", "--test", "test_compile_error"] + _run_cargo(session, *command, env=env) + _run_cargo(session, *command, "--features=full", env=env) + _run_cargo(session, *command, "--features=abi3,full", env=env) + + def _build_docs_for_ffi_check(session: nox.Session) -> None: # pyo3-ffi-check needs to scrape docs of pyo3-ffi _run_cargo(session, "doc", _FFI_CHECK, "-p", "pyo3-ffi", "--no-deps") @@ -813,12 +823,23 @@ def _get_coverage_env() -> Dict[str, str]: def _run(session: nox.Session, *args: str, **kwargs: Any) -> None: """Wrapper for _run(session, which creates nice groups on GitHub Actions.""" is_github_actions = _is_github_actions() + failed = False if is_github_actions: # Insert ::group:: at the start of nox's command line output print("::group::", end="", flush=True, file=sys.stderr) - session.run(*args, **kwargs) - if is_github_actions: - print("::endgroup::", file=sys.stderr) + try: + session.run(*args, **kwargs) + except nox.command.CommandFailed: + failed = True + raise + finally: + if is_github_actions: + print("::endgroup::", file=sys.stderr) + # Defer the error message until after the group to make them easier + # to find in the log + if failed: + command = " ".join(args) + print(f"::error::`{command}` failed", file=sys.stderr) def _run_cargo( From 009cd32a444ceacd6142f8a5195d9b38ccfa5883 Mon Sep 17 00:00:00 2001 From: Lily Foote Date: Sat, 23 Mar 2024 01:13:24 +0000 Subject: [PATCH 101/936] Add into_mapping and into_sequence methods to Bound types (#3982) * Add Bound<'py, PyDict>.into_mapping method * Add Bound<'py, PyTuple>.into_sequence method * Add Bound<'py, PyList>.into_sequence method * Add newsfragment * Add tests for into_mapping and into_sequence --- newsfragments/3982.added.md | 1 + src/types/dict.rs | 21 +++++++++++++++++++ src/types/list.rs | 40 +++++++++++++++++++++++++++++++++++++ src/types/tuple.rs | 19 +++++++++++++++++- 4 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 newsfragments/3982.added.md diff --git a/newsfragments/3982.added.md b/newsfragments/3982.added.md new file mode 100644 index 00000000000..abc89574d38 --- /dev/null +++ b/newsfragments/3982.added.md @@ -0,0 +1 @@ +Added `Bound<'py, PyDict>::into_mapping`, `Bound<'py, PyList>::into_sequence` and `Bound<'py, PyTuple>::into_sequence`. diff --git a/src/types/dict.rs b/src/types/dict.rs index 2302004d238..43bade5a80c 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -365,6 +365,9 @@ pub trait PyDictMethods<'py>: crate::sealed::Sealed { /// Returns `self` cast as a `PyMapping`. fn as_mapping(&self) -> &Bound<'py, PyMapping>; + /// Returns `self` cast as a `PyMapping`. + fn into_mapping(self) -> Bound<'py, PyMapping>; + /// Update this dictionary with the key/value pairs from another. /// /// This is equivalent to the Python expression `self.update(other)`. If `other` is a `PyDict`, you may want @@ -511,6 +514,10 @@ impl<'py> PyDictMethods<'py> for Bound<'py, PyDict> { unsafe { self.downcast_unchecked() } } + fn into_mapping(self) -> Bound<'py, PyMapping> { + unsafe { self.into_any().downcast_into_unchecked() } + } + fn update(&self, other: &Bound<'_, PyMapping>) -> PyResult<()> { err::error_on_minusone(self.py(), unsafe { ffi::PyDict_Update(self.as_ptr(), other.as_ptr()) @@ -1367,6 +1374,20 @@ mod tests { }); } + #[test] + fn dict_into_mapping() { + Python::with_gil(|py| { + let mut map = HashMap::::new(); + map.insert(1, 1); + + let py_map = map.into_py_dict_bound(py); + + let py_mapping = py_map.into_mapping(); + assert_eq!(py_mapping.len().unwrap(), 1); + assert_eq!(py_mapping.get_item(1).unwrap().extract::().unwrap(), 1); + }); + } + #[cfg(not(PyPy))] fn abc_dict(py: Python<'_>) -> Bound<'_, PyDict> { let mut map = HashMap::<&'static str, i32>::new(); diff --git a/src/types/list.rs b/src/types/list.rs index 3aa7ddca3f0..19dbe59510f 100644 --- a/src/types/list.rs +++ b/src/types/list.rs @@ -295,6 +295,9 @@ pub trait PyListMethods<'py>: crate::sealed::Sealed { /// Returns `self` cast as a `PySequence`. fn as_sequence(&self) -> &Bound<'py, PySequence>; + /// Returns `self` cast as a `PySequence`. + fn into_sequence(self) -> Bound<'py, PySequence>; + /// Gets the list item at the specified index. /// # Example /// ``` @@ -408,6 +411,11 @@ impl<'py> PyListMethods<'py> for Bound<'py, PyList> { unsafe { self.downcast_unchecked() } } + /// Returns `self` cast as a `PySequence`. + fn into_sequence(self) -> Bound<'py, PySequence> { + unsafe { self.into_any().downcast_into_unchecked() } + } + /// Gets the list item at the specified index. /// # Example /// ``` @@ -715,6 +723,9 @@ 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; + use crate::types::sequence::PySequenceMethods; use crate::types::{PyList, PyTuple}; use crate::Python; use crate::{IntoPy, PyObject, ToPyObject}; @@ -934,6 +945,35 @@ mod tests { }); } + #[test] + fn test_as_sequence() { + Python::with_gil(|py| { + let list = PyList::new_bound(py, [1, 2, 3, 4]); + + assert_eq!(list.as_sequence().len().unwrap(), 4); + assert_eq!( + list.as_sequence() + .get_item(1) + .unwrap() + .extract::() + .unwrap(), + 2 + ); + }); + } + + #[test] + fn test_into_sequence() { + Python::with_gil(|py| { + let list = PyList::new_bound(py, [1, 2, 3, 4]); + + let sequence = list.into_sequence(); + + assert_eq!(sequence.len().unwrap(), 4); + assert_eq!(sequence.get_item(1).unwrap().extract::().unwrap(), 2); + }); + } + #[test] fn test_extract() { Python::with_gil(|py| { diff --git a/src/types/tuple.rs b/src/types/tuple.rs index d89830daa97..4dfe4436bf0 100644 --- a/src/types/tuple.rs +++ b/src/types/tuple.rs @@ -258,6 +258,9 @@ pub trait PyTupleMethods<'py>: crate::sealed::Sealed { /// Returns `self` cast as a `PySequence`. fn as_sequence(&self) -> &Bound<'py, PySequence>; + /// Returns `self` cast as a `PySequence`. + fn into_sequence(self) -> Bound<'py, PySequence>; + /// Takes the slice `self[low:high]` and returns it as a new tuple. /// /// Indices must be nonnegative, and out-of-range indices are clipped to @@ -353,6 +356,10 @@ impl<'py> PyTupleMethods<'py> for Bound<'py, PyTuple> { unsafe { self.downcast_unchecked() } } + fn into_sequence(self) -> Bound<'py, PySequence> { + unsafe { self.into_any().downcast_into_unchecked() } + } + fn get_slice(&self, low: usize, high: usize) -> Bound<'py, PyTuple> { unsafe { ffi::PyTuple_GetSlice(self.as_ptr(), get_ssize_index(low), get_ssize_index(high)) @@ -1415,7 +1422,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_bound(py, vec![1, 2, 3]); let sequence = tuple.as_sequence(); assert!(tuple.get_item(0).unwrap().eq(1).unwrap()); assert!(sequence.get_item(0).unwrap().eq(1).unwrap()); @@ -1425,6 +1432,16 @@ mod tests { }) } + #[test] + fn test_tuple_into_sequence() { + Python::with_gil(|py| { + let tuple = PyTuple::new_bound(py, vec![1, 2, 3]); + let sequence = tuple.into_sequence(); + assert!(sequence.get_item(0).unwrap().eq(1).unwrap()); + assert_eq!(sequence.len().unwrap(), 3); + }) + } + #[test] fn test_bound_tuple_get_item() { Python::with_gil(|py| { From aeb74c7093a0979c2ff7436150b85d8849c5bd1c Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sat, 23 Mar 2024 20:16:37 +0000 Subject: [PATCH 102/936] ci: use macos-14 runners (#3985) * ci: use macos-14 runners * use arm64 python architecture --- .github/workflows/ci.yml | 42 ++++++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 148938fe016..5ee0776ebed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -74,9 +74,9 @@ jobs: rust: [stable] platform: [ { - os: "macos-latest", - python-architecture: "x64", - rust-target: "x86_64-apple-darwin", + os: "macos-14", # first available arm macos runner + python-architecture: "arm64", + rust-target: "aarch64-apple-darwin", }, { os: "ubuntu-latest", @@ -119,7 +119,7 @@ jobs: rust-target: "x86_64-unknown-linux-gnu", } name: clippy/${{ matrix.platform.rust-target }}/${{ matrix.rust }} - continue-on-error: ${{ matrix.platform.rust != 'stable' }} + continue-on-error: ${{ matrix.rust != 'stable' }} steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@master @@ -161,7 +161,12 @@ jobs: platform: [ { - os: "macos-latest", + os: "macos-14", # first available arm macos runner + python-architecture: "arm64", + rust-target: "aarch64-apple-darwin", + }, + { + os: "macos-13", # last available x86_64 macos runner python-architecture: "x64", rust-target: "x86_64-apple-darwin", }, @@ -226,8 +231,13 @@ 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-latest", + os: "macos-13", python-architecture: "x64", rust-target: "x86_64-apple-darwin", }, @@ -287,6 +297,18 @@ jobs: } extra-features: "multiple-pymethods" + # test arm macos runner with the latest Python version + # NB: if the full matrix switchess to arm, switch to x86_64 here + - rust: stable + python-version: "3.12" + platform: + { + os: "macos-14", + python-architecture: "arm64", + rust-target: "aarch64-apple-darwin", + } + extra-features: "multiple-pymethods" + valgrind: if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || github.event_name != 'pull_request' }} needs: [fmt] @@ -343,13 +365,13 @@ jobs: coverage: needs: [fmt] - name: coverage-${{ matrix.os }} + name: coverage ${{ matrix.os }} strategy: matrix: - os: ["windows", "macos", "ubuntu"] - runs-on: ${{ matrix.os }}-latest + os: ["windows-latest", "macos-14", "ubuntu-latest"] # first available arm macos runner + runs-on: ${{ matrix.os }} steps: - - if: ${{ github.event_name == 'pull_request' && matrix.os != 'ubuntu' }} + - if: ${{ github.event_name == 'pull_request' && matrix.os != 'ubuntu-latest' }} id: should-skip shell: bash run: echo 'skip=true' >> $GITHUB_OUTPUT From 7d319a906e3a5173b2db1fddf239c21921297895 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sat, 23 Mar 2024 20:17:33 +0000 Subject: [PATCH 103/936] ci: tidy up benchmarks a little (#3986) --- pyo3-benches/benches/bench_bigint.rs | 34 ++++-------- pyo3-benches/benches/bench_decimal.rs | 10 ++-- pyo3-benches/benches/bench_dict.rs | 10 ++-- pyo3-benches/benches/bench_extract.rs | 63 ++++++++-------------- pyo3-benches/benches/bench_frompyobject.rs | 50 +++++++---------- pyo3-benches/benches/bench_list.rs | 2 - pyo3-benches/benches/bench_set.rs | 23 +++++--- pyo3-benches/benches/bench_tuple.rs | 8 +-- 8 files changed, 81 insertions(+), 119 deletions(-) diff --git a/pyo3-benches/benches/bench_bigint.rs b/pyo3-benches/benches/bench_bigint.rs index 4a50b437d95..99635a70279 100644 --- a/pyo3-benches/benches/bench_bigint.rs +++ b/pyo3-benches/benches/bench_bigint.rs @@ -1,17 +1,18 @@ -use codspeed_criterion_compat::{black_box, criterion_group, criterion_main, Bencher, Criterion}; +use std::hint::black_box; + +use codspeed_criterion_compat::{criterion_group, criterion_main, Bencher, Criterion}; +use num_bigint::BigInt; use pyo3::prelude::*; use pyo3::types::PyDict; -use num_bigint::BigInt; - fn extract_bigint_extract_fail(bench: &mut Bencher<'_>) { Python::with_gil(|py| { let d = PyDict::new_bound(py).into_any(); bench.iter(|| match black_box(&d).extract::() { Ok(v) => panic!("should err {}", v), - Err(e) => black_box(e), + Err(e) => e, }); }); } @@ -20,10 +21,7 @@ fn extract_bigint_small(bench: &mut Bencher<'_>) { Python::with_gil(|py| { let int = py.eval_bound("-42", None, None).unwrap(); - bench.iter(|| { - let v = black_box(&int).extract::().unwrap(); - black_box(v); - }); + bench.iter_with_large_drop(|| black_box(&int).extract::().unwrap()); }); } @@ -31,10 +29,7 @@ fn extract_bigint_big_negative(bench: &mut Bencher<'_>) { Python::with_gil(|py| { let int = py.eval_bound("-10**300", None, None).unwrap(); - bench.iter(|| { - let v = black_box(&int).extract::().unwrap(); - black_box(v); - }); + bench.iter_with_large_drop(|| black_box(&int).extract::().unwrap()); }); } @@ -42,10 +37,7 @@ fn extract_bigint_big_positive(bench: &mut Bencher<'_>) { Python::with_gil(|py| { let int = py.eval_bound("10**300", None, None).unwrap(); - bench.iter(|| { - let v = black_box(&int).extract::().unwrap(); - black_box(v); - }); + bench.iter_with_large_drop(|| black_box(&int).extract::().unwrap()); }); } @@ -53,10 +45,7 @@ fn extract_bigint_huge_negative(bench: &mut Bencher<'_>) { Python::with_gil(|py| { let int = py.eval_bound("-10**3000", None, None).unwrap(); - bench.iter(|| { - let v = black_box(&int).extract::().unwrap(); - black_box(v); - }); + bench.iter_with_large_drop(|| black_box(&int).extract::().unwrap()); }); } @@ -64,10 +53,7 @@ fn extract_bigint_huge_positive(bench: &mut Bencher<'_>) { Python::with_gil(|py| { let int = py.eval_bound("10**3000", None, None).unwrap(); - bench.iter(|| { - let v = black_box(&int).extract::().unwrap(); - black_box(v); - }); + bench.iter_with_large_drop(|| black_box(&int).extract::().unwrap()); }); } diff --git a/pyo3-benches/benches/bench_decimal.rs b/pyo3-benches/benches/bench_decimal.rs index 6db6704bf8e..53b79abbd38 100644 --- a/pyo3-benches/benches/bench_decimal.rs +++ b/pyo3-benches/benches/bench_decimal.rs @@ -1,8 +1,10 @@ -use codspeed_criterion_compat::{black_box, criterion_group, criterion_main, Bencher, Criterion}; +use std::hint::black_box; + +use codspeed_criterion_compat::{criterion_group, criterion_main, Bencher, Criterion}; +use rust_decimal::Decimal; use pyo3::prelude::*; use pyo3::types::PyDict; -use rust_decimal::Decimal; fn decimal_via_extract(b: &mut Bencher<'_>) { Python::with_gil(|py| { @@ -18,9 +20,7 @@ py_dec = decimal.Decimal("0.0") .unwrap(); let py_dec = locals.get_item("py_dec").unwrap().unwrap(); - b.iter(|| { - let _: Decimal = black_box(&py_dec).extract().unwrap(); - }); + b.iter(|| black_box(&py_dec).extract::().unwrap()); }) } diff --git a/pyo3-benches/benches/bench_dict.rs b/pyo3-benches/benches/bench_dict.rs index e6a1e2e6b0b..8c3dfe023c8 100644 --- a/pyo3-benches/benches/bench_dict.rs +++ b/pyo3-benches/benches/bench_dict.rs @@ -1,9 +1,10 @@ +use std::collections::{BTreeMap, HashMap}; +use std::hint::black_box; + use codspeed_criterion_compat::{criterion_group, criterion_main, Bencher, Criterion}; use pyo3::types::IntoPyDict; use pyo3::{prelude::*, types::PyMapping}; -use std::collections::{BTreeMap, HashMap}; -use std::hint::black_box; fn iter_dict(b: &mut Bencher<'_>) { Python::with_gil(|py| { @@ -69,7 +70,6 @@ fn extract_hashbrown_map(b: &mut Bencher<'_>) { }); } -#[cfg(not(codspeed))] fn mapping_from_dict(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 100_000; @@ -84,12 +84,10 @@ fn criterion_benchmark(c: &mut Criterion) { c.bench_function("dict_get_item", dict_get_item); c.bench_function("extract_hashmap", extract_hashmap); c.bench_function("extract_btreemap", extract_btreemap); + c.bench_function("mapping_from_dict", mapping_from_dict); #[cfg(feature = "hashbrown")] c.bench_function("extract_hashbrown_map", extract_hashbrown_map); - - #[cfg(not(codspeed))] - c.bench_function("mapping_from_dict", mapping_from_dict); } criterion_group!(benches, criterion_benchmark); diff --git a/pyo3-benches/benches/bench_extract.rs b/pyo3-benches/benches/bench_extract.rs index 434d8eb5b33..9bb7ef60ab4 100644 --- a/pyo3-benches/benches/bench_extract.rs +++ b/pyo3-benches/benches/bench_extract.rs @@ -1,4 +1,6 @@ -use codspeed_criterion_compat::{black_box, criterion_group, criterion_main, Bencher, Criterion}; +use std::hint::black_box; + +use codspeed_criterion_compat::{criterion_group, criterion_main, Bencher, Criterion}; use pyo3::{ prelude::*, @@ -7,9 +9,9 @@ use pyo3::{ fn extract_str_extract_success(bench: &mut Bencher<'_>) { Python::with_gil(|py| { - let s = &PyString::new_bound(py, "Hello, World!"); + let s = PyString::new_bound(py, "Hello, World!").into_any(); - bench.iter(|| black_box(s).extract::<&str>().unwrap()); + bench.iter(|| black_box(&s).extract::<&str>().unwrap()); }); } @@ -19,7 +21,7 @@ fn extract_str_extract_fail(bench: &mut Bencher<'_>) { bench.iter(|| match black_box(&d).extract::<&str>() { Ok(v) => panic!("should err {}", v), - Err(e) => black_box(e), + Err(e) => e, }); }); } @@ -27,10 +29,10 @@ fn extract_str_extract_fail(bench: &mut Bencher<'_>) { #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] fn extract_str_downcast_success(bench: &mut Bencher<'_>) { Python::with_gil(|py| { - let s = &PyString::new_bound(py, "Hello, World!"); + let s = PyString::new_bound(py, "Hello, World!").into_any(); bench.iter(|| { - let py_str = black_box(s).downcast::().unwrap(); + let py_str = black_box(&s).downcast::().unwrap(); py_str.to_str().unwrap() }); }); @@ -42,20 +44,16 @@ fn extract_str_downcast_fail(bench: &mut Bencher<'_>) { bench.iter(|| match black_box(&d).downcast::() { Ok(v) => panic!("should err {}", v), - Err(e) => black_box(e), + Err(e) => e, }); }); } fn extract_int_extract_success(bench: &mut Bencher<'_>) { Python::with_gil(|py| { - let int_obj: PyObject = 123.into_py(py); - let int = int_obj.as_ref(py); + let int = 123.to_object(py).into_bound(py); - bench.iter(|| { - let v = black_box(int).extract::().unwrap(); - black_box(v); - }); + bench.iter(|| black_box(&int).extract::().unwrap()); }); } @@ -65,26 +63,22 @@ fn extract_int_extract_fail(bench: &mut Bencher<'_>) { bench.iter(|| match black_box(&d).extract::() { Ok(v) => panic!("should err {}", v), - Err(e) => black_box(e), + Err(e) => e, }); }); } -#[cfg(not(codspeed))] fn extract_int_downcast_success(bench: &mut Bencher<'_>) { Python::with_gil(|py| { - let int_obj: PyObject = 123.into_py(py); - let int = int_obj.as_ref(py); + let int = 123.to_object(py).into_bound(py); bench.iter(|| { - let py_int = black_box(int).downcast::().unwrap(); - let v = py_int.extract::().unwrap(); - black_box(v); + let py_int = black_box(&int).downcast::().unwrap(); + py_int.extract::().unwrap() }); }); } -#[cfg(not(codspeed))] fn extract_int_downcast_fail(bench: &mut Bencher<'_>) { Python::with_gil(|py| { let d = PyDict::new_bound(py).into_any(); @@ -96,16 +90,11 @@ fn extract_int_downcast_fail(bench: &mut Bencher<'_>) { }); } -#[cfg(not(codspeed))] fn extract_float_extract_success(bench: &mut Bencher<'_>) { Python::with_gil(|py| { - let float_obj: PyObject = 23.42.into_py(py); - let float = float_obj.as_ref(py); + let float = 23.42.to_object(py).into_bound(py); - bench.iter(|| { - let v = black_box(float).extract::().unwrap(); - black_box(v); - }); + bench.iter(|| black_box(&float).extract::().unwrap()); }); } @@ -115,21 +104,18 @@ fn extract_float_extract_fail(bench: &mut Bencher<'_>) { bench.iter(|| match black_box(&d).extract::() { Ok(v) => panic!("should err {}", v), - Err(e) => black_box(e), + Err(e) => e, }); }); } -#[cfg(not(codspeed))] fn extract_float_downcast_success(bench: &mut Bencher<'_>) { Python::with_gil(|py| { - let float_obj: PyObject = 23.42.into_py(py); - let float = float_obj.as_ref(py); + let float = 23.42.to_object(py).into_bound(py); bench.iter(|| { - let py_int = black_box(float).downcast::().unwrap(); - let v = py_int.extract::().unwrap(); - black_box(v); + let py_float = black_box(&float).downcast::().unwrap(); + py_float.value() }); }); } @@ -140,7 +126,7 @@ fn extract_float_downcast_fail(bench: &mut Bencher<'_>) { bench.iter(|| match black_box(&d).downcast::() { Ok(v) => panic!("should err {}", v), - Err(e) => black_box(e), + Err(e) => e, }); }); } @@ -151,20 +137,15 @@ fn criterion_benchmark(c: &mut Criterion) { #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] c.bench_function("extract_str_downcast_success", extract_str_downcast_success); c.bench_function("extract_str_downcast_fail", extract_str_downcast_fail); - #[cfg(not(codspeed))] c.bench_function("extract_int_extract_success", extract_int_extract_success); c.bench_function("extract_int_extract_fail", extract_int_extract_fail); - #[cfg(not(codspeed))] c.bench_function("extract_int_downcast_success", extract_int_downcast_success); - #[cfg(not(codspeed))] c.bench_function("extract_int_downcast_fail", extract_int_downcast_fail); - #[cfg(not(codspeed))] c.bench_function( "extract_float_extract_success", extract_float_extract_success, ); c.bench_function("extract_float_extract_fail", extract_float_extract_fail); - #[cfg(not(codspeed))] c.bench_function( "extract_float_downcast_success", extract_float_downcast_success, diff --git a/pyo3-benches/benches/bench_frompyobject.rs b/pyo3-benches/benches/bench_frompyobject.rs index e78c48fcaa1..f53f116a154 100644 --- a/pyo3-benches/benches/bench_frompyobject.rs +++ b/pyo3-benches/benches/bench_frompyobject.rs @@ -1,11 +1,14 @@ -use codspeed_criterion_compat::{black_box, criterion_group, criterion_main, Bencher, Criterion}; +use std::hint::black_box; + +use codspeed_criterion_compat::{criterion_group, criterion_main, Bencher, Criterion}; use pyo3::{ prelude::*, - types::{PyFloat, PyList, PyString}, + types::{PyList, PyString}, }; #[derive(FromPyObject)] +#[allow(dead_code)] enum ManyTypes { Int(i32), Bytes(Vec), @@ -14,44 +17,41 @@ enum ManyTypes { fn enum_from_pyobject(b: &mut Bencher<'_>) { Python::with_gil(|py| { - let any: &Bound<'_, PyAny> = &PyString::new_bound(py, "hello world"); + let any = PyString::new_bound(py, "hello world").into_any(); - b.iter(|| any.extract::().unwrap()); + b.iter(|| black_box(&any).extract::().unwrap()); }) } -#[cfg(not(codspeed))] fn list_via_downcast(b: &mut Bencher<'_>) { Python::with_gil(|py| { - let any: &Bound<'_, PyAny> = &PyList::empty_bound(py); + let any = PyList::empty_bound(py).into_any(); - b.iter(|| black_box(any).downcast::().unwrap()); + b.iter(|| black_box(&any).downcast::().unwrap()); }) } -#[cfg(not(codspeed))] fn list_via_extract(b: &mut Bencher<'_>) { Python::with_gil(|py| { - let any: &Bound<'_, PyAny> = &PyList::empty_bound(py); + let any = PyList::empty_bound(py).into_any(); - b.iter(|| black_box(any).extract::>().unwrap()); + b.iter(|| black_box(&any).extract::>().unwrap()); }) } -#[cfg(not(codspeed))] fn not_a_list_via_downcast(b: &mut Bencher<'_>) { Python::with_gil(|py| { - let any: &Bound<'_, PyAny> = &PyString::new_bound(py, "foobar"); + let any = PyString::new_bound(py, "foobar").into_any(); - b.iter(|| black_box(any).downcast::().unwrap_err()); + b.iter(|| black_box(&any).downcast::().unwrap_err()); }) } fn not_a_list_via_extract(b: &mut Bencher<'_>) { Python::with_gil(|py| { - let any: &Bound<'_, PyAny> = &PyString::new_bound(py, "foobar"); + let any = PyString::new_bound(py, "foobar").into_any(); - b.iter(|| black_box(any).extract::>().unwrap_err()); + b.iter(|| black_box(&any).extract::>().unwrap_err()); }) } @@ -63,9 +63,9 @@ enum ListOrNotList<'a> { fn not_a_list_via_extract_enum(b: &mut Bencher<'_>) { Python::with_gil(|py| { - let any: &Bound<'_, PyAny> = &PyString::new_bound(py, "foobar"); + let any = PyString::new_bound(py, "foobar").into_any(); - b.iter(|| match black_box(any).extract::>() { + b.iter(|| match black_box(&any).extract::>() { Ok(ListOrNotList::List(_list)) => panic!(), Ok(ListOrNotList::NotList(any)) => any, Err(_) => panic!(), @@ -73,26 +73,16 @@ fn not_a_list_via_extract_enum(b: &mut Bencher<'_>) { }) } -#[cfg(not(codspeed))] -fn f64_from_pyobject(b: &mut Bencher<'_>) { - Python::with_gil(|py| { - let obj = &PyFloat::new_bound(py, 1.234); - b.iter(|| black_box(obj).extract::().unwrap()); - }) -} - fn criterion_benchmark(c: &mut Criterion) { c.bench_function("enum_from_pyobject", enum_from_pyobject); - #[cfg(not(codspeed))] + c.bench_function("list_via_downcast", list_via_downcast); - #[cfg(not(codspeed))] + c.bench_function("list_via_extract", list_via_extract); - #[cfg(not(codspeed))] + c.bench_function("not_a_list_via_downcast", not_a_list_via_downcast); c.bench_function("not_a_list_via_extract", not_a_list_via_extract); c.bench_function("not_a_list_via_extract_enum", not_a_list_via_extract_enum); - #[cfg(not(codspeed))] - c.bench_function("f64_from_pyobject", f64_from_pyobject); } criterion_group!(benches, criterion_benchmark); diff --git a/pyo3-benches/benches/bench_list.rs b/pyo3-benches/benches/bench_list.rs index 774dddcea38..dcbdb4779cb 100644 --- a/pyo3-benches/benches/bench_list.rs +++ b/pyo3-benches/benches/bench_list.rs @@ -55,7 +55,6 @@ fn list_get_item_unchecked(b: &mut Bencher<'_>) { }); } -#[cfg(not(codspeed))] fn sequence_from_list(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 50_000; @@ -70,7 +69,6 @@ fn criterion_benchmark(c: &mut Criterion) { c.bench_function("list_get_item", list_get_item); #[cfg(not(Py_LIMITED_API))] c.bench_function("list_get_item_unchecked", list_get_item_unchecked); - #[cfg(not(codspeed))] 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 b04cb491304..18134a15bd5 100644 --- a/pyo3-benches/benches/bench_set.rs +++ b/pyo3-benches/benches/bench_set.rs @@ -2,7 +2,10 @@ use codspeed_criterion_compat::{criterion_group, criterion_main, Bencher, Criter use pyo3::prelude::*; use pyo3::types::PySet; -use std::collections::{BTreeSet, HashSet}; +use std::{ + collections::{BTreeSet, HashSet}, + hint::black_box, +}; fn set_new(b: &mut Bencher<'_>) { Python::with_gil(|py| { @@ -31,16 +34,20 @@ fn iter_set(b: &mut Bencher<'_>) { fn extract_hashset(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 100_000; - let set = PySet::new_bound(py, &(0..LEN).collect::>()).unwrap(); - b.iter_with_large_drop(|| HashSet::::extract(set.as_gil_ref())); + let any = PySet::new_bound(py, &(0..LEN).collect::>()) + .unwrap() + .into_any(); + b.iter_with_large_drop(|| black_box(&any).extract::>()); }); } fn extract_btreeset(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 100_000; - let set = PySet::new_bound(py, &(0..LEN).collect::>()).unwrap(); - b.iter_with_large_drop(|| BTreeSet::::extract(set.as_gil_ref())); + let any = PySet::new_bound(py, &(0..LEN).collect::>()) + .unwrap() + .into_any(); + b.iter_with_large_drop(|| black_box(&any).extract::>()); }); } @@ -48,8 +55,10 @@ fn extract_btreeset(b: &mut Bencher<'_>) { fn extract_hashbrown_set(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 100_000; - let set = PySet::new_bound(py, &(0..LEN).collect::>()).unwrap(); - b.iter_with_large_drop(|| hashbrown::HashSet::::extract(set.as_gil_ref())); + let any = PySet::new_bound(py, &(0..LEN).collect::>()) + .unwrap() + .into_any(); + b.iter_with_large_drop(|| black_box(&any).extract::>()); }); } diff --git a/pyo3-benches/benches/bench_tuple.rs b/pyo3-benches/benches/bench_tuple.rs index 22bf7588ed1..3c0b56a0234 100644 --- a/pyo3-benches/benches/bench_tuple.rs +++ b/pyo3-benches/benches/bench_tuple.rs @@ -1,3 +1,5 @@ +use std::hint::black_box; + use codspeed_criterion_compat::{criterion_group, criterion_main, Bencher, Criterion}; use pyo3::prelude::*; @@ -89,12 +91,11 @@ fn tuple_get_borrowed_item_unchecked(b: &mut Bencher<'_>) { }); } -#[cfg(not(codspeed))] fn sequence_from_tuple(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 50_000; - let tuple = PyTuple::new_bound(py, 0..LEN).to_object(py); - b.iter(|| tuple.downcast::(py).unwrap()); + let tuple = PyTuple::new_bound(py, 0..LEN).into_any(); + b.iter(|| black_box(&tuple).downcast::().unwrap()); }); } @@ -132,7 +133,6 @@ fn criterion_benchmark(c: &mut Criterion) { "tuple_get_borrowed_item_unchecked", tuple_get_borrowed_item_unchecked, ); - #[cfg(not(codspeed))] c.bench_function("sequence_from_tuple", sequence_from_tuple); c.bench_function("tuple_new_list", tuple_new_list); c.bench_function("tuple_to_list", tuple_to_list); From 54ffaecd65a746e702e8ec904d2af4f3a8aeaee9 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Mon, 25 Mar 2024 17:21:27 +0100 Subject: [PATCH 104/936] add `AsRef` impls for `PyBackedStr` and `PyBackedBytes` (#3991) * add `AsRef` impls for `PyBackedStr` and `PyBackedBytes` * add newsfragment --- newsfragments/3991.added.md | 1 + src/pybacked.rs | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) create mode 100644 newsfragments/3991.added.md diff --git a/newsfragments/3991.added.md b/newsfragments/3991.added.md new file mode 100644 index 00000000000..1a59ecee79c --- /dev/null +++ b/newsfragments/3991.added.md @@ -0,0 +1 @@ +implemented `AsRef` and `AsRef<[u8]>` for `PyBackedStr`, `AsRef<[u8]>` for `PyBackedBytes`. diff --git a/src/pybacked.rs b/src/pybacked.rs index bd29c830e65..dbb0865285e 100644 --- a/src/pybacked.rs +++ b/src/pybacked.rs @@ -27,6 +27,18 @@ impl Deref for PyBackedStr { } } +impl AsRef for PyBackedStr { + fn as_ref(&self) -> &str { + self + } +} + +impl AsRef<[u8]> for PyBackedStr { + fn as_ref(&self) -> &[u8] { + self.as_bytes() + } +} + impl TryFrom> for PyBackedStr { type Error = PyErr; fn try_from(py_string: Bound<'_, PyString>) -> Result { @@ -82,6 +94,12 @@ impl Deref for PyBackedBytes { } } +impl AsRef<[u8]> for PyBackedBytes { + fn as_ref(&self) -> &[u8] { + self + } +} + impl From> for PyBackedBytes { fn from(py_bytes: Bound<'_, PyBytes>) -> Self { let b = py_bytes.as_bytes(); From 9a38e709bb43bdd8e43aa7c2fce603ed30ed469f Mon Sep 17 00:00:00 2001 From: Tim Felgentreff Date: Mon, 25 Mar 2024 19:54:52 +0100 Subject: [PATCH 105/936] Basic GraalPy Support (#3247) * graalpy: recognize graalpy implementation when building * graalpy: global Ellipse, None, NotImplemented, True, and False are only available as pointers * graalpy: PyObject struct is opaque, use functions for everything * graalpy: missing many of the same functions as pypy * graalpy: do not have 128bit conversion functions * graalpy: add functions for datetime accessor macros * graalpy: add implementations for list macro functions * graalpy: skip tuple macros * graalpy: always use extern Py_CompileString function * graalpy: disable assertion that does not apply to graalpy * graalpy: floatobject structure is opaque on graalpy * graalpy: ignore gc dependent test * graalpy: add CI config * graalpy: run rust fmt * graalpy: add changelog entry * graalpy: discover interpreter on PATH * graalpy: interpreter id is not applicable to graalpy (just like pypy) * graalpy: skip tests that cannot work on GraalPy * graalpy: fix constructing normalized Err instances Co-authored-by: David Hewitt * graalpy: correct capi library name, but skip rust tests due to missing symbols * graalpy: no support for C extensions on windows in latest release * graalpy: declare support versions * graalpy: frame, code, method, and function objects access from C API is mostly missing * graalpy: take care only to expose C structure that GraalPy allocates * graalpy: Bail out if graalpy version is less than what we support --------- Co-authored-by: David Hewitt --- .github/workflows/build.yml | 15 +- .github/workflows/ci.yml | 1 + README.md | 2 +- newsfragments/3247.added.md | 1 + pyo3-build-config/src/impl_.rs | 69 +++++++- pyo3-build-config/src/import_lib.rs | 5 +- pyo3-build-config/src/lib.rs | 1 + pyo3-ffi/build.rs | 29 ++++ pyo3-ffi/src/abstract_.rs | 1 + pyo3-ffi/src/boolobject.rs | 18 ++- pyo3-ffi/src/bytearrayobject.rs | 4 +- pyo3-ffi/src/complexobject.rs | 1 + pyo3-ffi/src/cpython/abstract_.rs | 39 ++--- pyo3-ffi/src/cpython/bytesobject.rs | 6 +- pyo3-ffi/src/cpython/code.rs | 24 +-- pyo3-ffi/src/cpython/compile.rs | 4 +- pyo3-ffi/src/cpython/dictobject.rs | 1 + pyo3-ffi/src/cpython/floatobject.rs | 8 +- pyo3-ffi/src/cpython/frameobject.rs | 15 +- pyo3-ffi/src/cpython/funcobject.rs | 9 +- pyo3-ffi/src/cpython/genobject.rs | 4 +- pyo3-ffi/src/cpython/listobject.rs | 8 +- pyo3-ffi/src/cpython/methodobject.rs | 6 + pyo3-ffi/src/cpython/mod.rs | 2 +- pyo3-ffi/src/cpython/object.rs | 2 + pyo3-ffi/src/cpython/objimpl.rs | 10 +- pyo3-ffi/src/cpython/pyerrors.rs | 42 ++--- pyo3-ffi/src/cpython/pymem.rs | 8 +- pyo3-ffi/src/cpython/pythonrun.rs | 43 ++--- pyo3-ffi/src/cpython/tupleobject.rs | 5 +- pyo3-ffi/src/cpython/unicodeobject.rs | 51 ++++-- pyo3-ffi/src/cpython/weakrefobject.rs | 2 +- pyo3-ffi/src/datetime.rs | 224 ++++++++++++++++++++------ pyo3-ffi/src/dictobject.rs | 2 +- pyo3-ffi/src/listobject.rs | 8 +- pyo3-ffi/src/methodobject.rs | 2 +- pyo3-ffi/src/object.rs | 55 ++++++- pyo3-ffi/src/pyframe.rs | 2 + pyo3-ffi/src/pyhash.rs | 12 +- pyo3-ffi/src/pythonrun.rs | 4 +- pyo3-ffi/src/setobject.rs | 10 +- pyo3-ffi/src/sliceobject.rs | 12 +- pyo3-ffi/src/structseq.rs | 4 +- pyo3-ffi/src/weakrefobject.rs | 4 +- pytests/tests/test_awaitable.py | 9 ++ pytests/tests/test_misc.py | 4 +- pytests/tests/test_objstore.py | 5 + src/conversions/std/num.rs | 4 +- src/err/mod.rs | 13 +- src/exceptions.rs | 16 +- src/gil.rs | 8 +- src/impl_/pymodule.rs | 22 ++- src/impl_/trampoline.rs | 6 +- src/lib.rs | 2 +- src/macros.rs | 2 +- src/marker.rs | 2 +- src/types/any.rs | 11 +- src/types/complex.rs | 10 +- src/types/datetime.rs | 32 ++++ src/types/dict.rs | 38 ++--- src/types/frozenset.rs | 4 +- src/types/mod.rs | 16 +- src/types/sequence.rs | 4 +- src/types/set.rs | 4 +- src/types/string.rs | 8 +- src/types/tuple.rs | 44 ++--- 66 files changed, 731 insertions(+), 308 deletions(-) create mode 100644 newsfragments/3247.added.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 25c8014f762..d2f74401dd6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,6 +24,7 @@ jobs: build: continue-on-error: ${{ endsWith(inputs.python-version, '-dev') || contains(fromJSON('["3.7", "pypy3.7"]'), inputs.python-version) || inputs.rust == 'beta' || inputs.rust == 'nightly' }} runs-on: ${{ inputs.os }} + if: ${{ !(startsWith(inputs.python-version, 'graalpy') && startsWith(inputs.os, 'windows')) }} steps: - uses: actions/checkout@v4 @@ -65,6 +66,7 @@ jobs: run: nox -s docs - name: Build (no features) + if: ${{ !startsWith(inputs.python-version, 'graalpy') }} run: cargo build --lib --tests --no-default-features # --no-default-features when used with `cargo build/test -p` doesn't seem to work! @@ -74,7 +76,7 @@ jobs: cargo build --no-default-features # Run tests (except on PyPy, because no embedding API). - - if: ${{ !startsWith(inputs.python-version, 'pypy') }} + - if: ${{ !startsWith(inputs.python-version, 'pypy') && !startsWith(inputs.python-version, 'graalpy') }} name: Test (no features) run: cargo test --no-default-features --lib --tests @@ -85,6 +87,7 @@ jobs: cargo test --no-default-features - name: Build (all additive features) + if: ${{ !startsWith(inputs.python-version, 'graalpy') }} run: cargo build --lib --tests --no-default-features --features "full ${{ inputs.extra-features }}" - if: ${{ startsWith(inputs.python-version, 'pypy') }} @@ -92,17 +95,17 @@ jobs: run: cargo build --lib --tests --no-default-features --features "abi3-py37 full ${{ inputs.extra-features }}" # Run tests (except on PyPy, because no embedding API). - - if: ${{ !startsWith(inputs.python-version, 'pypy') }} + - if: ${{ !startsWith(inputs.python-version, 'pypy') && !startsWith(inputs.python-version, 'graalpy') }} name: Test run: cargo test --no-default-features --features "full ${{ inputs.extra-features }}" # Run tests again, but in abi3 mode - - if: ${{ !startsWith(inputs.python-version, 'pypy') }} + - if: ${{ !startsWith(inputs.python-version, 'pypy') && !startsWith(inputs.python-version, 'graalpy') }} name: Test (abi3) run: cargo test --no-default-features --features "abi3 full ${{ inputs.extra-features }}" # Run tests again, for abi3-py37 (the minimal Python version) - - if: ${{ (!startsWith(inputs.python-version, 'pypy')) && (inputs.python-version != '3.7') }} + - if: ${{ (!startsWith(inputs.python-version, 'pypy') && !startsWith(inputs.python-version, 'graalpy')) && (inputs.python-version != '3.7') }} name: Test (abi3-py37) run: cargo test --no-default-features --features "abi3-py37 full ${{ inputs.extra-features }}" @@ -120,7 +123,7 @@ jobs: - uses: dorny/paths-filter@v3 # pypy 3.7 and 3.8 are not PEP 3123 compliant so fail checks here - if: ${{ inputs.rust == 'stable' && inputs.python-version != 'pypy3.7' && inputs.python-version != 'pypy3.8' }} + if: ${{ inputs.rust == 'stable' && inputs.python-version != 'pypy3.7' && inputs.python-version != 'pypy3.8' && !startsWith(inputs.python-version, 'graalpy') }} id: ffi-changes with: base: ${{ github.event.pull_request.base.ref || github.event.merge_group.base_ref }} @@ -135,7 +138,7 @@ jobs: - name: Run pyo3-ffi-check # pypy 3.7 and 3.8 are not PEP 3123 compliant so fail checks here, nor # is pypy 3.9 on windows - if: ${{ endsWith(inputs.python-version, '-dev') || (steps.ffi-changes.outputs.changed == 'true' && inputs.rust == 'stable' && inputs.python-version != 'pypy3.7' && inputs.python-version != 'pypy3.8' && !(inputs.python-version == 'pypy3.9' && contains(inputs.os, 'windows'))) }} + if: ${{ endsWith(inputs.python-version, '-dev') || (steps.ffi-changes.outputs.changed == 'true' && inputs.rust == 'stable' && inputs.python-version != 'pypy3.7' && inputs.python-version != 'pypy3.8' && !startsWith(inputs.python-version, 'graalpy') && !(inputs.python-version == 'pypy3.9' && contains(inputs.os, 'windows'))) }} run: nox -s ffi-check env: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5ee0776ebed..63f6c31a599 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -228,6 +228,7 @@ jobs: "pypy3.8", "pypy3.9", "pypy3.10", + "graalpy24.0", ] platform: [ diff --git a/README.md b/README.md index c11754c3ca1..461e56612ad 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ ## Usage PyO3 supports the following software versions: - - Python 3.7 and up (CPython and PyPy) + - Python 3.7 and up (CPython, PyPy, and GraalPy) - Rust 1.56 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/newsfragments/3247.added.md b/newsfragments/3247.added.md new file mode 100644 index 00000000000..dd2641c3194 --- /dev/null +++ b/newsfragments/3247.added.md @@ -0,0 +1 @@ +Added support for running PyO3 extensions on GraalPy. diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 7f02f744fc2..d5373db9655 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -32,6 +32,12 @@ use crate::{ /// Minimum Python version PyO3 supports. const MINIMUM_SUPPORTED_VERSION: PythonVersion = PythonVersion { major: 3, minor: 7 }; +/// GraalPy may implement the same CPython version over multiple releases. +const MINIMUM_SUPPORTED_VERSION_GRAALPY: PythonVersion = PythonVersion { + major: 24, + minor: 0, +}; + /// Maximum Python version that can be used as minimum required Python version with abi3. const ABI3_MAX_MINOR: u8 = 12; @@ -173,6 +179,11 @@ impl InterpreterConfig { See https://foss.heptapod.net/pypy/pypy/-/issues/3397 for more information." )); } + } else if self.implementation.is_graalpy() { + println!("cargo:rustc-cfg=GraalPy"); + if self.abi3 { + warn!("GraalPy does not support abi3 so the build artifacts will be version-specific."); + } } else if self.abi3 { out.push("cargo:rustc-cfg=Py_LIMITED_API".to_owned()); } @@ -197,6 +208,12 @@ import sys from sysconfig import get_config_var, get_platform PYPY = platform.python_implementation() == "PyPy" +GRAALPY = platform.python_implementation() == "GraalVM" + +if GRAALPY: + graalpy_ver = map(int, __graalpython__.get_graalvm_version().split('.')); + print("graalpy_major", next(graalpy_ver)) + print("graalpy_minor", next(graalpy_ver)) # sys.base_prefix is missing on Python versions older than 3.3; this allows the script to continue # so that the version mismatch can be reported in a nicer way later. @@ -226,7 +243,7 @@ SHARED = bool(get_config_var("Py_ENABLE_SHARED")) print("implementation", platform.python_implementation()) print("version_major", sys.version_info[0]) print("version_minor", sys.version_info[1]) -print("shared", PYPY or ANACONDA or WINDOWS or FRAMEWORK or SHARED) +print("shared", PYPY or GRAALPY or ANACONDA or WINDOWS or FRAMEWORK or SHARED) print_if_set("ld_version", get_config_var("LDVERSION")) print_if_set("libdir", get_config_var("LIBDIR")) print_if_set("base_prefix", base_prefix) @@ -244,6 +261,23 @@ print("ext_suffix", get_config_var("EXT_SUFFIX")) interpreter.as_ref().display() ); + if let Some(value) = map.get("graalpy_major") { + let graalpy_version = PythonVersion { + major: value + .parse() + .context("failed to parse GraalPy major version")?, + minor: map["graalpy_minor"] + .parse() + .context("failed to parse GraalPy minor version")?, + }; + ensure!( + graalpy_version >= MINIMUM_SUPPORTED_VERSION_GRAALPY, + "At least GraalPy version {} needed, got {}", + MINIMUM_SUPPORTED_VERSION_GRAALPY, + graalpy_version + ); + }; + let shared = map["shared"].as_str() == "True"; let version = PythonVersion { @@ -588,7 +622,7 @@ print("ext_suffix", get_config_var("EXT_SUFFIX")) /// Lowers the configured version to the abi3 version, if set. fn fixup_for_abi3_version(&mut self, abi3_version: Option) -> Result<()> { // PyPy doesn't support abi3; don't adjust the version - if self.implementation.is_pypy() { + if self.implementation.is_pypy() || self.implementation.is_graalpy() { return Ok(()); } @@ -647,6 +681,7 @@ impl FromStr for PythonVersion { pub enum PythonImplementation { CPython, PyPy, + GraalPy, } impl PythonImplementation { @@ -655,12 +690,19 @@ impl PythonImplementation { self == PythonImplementation::PyPy } + #[doc(hidden)] + pub fn is_graalpy(self) -> bool { + self == PythonImplementation::GraalPy + } + #[doc(hidden)] pub fn from_soabi(soabi: &str) -> Result { if soabi.starts_with("pypy") { Ok(PythonImplementation::PyPy) } else if soabi.starts_with("cpython") { Ok(PythonImplementation::CPython) + } else if soabi.starts_with("graalpy") { + Ok(PythonImplementation::GraalPy) } else { bail!("unsupported Python interpreter"); } @@ -672,6 +714,7 @@ impl Display for PythonImplementation { match self { PythonImplementation::CPython => write!(f, "CPython"), PythonImplementation::PyPy => write!(f, "PyPy"), + PythonImplementation::GraalPy => write!(f, "GraalVM"), } } } @@ -682,6 +725,7 @@ impl FromStr for PythonImplementation { match s { "CPython" => Ok(PythonImplementation::CPython), "PyPy" => Ok(PythonImplementation::PyPy), + "GraalVM" => Ok(PythonImplementation::GraalPy), _ => bail!("unknown interpreter: {}", s), } } @@ -760,7 +804,7 @@ pub struct CrossCompileConfig { /// The version of the Python library to link against. version: Option, - /// The target Python implementation hint (CPython or PyPy) + /// The target Python implementation hint (CPython, PyPy, GraalPy, ...) implementation: Option, /// The compile target triple (e.g. aarch64-unknown-linux-gnu) @@ -1264,6 +1308,15 @@ fn is_pypy_lib_dir(path: &str, v: &Option) -> bool { path == "lib_pypy" || path.starts_with(&pypy_version_pat) } +fn is_graalpy_lib_dir(path: &str, v: &Option) -> bool { + let graalpy_version_pat = if let Some(v) = v { + format!("graalpy{}", v) + } else { + "graalpy2".into() + }; + path == "lib_graalpython" || path.starts_with(&graalpy_version_pat) +} + fn is_cpython_lib_dir(path: &str, v: &Option) -> bool { let cpython_version_pat = if let Some(v) = v { format!("python{}", v) @@ -1297,6 +1350,7 @@ fn search_lib_dir(path: impl AsRef, cross: &CrossCompileConfig) -> Vec Result InterpreterConfig { - // FIXME: PyPy does not support the Stable ABI yet. + // FIXME: PyPy & GraalPy do not support the Stable ABI. let implementation = PythonImplementation::CPython; let abi3 = true; @@ -1524,7 +1578,7 @@ fn default_lib_name_windows( // CPython bug: linking against python3_d.dll raises error // https://github.com/python/cpython/issues/101614 format!("python{}{}_d", version.major, version.minor) - } else if abi3 && !implementation.is_pypy() { + } else if abi3 && !(implementation.is_pypy() || implementation.is_graalpy()) { WINDOWS_ABI3_LIB_NAME.to_owned() } else if mingw { // https://packages.msys2.org/base/mingw-w64-python @@ -1562,6 +1616,7 @@ fn default_lib_name_unix( format!("pypy{}-c", version.major) } } + PythonImplementation::GraalPy => "python-native".to_string(), } } @@ -1662,7 +1717,9 @@ pub fn find_interpreter() -> Result { .find(|bin| { if let Ok(out) = Command::new(bin).arg("--version").output() { // begin with `Python 3.X.X :: additional info` - out.stdout.starts_with(b"Python 3") || out.stderr.starts_with(b"Python 3") + out.stdout.starts_with(b"Python 3") + || out.stderr.starts_with(b"Python 3") + || out.stdout.starts_with(b"GraalPy 3") } else { false } diff --git a/pyo3-build-config/src/import_lib.rs b/pyo3-build-config/src/import_lib.rs index dc21638c3db..0925a861b5b 100644 --- a/pyo3-build-config/src/import_lib.rs +++ b/pyo3-build-config/src/import_lib.rs @@ -7,7 +7,7 @@ use python3_dll_a::ImportLibraryGenerator; use target_lexicon::{Architecture, OperatingSystem, Triple}; use super::{PythonImplementation, PythonVersion}; -use crate::errors::{Context, Result}; +use crate::errors::{Context, Error, Result}; /// Generates the `python3.dll` or `pythonXY.dll` import library for Windows targets. /// @@ -42,6 +42,9 @@ pub(super) fn generate_import_lib( let implementation = match py_impl { PythonImplementation::CPython => python3_dll_a::PythonImplementation::CPython, PythonImplementation::PyPy => python3_dll_a::PythonImplementation::PyPy, + PythonImplementation::GraalPy => { + return Err(Error::from("No support for GraalPy on Windows")) + } }; ImportLibraryGenerator::new(&arch, &env) diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index e7e59d3b79d..eab7376d0ea 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -38,6 +38,7 @@ use target_lexicon::OperatingSystem; /// | `#[cfg(Py_3_7)]`, `#[cfg(Py_3_8)]`, `#[cfg(Py_3_9)]`, `#[cfg(Py_3_10)]` | These attributes mark code only for a given Python version and up. For example, `#[cfg(Py_3_7)]` marks code which can run on Python 3.7 **and newer**. | /// | `#[cfg(Py_LIMITED_API)]` | This marks code which is run when compiling with PyO3's `abi3` feature enabled. | /// | `#[cfg(PyPy)]` | This marks code which is run when compiling for PyPy. | +/// | `#[cfg(GraalPy)]` | This marks code which is run when compiling for GraalPy. | /// /// For examples of how to use these attributes, [see PyO3's guide](https://pyo3.rs/latest/building-and-distribution/multiple_python_versions.html). #[cfg(feature = "resolve-config")] diff --git a/pyo3-ffi/build.rs b/pyo3-ffi/build.rs index 286767d8f25..a5ab352b6a7 100644 --- a/pyo3-ffi/build.rs +++ b/pyo3-ffi/build.rs @@ -29,6 +29,17 @@ const SUPPORTED_VERSIONS_PYPY: SupportedVersions = SupportedVersions { }, }; +const SUPPORTED_VERSIONS_GRAALPY: SupportedVersions = SupportedVersions { + min: PythonVersion { + major: 3, + minor: 10, + }, + max: PythonVersion { + major: 3, + minor: 11, + }, +}; + fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { // This is an undocumented env var which is only really intended to be used in CI / for testing // and development. @@ -73,6 +84,24 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { std::env::var("CARGO_PKG_VERSION").unwrap() ); } + PythonImplementation::GraalPy => { + let versions = SUPPORTED_VERSIONS_GRAALPY; + ensure!( + interpreter_config.version >= versions.min, + "the configured GraalPy interpreter version ({}) is lower than PyO3's minimum supported version ({})", + interpreter_config.version, + versions.min, + ); + // GraalPy does not support abi3, so we cannot offer forward compatibility + ensure!( + interpreter_config.version <= versions.max, + "the configured GraalPy interpreter version ({}) is newer than PyO3's maximum supported version ({})\n\ + = help: please check if an updated version of PyO3 is available. Current version: {}", + interpreter_config.version, + versions.max, + std::env::var("CARGO_PKG_VERSION").unwrap() + ); + } } Ok(()) diff --git a/pyo3-ffi/src/abstract_.rs b/pyo3-ffi/src/abstract_.rs index 0b3b7dbb3c2..b5bf9cc3d35 100644 --- a/pyo3-ffi/src/abstract_.rs +++ b/pyo3-ffi/src/abstract_.rs @@ -23,6 +23,7 @@ pub unsafe fn PyObject_DelAttr(o: *mut PyObject, attr_name: *mut PyObject) -> c_ extern "C" { #[cfg(all( not(PyPy), + not(GraalPy), any(Py_3_10, all(not(Py_LIMITED_API), Py_3_9)) // Added to python in 3.9 but to limited API in 3.10 ))] #[cfg_attr(PyPy, link_name = "PyPyObject_CallNoArgs")] diff --git a/pyo3-ffi/src/boolobject.rs b/pyo3-ffi/src/boolobject.rs index 78972ff0835..10b5969fa4f 100644 --- a/pyo3-ffi/src/boolobject.rs +++ b/pyo3-ffi/src/boolobject.rs @@ -1,3 +1,4 @@ +#[cfg(not(GraalPy))] use crate::longobject::PyLongObject; use crate::object::*; use std::os::raw::{c_int, c_long}; @@ -16,20 +17,33 @@ pub unsafe fn PyBool_Check(op: *mut PyObject) -> c_int { #[cfg_attr(windows, link(name = "pythonXY"))] extern "C" { + #[cfg(not(GraalPy))] #[cfg_attr(PyPy, link_name = "_PyPy_FalseStruct")] static mut _Py_FalseStruct: PyLongObject; + #[cfg(not(GraalPy))] #[cfg_attr(PyPy, link_name = "_PyPy_TrueStruct")] static mut _Py_TrueStruct: PyLongObject; + + #[cfg(GraalPy)] + static mut _Py_FalseStructReference: *mut PyObject; + #[cfg(GraalPy)] + static mut _Py_TrueStructReference: *mut PyObject; } #[inline] pub unsafe fn Py_False() -> *mut PyObject { - addr_of_mut!(_Py_FalseStruct) as *mut PyObject + #[cfg(not(GraalPy))] + return addr_of_mut!(_Py_FalseStruct) as *mut PyObject; + #[cfg(GraalPy)] + return _Py_FalseStructReference; } #[inline] pub unsafe fn Py_True() -> *mut PyObject { - addr_of_mut!(_Py_TrueStruct) as *mut PyObject + #[cfg(not(GraalPy))] + return addr_of_mut!(_Py_TrueStruct) as *mut PyObject; + #[cfg(GraalPy)] + return _Py_TrueStructReference; } #[inline] diff --git a/pyo3-ffi/src/bytearrayobject.rs b/pyo3-ffi/src/bytearrayobject.rs index a37deb410f7..c09eac5b22c 100644 --- a/pyo3-ffi/src/bytearrayobject.rs +++ b/pyo3-ffi/src/bytearrayobject.rs @@ -3,7 +3,7 @@ use crate::pyport::Py_ssize_t; use std::os::raw::{c_char, c_int}; use std::ptr::addr_of_mut; -#[cfg(not(any(PyPy, Py_LIMITED_API)))] +#[cfg(not(any(PyPy, GraalPy, Py_LIMITED_API)))] #[repr(C)] #[derive(Copy, Clone)] pub struct PyByteArrayObject { @@ -17,7 +17,7 @@ pub struct PyByteArrayObject { pub ob_exports: c_int, } -#[cfg(any(PyPy, Py_LIMITED_API))] +#[cfg(any(PyPy, GraalPy, Py_LIMITED_API))] opaque_struct!(PyByteArrayObject); #[cfg_attr(windows, link(name = "pythonXY"))] diff --git a/pyo3-ffi/src/complexobject.rs b/pyo3-ffi/src/complexobject.rs index 339f5d8c81a..a03d9b00932 100644 --- a/pyo3-ffi/src/complexobject.rs +++ b/pyo3-ffi/src/complexobject.rs @@ -30,6 +30,7 @@ extern "C" { // non-limited pub struct PyComplexObject { pub ob_base: PyObject, + #[cfg(not(GraalPy))] pub cval: Py_complex, } diff --git a/pyo3-ffi/src/cpython/abstract_.rs b/pyo3-ffi/src/cpython/abstract_.rs index d2e3ca9d67a..cf95f6711d4 100644 --- a/pyo3-ffi/src/cpython/abstract_.rs +++ b/pyo3-ffi/src/cpython/abstract_.rs @@ -6,7 +6,7 @@ use crate::Py_buffer; #[cfg(Py_3_8)] use crate::pyport::PY_SSIZE_T_MAX; -#[cfg(all(Py_3_8, not(PyPy)))] +#[cfg(all(Py_3_8, not(any(PyPy, GraalPy))))] use crate::{ vectorcallfunc, PyCallable_Check, PyThreadState, PyThreadState_GET, PyTuple_Check, PyType_HasFeature, Py_TPFLAGS_HAVE_VECTORCALL, @@ -15,15 +15,15 @@ use crate::{ use libc::size_t; extern "C" { - #[cfg(all(Py_3_8, not(PyPy)))] + #[cfg(all(Py_3_8, not(any(PyPy, GraalPy))))] pub fn _PyStack_AsDict(values: *const *mut PyObject, kwnames: *mut PyObject) -> *mut PyObject; } -#[cfg(all(Py_3_8, not(PyPy)))] +#[cfg(all(Py_3_8, not(any(PyPy, GraalPy))))] const _PY_FASTCALL_SMALL_STACK: size_t = 5; extern "C" { - #[cfg(all(Py_3_8, not(PyPy)))] + #[cfg(all(Py_3_8, not(any(PyPy, GraalPy))))] pub fn _Py_CheckFunctionResult( tstate: *mut PyThreadState, callable: *mut PyObject, @@ -31,7 +31,7 @@ extern "C" { where_: *const c_char, ) -> *mut PyObject; - #[cfg(all(Py_3_8, not(PyPy)))] + #[cfg(all(Py_3_8, not(any(PyPy, GraalPy))))] pub fn _PyObject_MakeTpCall( tstate: *mut PyThreadState, callable: *mut PyObject, @@ -52,7 +52,7 @@ pub unsafe fn PyVectorcall_NARGS(n: size_t) -> Py_ssize_t { (n as Py_ssize_t) & !PY_VECTORCALL_ARGUMENTS_OFFSET } -#[cfg(all(Py_3_8, not(PyPy)))] +#[cfg(all(Py_3_8, not(any(PyPy, GraalPy))))] #[inline(always)] pub unsafe fn PyVectorcall_Function(callable: *mut PyObject) -> Option { assert!(!callable.is_null()); @@ -67,7 +67,7 @@ pub unsafe fn PyVectorcall_Function(callable: *mut PyObject) -> Option *mut PyObject; #[cfg(Py_3_8)] - #[cfg_attr(all(not(PyPy), not(Py_3_9)), link_name = "_PyObject_VectorcallDict")] + #[cfg_attr( + all(not(any(PyPy, GraalPy)), not(Py_3_9)), + link_name = "_PyObject_VectorcallDict" + )] #[cfg_attr(all(PyPy, not(Py_3_9)), link_name = "_PyPyObject_VectorcallDict")] #[cfg_attr(all(PyPy, Py_3_9), link_name = "PyPyObject_VectorcallDict")] pub fn PyObject_VectorcallDict( @@ -134,7 +137,7 @@ extern "C" { ) -> *mut PyObject; } -#[cfg(all(Py_3_8, not(PyPy)))] +#[cfg(all(Py_3_8, not(any(PyPy, GraalPy))))] #[inline(always)] pub unsafe fn _PyObject_FastCallTstate( tstate: *mut PyThreadState, @@ -145,7 +148,7 @@ pub unsafe fn _PyObject_FastCallTstate( _PyObject_VectorcallTstate(tstate, func, args, nargs as size_t, std::ptr::null_mut()) } -#[cfg(all(Py_3_8, not(PyPy)))] +#[cfg(all(Py_3_8, not(any(PyPy, GraalPy))))] #[inline(always)] pub unsafe fn _PyObject_FastCall( func: *mut PyObject, @@ -155,7 +158,7 @@ pub unsafe fn _PyObject_FastCall( _PyObject_FastCallTstate(PyThreadState_GET(), func, args, nargs) } -#[cfg(all(Py_3_8, not(PyPy)))] +#[cfg(all(Py_3_8, not(any(PyPy, GraalPy))))] #[inline(always)] pub unsafe fn _PyObject_CallNoArg(func: *mut PyObject) -> *mut PyObject { _PyObject_VectorcallTstate( @@ -173,7 +176,7 @@ extern "C" { pub fn _PyObject_CallNoArg(func: *mut PyObject) -> *mut PyObject; } -#[cfg(all(Py_3_8, not(PyPy)))] +#[cfg(all(Py_3_8, not(any(PyPy, GraalPy))))] #[inline(always)] pub unsafe fn PyObject_CallOneArg(func: *mut PyObject, arg: *mut PyObject) -> *mut PyObject { assert!(!arg.is_null()); @@ -185,7 +188,7 @@ pub unsafe fn PyObject_CallOneArg(func: *mut PyObject, arg: *mut PyObject) -> *m } extern "C" { - #[cfg(all(Py_3_9, not(PyPy)))] + #[cfg(all(Py_3_9, not(any(PyPy, GraalPy))))] pub fn PyObject_VectorcallMethod( name: *mut PyObject, args: *const *mut PyObject, @@ -194,7 +197,7 @@ extern "C" { ) -> *mut PyObject; } -#[cfg(all(Py_3_9, not(PyPy)))] +#[cfg(all(Py_3_9, not(any(PyPy, GraalPy))))] #[inline(always)] pub unsafe fn PyObject_CallMethodNoArgs( self_: *mut PyObject, @@ -208,7 +211,7 @@ pub unsafe fn PyObject_CallMethodNoArgs( ) } -#[cfg(all(Py_3_9, not(PyPy)))] +#[cfg(all(Py_3_9, not(any(PyPy, GraalPy))))] #[inline(always)] pub unsafe fn PyObject_CallMethodOneArg( self_: *mut PyObject, @@ -236,7 +239,7 @@ extern "C" { pub fn PyObject_LengthHint(o: *mut PyObject, arg1: Py_ssize_t) -> Py_ssize_t; #[cfg(not(Py_3_11))] // moved to src/buffer.rs from 3.11 - #[cfg(all(Py_3_9, not(PyPy)))] + #[cfg(all(Py_3_9, not(any(PyPy, GraalPy))))] pub fn PyObject_CheckBuffer(obj: *mut PyObject) -> c_int; } @@ -308,7 +311,7 @@ pub const PY_ITERSEARCH_INDEX: c_int = 2; pub const PY_ITERSEARCH_CONTAINS: c_int = 3; extern "C" { - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub fn _PySequence_IterSearch( seq: *mut PyObject, obj: *mut PyObject, diff --git a/pyo3-ffi/src/cpython/bytesobject.rs b/pyo3-ffi/src/cpython/bytesobject.rs index 912fc0ac427..fb0b38cf1d8 100644 --- a/pyo3-ffi/src/cpython/bytesobject.rs +++ b/pyo3-ffi/src/cpython/bytesobject.rs @@ -1,10 +1,10 @@ use crate::object::*; use crate::Py_ssize_t; -#[cfg(not(any(PyPy, Py_LIMITED_API)))] +#[cfg(not(any(PyPy, GraalPy, Py_LIMITED_API)))] use std::os::raw::c_char; use std::os::raw::c_int; -#[cfg(not(any(PyPy, Py_LIMITED_API)))] +#[cfg(not(any(PyPy, GraalPy, Py_LIMITED_API)))] #[repr(C)] #[derive(Copy, Clone)] pub struct PyBytesObject { @@ -13,7 +13,7 @@ pub struct PyBytesObject { pub ob_sval: [c_char; 1], } -#[cfg(any(PyPy, Py_LIMITED_API))] +#[cfg(any(PyPy, GraalPy, Py_LIMITED_API))] opaque_struct!(PyBytesObject); extern "C" { diff --git a/pyo3-ffi/src/cpython/code.rs b/pyo3-ffi/src/cpython/code.rs index 498eab59cce..74586eac595 100644 --- a/pyo3-ffi/src/cpython/code.rs +++ b/pyo3-ffi/src/cpython/code.rs @@ -3,10 +3,10 @@ use crate::pyport::Py_ssize_t; #[allow(unused_imports)] use std::os::raw::{c_char, c_int, c_short, c_uchar, c_void}; -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] use std::ptr::addr_of_mut; -#[cfg(all(Py_3_8, not(PyPy), not(Py_3_11)))] +#[cfg(all(Py_3_8, not(any(PyPy, GraalPy)), not(Py_3_11)))] opaque_struct!(_PyOpcache); #[cfg(Py_3_12)] @@ -73,10 +73,10 @@ pub struct _PyCoMonitoringData { pub per_instruction_tools: *mut u8, } -#[cfg(all(not(PyPy), not(Py_3_7)))] +#[cfg(all(not(any(PyPy, GraalPy)), not(Py_3_7)))] opaque_struct!(PyCodeObject); -#[cfg(all(not(PyPy), Py_3_7, not(Py_3_8)))] +#[cfg(all(not(any(PyPy, GraalPy)), Py_3_7, not(Py_3_8)))] #[repr(C)] #[derive(Copy, Clone)] pub struct PyCodeObject { @@ -102,7 +102,7 @@ pub struct PyCodeObject { pub co_extra: *mut c_void, } -#[cfg(all(not(PyPy), Py_3_8, not(Py_3_11)))] +#[cfg(all(not(any(PyPy, GraalPy)), Py_3_8, not(Py_3_11)))] #[repr(C)] #[derive(Copy, Clone)] pub struct PyCodeObject { @@ -136,7 +136,7 @@ pub struct PyCodeObject { pub co_opcache_size: c_uchar, } -#[cfg(all(not(PyPy), Py_3_11))] +#[cfg(all(not(any(PyPy, GraalPy)), Py_3_11))] #[repr(C)] #[derive(Copy, Clone)] pub struct PyCodeObject { @@ -230,26 +230,26 @@ pub const CO_FUTURE_GENERATOR_STOP: c_int = 0x8_0000; pub const CO_MAXBLOCKS: usize = 20; -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] #[cfg_attr(windows, link(name = "pythonXY"))] extern "C" { pub static mut PyCode_Type: PyTypeObject; } #[inline] -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] pub unsafe fn PyCode_Check(op: *mut PyObject) -> c_int { (Py_TYPE(op) == addr_of_mut!(PyCode_Type)) as c_int } #[inline] -#[cfg(all(not(PyPy), Py_3_10, not(Py_3_11)))] +#[cfg(all(not(any(PyPy, GraalPy)), Py_3_10, not(Py_3_11)))] pub unsafe fn PyCode_GetNumFree(op: *mut PyCodeObject) -> Py_ssize_t { crate::PyTuple_GET_SIZE((*op).co_freevars) } #[inline] -#[cfg(all(not(Py_3_10), Py_3_11, not(PyPy)))] +#[cfg(all(not(Py_3_10), Py_3_11, not(any(PyPy, GraalPy))))] pub unsafe fn PyCode_GetNumFree(op: *mut PyCodeObject) -> c_int { (*op).co_nfreevars } @@ -265,6 +265,7 @@ extern "C" { } extern "C" { + #[cfg(not(GraalPy))] #[cfg_attr(PyPy, link_name = "PyPyCode_New")] pub fn PyCode_New( argcount: c_int, @@ -283,6 +284,7 @@ extern "C" { firstlineno: c_int, lnotab: *mut PyObject, ) -> *mut PyCodeObject; + #[cfg(not(GraalPy))] #[cfg(Py_3_8)] pub fn PyCode_NewWithPosOnlyArgs( argcount: c_int, @@ -302,12 +304,14 @@ extern "C" { firstlineno: c_int, lnotab: *mut PyObject, ) -> *mut PyCodeObject; + #[cfg(not(GraalPy))] #[cfg_attr(PyPy, link_name = "PyPyCode_NewEmpty")] pub fn PyCode_NewEmpty( filename: *const c_char, funcname: *const c_char, firstlineno: c_int, ) -> *mut PyCodeObject; + #[cfg(not(GraalPy))] pub fn PyCode_Addr2Line(arg1: *mut PyCodeObject, arg2: c_int) -> c_int; // skipped PyCodeAddressRange "for internal use only" // skipped _PyCode_CheckLineNumber diff --git a/pyo3-ffi/src/cpython/compile.rs b/pyo3-ffi/src/cpython/compile.rs index 71af81e83e5..8bce9dacb3b 100644 --- a/pyo3-ffi/src/cpython/compile.rs +++ b/pyo3-ffi/src/cpython/compile.rs @@ -30,7 +30,7 @@ pub struct PyCompilerFlags { // skipped non-limited _PyCompilerFlags_INIT -#[cfg(all(Py_3_12, not(PyPy)))] +#[cfg(all(Py_3_12, not(any(PyPy, GraalPy))))] #[repr(C)] #[derive(Copy, Clone)] pub struct _PyCompilerSrcLocation { @@ -42,7 +42,7 @@ pub struct _PyCompilerSrcLocation { // skipped SRC_LOCATION_FROM_AST -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] #[repr(C)] #[derive(Copy, Clone)] pub struct PyFutureFeatures { diff --git a/pyo3-ffi/src/cpython/dictobject.rs b/pyo3-ffi/src/cpython/dictobject.rs index 4af990a2d9a..74b970ebac2 100644 --- a/pyo3-ffi/src/cpython/dictobject.rs +++ b/pyo3-ffi/src/cpython/dictobject.rs @@ -7,6 +7,7 @@ opaque_struct!(PyDictKeysObject); #[cfg(Py_3_11)] opaque_struct!(PyDictValues); +#[cfg(not(GraalPy))] #[repr(C)] #[derive(Debug)] pub struct PyDictObject { diff --git a/pyo3-ffi/src/cpython/floatobject.rs b/pyo3-ffi/src/cpython/floatobject.rs index e33da0b91b9..8c7ee88543d 100644 --- a/pyo3-ffi/src/cpython/floatobject.rs +++ b/pyo3-ffi/src/cpython/floatobject.rs @@ -1,9 +1,12 @@ +#[cfg(GraalPy)] +use crate::PyFloat_AsDouble; use crate::{PyFloat_Check, PyObject}; use std::os::raw::c_double; #[repr(C)] pub struct PyFloatObject { pub ob_base: PyObject, + #[cfg(not(GraalPy))] pub ob_fval: c_double, } @@ -15,7 +18,10 @@ pub unsafe fn _PyFloat_CAST(op: *mut PyObject) -> *mut PyFloatObject { #[inline] pub unsafe fn PyFloat_AS_DOUBLE(op: *mut PyObject) -> c_double { - (*_PyFloat_CAST(op)).ob_fval + #[cfg(not(GraalPy))] + return (*_PyFloat_CAST(op)).ob_fval; + #[cfg(GraalPy)] + return PyFloat_AsDouble(op); } // skipped PyFloat_Pack2 diff --git a/pyo3-ffi/src/cpython/frameobject.rs b/pyo3-ffi/src/cpython/frameobject.rs index 7410000ef45..a85818ace0a 100644 --- a/pyo3-ffi/src/cpython/frameobject.rs +++ b/pyo3-ffi/src/cpython/frameobject.rs @@ -1,17 +1,19 @@ +#[cfg(not(GraalPy))] use crate::cpython::code::PyCodeObject; use crate::object::*; +#[cfg(not(GraalPy))] use crate::pystate::PyThreadState; -#[cfg(not(any(PyPy, Py_3_11)))] +#[cfg(not(any(PyPy, GraalPy, Py_3_11)))] use std::os::raw::c_char; use std::os::raw::c_int; use std::ptr::addr_of_mut; -#[cfg(not(any(PyPy, Py_3_11)))] +#[cfg(not(any(PyPy, GraalPy, Py_3_11)))] pub type PyFrameState = c_char; #[repr(C)] #[derive(Copy, Clone)] -#[cfg(not(any(PyPy, Py_3_11)))] +#[cfg(not(any(PyPy, GraalPy, Py_3_11)))] pub struct PyTryBlock { pub b_type: c_int, pub b_handler: c_int, @@ -20,7 +22,7 @@ pub struct PyTryBlock { #[repr(C)] #[derive(Copy, Clone)] -#[cfg(not(any(PyPy, Py_3_11)))] +#[cfg(not(any(PyPy, GraalPy, Py_3_11)))] pub struct PyFrameObject { pub ob_base: PyVarObject, pub f_back: *mut PyFrameObject, @@ -51,7 +53,7 @@ pub struct PyFrameObject { pub f_localsplus: [*mut PyObject; 1], } -#[cfg(any(PyPy, Py_3_11))] +#[cfg(any(PyPy, GraalPy, Py_3_11))] opaque_struct!(PyFrameObject); // skipped _PyFrame_IsRunnable @@ -69,6 +71,7 @@ pub unsafe fn PyFrame_Check(op: *mut PyObject) -> c_int { } extern "C" { + #[cfg(not(GraalPy))] #[cfg_attr(PyPy, link_name = "PyPyFrame_New")] pub fn PyFrame_New( tstate: *mut PyThreadState, @@ -79,7 +82,7 @@ extern "C" { // skipped _PyFrame_New_NoTrack pub fn PyFrame_BlockSetup(f: *mut PyFrameObject, _type: c_int, handler: c_int, level: c_int); - #[cfg(not(any(PyPy, Py_3_11)))] + #[cfg(not(any(PyPy, GraalPy, Py_3_11)))] pub fn PyFrame_BlockPop(f: *mut PyFrameObject) -> *mut PyTryBlock; pub fn PyFrame_LocalsToFast(f: *mut PyFrameObject, clear: c_int); diff --git a/pyo3-ffi/src/cpython/funcobject.rs b/pyo3-ffi/src/cpython/funcobject.rs index 1e9ee0cc18c..25de30d57f7 100644 --- a/pyo3-ffi/src/cpython/funcobject.rs +++ b/pyo3-ffi/src/cpython/funcobject.rs @@ -4,7 +4,7 @@ use std::ptr::addr_of_mut; use crate::PyObject; -#[cfg(all(not(PyPy), not(Py_3_10)))] +#[cfg(all(not(any(PyPy, GraalPy)), not(Py_3_10)))] #[repr(C)] pub struct PyFunctionObject { pub ob_base: PyObject, @@ -24,7 +24,7 @@ pub struct PyFunctionObject { pub vectorcall: Option, } -#[cfg(all(not(PyPy), Py_3_10))] +#[cfg(all(not(any(PyPy, GraalPy)), Py_3_10))] #[repr(C)] pub struct PyFunctionObject { pub ob_base: PyObject, @@ -55,6 +55,11 @@ pub struct PyFunctionObject { pub func_name: *mut PyObject, } +#[cfg(GraalPy)] +pub struct PyFunctionObject { + pub ob_base: PyObject, +} + #[cfg_attr(windows, link(name = "pythonXY"))] extern "C" { #[cfg(not(all(PyPy, not(Py_3_8))))] diff --git a/pyo3-ffi/src/cpython/genobject.rs b/pyo3-ffi/src/cpython/genobject.rs index aaa03f82eef..73ebdb491ff 100644 --- a/pyo3-ffi/src/cpython/genobject.rs +++ b/pyo3-ffi/src/cpython/genobject.rs @@ -1,13 +1,13 @@ use crate::object::*; use crate::PyFrameObject; -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] use crate::_PyErr_StackItem; #[cfg(Py_3_11)] use std::os::raw::c_char; use std::os::raw::c_int; use std::ptr::addr_of_mut; -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] #[repr(C)] #[derive(Copy, Clone)] pub struct PyGenObject { diff --git a/pyo3-ffi/src/cpython/listobject.rs b/pyo3-ffi/src/cpython/listobject.rs index 7fb2228fadd..ea15cfc1ff5 100644 --- a/pyo3-ffi/src/cpython/listobject.rs +++ b/pyo3-ffi/src/cpython/listobject.rs @@ -2,7 +2,7 @@ use crate::object::*; #[cfg(not(PyPy))] use crate::pyport::Py_ssize_t; -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] #[repr(C)] #[derive(Copy, Clone)] pub struct PyListObject { @@ -11,7 +11,7 @@ pub struct PyListObject { pub allocated: Py_ssize_t, } -#[cfg(PyPy)] +#[cfg(any(PyPy, GraalPy))] pub struct PyListObject { pub ob_base: PyObject, } @@ -22,14 +22,14 @@ pub struct PyListObject { /// Macro, trading safety for speed #[inline] -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] pub unsafe fn PyList_GET_ITEM(op: *mut PyObject, i: Py_ssize_t) -> *mut PyObject { *(*(op as *mut PyListObject)).ob_item.offset(i) } /// Macro, *only* to be used to fill in brand new lists #[inline] -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] pub unsafe fn PyList_SET_ITEM(op: *mut PyObject, i: Py_ssize_t, v: *mut PyObject) { *(*(op as *mut PyListObject)).ob_item.offset(i) = v; } diff --git a/pyo3-ffi/src/cpython/methodobject.rs b/pyo3-ffi/src/cpython/methodobject.rs index 7d9659785ba..97ad9ce35f0 100644 --- a/pyo3-ffi/src/cpython/methodobject.rs +++ b/pyo3-ffi/src/cpython/methodobject.rs @@ -1,8 +1,10 @@ use crate::object::*; +#[cfg(not(GraalPy))] use crate::{PyCFunctionObject, PyMethodDefPointer, METH_METHOD, METH_STATIC}; use std::os::raw::c_int; use std::ptr::addr_of_mut; +#[cfg(not(GraalPy))] pub struct PyCMethodObject { pub func: PyCFunctionObject, pub mm_class: *mut PyTypeObject, @@ -23,6 +25,7 @@ pub unsafe fn PyCMethod_Check(op: *mut PyObject) -> c_int { PyObject_TypeCheck(op, addr_of_mut!(PyCMethod_Type)) } +#[cfg(not(GraalPy))] #[inline] pub unsafe fn PyCFunction_GET_FUNCTION(func: *mut PyObject) -> PyMethodDefPointer { debug_assert_eq!(PyCMethod_Check(func), 1); @@ -31,6 +34,7 @@ pub unsafe fn PyCFunction_GET_FUNCTION(func: *mut PyObject) -> PyMethodDefPointe (*(*func).m_ml).ml_meth } +#[cfg(not(GraalPy))] #[inline] pub unsafe fn PyCFunction_GET_SELF(func: *mut PyObject) -> *mut PyObject { debug_assert_eq!(PyCMethod_Check(func), 1); @@ -43,6 +47,7 @@ pub unsafe fn PyCFunction_GET_SELF(func: *mut PyObject) -> *mut PyObject { } } +#[cfg(not(GraalPy))] #[inline] pub unsafe fn PyCFunction_GET_FLAGS(func: *mut PyObject) -> c_int { debug_assert_eq!(PyCMethod_Check(func), 1); @@ -51,6 +56,7 @@ pub unsafe fn PyCFunction_GET_FLAGS(func: *mut PyObject) -> c_int { (*(*func).m_ml).ml_flags } +#[cfg(not(GraalPy))] #[inline] pub unsafe fn PyCFunction_GET_CLASS(func: *mut PyObject) -> *mut PyTypeObject { debug_assert_eq!(PyCMethod_Check(func), 1); diff --git a/pyo3-ffi/src/cpython/mod.rs b/pyo3-ffi/src/cpython/mod.rs index 738ba37652e..1ab0e3c893f 100644 --- a/pyo3-ffi/src/cpython/mod.rs +++ b/pyo3-ffi/src/cpython/mod.rs @@ -68,5 +68,5 @@ pub use self::pystate::*; pub use self::pythonrun::*; pub use self::tupleobject::*; pub use self::unicodeobject::*; -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] pub use self::weakrefobject::*; diff --git a/pyo3-ffi/src/cpython/object.rs b/pyo3-ffi/src/cpython/object.rs index d0c1634082b..0f1778f6a3d 100644 --- a/pyo3-ffi/src/cpython/object.rs +++ b/pyo3-ffi/src/cpython/object.rs @@ -217,6 +217,8 @@ pub struct PyTypeObject { pub ob_size: Py_ssize_t, #[cfg(not(all(PyPy, not(Py_3_9))))] pub ob_base: object::PyVarObject, + #[cfg(GraalPy)] + pub ob_size: Py_ssize_t, pub tp_name: *const c_char, pub tp_basicsize: Py_ssize_t, pub tp_itemsize: Py_ssize_t, diff --git a/pyo3-ffi/src/cpython/objimpl.rs b/pyo3-ffi/src/cpython/objimpl.rs index 36a4380d122..3e0270ddc8f 100644 --- a/pyo3-ffi/src/cpython/objimpl.rs +++ b/pyo3-ffi/src/cpython/objimpl.rs @@ -1,7 +1,7 @@ use libc::size_t; use std::os::raw::c_int; -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] use std::os::raw::c_void; use crate::object::*; @@ -14,7 +14,7 @@ extern "C" { pub fn _Py_GetAllocatedBlocks() -> crate::Py_ssize_t; } -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] #[repr(C)] #[derive(Copy, Clone)] pub struct PyObjectArenaAllocator { @@ -23,7 +23,7 @@ pub struct PyObjectArenaAllocator { pub free: Option, } -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] impl Default for PyObjectArenaAllocator { #[inline] fn default() -> Self { @@ -32,9 +32,9 @@ impl Default for PyObjectArenaAllocator { } extern "C" { - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub fn PyObject_GetArenaAllocator(allocator: *mut PyObjectArenaAllocator); - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub fn PyObject_SetArenaAllocator(allocator: *mut PyObjectArenaAllocator); #[cfg(Py_3_9)] diff --git a/pyo3-ffi/src/cpython/pyerrors.rs b/pyo3-ffi/src/cpython/pyerrors.rs index fe7b4d4b045..6d17ebc8124 100644 --- a/pyo3-ffi/src/cpython/pyerrors.rs +++ b/pyo3-ffi/src/cpython/pyerrors.rs @@ -1,28 +1,28 @@ use crate::PyObject; -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] use crate::Py_ssize_t; #[repr(C)] #[derive(Debug)] pub struct PyBaseExceptionObject { pub ob_base: PyObject, - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub dict: *mut PyObject, - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub args: *mut PyObject, - #[cfg(all(Py_3_11, not(PyPy)))] + #[cfg(all(Py_3_11, not(any(PyPy, GraalPy))))] pub notes: *mut PyObject, - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub traceback: *mut PyObject, - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub context: *mut PyObject, - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub cause: *mut PyObject, - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub suppress_context: char, } -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] #[repr(C)] #[derive(Debug)] pub struct PySyntaxErrorObject { @@ -48,7 +48,7 @@ pub struct PySyntaxErrorObject { pub print_file_and_line: *mut PyObject, } -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] #[repr(C)] #[derive(Debug)] pub struct PyImportErrorObject { @@ -69,7 +69,7 @@ pub struct PyImportErrorObject { pub name_from: *mut PyObject, } -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] #[repr(C)] #[derive(Debug)] pub struct PyUnicodeErrorObject { @@ -90,7 +90,7 @@ pub struct PyUnicodeErrorObject { pub reason: *mut PyObject, } -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] #[repr(C)] #[derive(Debug)] pub struct PySystemExitObject { @@ -107,7 +107,7 @@ pub struct PySystemExitObject { pub code: *mut PyObject, } -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] #[repr(C)] #[derive(Debug)] pub struct PyOSErrorObject { @@ -134,26 +134,26 @@ pub struct PyOSErrorObject { #[derive(Debug)] pub struct PyStopIterationObject { pub ob_base: PyObject, - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub dict: *mut PyObject, - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub args: *mut PyObject, - #[cfg(all(Py_3_11, not(PyPy)))] + #[cfg(all(Py_3_11, not(any(PyPy, GraalPy))))] pub notes: *mut PyObject, - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub traceback: *mut PyObject, - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub context: *mut PyObject, - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub cause: *mut PyObject, - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub suppress_context: char, pub value: *mut PyObject, } extern "C" { - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub fn _PyErr_ChainExceptions(typ: *mut PyObject, val: *mut PyObject, tb: *mut PyObject); } diff --git a/pyo3-ffi/src/cpython/pymem.rs b/pyo3-ffi/src/cpython/pymem.rs index 2dfb3f3bcfa..c400fa30b05 100644 --- a/pyo3-ffi/src/cpython/pymem.rs +++ b/pyo3-ffi/src/cpython/pymem.rs @@ -26,7 +26,7 @@ pub enum PyMemAllocatorDomain { } // skipped PyMemAllocatorName -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] #[repr(C)] #[derive(Copy, Clone)] pub struct PyMemAllocatorEx { @@ -40,10 +40,10 @@ pub struct PyMemAllocatorEx { } extern "C" { - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub fn PyMem_GetAllocator(domain: PyMemAllocatorDomain, allocator: *mut PyMemAllocatorEx); - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub fn PyMem_SetAllocator(domain: PyMemAllocatorDomain, allocator: *mut PyMemAllocatorEx); - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub fn PyMem_SetupDebugHooks(); } diff --git a/pyo3-ffi/src/cpython/pythonrun.rs b/pyo3-ffi/src/cpython/pythonrun.rs index a92528b7fdc..94863166e11 100644 --- a/pyo3-ffi/src/cpython/pythonrun.rs +++ b/pyo3-ffi/src/cpython/pythonrun.rs @@ -1,8 +1,8 @@ use crate::object::*; -#[cfg(not(any(PyPy, Py_LIMITED_API, Py_3_10)))] +#[cfg(not(any(PyPy, GraalPy, Py_LIMITED_API, Py_3_10)))] use crate::pyarena::PyArena; use crate::PyCompilerFlags; -#[cfg(not(any(PyPy, Py_3_10)))] +#[cfg(not(any(PyPy, GraalPy, Py_3_10)))] use crate::{_mod, _node}; use libc::FILE; use std::os::raw::{c_char, c_int}; @@ -54,7 +54,7 @@ extern "C" { flags: *mut PyCompilerFlags, ) -> c_int; - #[cfg(not(any(PyPy, Py_3_10)))] + #[cfg(not(any(PyPy, GraalPy, Py_3_10)))] pub fn PyParser_ASTFromString( s: *const c_char, filename: *const c_char, @@ -62,7 +62,7 @@ extern "C" { flags: *mut PyCompilerFlags, arena: *mut PyArena, ) -> *mut _mod; - #[cfg(not(any(PyPy, Py_3_10)))] + #[cfg(not(any(PyPy, GraalPy, Py_3_10)))] pub fn PyParser_ASTFromStringObject( s: *const c_char, filename: *mut PyObject, @@ -70,7 +70,7 @@ extern "C" { flags: *mut PyCompilerFlags, arena: *mut PyArena, ) -> *mut _mod; - #[cfg(not(any(PyPy, Py_3_10)))] + #[cfg(not(any(PyPy, GraalPy, Py_3_10)))] pub fn PyParser_ASTFromFile( fp: *mut FILE, filename: *const c_char, @@ -82,7 +82,7 @@ extern "C" { errcode: *mut c_int, arena: *mut PyArena, ) -> *mut _mod; - #[cfg(not(any(PyPy, Py_3_10)))] + #[cfg(not(any(PyPy, GraalPy, Py_3_10)))] pub fn PyParser_ASTFromFileObject( fp: *mut FILE, filename: *mut PyObject, @@ -105,7 +105,7 @@ extern "C" { arg4: *mut PyObject, arg5: *mut PyCompilerFlags, ) -> *mut PyObject; - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub fn PyRun_FileExFlags( fp: *mut FILE, filename: *const c_char, @@ -116,7 +116,7 @@ extern "C" { flags: *mut PyCompilerFlags, ) -> *mut PyObject; - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub fn Py_CompileStringExFlags( str: *const c_char, filename: *const c_char, @@ -135,6 +135,7 @@ extern "C" { } #[inline] +#[cfg(not(GraalPy))] pub unsafe fn Py_CompileString(string: *const c_char, p: *const c_char, s: c_int) -> *mut PyObject { #[cfg(not(PyPy))] return Py_CompileStringExFlags(string, p, s, std::ptr::null_mut(), -1); @@ -144,7 +145,7 @@ pub unsafe fn Py_CompileString(string: *const c_char, p: *const c_char, s: c_int } #[inline] -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] pub unsafe fn Py_CompileStringFlags( string: *const c_char, p: *const c_char, @@ -164,11 +165,11 @@ extern "C" { g: *mut PyObject, l: *mut PyObject, ) -> *mut PyObject; - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub fn PyRun_AnyFile(fp: *mut FILE, name: *const c_char) -> c_int; - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub fn PyRun_AnyFileEx(fp: *mut FILE, name: *const c_char, closeit: c_int) -> c_int; - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub fn PyRun_AnyFileFlags( arg1: *mut FILE, arg2: *const c_char, @@ -176,13 +177,13 @@ extern "C" { ) -> c_int; #[cfg_attr(PyPy, link_name = "PyPyRun_SimpleString")] pub fn PyRun_SimpleString(s: *const c_char) -> c_int; - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub fn PyRun_SimpleFile(f: *mut FILE, p: *const c_char) -> c_int; - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub fn PyRun_SimpleFileEx(f: *mut FILE, p: *const c_char, c: c_int) -> c_int; - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub fn PyRun_InteractiveOne(f: *mut FILE, p: *const c_char) -> c_int; - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub fn PyRun_InteractiveLoop(f: *mut FILE, p: *const c_char) -> c_int; #[cfg_attr(PyPy, link_name = "PyPyRun_File")] pub fn PyRun_File( @@ -192,7 +193,7 @@ extern "C" { g: *mut PyObject, l: *mut PyObject, ) -> *mut PyObject; - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub fn PyRun_FileEx( fp: *mut FILE, p: *const c_char, @@ -201,7 +202,7 @@ extern "C" { l: *mut PyObject, c: c_int, ) -> *mut PyObject; - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub fn PyRun_FileFlags( fp: *mut FILE, p: *const c_char, @@ -218,14 +219,14 @@ extern "C" { // skipped macro PyRun_AnyFileFlags extern "C" { - #[cfg(not(any(PyPy, Py_3_10)))] + #[cfg(not(any(PyPy, GraalPy, Py_3_10)))] #[cfg_attr(Py_3_9, deprecated(note = "Python 3.9"))] pub fn PyParser_SimpleParseStringFlags( arg1: *const c_char, arg2: c_int, arg3: c_int, ) -> *mut _node; - #[cfg(not(any(PyPy, Py_3_10)))] + #[cfg(not(any(PyPy, GraalPy, Py_3_10)))] #[cfg_attr(Py_3_9, deprecated(note = "Python 3.9"))] pub fn PyParser_SimpleParseStringFlagsFilename( arg1: *const c_char, @@ -233,7 +234,7 @@ extern "C" { arg3: c_int, arg4: c_int, ) -> *mut _node; - #[cfg(not(any(PyPy, Py_3_10)))] + #[cfg(not(any(PyPy, GraalPy, Py_3_10)))] #[cfg_attr(Py_3_9, deprecated(note = "Python 3.9"))] pub fn PyParser_SimpleParseFileFlags( arg1: *mut FILE, diff --git a/pyo3-ffi/src/cpython/tupleobject.rs b/pyo3-ffi/src/cpython/tupleobject.rs index 24dde268526..4ed8520daf3 100644 --- a/pyo3-ffi/src/cpython/tupleobject.rs +++ b/pyo3-ffi/src/cpython/tupleobject.rs @@ -5,6 +5,7 @@ use crate::pyport::Py_ssize_t; #[repr(C)] pub struct PyTupleObject { pub ob_base: PyVarObject, + #[cfg(not(GraalPy))] pub ob_item: [*mut PyObject; 1], } @@ -22,14 +23,14 @@ pub unsafe fn PyTuple_GET_SIZE(op: *mut PyObject) -> Py_ssize_t { } #[inline] -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] pub unsafe fn PyTuple_GET_ITEM(op: *mut PyObject, i: Py_ssize_t) -> *mut PyObject { *(*(op as *mut PyTupleObject)).ob_item.as_ptr().offset(i) } /// Macro, *only* to be used to fill in brand new tuples #[inline] -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] pub unsafe fn PyTuple_SET_ITEM(op: *mut PyObject, i: Py_ssize_t, v: *mut PyObject) { *(*(op as *mut PyTupleObject)).ob_item.as_mut_ptr().offset(i) = v; } diff --git a/pyo3-ffi/src/cpython/unicodeobject.rs b/pyo3-ffi/src/cpython/unicodeobject.rs index f618ecf0a84..9ab523a2d7f 100644 --- a/pyo3-ffi/src/cpython/unicodeobject.rs +++ b/pyo3-ffi/src/cpython/unicodeobject.rs @@ -1,7 +1,7 @@ -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] use crate::Py_hash_t; use crate::{PyObject, Py_UCS1, Py_UCS2, Py_UCS4, Py_UNICODE, Py_ssize_t}; -#[cfg(not(Py_3_12))] +#[cfg(not(any(Py_3_12, GraalPy)))] use libc::wchar_t; use std::os::raw::{c_char, c_int, c_uint, c_void}; @@ -44,6 +44,7 @@ impl BitfieldUnit { } } +#[cfg(not(GraalPy))] impl BitfieldUnit where Storage: AsRef<[u8]> + AsMut<[u8]>, @@ -117,23 +118,31 @@ where } } +#[cfg(not(GraalPy))] const STATE_INTERNED_INDEX: usize = 0; +#[cfg(not(GraalPy))] const STATE_INTERNED_WIDTH: u8 = 2; +#[cfg(not(GraalPy))] const STATE_KIND_INDEX: usize = STATE_INTERNED_WIDTH as usize; +#[cfg(not(GraalPy))] const STATE_KIND_WIDTH: u8 = 3; +#[cfg(not(GraalPy))] const STATE_COMPACT_INDEX: usize = (STATE_INTERNED_WIDTH + STATE_KIND_WIDTH) as usize; +#[cfg(not(GraalPy))] const STATE_COMPACT_WIDTH: u8 = 1; +#[cfg(not(GraalPy))] const STATE_ASCII_INDEX: usize = (STATE_INTERNED_WIDTH + STATE_KIND_WIDTH + STATE_COMPACT_WIDTH) as usize; +#[cfg(not(GraalPy))] const STATE_ASCII_WIDTH: u8 = 1; -#[cfg(not(Py_3_12))] +#[cfg(not(any(Py_3_12, GraalPy)))] const STATE_READY_INDEX: usize = (STATE_INTERNED_WIDTH + STATE_KIND_WIDTH + STATE_COMPACT_WIDTH + STATE_ASCII_WIDTH) as usize; -#[cfg(not(Py_3_12))] +#[cfg(not(any(Py_3_12, GraalPy)))] const STATE_READY_WIDTH: u8 = 1; // generated by bindgen v0.63.0 (with small adaptations) @@ -153,6 +162,7 @@ struct PyASCIIObjectState { } // c_uint and u32 are not necessarily the same type on all targets / architectures +#[cfg(not(GraalPy))] #[allow(clippy::useless_transmute)] impl PyASCIIObjectState { #[inline] @@ -241,8 +251,9 @@ impl From for u32 { #[repr(C)] pub struct PyASCIIObject { pub ob_base: PyObject, + #[cfg(not(GraalPy))] pub length: Py_ssize_t, - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub hash: Py_hash_t, /// A bit field with various properties. /// @@ -255,12 +266,14 @@ pub struct PyASCIIObject { /// unsigned int ascii:1; /// unsigned int ready:1; /// unsigned int :24; + #[cfg(not(GraalPy))] pub state: u32, - #[cfg(not(Py_3_12))] + #[cfg(not(any(Py_3_12, GraalPy)))] pub wstr: *mut wchar_t, } /// Interacting with the bitfield is not actually well-defined, so we mark these APIs unsafe. +#[cfg(not(GraalPy))] impl PyASCIIObject { #[cfg_attr(not(Py_3_12), allow(rustdoc::broken_intra_doc_links))] // SSTATE_INTERNED_IMMORTAL_STATIC requires 3.12 /// Get the `interned` field of the [`PyASCIIObject`] state bitfield. @@ -367,9 +380,11 @@ impl PyASCIIObject { #[repr(C)] pub struct PyCompactUnicodeObject { pub _base: PyASCIIObject, + #[cfg(not(GraalPy))] pub utf8_length: Py_ssize_t, + #[cfg(not(GraalPy))] pub utf8: *mut c_char, - #[cfg(not(Py_3_12))] + #[cfg(not(any(Py_3_12, GraalPy)))] pub wstr_length: Py_ssize_t, } @@ -384,11 +399,12 @@ pub union PyUnicodeObjectData { #[repr(C)] pub struct PyUnicodeObject { pub _base: PyCompactUnicodeObject, + #[cfg(not(GraalPy))] pub data: PyUnicodeObjectData, } extern "C" { - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub fn _PyUnicode_CheckConsistency(op: *mut PyObject, check_content: c_int) -> c_int; } @@ -403,6 +419,7 @@ pub const SSTATE_INTERNED_IMMORTAL: c_uint = 2; #[cfg(Py_3_12)] pub const SSTATE_INTERNED_IMMORTAL_STATIC: c_uint = 3; +#[cfg(not(GraalPy))] #[inline] pub unsafe fn PyUnicode_IS_ASCII(op: *mut PyObject) -> c_uint { debug_assert!(crate::PyUnicode_Check(op) != 0); @@ -412,11 +429,13 @@ pub unsafe fn PyUnicode_IS_ASCII(op: *mut PyObject) -> c_uint { (*(op as *mut PyASCIIObject)).ascii() } +#[cfg(not(GraalPy))] #[inline] pub unsafe fn PyUnicode_IS_COMPACT(op: *mut PyObject) -> c_uint { (*(op as *mut PyASCIIObject)).compact() } +#[cfg(not(GraalPy))] #[inline] pub unsafe fn PyUnicode_IS_COMPACT_ASCII(op: *mut PyObject) -> c_uint { ((*(op as *mut PyASCIIObject)).ascii() != 0 && PyUnicode_IS_COMPACT(op) != 0).into() @@ -430,21 +449,25 @@ pub const PyUnicode_1BYTE_KIND: c_uint = 1; pub const PyUnicode_2BYTE_KIND: c_uint = 2; pub const PyUnicode_4BYTE_KIND: c_uint = 4; +#[cfg(not(GraalPy))] #[inline] pub unsafe fn PyUnicode_1BYTE_DATA(op: *mut PyObject) -> *mut Py_UCS1 { PyUnicode_DATA(op) as *mut Py_UCS1 } +#[cfg(not(GraalPy))] #[inline] pub unsafe fn PyUnicode_2BYTE_DATA(op: *mut PyObject) -> *mut Py_UCS2 { PyUnicode_DATA(op) as *mut Py_UCS2 } +#[cfg(not(GraalPy))] #[inline] pub unsafe fn PyUnicode_4BYTE_DATA(op: *mut PyObject) -> *mut Py_UCS4 { PyUnicode_DATA(op) as *mut Py_UCS4 } +#[cfg(not(GraalPy))] #[inline] pub unsafe fn PyUnicode_KIND(op: *mut PyObject) -> c_uint { debug_assert!(crate::PyUnicode_Check(op) != 0); @@ -454,6 +477,7 @@ pub unsafe fn PyUnicode_KIND(op: *mut PyObject) -> c_uint { (*(op as *mut PyASCIIObject)).kind() } +#[cfg(not(GraalPy))] #[inline] pub unsafe fn _PyUnicode_COMPACT_DATA(op: *mut PyObject) -> *mut c_void { if PyUnicode_IS_ASCII(op) != 0 { @@ -463,6 +487,7 @@ pub unsafe fn _PyUnicode_COMPACT_DATA(op: *mut PyObject) -> *mut c_void { } } +#[cfg(not(GraalPy))] #[inline] pub unsafe fn _PyUnicode_NONCOMPACT_DATA(op: *mut PyObject) -> *mut c_void { debug_assert!(!(*(op as *mut PyUnicodeObject)).data.any.is_null()); @@ -470,6 +495,7 @@ pub unsafe fn _PyUnicode_NONCOMPACT_DATA(op: *mut PyObject) -> *mut c_void { (*(op as *mut PyUnicodeObject)).data.any } +#[cfg(not(GraalPy))] #[inline] pub unsafe fn PyUnicode_DATA(op: *mut PyObject) -> *mut c_void { debug_assert!(crate::PyUnicode_Check(op) != 0); @@ -485,6 +511,7 @@ pub unsafe fn PyUnicode_DATA(op: *mut PyObject) -> *mut c_void { // skipped PyUnicode_READ // skipped PyUnicode_READ_CHAR +#[cfg(not(GraalPy))] #[inline] pub unsafe fn PyUnicode_GET_LENGTH(op: *mut PyObject) -> Py_ssize_t { debug_assert!(crate::PyUnicode_Check(op) != 0); @@ -494,26 +521,26 @@ pub unsafe fn PyUnicode_GET_LENGTH(op: *mut PyObject) -> Py_ssize_t { (*(op as *mut PyASCIIObject)).length } -#[cfg(Py_3_12)] +#[cfg(any(Py_3_12, GraalPy))] #[inline] pub unsafe fn PyUnicode_IS_READY(_op: *mut PyObject) -> c_uint { // kept in CPython for backwards compatibility 1 } -#[cfg(not(Py_3_12))] +#[cfg(not(any(GraalPy, Py_3_12)))] #[inline] pub unsafe fn PyUnicode_IS_READY(op: *mut PyObject) -> c_uint { (*(op as *mut PyASCIIObject)).ready() } -#[cfg(Py_3_12)] +#[cfg(any(Py_3_12, GraalPy))] #[inline] pub unsafe fn PyUnicode_READY(_op: *mut PyObject) -> c_int { 0 } -#[cfg(not(Py_3_12))] +#[cfg(not(any(Py_3_12, GraalPy)))] #[inline] pub unsafe fn PyUnicode_READY(op: *mut PyObject) -> c_int { debug_assert!(crate::PyUnicode_Check(op) != 0); diff --git a/pyo3-ffi/src/cpython/weakrefobject.rs b/pyo3-ffi/src/cpython/weakrefobject.rs index 5a5f85c5f0c..3a232c7ed38 100644 --- a/pyo3-ffi/src/cpython/weakrefobject.rs +++ b/pyo3-ffi/src/cpython/weakrefobject.rs @@ -1,4 +1,4 @@ -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] pub struct _PyWeakReference { pub ob_base: crate::PyObject, pub wr_object: *mut crate::PyObject, diff --git a/pyo3-ffi/src/datetime.rs b/pyo3-ffi/src/datetime.rs index 7e5a250990f..a20b76aa91d 100644 --- a/pyo3-ffi/src/datetime.rs +++ b/pyo3-ffi/src/datetime.rs @@ -9,17 +9,16 @@ //! Support for `PyDateTime_CAPI` is limited as of PyPy 7.0.0. //! `DateTime_FromTimestamp` and `Date_FromTimestamp` are currently not supported. +#[cfg(GraalPy)] +use crate::{PyLong_AsLong, PyLong_Check, PyObject_GetAttrString, Py_DecRef}; use crate::{PyObject, PyObject_TypeCheck, PyTypeObject, Py_TYPE}; use std::cell::UnsafeCell; use std::os::raw::{c_char, c_int}; use std::ptr; #[cfg(not(PyPy))] -use { - crate::{PyCapsule_Import, Py_hash_t}, - std::ffi::CString, - std::os::raw::c_uchar, -}; - +use {crate::PyCapsule_Import, std::ffi::CString}; +#[cfg(not(any(PyPy, GraalPy)))] +use {crate::Py_hash_t, std::os::raw::c_uchar}; // Type struct wrappers const _PyDateTime_DATE_DATASIZE: usize = 4; const _PyDateTime_TIME_DATASIZE: usize = 6; @@ -30,26 +29,27 @@ const _PyDateTime_DATETIME_DATASIZE: usize = 10; /// Structure representing a `datetime.timedelta`. pub struct PyDateTime_Delta { pub ob_base: PyObject, - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub hashcode: Py_hash_t, + #[cfg(not(GraalPy))] pub days: c_int, + #[cfg(not(GraalPy))] pub seconds: c_int, + #[cfg(not(GraalPy))] pub microseconds: c_int, } // skipped non-limited PyDateTime_TZInfo // skipped non-limited _PyDateTime_BaseTZInfo -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] #[repr(C)] #[derive(Debug, Copy, Clone)] /// Structure representing a `datetime.time` without a `tzinfo` member. pub struct _PyDateTime_BaseTime { pub ob_base: PyObject, - #[cfg(not(PyPy))] pub hashcode: Py_hash_t, pub hastzinfo: c_char, - #[cfg(not(PyPy))] pub data: [c_uchar; _PyDateTime_TIME_DATASIZE], } @@ -58,17 +58,19 @@ pub struct _PyDateTime_BaseTime { /// Structure representing a `datetime.time`. pub struct PyDateTime_Time { pub ob_base: PyObject, - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub hashcode: Py_hash_t, + #[cfg(not(GraalPy))] pub hastzinfo: c_char, - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub data: [c_uchar; _PyDateTime_TIME_DATASIZE], - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub fold: c_uchar, /// # Safety /// /// Care should be taken when reading this field. If the time does not have a /// tzinfo then CPython may allocate as a `_PyDateTime_BaseTime` without this field. + #[cfg(not(GraalPy))] pub tzinfo: *mut PyObject, } @@ -77,24 +79,22 @@ pub struct PyDateTime_Time { /// Structure representing a `datetime.date` pub struct PyDateTime_Date { pub ob_base: PyObject, - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub hashcode: Py_hash_t, - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub hastzinfo: c_char, - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub data: [c_uchar; _PyDateTime_DATE_DATASIZE], } -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] #[repr(C)] #[derive(Debug, Copy, Clone)] /// Structure representing a `datetime.datetime` without a `tzinfo` member. pub struct _PyDateTime_BaseDateTime { pub ob_base: PyObject, - #[cfg(not(PyPy))] pub hashcode: Py_hash_t, pub hastzinfo: c_char, - #[cfg(not(PyPy))] pub data: [c_uchar; _PyDateTime_DATETIME_DATASIZE], } @@ -103,17 +103,19 @@ pub struct _PyDateTime_BaseDateTime { /// Structure representing a `datetime.datetime`. pub struct PyDateTime_DateTime { pub ob_base: PyObject, - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub hashcode: Py_hash_t, + #[cfg(not(GraalPy))] pub hastzinfo: c_char, - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub data: [c_uchar; _PyDateTime_DATETIME_DATASIZE], - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub fold: c_uchar, /// # Safety /// /// Care should be taken when reading this field. If the time does not have a /// tzinfo then CPython may allocate as a `_PyDateTime_BaseDateTime` without this field. + #[cfg(not(GraalPy))] pub tzinfo: *mut PyObject, } @@ -121,7 +123,7 @@ pub struct PyDateTime_DateTime { // Accessor functions for PyDateTime_Date and PyDateTime_DateTime #[inline] -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] /// Retrieve the year component of a `PyDateTime_Date` or `PyDateTime_DateTime`. /// Returns a signed integer greater than 0. pub unsafe fn PyDateTime_GET_YEAR(o: *mut PyObject) -> c_int { @@ -131,7 +133,7 @@ pub unsafe fn PyDateTime_GET_YEAR(o: *mut PyObject) -> c_int { } #[inline] -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] /// Retrieve the month component of a `PyDateTime_Date` or `PyDateTime_DateTime`. /// Returns a signed integer in the range `[1, 12]`. pub unsafe fn PyDateTime_GET_MONTH(o: *mut PyObject) -> c_int { @@ -140,7 +142,7 @@ pub unsafe fn PyDateTime_GET_MONTH(o: *mut PyObject) -> c_int { } #[inline] -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] /// Retrieve the day component of a `PyDateTime_Date` or `PyDateTime_DateTime`. /// Returns a signed integer in the interval `[1, 31]`. pub unsafe fn PyDateTime_GET_DAY(o: *mut PyObject) -> c_int { @@ -149,28 +151,28 @@ pub unsafe fn PyDateTime_GET_DAY(o: *mut PyObject) -> c_int { } // Accessor macros for times -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] macro_rules! _PyDateTime_GET_HOUR { ($o: expr, $offset:expr) => { c_int::from((*$o).data[$offset + 0]) }; } -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] macro_rules! _PyDateTime_GET_MINUTE { ($o: expr, $offset:expr) => { c_int::from((*$o).data[$offset + 1]) }; } -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] macro_rules! _PyDateTime_GET_SECOND { ($o: expr, $offset:expr) => { c_int::from((*$o).data[$offset + 2]) }; } -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] macro_rules! _PyDateTime_GET_MICROSECOND { ($o: expr, $offset:expr) => { (c_int::from((*$o).data[$offset + 3]) << 16) @@ -179,14 +181,14 @@ macro_rules! _PyDateTime_GET_MICROSECOND { }; } -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] macro_rules! _PyDateTime_GET_FOLD { ($o: expr) => { (*$o).fold }; } -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] macro_rules! _PyDateTime_GET_TZINFO { ($o: expr) => { if (*$o).hastzinfo != 0 { @@ -199,7 +201,7 @@ macro_rules! _PyDateTime_GET_TZINFO { // Accessor functions for DateTime #[inline] -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] /// Retrieve the hour component of a `PyDateTime_DateTime`. /// Returns a signed integer in the interval `[0, 23]` pub unsafe fn PyDateTime_DATE_GET_HOUR(o: *mut PyObject) -> c_int { @@ -207,7 +209,7 @@ pub unsafe fn PyDateTime_DATE_GET_HOUR(o: *mut PyObject) -> c_int { } #[inline] -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] /// Retrieve the minute component of a `PyDateTime_DateTime`. /// Returns a signed integer in the interval `[0, 59]` pub unsafe fn PyDateTime_DATE_GET_MINUTE(o: *mut PyObject) -> c_int { @@ -215,7 +217,7 @@ pub unsafe fn PyDateTime_DATE_GET_MINUTE(o: *mut PyObject) -> c_int { } #[inline] -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] /// Retrieve the second component of a `PyDateTime_DateTime`. /// Returns a signed integer in the interval `[0, 59]` pub unsafe fn PyDateTime_DATE_GET_SECOND(o: *mut PyObject) -> c_int { @@ -223,7 +225,7 @@ pub unsafe fn PyDateTime_DATE_GET_SECOND(o: *mut PyObject) -> c_int { } #[inline] -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] /// Retrieve the microsecond component of a `PyDateTime_DateTime`. /// Returns a signed integer in the interval `[0, 999999]` pub unsafe fn PyDateTime_DATE_GET_MICROSECOND(o: *mut PyObject) -> c_int { @@ -231,7 +233,7 @@ pub unsafe fn PyDateTime_DATE_GET_MICROSECOND(o: *mut PyObject) -> c_int { } #[inline] -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] /// Retrieve the fold component of a `PyDateTime_DateTime`. /// Returns a signed integer in the interval `[0, 1]` pub unsafe fn PyDateTime_DATE_GET_FOLD(o: *mut PyObject) -> c_uchar { @@ -239,7 +241,7 @@ pub unsafe fn PyDateTime_DATE_GET_FOLD(o: *mut PyObject) -> c_uchar { } #[inline] -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] /// Retrieve the tzinfo component of a `PyDateTime_DateTime`. /// Returns a pointer to a `PyObject` that should be either NULL or an instance /// of a `datetime.tzinfo` subclass. @@ -249,7 +251,7 @@ pub unsafe fn PyDateTime_DATE_GET_TZINFO(o: *mut PyObject) -> *mut PyObject { // Accessor functions for Time #[inline] -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] /// Retrieve the hour component of a `PyDateTime_Time`. /// Returns a signed integer in the interval `[0, 23]` pub unsafe fn PyDateTime_TIME_GET_HOUR(o: *mut PyObject) -> c_int { @@ -257,7 +259,7 @@ pub unsafe fn PyDateTime_TIME_GET_HOUR(o: *mut PyObject) -> c_int { } #[inline] -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] /// Retrieve the minute component of a `PyDateTime_Time`. /// Returns a signed integer in the interval `[0, 59]` pub unsafe fn PyDateTime_TIME_GET_MINUTE(o: *mut PyObject) -> c_int { @@ -265,7 +267,7 @@ pub unsafe fn PyDateTime_TIME_GET_MINUTE(o: *mut PyObject) -> c_int { } #[inline] -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] /// Retrieve the second component of a `PyDateTime_DateTime`. /// Returns a signed integer in the interval `[0, 59]` pub unsafe fn PyDateTime_TIME_GET_SECOND(o: *mut PyObject) -> c_int { @@ -273,14 +275,14 @@ pub unsafe fn PyDateTime_TIME_GET_SECOND(o: *mut PyObject) -> c_int { } #[inline] -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] /// Retrieve the microsecond component of a `PyDateTime_DateTime`. /// Returns a signed integer in the interval `[0, 999999]` pub unsafe fn PyDateTime_TIME_GET_MICROSECOND(o: *mut PyObject) -> c_int { _PyDateTime_GET_MICROSECOND!((o as *mut PyDateTime_Time), 0) } -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] #[inline] /// Retrieve the fold component of a `PyDateTime_Time`. /// Returns a signed integer in the interval `[0, 1]` @@ -289,7 +291,7 @@ pub unsafe fn PyDateTime_TIME_GET_FOLD(o: *mut PyObject) -> c_uchar { } #[inline] -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] /// Retrieve the tzinfo component of a `PyDateTime_Time`. /// Returns a pointer to a `PyObject` that should be either NULL or an instance /// of a `datetime.tzinfo` subclass. @@ -298,7 +300,7 @@ pub unsafe fn PyDateTime_TIME_GET_TZINFO(o: *mut PyObject) -> *mut PyObject { } // Accessor functions -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] macro_rules! _access_field { ($obj:expr, $type: ident, $field:ident) => { (*($obj as *mut $type)).$field @@ -306,7 +308,7 @@ macro_rules! _access_field { } // Accessor functions for PyDateTime_Delta -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] macro_rules! _access_delta_field { ($obj:expr, $field:ident) => { _access_field!($obj, PyDateTime_Delta, $field) @@ -314,7 +316,7 @@ macro_rules! _access_delta_field { } #[inline] -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] /// Retrieve the days component of a `PyDateTime_Delta`. /// /// Returns a signed integer in the interval [-999999999, 999999999]. @@ -326,7 +328,7 @@ pub unsafe fn PyDateTime_DELTA_GET_DAYS(o: *mut PyObject) -> c_int { } #[inline] -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] /// Retrieve the seconds component of a `PyDateTime_Delta`. /// /// Returns a signed integer in the interval [0, 86399]. @@ -338,7 +340,7 @@ pub unsafe fn PyDateTime_DELTA_GET_SECONDS(o: *mut PyObject) -> c_int { } #[inline] -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] /// Retrieve the seconds component of a `PyDateTime_Delta`. /// /// Returns a signed integer in the interval [0, 999999]. @@ -349,6 +351,132 @@ pub unsafe fn PyDateTime_DELTA_GET_MICROSECONDS(o: *mut PyObject) -> c_int { _access_delta_field!(o, microseconds) } +// Accessor functions for GraalPy. The macros on GraalPy work differently, +// but copying them seems suboptimal +#[inline] +#[cfg(GraalPy)] +pub unsafe fn _get_attr(obj: *mut PyObject, field: &str) -> c_int { + let result = PyObject_GetAttrString(obj, field.as_ptr() as *const c_char); + Py_DecRef(result); // the original macros are borrowing + if PyLong_Check(result) == 1 { + PyLong_AsLong(result) as c_int + } else { + 0 + } +} + +#[inline] +#[cfg(GraalPy)] +pub unsafe fn PyDateTime_GET_YEAR(o: *mut PyObject) -> c_int { + _get_attr(o, "year\0") +} + +#[inline] +#[cfg(GraalPy)] +pub unsafe fn PyDateTime_GET_MONTH(o: *mut PyObject) -> c_int { + _get_attr(o, "month\0") +} + +#[inline] +#[cfg(GraalPy)] +pub unsafe fn PyDateTime_GET_DAY(o: *mut PyObject) -> c_int { + _get_attr(o, "day\0") +} + +#[inline] +#[cfg(GraalPy)] +pub unsafe fn PyDateTime_DATE_GET_HOUR(o: *mut PyObject) -> c_int { + _get_attr(o, "hour\0") +} + +#[inline] +#[cfg(GraalPy)] +pub unsafe fn PyDateTime_DATE_GET_MINUTE(o: *mut PyObject) -> c_int { + _get_attr(o, "minute\0") +} + +#[inline] +#[cfg(GraalPy)] +pub unsafe fn PyDateTime_DATE_GET_SECOND(o: *mut PyObject) -> c_int { + _get_attr(o, "second\0") +} + +#[inline] +#[cfg(GraalPy)] +pub unsafe fn PyDateTime_DATE_GET_MICROSECOND(o: *mut PyObject) -> c_int { + _get_attr(o, "microsecond\0") +} + +#[inline] +#[cfg(GraalPy)] +pub unsafe fn PyDateTime_DATE_GET_FOLD(o: *mut PyObject) -> c_int { + _get_attr(o, "fold\0") +} + +#[inline] +#[cfg(GraalPy)] +pub unsafe fn PyDateTime_DATE_GET_TZINFO(o: *mut PyObject) -> *mut PyObject { + let res = PyObject_GetAttrString(o, "tzinfo\0".as_ptr() as *const c_char); + Py_DecRef(res); // the original macros are borrowing + res +} + +#[inline] +#[cfg(GraalPy)] +pub unsafe fn PyDateTime_TIME_GET_HOUR(o: *mut PyObject) -> c_int { + _get_attr(o, "hour\0") +} + +#[inline] +#[cfg(GraalPy)] +pub unsafe fn PyDateTime_TIME_GET_MINUTE(o: *mut PyObject) -> c_int { + _get_attr(o, "minute\0") +} + +#[inline] +#[cfg(GraalPy)] +pub unsafe fn PyDateTime_TIME_GET_SECOND(o: *mut PyObject) -> c_int { + _get_attr(o, "second\0") +} + +#[inline] +#[cfg(GraalPy)] +pub unsafe fn PyDateTime_TIME_GET_MICROSECOND(o: *mut PyObject) -> c_int { + _get_attr(o, "microsecond\0") +} + +#[inline] +#[cfg(GraalPy)] +pub unsafe fn PyDateTime_TIME_GET_FOLD(o: *mut PyObject) -> c_int { + _get_attr(o, "fold\0") +} + +#[inline] +#[cfg(GraalPy)] +pub unsafe fn PyDateTime_TIME_GET_TZINFO(o: *mut PyObject) -> *mut PyObject { + let res = PyObject_GetAttrString(o, "tzinfo\0".as_ptr() as *const c_char); + Py_DecRef(res); // the original macros are borrowing + res +} + +#[inline] +#[cfg(GraalPy)] +pub unsafe fn PyDateTime_DELTA_GET_DAYS(o: *mut PyObject) -> c_int { + _get_attr(o, "days\0") +} + +#[inline] +#[cfg(GraalPy)] +pub unsafe fn PyDateTime_DELTA_GET_SECONDS(o: *mut PyObject) -> c_int { + _get_attr(o, "seconds\0") +} + +#[inline] +#[cfg(GraalPy)] +pub unsafe fn PyDateTime_DELTA_GET_MICROSECONDS(o: *mut PyObject) -> c_int { + _get_attr(o, "microseconds\0") +} + #[cfg(PyPy)] extern "C" { // skipped _PyDateTime_HAS_TZINFO (not in PyPy) diff --git a/pyo3-ffi/src/dictobject.rs b/pyo3-ffi/src/dictobject.rs index 8d522df97e2..99fc56b246b 100644 --- a/pyo3-ffi/src/dictobject.rs +++ b/pyo3-ffi/src/dictobject.rs @@ -109,6 +109,6 @@ extern "C" { pub static mut PyDictRevIterItem_Type: PyTypeObject; } -#[cfg(any(PyPy, Py_LIMITED_API))] +#[cfg(any(PyPy, GraalPy, Py_LIMITED_API))] // TODO: remove (see https://github.com/PyO3/pyo3/pull/1341#issuecomment-751515985) opaque_struct!(PyDictObject); diff --git a/pyo3-ffi/src/listobject.rs b/pyo3-ffi/src/listobject.rs index 32c6a2dc26a..a6f47eadf83 100644 --- a/pyo3-ffi/src/listobject.rs +++ b/pyo3-ffi/src/listobject.rs @@ -54,14 +54,16 @@ extern "C" { #[cfg_attr(PyPy, link_name = "PyPyList_AsTuple")] pub fn PyList_AsTuple(arg1: *mut PyObject) -> *mut PyObject; - // CPython macros exported as functions on PyPy - #[cfg(PyPy)] + // CPython macros exported as functions on PyPy or GraalPy + #[cfg(any(PyPy, GraalPy))] #[cfg_attr(PyPy, link_name = "PyPyList_GET_ITEM")] + #[cfg_attr(GraalPy, link_name = "PyList_GetItem")] pub fn PyList_GET_ITEM(arg1: *mut PyObject, arg2: Py_ssize_t) -> *mut PyObject; #[cfg(PyPy)] #[cfg_attr(PyPy, link_name = "PyPyList_GET_SIZE")] pub fn PyList_GET_SIZE(arg1: *mut PyObject) -> Py_ssize_t; - #[cfg(PyPy)] + #[cfg(any(PyPy, GraalPy))] #[cfg_attr(PyPy, link_name = "PyPyList_SET_ITEM")] + #[cfg_attr(GraalPy, link_name = "_PyList_SET_ITEM")] pub fn PyList_SET_ITEM(arg1: *mut PyObject, arg2: Py_ssize_t, arg3: *mut PyObject); } diff --git a/pyo3-ffi/src/methodobject.rs b/pyo3-ffi/src/methodobject.rs index e80d5668e78..3ed6b770e54 100644 --- a/pyo3-ffi/src/methodobject.rs +++ b/pyo3-ffi/src/methodobject.rs @@ -4,7 +4,7 @@ use crate::PyObject_TypeCheck; use std::os::raw::{c_char, c_int, c_void}; use std::{mem, ptr}; -#[cfg(all(Py_3_9, not(Py_LIMITED_API)))] +#[cfg(all(Py_3_9, not(Py_LIMITED_API), not(GraalPy)))] pub struct PyCFunctionObject { pub ob_base: PyObject, pub m_ml: *mut PyMethodDef, diff --git a/pyo3-ffi/src/object.rs b/pyo3-ffi/src/object.rs index 0e0243cd764..b33ee558a37 100644 --- a/pyo3-ffi/src/object.rs +++ b/pyo3-ffi/src/object.rs @@ -79,6 +79,7 @@ pub struct PyObject { #[derive(Debug, Copy, Clone)] pub struct PyVarObject { pub ob_base: PyObject, + #[cfg(not(GraalPy))] pub ob_size: Py_ssize_t, } @@ -98,12 +99,18 @@ pub unsafe fn Py_REFCNT(ob: *mut PyObject) -> Py_ssize_t { #[inline] #[cfg(not(Py_3_12))] pub unsafe fn Py_REFCNT(ob: *mut PyObject) -> Py_ssize_t { - (*ob).ob_refcnt + #[cfg(not(GraalPy))] + return (*ob).ob_refcnt; + #[cfg(GraalPy)] + return _Py_REFCNT(ob); } #[inline] pub unsafe fn Py_TYPE(ob: *mut PyObject) -> *mut PyTypeObject { - (*ob).ob_type + #[cfg(not(GraalPy))] + return (*ob).ob_type; + #[cfg(GraalPy)] + return _Py_TYPE(ob); } // PyLong_Type defined in longobject.rs @@ -111,9 +118,14 @@ pub unsafe fn Py_TYPE(ob: *mut PyObject) -> *mut PyTypeObject { #[inline] pub unsafe fn Py_SIZE(ob: *mut PyObject) -> Py_ssize_t { - debug_assert_ne!((*ob).ob_type, std::ptr::addr_of_mut!(crate::PyLong_Type)); - debug_assert_ne!((*ob).ob_type, std::ptr::addr_of_mut!(crate::PyBool_Type)); - (*ob.cast::()).ob_size + #[cfg(not(GraalPy))] + { + debug_assert_ne!((*ob).ob_type, std::ptr::addr_of_mut!(crate::PyLong_Type)); + debug_assert_ne!((*ob).ob_type, std::ptr::addr_of_mut!(crate::PyBool_Type)); + (*ob.cast::()).ob_size + } + #[cfg(GraalPy)] + _Py_SIZE(ob) } #[inline] @@ -464,8 +476,10 @@ extern "C" { pub fn _Py_Dealloc(arg1: *mut PyObject); #[cfg_attr(PyPy, link_name = "PyPy_IncRef")] + #[cfg_attr(GraalPy, link_name = "_Py_IncRef")] pub fn Py_IncRef(o: *mut PyObject); #[cfg_attr(PyPy, link_name = "PyPy_DecRef")] + #[cfg_attr(GraalPy, link_name = "_Py_DecRef")] pub fn Py_DecRef(o: *mut PyObject); #[cfg(Py_3_10)] @@ -474,11 +488,21 @@ extern "C" { #[cfg(Py_3_10)] #[cfg_attr(PyPy, link_name = "_PyPy_DecRef")] pub fn _Py_DecRef(o: *mut PyObject); + + #[cfg(GraalPy)] + pub fn _Py_REFCNT(arg1: *const PyObject) -> Py_ssize_t; + + #[cfg(GraalPy)] + pub fn _Py_TYPE(arg1: *const PyObject) -> *mut PyTypeObject; + + #[cfg(GraalPy)] + pub fn _Py_SIZE(arg1: *const PyObject) -> Py_ssize_t; } #[inline(always)] pub unsafe fn Py_INCREF(op: *mut PyObject) { #[cfg(any( + GraalPy, all(Py_LIMITED_API, Py_3_12), all( py_sys_config = "Py_REF_DEBUG", @@ -499,6 +523,7 @@ pub unsafe fn Py_INCREF(op: *mut PyObject) { all(Py_LIMITED_API, not(Py_3_12)), all( not(Py_LIMITED_API), + not(GraalPy), any( not(py_sys_config = "Py_REF_DEBUG"), all(py_sys_config = "Py_REF_DEBUG", Py_3_12), @@ -544,6 +569,7 @@ pub unsafe fn Py_INCREF(op: *mut PyObject) { )] pub unsafe fn Py_DECREF(op: *mut PyObject) { #[cfg(any( + GraalPy, all(Py_LIMITED_API, Py_3_12), all( py_sys_config = "Py_REF_DEBUG", @@ -564,6 +590,7 @@ pub unsafe fn Py_DECREF(op: *mut PyObject) { all(Py_LIMITED_API, not(Py_3_12)), all( not(Py_LIMITED_API), + not(GraalPy), any( not(py_sys_config = "Py_REF_DEBUG"), all(py_sys_config = "Py_REF_DEBUG", Py_3_12), @@ -669,13 +696,20 @@ pub unsafe fn Py_XNewRef(obj: *mut PyObject) -> *mut PyObject { #[cfg_attr(windows, link(name = "pythonXY"))] extern "C" { + #[cfg(not(GraalPy))] #[cfg_attr(PyPy, link_name = "_PyPy_NoneStruct")] static mut _Py_NoneStruct: PyObject; + + #[cfg(GraalPy)] + static mut _Py_NoneStructReference: *mut PyObject; } #[inline] pub unsafe fn Py_None() -> *mut PyObject { - ptr::addr_of_mut!(_Py_NoneStruct) + #[cfg(not(GraalPy))] + return ptr::addr_of_mut!(_Py_NoneStruct); + #[cfg(GraalPy)] + return _Py_NoneStructReference; } #[inline] @@ -687,13 +721,20 @@ pub unsafe fn Py_IsNone(x: *mut PyObject) -> c_int { #[cfg_attr(windows, link(name = "pythonXY"))] extern "C" { + #[cfg(not(GraalPy))] #[cfg_attr(PyPy, link_name = "_PyPy_NotImplementedStruct")] static mut _Py_NotImplementedStruct: PyObject; + + #[cfg(GraalPy)] + static mut _Py_NotImplementedStructReference: *mut PyObject; } #[inline] pub unsafe fn Py_NotImplemented() -> *mut PyObject { - ptr::addr_of_mut!(_Py_NotImplementedStruct) + #[cfg(not(GraalPy))] + return ptr::addr_of_mut!(_Py_NotImplementedStruct); + #[cfg(GraalPy)] + return _Py_NotImplementedStructReference; } // skipped Py_RETURN_NOTIMPLEMENTED diff --git a/pyo3-ffi/src/pyframe.rs b/pyo3-ffi/src/pyframe.rs index 43a9d1f6777..4dd3d2b31a5 100644 --- a/pyo3-ffi/src/pyframe.rs +++ b/pyo3-ffi/src/pyframe.rs @@ -1,3 +1,4 @@ +#[cfg(not(GraalPy))] #[cfg(any(Py_3_10, all(Py_3_9, not(Py_LIMITED_API))))] use crate::PyCodeObject; #[cfg(not(Py_LIMITED_API))] @@ -9,6 +10,7 @@ opaque_struct!(PyFrameObject); extern "C" { pub fn PyFrame_GetLineNumber(f: *mut PyFrameObject) -> c_int; + #[cfg(not(GraalPy))] #[cfg(any(Py_3_10, all(Py_3_9, not(Py_LIMITED_API))))] pub fn PyFrame_GetCode(f: *mut PyFrameObject) -> *mut PyCodeObject; } diff --git a/pyo3-ffi/src/pyhash.rs b/pyo3-ffi/src/pyhash.rs index f8072d85108..f42f9730f1b 100644 --- a/pyo3-ffi/src/pyhash.rs +++ b/pyo3-ffi/src/pyhash.rs @@ -1,6 +1,6 @@ -#[cfg(not(any(Py_LIMITED_API, PyPy)))] +#[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] use crate::pyport::{Py_hash_t, Py_ssize_t}; -#[cfg(not(any(Py_LIMITED_API, PyPy)))] +#[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] use std::os::raw::{c_char, c_void}; use std::os::raw::{c_int, c_ulong}; @@ -10,7 +10,7 @@ extern "C" { // skipped non-limited _Py_HashPointer // skipped non-limited _Py_HashPointerRaw - #[cfg(not(any(Py_LIMITED_API, PyPy)))] + #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] pub fn _Py_HashBytes(src: *const c_void, len: Py_ssize_t) -> Py_hash_t; } @@ -20,7 +20,7 @@ pub const _PyHASH_MULTIPLIER: c_ulong = 1000003; // skipped non-limited _Py_HashSecret_t -#[cfg(not(any(Py_LIMITED_API, PyPy)))] +#[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] #[repr(C)] #[derive(Copy, Clone)] pub struct PyHash_FuncDef { @@ -30,7 +30,7 @@ pub struct PyHash_FuncDef { pub seed_bits: c_int, } -#[cfg(not(any(Py_LIMITED_API, PyPy)))] +#[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] impl Default for PyHash_FuncDef { #[inline] fn default() -> Self { @@ -39,7 +39,7 @@ impl Default for PyHash_FuncDef { } extern "C" { - #[cfg(not(any(Py_LIMITED_API, PyPy)))] + #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] pub fn PyHash_GetFuncDef() -> *mut PyHash_FuncDef; } diff --git a/pyo3-ffi/src/pythonrun.rs b/pyo3-ffi/src/pythonrun.rs index e5f20de0058..10985b6068c 100644 --- a/pyo3-ffi/src/pythonrun.rs +++ b/pyo3-ffi/src/pythonrun.rs @@ -1,12 +1,12 @@ use crate::object::*; #[cfg(not(any(PyPy, Py_LIMITED_API, Py_3_10)))] use libc::FILE; -#[cfg(all(not(PyPy), any(Py_LIMITED_API, not(Py_3_10))))] +#[cfg(all(not(PyPy), any(Py_LIMITED_API, not(Py_3_10), GraalPy)))] use std::os::raw::c_char; use std::os::raw::c_int; extern "C" { - #[cfg(all(Py_LIMITED_API, not(PyPy)))] + #[cfg(any(all(Py_LIMITED_API, not(PyPy)), GraalPy))] pub fn Py_CompileString(string: *const c_char, p: *const c_char, s: c_int) -> *mut PyObject; #[cfg_attr(PyPy, link_name = "PyPyErr_Print")] diff --git a/pyo3-ffi/src/setobject.rs b/pyo3-ffi/src/setobject.rs index 84a368a7f27..9d5351fc798 100644 --- a/pyo3-ffi/src/setobject.rs +++ b/pyo3-ffi/src/setobject.rs @@ -1,5 +1,5 @@ use crate::object::*; -#[cfg(not(any(Py_LIMITED_API, PyPy)))] +#[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] use crate::pyport::Py_hash_t; use crate::pyport::Py_ssize_t; use std::os::raw::c_int; @@ -7,7 +7,7 @@ use std::ptr::addr_of_mut; pub const PySet_MINSIZE: usize = 8; -#[cfg(not(any(Py_LIMITED_API, PyPy)))] +#[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] #[repr(C)] #[derive(Debug)] pub struct setentry { @@ -15,7 +15,7 @@ pub struct setentry { pub hash: Py_hash_t, } -#[cfg(not(any(Py_LIMITED_API, PyPy)))] +#[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] #[repr(C)] #[derive(Debug)] pub struct PySetObject { @@ -32,7 +32,7 @@ pub struct PySetObject { // skipped #[inline] -#[cfg(all(not(PyPy), not(Py_LIMITED_API)))] +#[cfg(all(not(any(PyPy, GraalPy)), not(Py_LIMITED_API)))] pub unsafe fn PySet_GET_SIZE(so: *mut PyObject) -> Py_ssize_t { debug_assert_eq!(PyAnySet_Check(so), 1); let so = so.cast::(); @@ -92,7 +92,7 @@ extern "C" { } #[inline] -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] pub unsafe fn PyFrozenSet_CheckExact(ob: *mut PyObject) -> c_int { (Py_TYPE(ob) == addr_of_mut!(PyFrozenSet_Type)) as c_int } diff --git a/pyo3-ffi/src/sliceobject.rs b/pyo3-ffi/src/sliceobject.rs index 6f09906fcc4..a3ea153987c 100644 --- a/pyo3-ffi/src/sliceobject.rs +++ b/pyo3-ffi/src/sliceobject.rs @@ -5,21 +5,31 @@ use std::ptr::addr_of_mut; #[cfg_attr(windows, link(name = "pythonXY"))] extern "C" { + #[cfg(not(GraalPy))] #[cfg_attr(PyPy, link_name = "_PyPy_EllipsisObject")] static mut _Py_EllipsisObject: PyObject; + + #[cfg(GraalPy)] + static mut _Py_EllipsisObjectReference: *mut PyObject; } #[inline] pub unsafe fn Py_Ellipsis() -> *mut PyObject { - addr_of_mut!(_Py_EllipsisObject) + #[cfg(not(GraalPy))] + return addr_of_mut!(_Py_EllipsisObject); + #[cfg(GraalPy)] + return _Py_EllipsisObjectReference; } #[cfg(not(Py_LIMITED_API))] #[repr(C)] pub struct PySliceObject { pub ob_base: PyObject, + #[cfg(not(GraalPy))] pub start: *mut PyObject, + #[cfg(not(GraalPy))] pub stop: *mut PyObject, + #[cfg(not(GraalPy))] pub step: *mut PyObject, } diff --git a/pyo3-ffi/src/structseq.rs b/pyo3-ffi/src/structseq.rs index 6dfc1daf158..f8566787b51 100644 --- a/pyo3-ffi/src/structseq.rs +++ b/pyo3-ffi/src/structseq.rs @@ -42,13 +42,13 @@ extern "C" { #[cfg(not(Py_LIMITED_API))] pub type PyStructSequence = crate::PyTupleObject; -#[cfg(not(any(Py_LIMITED_API, PyPy)))] +#[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] #[inline] pub unsafe fn PyStructSequence_SET_ITEM(op: *mut PyObject, i: Py_ssize_t, v: *mut PyObject) { crate::PyTuple_SET_ITEM(op, i, v) } -#[cfg(not(any(Py_LIMITED_API, PyPy)))] +#[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] #[inline] pub unsafe fn PyStructSequence_GET_ITEM(op: *mut PyObject, i: Py_ssize_t) -> *mut PyObject { crate::PyTuple_GET_ITEM(op, i) diff --git a/pyo3-ffi/src/weakrefobject.rs b/pyo3-ffi/src/weakrefobject.rs index d065ae23e0f..7e11a9012e7 100644 --- a/pyo3-ffi/src/weakrefobject.rs +++ b/pyo3-ffi/src/weakrefobject.rs @@ -3,10 +3,10 @@ use std::os::raw::c_int; #[cfg(not(PyPy))] use std::ptr::addr_of_mut; -#[cfg(all(not(PyPy), Py_LIMITED_API))] +#[cfg(all(not(PyPy), Py_LIMITED_API, not(GraalPy)))] opaque_struct!(PyWeakReference); -#[cfg(all(not(PyPy), not(Py_LIMITED_API)))] +#[cfg(all(not(PyPy), not(Py_LIMITED_API), not(GraalPy)))] pub use crate::_PyWeakReference as PyWeakReference; #[cfg_attr(windows, link(name = "pythonXY"))] diff --git a/pytests/tests/test_awaitable.py b/pytests/tests/test_awaitable.py index 2bada317517..40f1e1cc01d 100644 --- a/pytests/tests/test_awaitable.py +++ b/pytests/tests/test_awaitable.py @@ -1,13 +1,22 @@ import pytest +import sys from pyo3_pytests.awaitable import IterAwaitable, FutureAwaitable +@pytest.mark.skipif( + sys.implementation.name == "graalpy", + reason="GraalPy's asyncio module has a bug with native classes, see oracle/graalpython#365", +) @pytest.mark.asyncio async def test_iter_awaitable(): assert await IterAwaitable(5) == 5 +@pytest.mark.skipif( + sys.implementation.name == "graalpy", + reason="GraalPy's asyncio module has a bug with native classes, see oracle/graalpython#365", +) @pytest.mark.asyncio async def test_future_awaitable(): assert await FutureAwaitable(5) == 5 diff --git a/pytests/tests/test_misc.py b/pytests/tests/test_misc.py index 6645f942f1a..de75f1c8a80 100644 --- a/pytests/tests/test_misc.py +++ b/pytests/tests/test_misc.py @@ -27,8 +27,8 @@ def test_multiple_imports_same_interpreter_ok(): reason="Cannot identify subinterpreters on Python older than 3.9", ) @pytest.mark.skipif( - platform.python_implementation() == "PyPy", - reason="PyPy does not support subinterpreters", + platform.python_implementation() in ("PyPy", "GraalVM"), + reason="PyPy and GraalPy do not support subinterpreters", ) def test_import_in_subinterpreter_forbidden(): import _xxsubinterpreters diff --git a/pytests/tests/test_objstore.py b/pytests/tests/test_objstore.py index bfd8bad84df..3f0d23fa97c 100644 --- a/pytests/tests/test_objstore.py +++ b/pytests/tests/test_objstore.py @@ -12,6 +12,11 @@ def test_objstore_doesnot_leak_memory(): # check refcount on PyPy getrefcount = getattr(sys, "getrefcount", lambda obj: 0) + if sys.implementation.name == "graalpy": + # GraalPy has an incomplete sys.getrefcount implementation + def getrefcount(obj): + return 0 + before = getrefcount(message) store = ObjStore() for _ in range(N): diff --git a/src/conversions/std/num.rs b/src/conversions/std/num.rs index 027982461e9..44843141440 100644 --- a/src/conversions/std/num.rs +++ b/src/conversions/std/num.rs @@ -175,7 +175,7 @@ int_convert_u64_or_i64!( true ); -#[cfg(not(Py_LIMITED_API))] +#[cfg(all(not(Py_LIMITED_API), not(GraalPy)))] mod fast_128bit_int_conversion { use super::*; @@ -242,7 +242,7 @@ mod fast_128bit_int_conversion { } // For ABI3 we implement the conversion manually. -#[cfg(Py_LIMITED_API)] +#[cfg(any(Py_LIMITED_API, GraalPy))] mod slow_128bit_int_conversion { use super::*; const SHIFT: usize = 64; diff --git a/src/err/mod.rs b/src/err/mod.rs index de1e621fc13..8dd16b26b47 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -863,8 +863,17 @@ 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; - unsafe { ffi::PyException_GetCause(self.value_bound(py).as_ptr()).assume_owned_or_opt(py) } - .map(Self::from_value_bound) + let obj = unsafe { + ffi::PyException_GetCause(self.value_bound(py).as_ptr()).assume_owned_or_opt(py) + }; + // PyException_GetCause is documented as potentially returning PyNone, but only GraalPy seems to actually do that + #[cfg(GraalPy)] + if let Some(cause) = &obj { + if cause.is_none() { + return None; + } + } + obj.map(Self::from_value_bound) } /// Set the cause associated with the exception, pass `None` to clear it. diff --git a/src/exceptions.rs b/src/exceptions.rs index bd9c89c425f..add958257ab 100644 --- a/src/exceptions.rs +++ b/src/exceptions.rs @@ -409,14 +409,14 @@ impl_native_exception!( PyExc_FloatingPointError, native_doc!("FloatingPointError") ); -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] impl_native_exception!( PyOSError, PyExc_OSError, native_doc!("OSError"), ffi::PyOSErrorObject ); -#[cfg(PyPy)] +#[cfg(any(PyPy, GraalPy))] impl_native_exception!(PyOSError, PyExc_OSError, native_doc!("OSError")); impl_native_exception!(PyImportError, PyExc_ImportError, native_doc!("ImportError")); @@ -455,14 +455,14 @@ impl_native_exception!( PyExc_NotImplementedError, native_doc!("NotImplementedError") ); -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] impl_native_exception!( PySyntaxError, PyExc_SyntaxError, native_doc!("SyntaxError"), ffi::PySyntaxErrorObject ); -#[cfg(PyPy)] +#[cfg(any(PyPy, GraalPy))] impl_native_exception!(PySyntaxError, PyExc_SyntaxError, native_doc!("SyntaxError")); impl_native_exception!( PyReferenceError, @@ -470,14 +470,14 @@ impl_native_exception!( native_doc!("ReferenceError") ); impl_native_exception!(PySystemError, PyExc_SystemError, native_doc!("SystemError")); -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] impl_native_exception!( PySystemExit, PyExc_SystemExit, native_doc!("SystemExit"), ffi::PySystemExitObject ); -#[cfg(PyPy)] +#[cfg(any(PyPy, GraalPy))] impl_native_exception!(PySystemExit, PyExc_SystemExit, native_doc!("SystemExit")); impl_native_exception!(PyTypeError, PyExc_TypeError, native_doc!("TypeError")); impl_native_exception!( @@ -485,14 +485,14 @@ impl_native_exception!( PyExc_UnboundLocalError, native_doc!("UnboundLocalError") ); -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] impl_native_exception!( PyUnicodeError, PyExc_UnicodeError, native_doc!("UnicodeError"), ffi::PyUnicodeErrorObject ); -#[cfg(PyPy)] +#[cfg(any(PyPy, GraalPy))] impl_native_exception!( PyUnicodeError, PyExc_UnicodeError, diff --git a/src/gil.rs b/src/gil.rs index 5ca3167e66c..bb07489fa1f 100644 --- a/src/gil.rs +++ b/src/gil.rs @@ -78,7 +78,7 @@ fn gil_is_acquired() -> bool { /// Python::with_gil(|py| py.run_bound("print('Hello World')", None, None)) /// # } /// ``` -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] pub fn prepare_freethreaded_python() { // Protect against race conditions when Python is not yet initialized and multiple threads // concurrently call 'prepare_freethreaded_python()'. Note that we do not protect against @@ -125,7 +125,7 @@ pub fn prepare_freethreaded_python() { /// }); /// } /// ``` -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] pub unsafe fn with_embedded_python_interpreter(f: F) -> R where F: for<'p> FnOnce(Python<'p>) -> R, @@ -182,14 +182,14 @@ impl GILGuard { // auto-initialize so this avoids breaking existing builds. // - Otherwise, just check the GIL is initialized. cfg_if::cfg_if! { - if #[cfg(all(feature = "auto-initialize", not(PyPy)))] { + if #[cfg(all(feature = "auto-initialize", not(any(PyPy, GraalPy))))] { prepare_freethreaded_python(); } else { // This is a "hack" to make running `cargo test` for PyO3 convenient (i.e. no need // to specify `--features auto-initialize` manually. Tests within the crate itself // all depend on the auto-initialize feature for conciseness but Cargo does not // provide a mechanism to specify required features for tests. - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] if option_env!("CARGO_PRIMARY_PACKAGE").is_some() { prepare_freethreaded_python(); } diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index 9ca80e3918c..ba13f7a9841 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -2,10 +2,14 @@ use std::cell::UnsafeCell; -#[cfg(all(not(PyPy), Py_3_9, not(all(windows, Py_LIMITED_API, not(Py_3_10)))))] +#[cfg(all( + not(any(PyPy, GraalPy)), + Py_3_9, + not(all(windows, Py_LIMITED_API, not(Py_3_10))) +))] use portable_atomic::{AtomicI64, Ordering}; -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] use crate::exceptions::PyImportError; use crate::{ffi, sync::GILOnceCell, types::PyModule, Bound, Py, PyResult, Python}; @@ -15,7 +19,11 @@ pub struct ModuleDef { ffi_def: UnsafeCell, initializer: ModuleInitializer, /// Interpreter ID where module was initialized (not applicable on PyPy). - #[cfg(all(not(PyPy), Py_3_9, not(all(windows, Py_LIMITED_API, not(Py_3_10)))))] + #[cfg(all( + not(any(PyPy, GraalPy)), + Py_3_9, + not(all(windows, Py_LIMITED_API, not(Py_3_10))) + ))] interpreter: AtomicI64, /// Initialized module object, cached to avoid reinitialization. module: GILOnceCell>, @@ -58,7 +66,11 @@ impl ModuleDef { ffi_def, initializer, // -1 is never expected to be a valid interpreter ID - #[cfg(all(not(PyPy), Py_3_9, not(all(windows, Py_LIMITED_API, not(Py_3_10)))))] + #[cfg(all( + not(any(PyPy, GraalPy)), + Py_3_9, + not(all(windows, Py_LIMITED_API, not(Py_3_10))) + ))] interpreter: AtomicI64::new(-1), module: GILOnceCell::new(), } @@ -85,7 +97,7 @@ impl ModuleDef { // that static data is not reused across interpreters. // // PyPy does not have subinterpreters, so no need to check interpreter ID. - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] { // PyInterpreterState_Get is only available on 3.9 and later, but is missing // from python3.dll for Windows stable API on 3.9 diff --git a/src/impl_/trampoline.rs b/src/impl_/trampoline.rs index 4d77f329125..db493817cba 100644 --- a/src/impl_/trampoline.rs +++ b/src/impl_/trampoline.rs @@ -24,12 +24,14 @@ pub unsafe fn module_init( } #[inline] +#[allow(clippy::used_underscore_binding)] pub unsafe fn noargs( slf: *mut ffi::PyObject, - args: *mut ffi::PyObject, + _args: *mut ffi::PyObject, f: for<'py> unsafe fn(Python<'py>, *mut ffi::PyObject) -> PyResult<*mut ffi::PyObject>, ) -> *mut ffi::PyObject { - debug_assert!(args.is_null()); + #[cfg(not(GraalPy))] // this is not specified and GraalPy does not pass null here + debug_assert!(_args.is_null()); trampoline(|py| f(py, slf)) } diff --git a/src/lib.rs b/src/lib.rs index fd5a520fdb5..e444912a63d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -321,7 +321,7 @@ pub use crate::err::{ }; #[allow(deprecated)] pub use crate::gil::GILPool; -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] pub use crate::gil::{prepare_freethreaded_python, with_embedded_python_interpreter}; pub use crate::instance::{Borrowed, Bound, Py, PyNativeType, PyObject}; pub use crate::marker::Python; diff --git a/src/macros.rs b/src/macros.rs index 648bac180ff..09e2acfb45b 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -201,7 +201,7 @@ macro_rules! wrap_pymodule { /// /// Use it before [`prepare_freethreaded_python`](crate::prepare_freethreaded_python) and /// leave feature `auto-initialize` off -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] #[macro_export] macro_rules! append_to_inittab { ($module:ident) => { diff --git a/src/marker.rs b/src/marker.rs index 08b9042491b..67119b5e55e 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -400,7 +400,7 @@ impl Python<'_> { /// If the [`auto-initialize`] feature is enabled and the Python runtime is not already /// initialized, this function will initialize it. See #[cfg_attr( - not(PyPy), + not(any(PyPy, GraalPy)), doc = "[`prepare_freethreaded_python`](crate::prepare_freethreaded_python)" )] #[cfg_attr(PyPy, doc = "`prepare_freethreaded_python`")] diff --git a/src/types/any.rs b/src/types/any.rs index 19855abbb9a..ab4f5727623 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -6,7 +6,7 @@ use crate::ffi_ptr_ext::FfiPtrExt; use crate::instance::Bound; use crate::py_result_ext::PyResultExt; use crate::type_object::{HasPyGilRef, PyTypeCheck, PyTypeInfo}; -#[cfg(not(PyPy))] +#[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}; @@ -929,7 +929,7 @@ impl PyAny { /// Return a proxy object that delegates method calls to a parent or sibling class of type. /// /// This is equivalent to the Python expression `super()` - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub fn py_super(&self) -> PyResult<&PySuper> { self.as_borrowed().py_super().map(Bound::into_gil_ref) } @@ -1708,7 +1708,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// Return a proxy object that delegates method calls to a parent or sibling class of type. /// /// This is equivalent to the Python expression `super()` - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] fn py_super(&self) -> PyResult>; } @@ -1975,6 +1975,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { cfg_if::cfg_if! { if #[cfg(all( not(PyPy), + not(GraalPy), any(Py_3_10, all(not(Py_LIMITED_API), Py_3_9)) // PyObject_CallNoArgs was added to python in 3.9 but to limited API in 3.10 ))] { // Optimized path on python 3.9+ @@ -2010,7 +2011,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { N: IntoPy>, { cfg_if::cfg_if! { - if #[cfg(all(Py_3_9, not(any(Py_LIMITED_API, PyPy))))] { + if #[cfg(all(Py_3_9, not(any(Py_LIMITED_API, PyPy, GraalPy))))] { let py = self.py(); // Optimized path on python 3.9+ @@ -2265,7 +2266,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { inner(self, value.to_object(py).into_bound(py)) } - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] fn py_super(&self) -> PyResult> { PySuper::new_bound(&self.get_type(), self) } diff --git a/src/types/complex.rs b/src/types/complex.rs index 80ecffdc5cf..e711b054fe3 100644 --- a/src/types/complex.rs +++ b/src/types/complex.rs @@ -53,7 +53,7 @@ impl PyComplex { } } -#[cfg(not(any(Py_LIMITED_API, PyPy)))] +#[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] mod not_limited_impls { use crate::ffi_ptr_ext::FfiPtrExt; use crate::Borrowed; @@ -264,10 +264,10 @@ pub trait PyComplexMethods<'py>: crate::sealed::Sealed { /// Returns the imaginary part of the complex number. fn imag(&self) -> c_double; /// Returns `|self|`. - #[cfg(not(any(Py_LIMITED_API, PyPy)))] + #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] fn abs(&self) -> c_double; /// Returns `self` raised to the power of `other`. - #[cfg(not(any(Py_LIMITED_API, PyPy)))] + #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] fn pow(&self, other: &Bound<'py, PyComplex>) -> Bound<'py, PyComplex>; } @@ -280,7 +280,7 @@ impl<'py> PyComplexMethods<'py> for Bound<'py, PyComplex> { unsafe { ffi::PyComplex_ImagAsDouble(self.as_ptr()) } } - #[cfg(not(any(Py_LIMITED_API, PyPy)))] + #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] fn abs(&self) -> c_double { unsafe { let val = (*self.as_ptr().cast::()).cval; @@ -288,7 +288,7 @@ impl<'py> PyComplexMethods<'py> for Bound<'py, PyComplex> { } } - #[cfg(not(any(Py_LIMITED_API, PyPy)))] + #[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 { diff --git a/src/types/datetime.rs b/src/types/datetime.rs index d46a2b77c33..e1620a6f647 100644 --- a/src/types/datetime.rs +++ b/src/types/datetime.rs @@ -11,6 +11,8 @@ use crate::ffi::{ PyDateTime_DATE_GET_FOLD, PyDateTime_DATE_GET_HOUR, PyDateTime_DATE_GET_MICROSECOND, PyDateTime_DATE_GET_MINUTE, PyDateTime_DATE_GET_SECOND, }; +#[cfg(GraalPy)] +use crate::ffi::{PyDateTime_DATE_GET_TZINFO, PyDateTime_TIME_GET_TZINFO, Py_IsNone}; use crate::ffi::{ PyDateTime_DELTA_GET_DAYS, PyDateTime_DELTA_GET_MICROSECONDS, PyDateTime_DELTA_GET_SECONDS, }; @@ -552,6 +554,7 @@ impl<'py> PyTzInfoAccess<'py> for &'py PyDateTime { impl<'py> PyTzInfoAccess<'py> for Bound<'py, PyDateTime> { fn get_tzinfo_bound(&self) -> Option> { let ptr = self.as_ptr() as *mut ffi::PyDateTime_DateTime; + #[cfg(not(GraalPy))] unsafe { if (*ptr).hastzinfo != 0 { Some( @@ -565,6 +568,20 @@ impl<'py> PyTzInfoAccess<'py> for Bound<'py, PyDateTime> { None } } + + #[cfg(GraalPy)] + unsafe { + let res = PyDateTime_DATE_GET_TZINFO(ptr as *mut ffi::PyObject); + if Py_IsNone(res) == 1 { + None + } else { + Some( + res.assume_borrowed(self.py()) + .to_owned() + .downcast_into_unchecked(), + ) + } + } } } @@ -740,6 +757,7 @@ impl<'py> PyTzInfoAccess<'py> for &'py PyTime { impl<'py> PyTzInfoAccess<'py> for Bound<'py, PyTime> { fn get_tzinfo_bound(&self) -> Option> { let ptr = self.as_ptr() as *mut ffi::PyDateTime_Time; + #[cfg(not(GraalPy))] unsafe { if (*ptr).hastzinfo != 0 { Some( @@ -753,6 +771,20 @@ impl<'py> PyTzInfoAccess<'py> for Bound<'py, PyTime> { None } } + + #[cfg(GraalPy)] + unsafe { + let res = PyDateTime_TIME_GET_TZINFO(ptr as *mut ffi::PyObject); + if Py_IsNone(res) == 1 { + None + } else { + Some( + res.assume_borrowed(self.py()) + .to_owned() + .downcast_into_unchecked(), + ) + } + } } } diff --git a/src/types/dict.rs b/src/types/dict.rs index 43bade5a80c..a4ba72a59c0 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -20,11 +20,11 @@ pyobject_native_type!( ); /// Represents a Python `dict_keys`. -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] #[repr(transparent)] pub struct PyDictKeys(PyAny); -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] pyobject_native_type_core!( PyDictKeys, pyobject_native_static_type_object!(ffi::PyDictKeys_Type), @@ -32,11 +32,11 @@ pyobject_native_type_core!( ); /// Represents a Python `dict_values`. -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] #[repr(transparent)] pub struct PyDictValues(PyAny); -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] pyobject_native_type_core!( PyDictValues, pyobject_native_static_type_object!(ffi::PyDictValues_Type), @@ -44,11 +44,11 @@ pyobject_native_type_core!( ); /// Represents a Python `dict_items`. -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] #[repr(transparent)] pub struct PyDictItems(PyAny); -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] pyobject_native_type_core!( PyDictItems, pyobject_native_static_type_object!(ffi::PyDictItems_Type), @@ -76,14 +76,14 @@ impl PyDict { /// Deprecated form of [`from_sequence_bound`][PyDict::from_sequence_bound]. #[cfg_attr( - all(not(PyPy), not(feature = "gil-refs")), + 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" ) )] #[inline] - #[cfg(not(PyPy))] + #[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) } @@ -95,7 +95,7 @@ impl PyDict { /// /// Returns an error on invalid input. In the case of key collisions, /// this keeps the last entry seen. - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub fn from_sequence_bound<'py>(seq: &Bound<'py, PyAny>) -> PyResult> { let py = seq.py(); let dict = Self::new_bound(py); @@ -542,12 +542,12 @@ impl<'a, 'py> Borrowed<'a, 'py, PyDict> { } fn dict_len(dict: &Bound<'_, PyDict>) -> Py_ssize_t { - #[cfg(any(not(Py_3_8), PyPy, Py_LIMITED_API))] + #[cfg(any(not(Py_3_8), PyPy, GraalPy, Py_LIMITED_API))] unsafe { ffi::PyDict_Size(dict.as_ptr()) } - #[cfg(all(Py_3_8, not(PyPy), not(Py_LIMITED_API)))] + #[cfg(all(Py_3_8, not(PyPy), not(GraalPy), not(Py_LIMITED_API)))] unsafe { (*dict.as_ptr().cast::()).ma_used } @@ -824,7 +824,7 @@ where #[cfg_attr(not(feature = "gil-refs"), allow(deprecated))] mod tests { use super::*; - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] use crate::exceptions; use crate::types::PyTuple; use std::collections::{BTreeMap, HashMap}; @@ -850,7 +850,7 @@ mod tests { } #[test] - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] fn test_from_sequence() { Python::with_gil(|py| { let items = PyList::new(py, &vec![("a", 1), ("b", 2)]); @@ -881,7 +881,7 @@ mod tests { } #[test] - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] fn test_from_sequence_err() { Python::with_gil(|py| { let items = PyList::new(py, &vec!["a", "b"]); @@ -955,7 +955,7 @@ mod tests { #[test] #[allow(deprecated)] - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] fn test_get_item_with_error() { Python::with_gil(|py| { let mut v = HashMap::new(); @@ -1388,7 +1388,7 @@ mod tests { }); } - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] fn abc_dict(py: Python<'_>) -> Bound<'_, PyDict> { let mut map = HashMap::<&'static str, i32>::new(); map.insert("a", 1); @@ -1398,7 +1398,7 @@ mod tests { } #[test] - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] fn dict_keys_view() { Python::with_gil(|py| { let dict = abc_dict(py); @@ -1410,7 +1410,7 @@ mod tests { } #[test] - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] fn dict_values_view() { Python::with_gil(|py| { let dict = abc_dict(py); @@ -1422,7 +1422,7 @@ mod tests { } #[test] - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] fn dict_items_view() { Python::with_gil(|py| { let dict = abc_dict(py); diff --git a/src/types/frozenset.rs b/src/types/frozenset.rs index 8a60efb8caa..36d5578f701 100644 --- a/src/types/frozenset.rs +++ b/src/types/frozenset.rs @@ -60,7 +60,7 @@ impl<'py> PyFrozenSetBuilder<'py> { #[repr(transparent)] pub struct PyFrozenSet(PyAny); -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] pyobject_native_type!( PyFrozenSet, ffi::PySetObject, @@ -68,7 +68,7 @@ pyobject_native_type!( #checkfunction=ffi::PyFrozenSet_Check ); -#[cfg(PyPy)] +#[cfg(any(PyPy, GraalPy))] pyobject_native_type_core!( PyFrozenSet, pyobject_native_static_type_object!(ffi::PyFrozenSet_Type), diff --git a/src/types/mod.rs b/src/types/mod.rs index cee45e8678d..5448999d4c7 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -5,7 +5,7 @@ pub use self::boolobject::{PyBool, PyBoolMethods}; pub use self::bytearray::{PyByteArray, PyByteArrayMethods}; pub use self::bytes::{PyBytes, PyBytesMethods}; pub use self::capsule::{PyCapsule, PyCapsuleMethods}; -#[cfg(all(not(Py_LIMITED_API), not(PyPy)))] +#[cfg(all(not(Py_LIMITED_API), not(PyPy), not(GraalPy)))] pub use self::code::PyCode; pub use self::complex::{PyComplex, PyComplexMethods}; #[allow(deprecated)] @@ -17,15 +17,15 @@ pub use self::datetime::{ PyTimeAccess, PyTzInfo, PyTzInfoAccess, }; pub use self::dict::{IntoPyDict, PyDict, PyDictMethods}; -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] pub use self::dict::{PyDictItems, PyDictKeys, PyDictValues}; pub use self::ellipsis::PyEllipsis; pub use self::float::{PyFloat, PyFloatMethods}; -#[cfg(all(not(Py_LIMITED_API), not(PyPy)))] +#[cfg(all(not(Py_LIMITED_API), not(PyPy), not(GraalPy)))] 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)))] +#[cfg(all(not(Py_LIMITED_API), not(PyPy), not(GraalPy)))] pub use self::function::PyFunction; pub use self::iterator::PyIterator; pub use self::list::{PyList, PyListMethods}; @@ -36,7 +36,7 @@ pub use self::none::PyNone; pub use self::notimplemented::PyNotImplemented; pub use self::num::PyLong; pub use self::num::PyLong as PyInt; -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] pub use self::pysuper::PySuper; pub use self::sequence::{PySequence, PySequenceMethods}; pub use self::set::{PySet, PySetMethods}; @@ -328,7 +328,7 @@ pub(crate) mod boolobject; pub(crate) mod bytearray; pub(crate) mod bytes; pub(crate) mod capsule; -#[cfg(all(not(Py_LIMITED_API), not(PyPy)))] +#[cfg(all(not(Py_LIMITED_API), not(PyPy), not(GraalPy)))] mod code; pub(crate) mod complex; #[cfg(not(Py_LIMITED_API))] @@ -336,7 +336,7 @@ pub(crate) mod datetime; pub(crate) mod dict; mod ellipsis; pub(crate) mod float; -#[cfg(all(not(Py_LIMITED_API), not(PyPy)))] +#[cfg(all(not(Py_LIMITED_API), not(PyPy), not(GraalPy)))] mod frame; pub(crate) mod frozenset; mod function; @@ -348,7 +348,7 @@ pub(crate) mod module; mod none; mod notimplemented; mod num; -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] mod pysuper; pub(crate) mod sequence; pub(crate) mod set; diff --git a/src/types/sequence.rs b/src/types/sequence.rs index 62abb66fa6e..2b37b6d14f0 100644 --- a/src/types/sequence.rs +++ b/src/types/sequence.rs @@ -134,7 +134,7 @@ impl PySequence { /// Returns the number of occurrences of `value` in self, that is, return the /// number of keys for which `self[key] == value`. #[inline] - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] pub fn count(&self, value: V) -> PyResult where V: ToPyObject, @@ -870,7 +870,7 @@ mod tests { } #[test] - #[cfg(not(PyPy))] + #[cfg(not(any(PyPy, GraalPy)))] fn test_seq_count() { Python::with_gil(|py| { let v: Vec = vec![1, 1, 2, 3, 5, 8]; diff --git a/src/types/set.rs b/src/types/set.rs index c043fa5ba4f..4f1fcf8499f 100644 --- a/src/types/set.rs +++ b/src/types/set.rs @@ -14,7 +14,7 @@ use std::ptr; #[repr(transparent)] pub struct PySet(PyAny); -#[cfg(not(PyPy))] +#[cfg(not(any(PyPy, GraalPy)))] pyobject_native_type!( PySet, ffi::PySetObject, @@ -22,7 +22,7 @@ pyobject_native_type!( #checkfunction=ffi::PySet_Check ); -#[cfg(PyPy)] +#[cfg(any(PyPy, GraalPy))] pyobject_native_type_core!( PySet, pyobject_native_static_type_object!(ffi::PySet_Type), diff --git a/src/types/string.rs b/src/types/string.rs index 81c41adb545..4bbc6fb86b0 100644 --- a/src/types/string.rs +++ b/src/types/string.rs @@ -270,7 +270,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(Py_LIMITED_API))] + #[cfg(not(any(Py_LIMITED_API, GraalPy)))] pub unsafe fn data(&self) -> PyResult> { self.as_borrowed().data() } @@ -319,7 +319,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(Py_LIMITED_API))] + #[cfg(not(any(Py_LIMITED_API, GraalPy)))] unsafe fn data(&self) -> PyResult>; } @@ -345,7 +345,7 @@ impl<'py> PyStringMethods<'py> for Bound<'py, PyString> { } } - #[cfg(not(Py_LIMITED_API))] + #[cfg(not(any(Py_LIMITED_API, GraalPy)))] unsafe fn data(&self) -> PyResult> { self.as_borrowed().data() } @@ -408,7 +408,7 @@ impl<'a> Borrowed<'a, '_, PyString> { Cow::Owned(String::from_utf8_lossy(bytes.as_bytes()).into_owned()) } - #[cfg(not(Py_LIMITED_API))] + #[cfg(not(any(Py_LIMITED_API, GraalPy)))] unsafe fn data(self) -> PyResult> { let ptr = self.as_ptr(); diff --git a/src/types/tuple.rs b/src/types/tuple.rs index 4dfe4436bf0..636a2f3e11f 100644 --- a/src/types/tuple.rs +++ b/src/types/tuple.rs @@ -34,9 +34,9 @@ fn new_from_iter<'py>( let mut counter: Py_ssize_t = 0; for obj in elements.take(len as usize) { - #[cfg(not(any(Py_LIMITED_API, PyPy)))] + #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] ffi::PyTuple_SET_ITEM(ptr, counter, obj.into_ptr()); - #[cfg(any(Py_LIMITED_API, PyPy))] + #[cfg(any(Py_LIMITED_API, PyPy, GraalPy))] ffi::PyTuple_SetItem(ptr, counter, obj.into_ptr()); counter += 1; } @@ -186,7 +186,7 @@ impl PyTuple { /// # Safety /// /// Caller must verify that the index is within the bounds of the tuple. - #[cfg(not(any(Py_LIMITED_API, PyPy)))] + #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] pub unsafe fn get_item_unchecked(&self, index: usize) -> &PyAny { self.as_borrowed() .get_borrowed_item_unchecked(index) @@ -194,7 +194,7 @@ impl PyTuple { } /// Returns `self` as a slice of objects. - #[cfg(not(Py_LIMITED_API))] + #[cfg(not(any(Py_LIMITED_API, GraalPy)))] pub fn as_slice(&self) -> &[&PyAny] { // This is safe because &PyAny has the same memory layout as *mut ffi::PyObject, // and because tuples are immutable. @@ -293,7 +293,7 @@ pub trait PyTupleMethods<'py>: crate::sealed::Sealed { /// # Safety /// /// Caller must verify that the index is within the bounds of the tuple. - #[cfg(not(any(Py_LIMITED_API, PyPy)))] + #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] unsafe fn get_item_unchecked(&self, index: usize) -> Bound<'py, PyAny>; /// Like [`get_item_unchecked`][PyTupleMethods::get_item_unchecked], but returns a borrowed object, @@ -302,11 +302,11 @@ pub trait PyTupleMethods<'py>: crate::sealed::Sealed { /// # Safety /// /// Caller must verify that the index is within the bounds of the tuple. - #[cfg(not(any(Py_LIMITED_API, PyPy)))] + #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] unsafe fn get_borrowed_item_unchecked<'a>(&'a self, index: usize) -> Borrowed<'a, 'py, PyAny>; /// Returns `self` as a slice of objects. - #[cfg(not(Py_LIMITED_API))] + #[cfg(not(any(Py_LIMITED_API, GraalPy)))] fn as_slice(&self) -> &[Bound<'py, PyAny>]; /// Determines if self contains `value`. @@ -339,9 +339,9 @@ pub trait PyTupleMethods<'py>: crate::sealed::Sealed { impl<'py> PyTupleMethods<'py> for Bound<'py, PyTuple> { fn len(&self) -> usize { unsafe { - #[cfg(not(any(Py_LIMITED_API, PyPy)))] + #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] let size = ffi::PyTuple_GET_SIZE(self.as_ptr()); - #[cfg(any(Py_LIMITED_API, PyPy))] + #[cfg(any(Py_LIMITED_API, PyPy, GraalPy))] let size = ffi::PyTuple_Size(self.as_ptr()); // non-negative Py_ssize_t should always fit into Rust uint size as usize @@ -376,17 +376,17 @@ impl<'py> PyTupleMethods<'py> for Bound<'py, PyTuple> { self.as_borrowed().get_borrowed_item(index) } - #[cfg(not(any(Py_LIMITED_API, PyPy)))] + #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] unsafe fn get_item_unchecked(&self, index: usize) -> Bound<'py, PyAny> { self.get_borrowed_item_unchecked(index).to_owned() } - #[cfg(not(any(Py_LIMITED_API, PyPy)))] + #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] unsafe fn get_borrowed_item_unchecked<'a>(&'a self, index: usize) -> Borrowed<'a, 'py, PyAny> { self.as_borrowed().get_borrowed_item_unchecked(index) } - #[cfg(not(Py_LIMITED_API))] + #[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. @@ -436,7 +436,7 @@ impl<'a, 'py> Borrowed<'a, 'py, PyTuple> { } } - #[cfg(not(any(Py_LIMITED_API, PyPy)))] + #[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()) } @@ -591,9 +591,9 @@ impl<'a, 'py> BorrowedTupleIterator<'a, 'py> { tuple: Borrowed<'a, 'py, PyTuple>, index: usize, ) -> Borrowed<'a, 'py, PyAny> { - #[cfg(any(Py_LIMITED_API, PyPy))] + #[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)))] + #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] let item = tuple.get_borrowed_item_unchecked(index); item } @@ -696,10 +696,10 @@ fn type_output() -> TypeInfo { { let t = obj.downcast::()?; if t.len() == $length { - #[cfg(any(Py_LIMITED_API, PyPy))] + #[cfg(any(Py_LIMITED_API, PyPy, GraalPy))] return Ok(($(t.get_borrowed_item($n)?.extract::<$T>()?,)+)); - #[cfg(not(any(Py_LIMITED_API, PyPy)))] + #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] unsafe {return Ok(($(t.get_borrowed_item_unchecked($n).extract::<$T>()?,)+));} } else { Err(wrong_tuple_length(t, $length)) @@ -718,9 +718,9 @@ fn array_into_tuple(py: Python<'_>, array: [PyObject; N]) -> Py< let ptr = ffi::PyTuple_New(N.try_into().expect("0 < N <= 12")); let tup = Py::from_owned_ptr(py, ptr); for (index, obj) in array.into_iter().enumerate() { - #[cfg(not(any(Py_LIMITED_API, PyPy)))] + #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] ffi::PyTuple_SET_ITEM(ptr, index as ffi::Py_ssize_t, obj.into_ptr()); - #[cfg(any(Py_LIMITED_API, PyPy))] + #[cfg(any(Py_LIMITED_API, PyPy, GraalPy))] ffi::PyTuple_SetItem(ptr, index as ffi::Py_ssize_t, obj.into_ptr()); } tup @@ -1010,7 +1010,7 @@ mod tests { } #[test] - #[cfg(not(Py_LIMITED_API))] + #[cfg(not(any(Py_LIMITED_API, GraalPy)))] fn test_as_slice() { Python::with_gil(|py| { let ob = (1, 2, 3).to_object(py); @@ -1112,7 +1112,7 @@ mod tests { }); } - #[cfg(not(any(Py_LIMITED_API, PyPy)))] + #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] #[test] fn test_tuple_get_item_unchecked_sanity() { Python::with_gil(|py| { @@ -1457,7 +1457,7 @@ mod tests { .unwrap(), 2 ); - #[cfg(not(any(Py_LIMITED_API, PyPy)))] + #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] { assert_eq!( unsafe { tuple.get_item_unchecked(2) } From 1be2fad9bfa900dc2df412e32613641d9175d759 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Mon, 25 Mar 2024 23:36:08 +0000 Subject: [PATCH 106/936] release: 0.21.0 (#3983) --- CHANGELOG.md | 48 ++++++++++--------- 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/3247.added.md | 1 - newsfragments/3947.changed.md | 1 - newsfragments/3963.added.md | 1 - newsfragments/3971.added.md | 1 - newsfragments/3982.added.md | 1 - newsfragments/3991.added.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, 45 insertions(+), 47 deletions(-) delete mode 100644 newsfragments/3247.added.md delete mode 100644 newsfragments/3947.changed.md delete mode 100644 newsfragments/3963.added.md delete mode 100644 newsfragments/3971.added.md delete mode 100644 newsfragments/3982.added.md delete mode 100644 newsfragments/3991.added.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 3581d789ff3..2e1ccb8af4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,71 +10,74 @@ To see unreleased changes, please see the [CHANGELOG on the main branch guide](h -## [0.21.0-beta.0] - 2024-03-10 +## [0.21.0] - 2024-03-25 ### Added +- Add support for GraalPy (24.0 and up). [#3247](https://github.com/PyO3/pyo3/pull/3247) - Add `PyMemoryView` type. [#3514](https://github.com/PyO3/pyo3/pull/3514) -- Support `async fn` in macros with coroutine implementation [#3540](https://github.com/PyO3/pyo3/pull/3540) +- Allow `async fn` in for `#[pyfunction]` and `#[pymethods]`, with the `experimental-async` feature. [#3540](https://github.com/PyO3/pyo3/pull/3540) [#3588](https://github.com/PyO3/pyo3/pull/3588) [#3599](https://github.com/PyO3/pyo3/pull/3599) [#3931](https://github.com/PyO3/pyo3/pull/3931) - Implement `PyTypeInfo` for `PyEllipsis`, `PyNone` and `PyNotImplemented`. [#3577](https://github.com/PyO3/pyo3/pull/3577) - Support `#[pyclass]` on enums that have non-unit variants. [#3582](https://github.com/PyO3/pyo3/pull/3582) -- Add `__name__`/`__qualname__` attributes to `Coroutine` [#3588](https://github.com/PyO3/pyo3/pull/3588) -- Add `coroutine::CancelHandle` to catch coroutine cancellation [#3599](https://github.com/PyO3/pyo3/pull/3599) -- Add support for extracting Rust set types from `frozenset`. [#3632](https://github.com/PyO3/pyo3/pull/3632) +- Support `chrono` feature with `abi3` feature. [#3664](https://github.com/PyO3/pyo3/pull/3664) - `FromPyObject`, `IntoPy` and `ToPyObject` are implemented on `std::duration::Duration` [#3670](https://github.com/PyO3/pyo3/pull/3670) - Add `PyString::to_cow`. Add `Py::to_str`, `Py::to_cow`, and `Py::to_string_lossy`, as ways to access Python string data safely beyond the GIL lifetime. [#3677](https://github.com/PyO3/pyo3/pull/3677) - Add `Bound` and `Borrowed` smart pointers as a new API for accessing Python objects. [#3686](https://github.com/PyO3/pyo3/pull/3686) -- Add `PyNativeType::as_bound` to convert "GIL refs" to the new `Bound` smart pointer. [#3692](https://github.com/PyO3/pyo3/pull/3692) -- Add `FromPyObject::extract_bound` method, which can be implemented to avoid using the GIL Ref API in `FromPyObject` implementations. [#3706](https://github.com/PyO3/pyo3/pull/3706) +- Add `PyNativeType::as_borrowed` to convert "GIL refs" to the new `Bound` smart pointer. [#3692](https://github.com/PyO3/pyo3/pull/3692) +- Add `FromPyObject::extract_bound` method, to migrate `FromPyObject` implementations to the Bound API. [#3706](https://github.com/PyO3/pyo3/pull/3706) - Add `gil-refs` feature to allow continued use of the deprecated GIL Refs APIs. [#3707](https://github.com/PyO3/pyo3/pull/3707) -- Added methods to `PyAnyMethods` for binary operators (`add`, `sub`, etc.) [#3712](https://github.com/PyO3/pyo3/pull/3712) -- `chrono-tz` feature allowing conversion between `chrono_tz::Tz` and `zoneinfo.ZoneInfo` [#3730](https://github.com/PyO3/pyo3/pull/3730) -- Add definition for `PyType_GetModuleByDef` to `pyo3_ffi`. [#3734](https://github.com/PyO3/pyo3/pull/3734) +- Add methods to `PyAnyMethods` for binary operators (`add`, `sub`, etc.) [#3712](https://github.com/PyO3/pyo3/pull/3712) +- Add `chrono-tz` feature allowing conversion between `chrono_tz::Tz` and `zoneinfo.ZoneInfo` [#3730](https://github.com/PyO3/pyo3/pull/3730) +- Add FFI definition `PyType_GetModuleByDef`. [#3734](https://github.com/PyO3/pyo3/pull/3734) - Conversion between `std::time::SystemTime` and `datetime.datetime` [#3736](https://github.com/PyO3/pyo3/pull/3736) - Add `Py::as_any` and `Py::into_any`. [#3785](https://github.com/PyO3/pyo3/pull/3785) - Add `PyStringMethods::encode_utf8`. [#3801](https://github.com/PyO3/pyo3/pull/3801) -- Add `PyBackedStr` and `PyBackedBytes`, as alternatives to `&str` and `&bytes` where a Python object owns the data. [#3802](https://github.com/PyO3/pyo3/pull/3802) -- The ability to create Python modules with a Rust `mod` block - behind the `experimental-declarative-modules` feature. [#3815](https://github.com/PyO3/pyo3/pull/3815) +- Add `PyBackedStr` and `PyBackedBytes`, as alternatives to `&str` and `&bytes` where a Python object owns the data. [#3802](https://github.com/PyO3/pyo3/pull/3802) [#3991](https://github.com/PyO3/pyo3/pull/3991) +- Allow `#[pymodule]` macro on Rust `mod` blocks, with the `experimental-declarative-modules` feature. [#3815](https://github.com/PyO3/pyo3/pull/3815) - Implement `ExactSizeIterator` for `set` and `frozenset` iterators on `abi3` feature. [#3849](https://github.com/PyO3/pyo3/pull/3849) - Add `Py::drop_ref` to explicitly drop a `Py`` and immediately decrease the Python reference count if the GIL is already held. [#3871](https://github.com/PyO3/pyo3/pull/3871) +- Allow `#[pymodule]` macro on single argument functions that take `&Bound<'_, PyModule>`. [#3905](https://github.com/PyO3/pyo3/pull/3905) - Implement `FromPyObject` for `Cow`. [#3928](https://github.com/PyO3/pyo3/pull/3928) -- Add `experimental-async` feature. [#3931](https://github.com/PyO3/pyo3/pull/3931) +- Implement `Default` for `GILOnceCell`. [#3971](https://github.com/PyO3/pyo3/pull/3971) +- Add `PyDictMethods::into_mapping`, `PyListMethods::into_sequence` and `PyTupleMethods::into_sequence`. [#3982](https://github.com/PyO3/pyo3/pull/3982) ### Changed -- - `PyDict::from_sequence` now takes a single argument of type `&PyAny` (previously took two arguments `Python` and `PyObject`). [#3532](https://github.com/PyO3/pyo3/pull/3532) +- `PyDict::from_sequence` now takes a single argument of type `&PyAny` (previously took two arguments `Python` and `PyObject`). [#3532](https://github.com/PyO3/pyo3/pull/3532) - Deprecate `Py::is_ellipsis` and `PyAny::is_ellipsis` in favour of `any.is(py.Ellipsis())`. [#3577](https://github.com/PyO3/pyo3/pull/3577) - Split some `PyTypeInfo` functionality into new traits `HasPyGilRef` and `PyTypeCheck`. [#3600](https://github.com/PyO3/pyo3/pull/3600) - Deprecate `PyTryFrom` and `PyTryInto` traits in favor of `any.downcast()` via the `PyTypeCheck` and `PyTypeInfo` traits. [#3601](https://github.com/PyO3/pyo3/pull/3601) - Allow async methods to accept `&self`/`&mut self` [#3609](https://github.com/PyO3/pyo3/pull/3609) -- Values of type `bool` can now be extracted from NumPy's `bool_`. [#3638](https://github.com/PyO3/pyo3/pull/3638) -- Add `AsRefSource` to `PyNativeType`. [#3653](https://github.com/PyO3/pyo3/pull/3653) -- Changed `.is_true` to `.is_truthy` on `PyAny` and `Py` to clarify that the test is not based on identity with or equality to the True singleton. [#3657](https://github.com/PyO3/pyo3/pull/3657) +- `FromPyObject` for set types now also accept `frozenset` objects as input. [#3632](https://github.com/PyO3/pyo3/pull/3632) +- `FromPyObject` for `bool` now also accepts NumPy's `bool_` as input. [#3638](https://github.com/PyO3/pyo3/pull/3638) +- Add `AsRefSource` associated type to `PyNativeType`. [#3653](https://github.com/PyO3/pyo3/pull/3653) +- Rename `.is_true` to `.is_truthy` on `PyAny` and `Py` to clarify that the test is not based on identity with or equality to the True singleton. [#3657](https://github.com/PyO3/pyo3/pull/3657) - `PyType::name` is now `PyType::qualname` whereas `PyType::name` efficiently accesses the full name which includes the module name. [#3660](https://github.com/PyO3/pyo3/pull/3660) - The `Iter(A)NextOutput` types are now deprecated and `__(a)next__` can directly return anything which can be converted into Python objects, i.e. awaitables do not need to be wrapped into `IterANextOutput` or `Option` any more. `Option` can still be used as well and returning `None` will trigger the fast path for `__next__`, stopping iteration without having to raise a `StopIteration` exception. [#3661](https://github.com/PyO3/pyo3/pull/3661) -- Implements `FromPyObject` on `chrono::DateTime` for all `Tz` and not only `FixedOffset` and `Utc` [#3663](https://github.com/PyO3/pyo3/pull/3663) -- `chrono` conversions are compatible with `abi3` [#3664](https://github.com/PyO3/pyo3/pull/3664) +- Implement `FromPyObject` on `chrono::DateTime` for all `Tz`, not just `FixedOffset` and `Utc`. [#3663](https://github.com/PyO3/pyo3/pull/3663) - Add lifetime parameter to `PyTzInfoAccess` trait. For the deprecated gil-ref API, the trait is now implemented for `&'py PyTime` and `&'py PyDateTime` instead of `PyTime` and `PyDate`. [#3679](https://github.com/PyO3/pyo3/pull/3679) - Calls to `__traverse__` become no-ops for unsendable pyclasses if on the wrong thread, thereby avoiding hard aborts at the cost of potential leakage. [#3689](https://github.com/PyO3/pyo3/pull/3689) - Include `PyNativeType` in `pyo3::prelude`. [#3692](https://github.com/PyO3/pyo3/pull/3692) - Improve performance of `extract::` (and other integer types) by avoiding call to `__index__()` converting the value to an integer for 3.10+. Gives performance improvement of around 30% for successful extraction. [#3742](https://github.com/PyO3/pyo3/pull/3742) - Relax bound of `FromPyObject` for `Py` to just `T: PyTypeCheck`. [#3776](https://github.com/PyO3/pyo3/pull/3776) - `PySet` and `PyFrozenSet` iterators now always iterate the equivalent of `iter(set)`. (A "fast path" with no noticeable performance benefit was removed.) [#3849](https://github.com/PyO3/pyo3/pull/3849) -- The `#[pymodule]` macro now supports module functions that take a single argument as a `&Bound<'_, PyModule>`. [#3905](https://github.com/PyO3/pyo3/pull/3905) - Move implementations of `FromPyObject` for `&str`, `Cow`, `&[u8]` and `Cow<[u8]>` onto a temporary trait `FromPyObjectBound` when `gil-refs` feature is deactivated. [#3928](https://github.com/PyO3/pyo3/pull/3928) +- Deprecate `GILPool`, `Python::with_pool`, and `Python::new_pool`. [#3947](https://github.com/PyO3/pyo3/pull/3947) ### Removed - Remove all functionality deprecated in PyO3 0.19. [#3603](https://github.com/PyO3/pyo3/pull/3603) -- Remove `PyCode` and `PyCode_Type` on PyPy: `PyCode_Type` is not exposed by PyPy. [#3934](https://github.com/PyO3/pyo3/pull/3934) ### Fixed - Match PyPy 7.3.14 in removing PyPy-only symbol `Py_MAX_NDIMS` in favour of `PyBUF_MAX_NDIM`. [#3757](https://github.com/PyO3/pyo3/pull/3757) - Fix segmentation fault using `datetime` types when an invalid `datetime` module is on sys.path. [#3818](https://github.com/PyO3/pyo3/pull/3818) - Fix `non_local_definitions` lint warning triggered by many PyO3 macros. [#3901](https://github.com/PyO3/pyo3/pull/3901) +- Disable `PyCode` and `PyCode_Type` on PyPy: `PyCode_Type` is not exposed by PyPy. [#3934](https://github.com/PyO3/pyo3/pull/3934) + +## [0.21.0-beta.0] - 2024-03-10 +Prerelease of PyO3 0.21. See (the GitHub diff)[https://github.com/pyo3/pyo3/compare/v0.21.0-beta.0...v0.21.0] for what changed between 0.21.0-beta.0 and the final release. ## [0.20.3] - 2024-02-23 @@ -1706,6 +1709,7 @@ Yanked - Initial release +[0.21.0]: https://github.com/pyo3/pyo3/compare/v0.20.3...v0.21.0 [0.21.0-beta.0]: https://github.com/pyo3/pyo3/compare/v0.20.3...v0.21.0-beta.0 [0.20.3]: https://github.com/pyo3/pyo3/compare/v0.20.2...v0.20.3 [0.20.2]: https://github.com/pyo3/pyo3/compare/v0.20.1...v0.20.2 diff --git a/Cargo.toml b/Cargo.toml index ba5c3461180..dfbeb2d601d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3" -version = "0.21.0-beta.0" +version = "0.21.0" 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.0-beta.0" } +pyo3-ffi = { path = "pyo3-ffi", version = "=0.21.0" } # support crates for macros feature -pyo3-macros = { path = "pyo3-macros", version = "=0.21.0-beta.0", optional = true } +pyo3-macros = { path = "pyo3-macros", version = "=0.21.0", 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.0-beta.0", features = ["resolve-config"] } +pyo3-build-config = { path = "pyo3-build-config", version = "=0.21.0", features = ["resolve-config"] } [features] default = ["macros"] diff --git a/README.md b/README.md index 461e56612ad..b28376e7f5e 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ name = "string_sum" crate-type = ["cdylib"] [dependencies] -pyo3 = { version = "0.20.3", features = ["extension-module"] } +pyo3 = { version = "0.21.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.20.3" +version = "0.21.0" features = ["auto-initialize"] ``` diff --git a/examples/decorator/.template/pre-script.rhai b/examples/decorator/.template/pre-script.rhai index f059903cc18..cebb06c0f61 100644 --- a/examples/decorator/.template/pre-script.rhai +++ b/examples/decorator/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.20.3"); +variable::set("PYO3_VERSION", "0.21.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 f059903cc18..cebb06c0f61 100644 --- a/examples/maturin-starter/.template/pre-script.rhai +++ b/examples/maturin-starter/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.20.3"); +variable::set("PYO3_VERSION", "0.21.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 90989a891ef..4b290f7f7e0 100644 --- a/examples/plugin/.template/pre-script.rhai +++ b/examples/plugin/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.20.3"); +variable::set("PYO3_VERSION", "0.21.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 d5520ae1d28..fbbe7ed0e7c 100644 --- a/examples/setuptools-rust-starter/.template/pre-script.rhai +++ b/examples/setuptools-rust-starter/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.20.3"); +variable::set("PYO3_VERSION", "0.21.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 f059903cc18..cebb06c0f61 100644 --- a/examples/word-count/.template/pre-script.rhai +++ b/examples/word-count/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.20.3"); +variable::set("PYO3_VERSION", "0.21.0"); file::rename(".template/Cargo.toml", "Cargo.toml"); file::rename(".template/pyproject.toml", "pyproject.toml"); file::delete(".template"); diff --git a/newsfragments/3247.added.md b/newsfragments/3247.added.md deleted file mode 100644 index dd2641c3194..00000000000 --- a/newsfragments/3247.added.md +++ /dev/null @@ -1 +0,0 @@ -Added support for running PyO3 extensions on GraalPy. diff --git a/newsfragments/3947.changed.md b/newsfragments/3947.changed.md deleted file mode 100644 index 4618e9378fb..00000000000 --- a/newsfragments/3947.changed.md +++ /dev/null @@ -1 +0,0 @@ -Deprecate `GILPool`, `Python::with_pool`, and `Python::new_pool`. diff --git a/newsfragments/3963.added.md b/newsfragments/3963.added.md deleted file mode 100644 index 86da17acc89..00000000000 --- a/newsfragments/3963.added.md +++ /dev/null @@ -1 +0,0 @@ -added `Borrowed::to_owned` \ No newline at end of file diff --git a/newsfragments/3971.added.md b/newsfragments/3971.added.md deleted file mode 100644 index 12c0d2265bc..00000000000 --- a/newsfragments/3971.added.md +++ /dev/null @@ -1 +0,0 @@ -Implement `Default` for `GILOnceCell`. diff --git a/newsfragments/3982.added.md b/newsfragments/3982.added.md deleted file mode 100644 index abc89574d38..00000000000 --- a/newsfragments/3982.added.md +++ /dev/null @@ -1 +0,0 @@ -Added `Bound<'py, PyDict>::into_mapping`, `Bound<'py, PyList>::into_sequence` and `Bound<'py, PyTuple>::into_sequence`. diff --git a/newsfragments/3991.added.md b/newsfragments/3991.added.md deleted file mode 100644 index 1a59ecee79c..00000000000 --- a/newsfragments/3991.added.md +++ /dev/null @@ -1 +0,0 @@ -implemented `AsRef` and `AsRef<[u8]>` for `PyBackedStr`, `AsRef<[u8]>` for `PyBackedBytes`. diff --git a/pyo3-build-config/Cargo.toml b/pyo3-build-config/Cargo.toml index c00427eb18e..f19ded99697 100644 --- a/pyo3-build-config/Cargo.toml +++ b/pyo3-build-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-build-config" -version = "0.21.0-beta.0" +version = "0.21.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 8dea584e304..091e5f898ef 100644 --- a/pyo3-ffi/Cargo.toml +++ b/pyo3-ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-ffi" -version = "0.21.0-beta.0" +version = "0.21.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.21.0-beta.0", features = ["resolve-config"] } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.21.0", features = ["resolve-config"] } [lints] workspace = true diff --git a/pyo3-macros-backend/Cargo.toml b/pyo3-macros-backend/Cargo.toml index abf71902e20..822dc372b47 100644 --- a/pyo3-macros-backend/Cargo.toml +++ b/pyo3-macros-backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-macros-backend" -version = "0.21.0-beta.0" +version = "0.21.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.4" proc-macro2 = { version = "1", default-features = false } -pyo3-build-config = { path = "../pyo3-build-config", version = "=0.21.0-beta.0", features = ["resolve-config"] } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.21.0", features = ["resolve-config"] } quote = { version = "1", default-features = false } [dependencies.syn] diff --git a/pyo3-macros/Cargo.toml b/pyo3-macros/Cargo.toml index 008224e78ef..a70ccb0ae7b 100644 --- a/pyo3-macros/Cargo.toml +++ b/pyo3-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-macros" -version = "0.21.0-beta.0" +version = "0.21.0" 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.0-beta.0" } +pyo3-macros-backend = { path = "../pyo3-macros-backend", version = "=0.21.0" } [lints] workspace = true diff --git a/pyproject.toml b/pyproject.toml index 5d5ef42b1db..1a3cfa4aaad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ [tool.towncrier] filename = "CHANGELOG.md" -version = "0.21.0-beta.0" +version = "0.21.0" start_string = "\n" template = ".towncrier.template.md" title_format = "## [{version}] - {project_date}" From 35faeff6f1db5bc4aa672355ae6174f8fe154fc4 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Tue, 26 Mar 2024 19:53:11 +0100 Subject: [PATCH 107/936] handle `#[pyo3(from_py_with = "")]` in `#[setter]` methods (#3995) * handle `#[pyo3(from_py_with = "")]` in `#[setter]` methods * add newsfragment --- newsfragments/3995.fixed.md | 1 + pyo3-macros-backend/src/pymethod.rs | 32 +++++++++++++++- tests/test_getter_setter.rs | 12 ++++++ tests/ui/deprecations.rs | 6 +++ tests/ui/deprecations.stderr | 58 ++++++++++++++++------------- 5 files changed, 81 insertions(+), 28 deletions(-) create mode 100644 newsfragments/3995.fixed.md diff --git a/newsfragments/3995.fixed.md b/newsfragments/3995.fixed.md new file mode 100644 index 00000000000..e47a71b790d --- /dev/null +++ b/newsfragments/3995.fixed.md @@ -0,0 +1 @@ +handle `#[pyo3(from_py_with = "")]` in `#[setter]` methods \ No newline at end of file diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index 4e6f46d96d5..1e476ca22f2 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -11,7 +11,7 @@ use crate::{ }; use crate::{quotes, utils}; use proc_macro2::{Span, TokenStream}; -use quote::{format_ident, quote, ToTokens}; +use quote::{format_ident, quote, quote_spanned, ToTokens}; use syn::{ext::IdentExt, spanned::Spanned, Result}; /// Generated code for a single pymethod item. @@ -586,6 +586,34 @@ pub fn impl_py_setter_def( } }; + let extract = if let PropertyType::Function { spec, .. } = &property_type { + Some(spec) + } else { + None + } + .and_then(|spec| { + let (_, args) = split_off_python_arg(&spec.signature.arguments); + let value_arg = &args[0]; + let from_py_with = &value_arg.attrs.from_py_with.as_ref()?.value; + let name = value_arg.name.to_string(); + + Some(quote_spanned! { from_py_with.span() => + let e = #pyo3_path::impl_::deprecations::GilRefs::new(); + let from_py_with = #pyo3_path::impl_::deprecations::inspect_fn(#from_py_with, &e); + e.from_py_with_arg(); + let _val = #pyo3_path::impl_::extract_argument::from_py_with( + &_value.into(), + #name, + from_py_with as fn(_) -> _, + )?; + }) + }) + .unwrap_or_else(|| { + quote! { + let _val = #pyo3_path::FromPyObject::extract_bound(_value.into())?; + } + }); + let mut cfg_attrs = TokenStream::new(); if let PropertyType::Descriptor { field, .. } = &property_type { for attr in field @@ -611,7 +639,7 @@ pub fn impl_py_setter_def( .ok_or_else(|| { #pyo3_path::exceptions::PyAttributeError::new_err("can't delete attribute") })?; - let _val = #pyo3_path::FromPyObject::extract_bound(_value.into())?; + #extract #init_holders let result = #setter_impl; #check_gil_refs diff --git a/tests/test_getter_setter.rs b/tests/test_getter_setter.rs index 1009ce1bd91..5f886639cc0 100644 --- a/tests/test_getter_setter.rs +++ b/tests/test_getter_setter.rs @@ -41,12 +41,21 @@ impl ClassWithProperties { self.num = value; } + #[setter] + fn set_from_len(&mut self, #[pyo3(from_py_with = "extract_len")] value: i32) { + self.num = value; + } + #[getter] fn get_data_list<'py>(&self, py: Python<'py>) -> Bound<'py, PyList> { PyList::new_bound(py, [self.num]) } } +fn extract_len(any: &Bound<'_, PyAny>) -> PyResult { + any.len().map(|len| len as i32) +} + #[test] fn class_with_properties() { Python::with_gil(|py| { @@ -64,6 +73,9 @@ fn class_with_properties() { py_run!(py, inst, "assert inst.get_num() == inst.unwrapped == 42"); py_run!(py, inst, "assert inst.data_list == [42]"); + py_run!(py, inst, "inst.from_len = [0, 0, 0]"); + py_run!(py, inst, "assert inst.get_num() == 3"); + let d = [("C", py.get_type_bound::())].into_py_dict_bound(py); py_assert!(py, *d, "C.DATA.__doc__ == 'a getter for data'"); }); diff --git a/tests/ui/deprecations.rs b/tests/ui/deprecations.rs index 6f3a1feda50..0ed4d09eebb 100644 --- a/tests/ui/deprecations.rs +++ b/tests/ui/deprecations.rs @@ -26,6 +26,12 @@ impl MyClass { #[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) {} } fn main() {} diff --git a/tests/ui/deprecations.stderr b/tests/ui/deprecations.stderr index 1d87f31c3f8..e2d9cf36ebb 100644 --- a/tests/ui/deprecations.stderr +++ b/tests/ui/deprecations.stderr @@ -34,70 +34,76 @@ error: use of deprecated method `pyo3::deprecations::GilRefs::::function_arg` 28 | fn static_method_gil_ref(_any: &PyAny) {} | ^ +error: use of deprecated method `pyo3::deprecations::GilRefs::::from_py_with_arg`: use `&Bound<'_, PyAny>` as the argument for this `from_py_with` extractor + --> tests/ui/deprecations.rs:31:53 + | +31 | fn set_foo_gil_ref(&self, #[pyo3(from_py_with = "extract_gil_ref")] _value: i32) {} + | ^^^^^^^^^^^^^^^^^ + error: use of deprecated method `pyo3::deprecations::GilRefs::::function_arg`: use `&Bound<'_, T>` instead for this function argument - --> tests/ui/deprecations.rs:41:43 + --> tests/ui/deprecations.rs:47:43 | -41 | fn pyfunction_with_module_gil_ref(module: &PyModule) -> PyResult<&str> { +47 | 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:51:19 + --> tests/ui/deprecations.rs:57:19 | -51 | fn module_gil_ref(m: &PyModule) -> PyResult<()> { +57 | 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:57:57 + --> tests/ui/deprecations.rs:63:57 | -57 | fn module_gil_ref_with_explicit_py_arg(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +63 | 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:90:27 + --> tests/ui/deprecations.rs:96:27 | -90 | #[pyo3(from_py_with = "extract_gil_ref")] _gil_ref: i32, +96 | #[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:96:29 - | -96 | fn pyfunction_gil_ref(_any: &PyAny) {} - | ^ + --> tests/ui/deprecations.rs:102:29 + | +102 | 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:99:36 - | -99 | fn pyfunction_option_gil_ref(_any: Option<&PyAny>) {} - | ^^^^^^ + --> tests/ui/deprecations.rs:105:36 + | +105 | 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:106:27 + --> tests/ui/deprecations.rs:112:27 | -106 | #[pyo3(from_py_with = "PyAny::len", item("my_object"))] +112 | #[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:116:27 + --> tests/ui/deprecations.rs:122:27 | -116 | #[pyo3(from_py_with = "PyAny::len")] usize, +122 | #[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:122:31 + --> tests/ui/deprecations.rs:128:31 | -122 | Zip(#[pyo3(from_py_with = "extract_gil_ref")] i32), +128 | 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:129:27 + --> tests/ui/deprecations.rs:135:27 | -129 | #[pyo3(from_py_with = "extract_gil_ref")] +135 | #[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:142:13 + --> tests/ui/deprecations.rs:148:13 | -142 | let _ = wrap_pyfunction!(double, py); +148 | 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 7c03d65cda997c28422e9a41c42d79f19e89aba9 Mon Sep 17 00:00:00 2001 From: tison Date: Wed, 27 Mar 2024 19:16:22 +0800 Subject: [PATCH 108/936] chore: add Python scripting in database example (#3999) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b28376e7f5e..7d79f028dcd 100644 --- a/README.md +++ b/README.md @@ -192,6 +192,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._ +- [greptimedb](https://github.com/GreptimeTeam/greptimedb/tree/main/src/script) _Support [Python scripting](https://docs.greptime.com/user-guide/python-scripts/overview) in the database_ - [haem](https://github.com/BooleanCat/haem) _A Python library for working on Bioinformatics problems._ - [html-py-ever](https://github.com/PyO3/setuptools-rust/tree/main/examples/html-py-ever) _Using [html5ever](https://github.com/servo/html5ever) through [kuchiki](https://github.com/kuchiki-rs/kuchiki) to speed up html parsing and css-selecting._ - [hyperjson](https://github.com/mre/hyperjson) _A hyper-fast Python module for reading/writing JSON data using Rust's serde-json._ From bcfc848703bed30b4ffc225aeb440de3e79cbead Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Wed, 27 Mar 2024 11:48:56 +0000 Subject: [PATCH 109/936] docs: fix link in CHANGELOG (#4001) --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e1ccb8af4a..047a4b21e1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -77,7 +77,7 @@ To see unreleased changes, please see the [CHANGELOG on the main branch guide](h ## [0.21.0-beta.0] - 2024-03-10 -Prerelease of PyO3 0.21. See (the GitHub diff)[https://github.com/pyo3/pyo3/compare/v0.21.0-beta.0...v0.21.0] for what changed between 0.21.0-beta.0 and the final release. +Prerelease of PyO3 0.21. See [the GitHub diff](https://github.com/pyo3/pyo3/compare/v0.21.0-beta.0...v0.21.0) for what changed between 0.21.0-beta.0 and the final release. ## [0.20.3] - 2024-02-23 From dd1710256d00ac2538f2fb8068fe63254b9f469c Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Wed, 27 Mar 2024 16:41:04 +0100 Subject: [PATCH 110/936] use `extract_argument` for `#[setter]` extraction (#3998) * use `extract_argument` for `#[setter]` extraction * add newsfragment --- newsfragments/3998.changed.md | 1 + newsfragments/3998.fixed.md | 1 + pyo3-macros-backend/src/pymethod.rs | 19 ++++++++-- tests/test_getter_setter.rs | 9 +++++ tests/ui/deprecations.rs | 6 ++++ tests/ui/deprecations.stderr | 54 ++++++++++++++++------------- 6 files changed, 64 insertions(+), 26 deletions(-) create mode 100644 newsfragments/3998.changed.md create mode 100644 newsfragments/3998.fixed.md diff --git a/newsfragments/3998.changed.md b/newsfragments/3998.changed.md new file mode 100644 index 00000000000..c02c6546c95 --- /dev/null +++ b/newsfragments/3998.changed.md @@ -0,0 +1 @@ +Warn on uses of GIL Refs for `#[setter]` function arguments. \ No newline at end of file diff --git a/newsfragments/3998.fixed.md b/newsfragments/3998.fixed.md new file mode 100644 index 00000000000..11f5d006ec0 --- /dev/null +++ b/newsfragments/3998.fixed.md @@ -0,0 +1 @@ +Allow extraction of `&Bound` in `#[setter]` methods. \ No newline at end of file diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index 1e476ca22f2..22802a01177 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -586,6 +586,8 @@ pub fn impl_py_setter_def( } }; + // TODO: rework this to make use of `impl_::params::impl_arg_param` which + // handles all these cases already. let extract = if let PropertyType::Function { spec, .. } = &property_type { Some(spec) } else { @@ -609,8 +611,21 @@ pub fn impl_py_setter_def( }) }) .unwrap_or_else(|| { + let (span, name) = match &property_type { + PropertyType::Descriptor { field, .. } => (field.ty.span(), field.ident.as_ref().map(|i|i.to_string()).unwrap_or_default()), + PropertyType::Function { spec, .. } => { + let (_, args) = split_off_python_arg(&spec.signature.arguments); + (args[0].ty.span(), args[0].name.to_string()) + } + }; + + let holder = holders.push_holder(span); + let gil_refs_checker = holders.push_gil_refs_checker(span); quote! { - let _val = #pyo3_path::FromPyObject::extract_bound(_value.into())?; + let _val = #pyo3_path::impl_::deprecations::inspect_type( + #pyo3_path::impl_::extract_argument::extract_argument(_value.into(), &mut #holder, #name)?, + &#gil_refs_checker + ); } }); @@ -639,8 +654,8 @@ pub fn impl_py_setter_def( .ok_or_else(|| { #pyo3_path::exceptions::PyAttributeError::new_err("can't delete attribute") })?; - #extract #init_holders + #extract let result = #setter_impl; #check_gil_refs #pyo3_path::callback::convert(py, result) diff --git a/tests/test_getter_setter.rs b/tests/test_getter_setter.rs index 5f886639cc0..b0b15d78cd0 100644 --- a/tests/test_getter_setter.rs +++ b/tests/test_getter_setter.rs @@ -46,6 +46,12 @@ impl ClassWithProperties { self.num = value; } + #[setter] + fn set_from_any(&mut self, value: &Bound<'_, PyAny>) -> PyResult<()> { + self.num = value.extract()?; + Ok(()) + } + #[getter] fn get_data_list<'py>(&self, py: Python<'py>) -> Bound<'py, PyList> { PyList::new_bound(py, [self.num]) @@ -76,6 +82,9 @@ fn class_with_properties() { py_run!(py, inst, "inst.from_len = [0, 0, 0]"); py_run!(py, inst, "assert inst.get_num() == 3"); + py_run!(py, inst, "inst.from_any = 15"); + py_run!(py, inst, "assert inst.get_num() == 15"); + let d = [("C", py.get_type_bound::())].into_py_dict_bound(py); py_assert!(py, *d, "C.DATA.__doc__ == 'a getter for data'"); }); diff --git a/tests/ui/deprecations.rs b/tests/ui/deprecations.rs index 0ed4d09eebb..dcc9b7b1d84 100644 --- a/tests/ui/deprecations.rs +++ b/tests/ui/deprecations.rs @@ -32,6 +32,12 @@ impl MyClass { #[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 main() {} diff --git a/tests/ui/deprecations.stderr b/tests/ui/deprecations.stderr index e2d9cf36ebb..e692702f23e 100644 --- a/tests/ui/deprecations.stderr +++ b/tests/ui/deprecations.stderr @@ -41,69 +41,75 @@ error: use of deprecated method `pyo3::deprecations::GilRefs::::from_py_with_ | ^^^^^^^^^^^^^^^^^ error: use of deprecated method `pyo3::deprecations::GilRefs::::function_arg`: use `&Bound<'_, T>` instead for this function argument - --> tests/ui/deprecations.rs:47:43 + --> tests/ui/deprecations.rs:37:39 | -47 | fn pyfunction_with_module_gil_ref(module: &PyModule) -> PyResult<&str> { +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:53:43 + | +53 | 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:57:19 + --> tests/ui/deprecations.rs:63:19 | -57 | fn module_gil_ref(m: &PyModule) -> PyResult<()> { +63 | 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:63:57 + --> tests/ui/deprecations.rs:69:57 | -63 | fn module_gil_ref_with_explicit_py_arg(_py: Python<'_>, m: &PyModule) -> PyResult<()> { +69 | 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:96:27 - | -96 | #[pyo3(from_py_with = "extract_gil_ref")] _gil_ref: i32, - | ^^^^^^^^^^^^^^^^^ + --> tests/ui/deprecations.rs:102:27 + | +102 | #[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:102:29 + --> tests/ui/deprecations.rs:108:29 | -102 | fn pyfunction_gil_ref(_any: &PyAny) {} +108 | 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:105:36 + --> tests/ui/deprecations.rs:111:36 | -105 | fn pyfunction_option_gil_ref(_any: Option<&PyAny>) {} +111 | 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:112:27 + --> tests/ui/deprecations.rs:118:27 | -112 | #[pyo3(from_py_with = "PyAny::len", item("my_object"))] +118 | #[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:122:27 + --> tests/ui/deprecations.rs:128:27 | -122 | #[pyo3(from_py_with = "PyAny::len")] usize, +128 | #[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:128:31 + --> tests/ui/deprecations.rs:134:31 | -128 | Zip(#[pyo3(from_py_with = "extract_gil_ref")] i32), +134 | 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:135:27 + --> tests/ui/deprecations.rs:141:27 | -135 | #[pyo3(from_py_with = "extract_gil_ref")] +141 | #[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:148:13 + --> tests/ui/deprecations.rs:154:13 | -148 | let _ = wrap_pyfunction!(double, py); +154 | 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 b053e83c083b3fca057cf877490739a4799895ef Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 29 Mar 2024 11:28:42 +0000 Subject: [PATCH 111/936] implement `Send` and `Sync` for `PyBackedStr` and `PyBackedBytes` (#4007) --- newsfragments/4007.fixed.md | 1 + src/pybacked.rs | 34 ++++++++++++++++++++++++++++------ 2 files changed, 29 insertions(+), 6 deletions(-) create mode 100644 newsfragments/4007.fixed.md diff --git a/newsfragments/4007.fixed.md b/newsfragments/4007.fixed.md new file mode 100644 index 00000000000..ff905fb4e94 --- /dev/null +++ b/newsfragments/4007.fixed.md @@ -0,0 +1 @@ +Implement `Send` and `Sync` for `PyBackedStr` and `PyBackedBytes`. diff --git a/src/pybacked.rs b/src/pybacked.rs index dbb0865285e..01b9c2a6f00 100644 --- a/src/pybacked.rs +++ b/src/pybacked.rs @@ -16,14 +16,14 @@ use crate::{ pub struct PyBackedStr { #[allow(dead_code)] // only held so that the storage is not dropped storage: Py, - data: NonNull<[u8]>, + data: NonNull, } impl Deref for PyBackedStr { type Target = str; fn deref(&self) -> &str { - // Safety: `data` is known to be immutable utf8 string and owned by self - unsafe { std::str::from_utf8_unchecked(self.data.as_ref()) } + // Safety: `data` is known to be immutable and owned by self + unsafe { self.data.as_ref() } } } @@ -39,13 +39,18 @@ impl AsRef<[u8]> for PyBackedStr { } } +// Safety: the underlying Python str (or bytes) is immutable and +// safe to share between threads +unsafe impl Send for PyBackedStr {} +unsafe impl Sync for PyBackedStr {} + impl TryFrom> for PyBackedStr { type Error = PyErr; fn try_from(py_string: Bound<'_, PyString>) -> Result { #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] { let s = py_string.to_str()?; - let data = NonNull::from(s.as_bytes()); + let data = NonNull::from(s); Ok(Self { storage: py_string.as_any().to_owned().unbind(), data, @@ -54,8 +59,8 @@ impl TryFrom> for PyBackedStr { #[cfg(not(any(Py_3_10, not(Py_LIMITED_API))))] { let bytes = py_string.encode_utf8()?; - let b = bytes.as_bytes(); - let data = NonNull::from(b); + let s = unsafe { std::str::from_utf8_unchecked(bytes.as_bytes()) }; + let data = NonNull::from(s); Ok(Self { storage: bytes.into_any().unbind(), data, @@ -100,6 +105,11 @@ impl AsRef<[u8]> for PyBackedBytes { } } +// Safety: the underlying Python bytes or Rust bytes is immutable and +// safe to share between threads +unsafe impl Send for PyBackedBytes {} +unsafe impl Sync for PyBackedBytes {} + impl From> for PyBackedBytes { fn from(py_bytes: Bound<'_, PyBytes>) -> Self { let b = py_bytes.as_bytes(); @@ -201,4 +211,16 @@ mod test { assert_eq!(&*py_backed_bytes, b"abcde"); }); } + + #[test] + fn test_backed_types_send_sync() { + fn is_send() {} + fn is_sync() {} + + is_send::(); + is_sync::(); + + is_send::(); + is_sync::(); + } } From cf74624de9c88e51914c7d2a2dbae5c4d4a6e63d Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 29 Mar 2024 11:54:33 +0000 Subject: [PATCH 112/936] refactor to remove add_to_module functions from generated code (#4009) * refactor to remove add_to_module functions from generated code * mrsv, newsfragment * clippy --- newsfragments/4009.fixed.md | 1 + pyo3-macros-backend/src/module.rs | 14 +++---- pyo3-macros-backend/src/pyclass.rs | 8 ++-- pyo3-macros-backend/src/pyfunction.rs | 10 +---- src/impl_/pymodule.rs | 57 +++++++++++++++++++++++++-- src/macros.rs | 10 ++--- src/types/mod.rs | 13 ++---- tests/test_declarative_module.rs | 2 +- 8 files changed, 75 insertions(+), 40 deletions(-) create mode 100644 newsfragments/4009.fixed.md diff --git a/newsfragments/4009.fixed.md b/newsfragments/4009.fixed.md new file mode 100644 index 00000000000..a5d378e12ad --- /dev/null +++ b/newsfragments/4009.fixed.md @@ -0,0 +1 @@ +Fix some uncovered code blocks emitted by `#[pymodule]`, `#[pyfunction]` and `#[pyclass]` macros. diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index dc2b3bfc86b..080a279a88c 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -267,7 +267,7 @@ pub fn pymodule_module_impl(mut module: syn::ItemMod) -> Result { use #pyo3_path::impl_::pymodule::PyAddToModule; #( #(#module_items_cfg_attrs)* - #module_items::add_to_module(module)?; + #module_items::_PYO3_DEF.add_to_module(module)?; )* #pymodule_init Ok(()) @@ -358,21 +358,19 @@ fn module_initialization(options: PyModuleOptions, ident: &syn::Ident) -> TokenS let pyinit_symbol = format!("PyInit_{}", name); quote! { + #[doc(hidden)] pub const __PYO3_NAME: &'static str = concat!(stringify!(#name), "\0"); pub(super) struct MakeDef; - pub static DEF: #pyo3_path::impl_::pymodule::ModuleDef = MakeDef::make_def(); - - pub fn add_to_module(module: &#pyo3_path::Bound<'_, #pyo3_path::types::PyModule>) -> #pyo3_path::PyResult<()> { - use #pyo3_path::prelude::PyModuleMethods; - module.add_submodule(DEF.make_module(module.py())?.bind(module.py())) - } + #[doc(hidden)] + pub static _PYO3_DEF: #pyo3_path::impl_::pymodule::ModuleDef = MakeDef::make_def(); /// This autogenerated function is called by the python interpreter when importing /// the module. + #[doc(hidden)] #[export_name = #pyinit_symbol] pub unsafe extern "C" fn __pyo3_init() -> *mut #pyo3_path::ffi::PyObject { - #pyo3_path::impl_::trampoline::module_init(|py| DEF.make_module(py)) + #pyo3_path::impl_::trampoline::module_init(|py| _PYO3_DEF.make_module(py)) } } } diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 5122d205640..88cf9149e4d 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -1620,11 +1620,9 @@ impl<'a> PyClassImplsBuilder<'a> { let Ctx { pyo3_path } = ctx; let cls = self.cls; quote! { - impl #pyo3_path::impl_::pymodule::PyAddToModule for #cls { - fn add_to_module(module: &#pyo3_path::Bound<'_, #pyo3_path::types::PyModule>) -> #pyo3_path::PyResult<()> { - use #pyo3_path::types::PyModuleMethods; - module.add_class::() - } + impl #cls { + #[doc(hidden)] + const _PYO3_DEF: #pyo3_path::impl_::pymodule::AddClassToModule = #pyo3_path::impl_::pymodule::AddClassToModule::new(); } } } diff --git a/pyo3-macros-backend/src/pyfunction.rs b/pyo3-macros-backend/src/pyfunction.rs index cce9f74824c..25d2536ecb5 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -269,13 +269,7 @@ pub fn impl_wrap_pyfunction( #[doc(hidden)] #vis mod #name { pub(crate) struct MakeDef; - pub const DEF: #pyo3_path::impl_::pymethods::PyMethodDef = MakeDef::DEF; - - pub fn add_to_module(module: &#pyo3_path::Bound<'_, #pyo3_path::types::PyModule>) -> #pyo3_path::PyResult<()> { - use #pyo3_path::prelude::PyModuleMethods; - use ::std::convert::Into; - module.add_function(#pyo3_path::types::PyCFunction::internal_new(module.py(), &DEF, module.into())?) - } + pub const _PYO3_DEF: #pyo3_path::impl_::pymethods::PyMethodDef = MakeDef::_PYO3_DEF; } // Generate the definition inside an anonymous function in the same scope as the original function - @@ -283,7 +277,7 @@ pub fn impl_wrap_pyfunction( // (and `super` doesn't always refer to the outer scope, e.g. if the `#[pyfunction] is // inside a function body) impl #name::MakeDef { - const DEF: #pyo3_path::impl_::pymethods::PyMethodDef = #methoddef; + const _PYO3_DEF: #pyo3_path::impl_::pymethods::PyMethodDef = #methoddef; } #[allow(non_snake_case)] diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index ba13f7a9841..20560aeb8d5 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; +use std::{cell::UnsafeCell, marker::PhantomData}; #[cfg(all( not(any(PyPy, GraalPy)), @@ -11,7 +11,12 @@ use portable_atomic::{AtomicI64, Ordering}; #[cfg(not(any(PyPy, GraalPy)))] use crate::exceptions::PyImportError; -use crate::{ffi, sync::GILOnceCell, types::PyModule, Bound, Py, PyResult, Python}; +use crate::{ + ffi, + sync::GILOnceCell, + types::{PyCFunction, PyModule, PyModuleMethods}, + Bound, Py, PyClass, PyMethodDef, PyResult, PyTypeInfo, Python, +}; /// `Sync` wrapper of `ffi::PyModuleDef`. pub struct ModuleDef { @@ -149,7 +154,53 @@ impl ModuleDef { /// /// Currently only implemented for classes. pub trait PyAddToModule { - fn add_to_module(module: &Bound<'_, PyModule>) -> PyResult<()>; + fn add_to_module(&'static self, module: &Bound<'_, PyModule>) -> PyResult<()>; +} + +/// For adding native types (non-pyclass) to a module. +pub struct AddTypeToModule(PhantomData); + +impl AddTypeToModule { + #[allow(clippy::new_without_default)] + pub const fn new() -> Self { + AddTypeToModule(PhantomData) + } +} + +impl PyAddToModule for AddTypeToModule { + fn add_to_module(&'static self, module: &Bound<'_, PyModule>) -> PyResult<()> { + module.add(T::NAME, T::type_object_bound(module.py())) + } +} + +/// For adding a class to a module. +pub struct AddClassToModule(PhantomData); + +impl AddClassToModule { + #[allow(clippy::new_without_default)] + pub const fn new() -> Self { + AddClassToModule(PhantomData) + } +} + +impl PyAddToModule for AddClassToModule { + fn add_to_module(&'static self, module: &Bound<'_, PyModule>) -> PyResult<()> { + module.add_class::() + } +} + +/// For adding a function to a module. +impl PyAddToModule for PyMethodDef { + fn add_to_module(&'static self, module: &Bound<'_, PyModule>) -> PyResult<()> { + module.add_function(PyCFunction::internal_new(module.py(), self, Some(module))?) + } +} + +/// For adding a module to a module. +impl PyAddToModule for ModuleDef { + fn add_to_module(&'static self, module: &Bound<'_, PyModule>) -> PyResult<()> { + module.add_submodule(self.make_module(module.py())?.bind(module.py())) + } } #[cfg(test)] diff --git a/src/macros.rs b/src/macros.rs index 09e2acfb45b..0267c2663eb 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -138,7 +138,7 @@ macro_rules! wrap_pyfunction { use $function as wrapped_pyfunction; $crate::impl_::pyfunction::WrapPyFunctionArg::wrap_pyfunction( py_or_module, - &wrapped_pyfunction::DEF, + &wrapped_pyfunction::_PYO3_DEF, ) } }; @@ -150,7 +150,7 @@ macro_rules! wrap_pyfunction { check_gil_refs.is_python(); $crate::impl_::pyfunction::WrapPyFunctionArg::wrap_pyfunction( py_or_module, - &wrapped_pyfunction::DEF, + &wrapped_pyfunction::_PYO3_DEF, ) }}; } @@ -166,7 +166,7 @@ macro_rules! wrap_pyfunction_bound { use $function as wrapped_pyfunction; $crate::impl_::pyfunction::WrapPyFunctionArg::wrap_pyfunction( $crate::impl_::pyfunction::OnlyBound(py_or_module), - &wrapped_pyfunction::DEF, + &wrapped_pyfunction::_PYO3_DEF, ) } }; @@ -174,7 +174,7 @@ macro_rules! wrap_pyfunction_bound { use $function as wrapped_pyfunction; $crate::impl_::pyfunction::WrapPyFunctionArg::wrap_pyfunction( $crate::impl_::pyfunction::OnlyBound($py_or_module), - &wrapped_pyfunction::DEF, + &wrapped_pyfunction::_PYO3_DEF, ) }}; } @@ -189,7 +189,7 @@ macro_rules! wrap_pymodule { ($module:path) => { &|py| { use $module as wrapped_pymodule; - wrapped_pymodule::DEF + wrapped_pymodule::_PYO3_DEF .make_module(py) .expect("failed to wrap pymodule") } diff --git a/src/types/mod.rs b/src/types/mod.rs index 5448999d4c7..a03d01b301a 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -250,16 +250,9 @@ macro_rules! pyobject_native_type_info( )? } - impl<$($generics,)*> $crate::impl_::pymodule::PyAddToModule for $name { - fn add_to_module( - module: &$crate::Bound<'_, $crate::types::PyModule>, - ) -> $crate::PyResult<()> { - use $crate::types::PyModuleMethods; - module.add( - ::NAME, - ::type_object_bound(module.py()), - ) - } + impl $name { + #[doc(hidden)] + 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 9eea8e2df07..5f8e2c9fa55 100644 --- a/tests/test_declarative_module.rs +++ b/tests/test_declarative_module.rs @@ -145,7 +145,7 @@ mod class_initialization_module { #[cfg(not(Py_LIMITED_API))] fn test_class_initialization_fails() { Python::with_gil(|py| { - let err = class_initialization_module::DEF + let err = class_initialization_module::_PYO3_DEF .make_module(py) .unwrap_err(); assert_eq!( From 60e3f44dcf0396d43ebd3cea60aba483db72cc81 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Fri, 29 Mar 2024 10:16:45 -0400 Subject: [PATCH 113/936] Ensure all arguments to _run are strings (#4013) Otherwise you get errors like https://github.com/PyO3/pyo3/actions/runs/8480723060/job/23236947935?pr=4012 --- noxfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index 3ee76da5f08..3d82c8d3746 100644 --- a/noxfile.py +++ b/noxfile.py @@ -406,7 +406,7 @@ def check_guide(session: nox.Session): session, "lychee", "--include-fragments", - PYO3_GUIDE_SRC, + str(PYO3_GUIDE_SRC), *remap_args, *session.posargs, ) From ae3ac7d87289da5c4456953aa1edf355a0b5b65e Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Fri, 29 Mar 2024 11:49:50 -0400 Subject: [PATCH 114/936] Fixed error string when failing to import an exception (#4012) --- newsfragments/4012.fixed.md | 1 + src/exceptions.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 newsfragments/4012.fixed.md diff --git a/newsfragments/4012.fixed.md b/newsfragments/4012.fixed.md new file mode 100644 index 00000000000..352ec928487 --- /dev/null +++ b/newsfragments/4012.fixed.md @@ -0,0 +1 @@ +Fixed the error message when a class referenced in `pyo3::import_exception!` does not exist diff --git a/src/exceptions.rs b/src/exceptions.rs index add958257ab..66b5b57dc0e 100644 --- a/src/exceptions.rs +++ b/src/exceptions.rs @@ -123,7 +123,7 @@ macro_rules! import_exception { ::std::panic!("Can not import module {}: {}\n{}", stringify!($module), err, traceback); }); let cls = imp.getattr(stringify!($name)).expect(concat!( - "Can not load exception class: {}.{}", + "Can not load exception class: ", stringify!($module), ".", stringify!($name) From c6f25e42a285c22a29c82b8da1ff8d365f9db632 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 29 Mar 2024 15:51:38 +0000 Subject: [PATCH 115/936] ci: relax check in pyobject_drop test (#4014) --- src/gil.rs | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/gil.rs b/src/gil.rs index bb07489fa1f..e2f36037755 100644 --- a/src/gil.rs +++ b/src/gil.rs @@ -556,11 +556,11 @@ mod tests { } #[cfg(not(target_arch = "wasm32"))] - fn pool_dirty_with( - inc_refs: Vec>, - dec_refs: Vec>, - ) -> bool { - *POOL.pointer_ops.lock() == (inc_refs, dec_refs) + fn pool_dec_refs_contains(obj: &PyObject) -> bool { + POOL.pointer_ops + .lock() + .1 + .contains(&unsafe { NonNull::new_unchecked(obj.as_ptr()) }) } #[test] @@ -633,11 +633,13 @@ mod tests { assert_eq!(obj.get_refcnt(py), 2); assert!(pool_inc_refs_does_not_contain(&obj)); + assert!(pool_dec_refs_does_not_contain(&obj)); - // With the GIL held, reference cound will be decreased immediately. + // With the GIL held, reference count will be decreased immediately. drop(reference); assert_eq!(obj.get_refcnt(py), 1); + assert!(pool_inc_refs_does_not_contain(&obj)); assert!(pool_dec_refs_does_not_contain(&obj)); }); } @@ -652,6 +654,7 @@ mod tests { assert_eq!(obj.get_refcnt(py), 2); assert!(pool_inc_refs_does_not_contain(&obj)); + assert!(pool_dec_refs_does_not_contain(&obj)); // Drop reference in a separate thread which doesn't have the GIL. std::thread::spawn(move || drop(reference)).join().unwrap(); @@ -659,10 +662,8 @@ mod tests { // The reference count should not have changed (the GIL has always // been held by this thread), it is remembered to release later. assert_eq!(obj.get_refcnt(py), 2); - assert!(pool_dirty_with( - vec![], - vec![NonNull::new(obj.as_ptr()).unwrap()] - )); + assert!(pool_inc_refs_does_not_contain(&obj)); + assert!(pool_dec_refs_contains(&obj)); obj }); From de03ca270adcabfe0881595879da09b7762527d6 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Fri, 29 Mar 2024 17:28:08 -0400 Subject: [PATCH 116/936] Remove dead code in macros backend (#4011) --- pyo3-macros-backend/src/method.rs | 10 ---------- pyo3-macros-backend/src/pyclass.rs | 12 ++---------- pyo3-macros-backend/src/pyfunction.rs | 3 --- 3 files changed, 2 insertions(+), 23 deletions(-) diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index 1a46d333c3b..31dfb075958 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -269,7 +269,6 @@ pub struct FnSpec<'a> { // r# can be removed by syn::ext::IdentExt::unraw() pub python_name: syn::Ident, pub signature: FunctionSignature<'a>, - pub output: syn::Type, pub convention: CallingConvention, pub text_signature: Option, pub asyncness: Option, @@ -277,13 +276,6 @@ pub struct FnSpec<'a> { pub deprecations: Deprecations<'a>, } -pub fn get_return_info(output: &syn::ReturnType) -> syn::Type { - match output { - syn::ReturnType::Default => syn::Type::Infer(syn::parse_quote! {_}), - syn::ReturnType::Type(_, ty) => *ty.clone(), - } -} - pub fn parse_method_receiver(arg: &syn::FnArg) -> Result { match arg { syn::FnArg::Receiver( @@ -329,7 +321,6 @@ impl<'a> FnSpec<'a> { ensure_signatures_on_valid_method(&fn_type, signature.as_ref(), text_signature.as_ref())?; let name = &sig.ident; - let ty = get_return_info(&sig.output); let python_name = python_name.as_ref().unwrap_or(name).unraw(); let arguments: Vec<_> = sig @@ -361,7 +352,6 @@ impl<'a> FnSpec<'a> { convention, python_name, signature, - output: ty, text_signature, asyncness: sig.asyncness, unsafety: sig.unsafety, diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 88cf9149e4d..cf8d9aae801 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -975,13 +975,8 @@ fn impl_complex_enum_struct_variant_cls( let field_type = field.ty; let field_with_type = quote! { #field_name: #field_type }; - let field_getter = complex_enum_variant_field_getter( - &variant_cls_type, - field_name, - field_type, - field.span, - ctx, - )?; + let field_getter = + complex_enum_variant_field_getter(&variant_cls_type, field_name, field.span, ctx)?; let field_getter_impl = quote! { fn #field_name(slf: #pyo3_path::PyRef) -> #pyo3_path::PyResult<#field_type> { @@ -1188,7 +1183,6 @@ fn complex_enum_struct_variant_new<'a>( name: &format_ident!("__pymethod_constructor__"), python_name: format_ident!("__new__"), signature, - output: variant_cls_type.clone(), convention: crate::method::CallingConvention::TpNew, text_signature: None, asyncness: None, @@ -1202,7 +1196,6 @@ fn complex_enum_struct_variant_new<'a>( fn complex_enum_variant_field_getter<'a>( variant_cls_type: &'a syn::Type, field_name: &'a syn::Ident, - field_type: &'a syn::Type, field_span: Span, ctx: &Ctx, ) -> Result { @@ -1215,7 +1208,6 @@ fn complex_enum_variant_field_getter<'a>( name: field_name, python_name: field_name.clone(), signature, - output: field_type.clone(), convention: crate::method::CallingConvention::Noargs, text_signature: None, asyncness: None, diff --git a/pyo3-macros-backend/src/pyfunction.rs b/pyo3-macros-backend/src/pyfunction.rs index 25d2536ecb5..9f9557a3664 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -240,15 +240,12 @@ pub fn impl_wrap_pyfunction( FunctionSignature::from_arguments(arguments)? }; - let ty = method::get_return_info(&func.sig.output); - let spec = method::FnSpec { tp, name: &func.sig.ident, convention: CallingConvention::from_signature(&signature), python_name, signature, - output: ty, text_signature, asyncness: func.sig.asyncness, unsafety: func.sig.unsafety, From 0e093a5911f0dd4e6e919c306359e41f9b98ccaf Mon Sep 17 00:00:00 2001 From: geo7 Date: Fri, 29 Mar 2024 22:45:47 +0000 Subject: [PATCH 117/936] Update getting-started.md (#4004) Missing word --- guide/src/getting-started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guide/src/getting-started.md b/guide/src/getting-started.md index ef9ca5254b8..59cf5ba6ded 100644 --- a/guide/src/getting-started.md +++ b/guide/src/getting-started.md @@ -33,7 +33,7 @@ You can read more about `pyenv`'s configuration options [here](https://github.co ### 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 you already install Python packages. +There are a number of build and Python package management systems such as [`setuptools-rust`](https://github.com/PyO3/setuptools-rust) or [manually](./building-and-distribution.md#manual-builds). We recommend the use of `maturin`, which you can install [here](https://maturin.rs/installation.html). It is developed to work with PyO3 and provides the most "batteries included" experience, especially if you are aiming to publish to PyPI. `maturin` is just a Python package, so you can add it in the same way you already install Python packages. System Python: ```bash From 4d033c4497029c12a8788c28f9ffba6fb18234f4 Mon Sep 17 00:00:00 2001 From: Rikus Honey Date: Sat, 30 Mar 2024 09:39:00 +0200 Subject: [PATCH 118/936] Fix broken hyperlink to types.md (#4018) --- guide/src/types.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/guide/src/types.md b/guide/src/types.md index 0f1fa3d0af1..a402e52090a 100644 --- a/guide/src/types.md +++ b/guide/src/types.md @@ -268,8 +268,8 @@ assert_eq!((x, y, z), (1, 2, 3)); # Python::with_gil(example).unwrap() ``` -To avoid copying data, [`#[pyclass]`][pyclass] types can directly reference Rust data stored within the Python objects without needing to `.extract()`. See the [corresponding documentation in the class section of the guide](./class. -md#bound-and-interior-mutability) for more detail. +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 From 74d9d23ba0d6d0866650bc105e9611bc8ca6d03b Mon Sep 17 00:00:00 2001 From: Weijie Guo Date: Sat, 30 Mar 2024 16:48:19 +0800 Subject: [PATCH 119/936] async method should allow args not only receiver (#4015) * async method should allow args not only receiver * add changelog md --- newsfragments/4015.fixed.md | 1 + pyo3-macros-backend/src/method.rs | 22 ++++++++++++++++++--- tests/test_coroutine.rs | 33 +++++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 newsfragments/4015.fixed.md diff --git a/newsfragments/4015.fixed.md b/newsfragments/4015.fixed.md new file mode 100644 index 00000000000..a8f4f636c76 --- /dev/null +++ b/newsfragments/4015.fixed.md @@ -0,0 +1 @@ +Fix the bug that an async `#[pymethod]` with receiver can't have any other args. \ No newline at end of file diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index 31dfb075958..f4fdb19377a 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -1,7 +1,7 @@ use std::fmt::Display; use proc_macro2::{Span, TokenStream}; -use quote::{quote, quote_spanned, ToTokens}; +use quote::{format_ident, quote, quote_spanned, ToTokens}; use syn::{ext::IdentExt, spanned::Spanned, Ident, Result}; use crate::utils::Ctx; @@ -518,17 +518,33 @@ 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 future = match self.tp { FnType::Fn(SelfType::Receiver { mutable: false, .. }) => { + let (arg_name, evaluate_arg) = evaluate_args(); quote! {{ + #evaluate_arg; let __guard = #pyo3_path::impl_::coroutine::RefGuard::<#cls>::new(&#pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(py, &_slf))?; - async move { function(&__guard, #(#args),*).await } + async move { function(&__guard, #(#arg_name),*).await } }} } FnType::Fn(SelfType::Receiver { mutable: true, .. }) => { + let (arg_name, evaluate_arg) = evaluate_args(); quote! {{ + #evaluate_arg; 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, #(#args),*).await } + async move { function(&mut __guard, #(#arg_name),*).await } }} } _ => { diff --git a/tests/test_coroutine.rs b/tests/test_coroutine.rs index 23f6a6722d4..0e698deb9af 100644 --- a/tests/test_coroutine.rs +++ b/tests/test_coroutine.rs @@ -245,6 +245,39 @@ 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] From 22e8dd10e1667c46c258c56a7aeb8ecf2e4fad30 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sat, 30 Mar 2024 20:29:39 +0000 Subject: [PATCH 120/936] add benchmark for class / method calls (#4016) --- pytests/src/pyclasses.rs | 6 ++++++ pytests/tests/test_pyclasses.py | 26 +++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/pytests/src/pyclasses.rs b/pytests/src/pyclasses.rs index 8e957c77955..ac817627cfe 100644 --- a/pytests/src/pyclasses.rs +++ b/pytests/src/pyclasses.rs @@ -11,6 +11,12 @@ impl EmptyClass { fn new() -> Self { EmptyClass {} } + + fn method(&self) {} + + fn __len__(&self) -> usize { + 0 + } } /// This is for demonstrating how to return a value from __next__ diff --git a/pytests/tests/test_pyclasses.py b/pytests/tests/test_pyclasses.py index 9a9b44b52e8..74f883e3808 100644 --- a/pytests/tests/test_pyclasses.py +++ b/pytests/tests/test_pyclasses.py @@ -8,14 +8,38 @@ def test_empty_class_init(benchmark): benchmark(pyclasses.EmptyClass) +def test_method_call(benchmark): + obj = pyclasses.EmptyClass() + assert benchmark(obj.method) is None + + +def test_proto_call(benchmark): + obj = pyclasses.EmptyClass() + assert benchmark(len, obj) == 0 + + class EmptyClassPy: - pass + def method(self): + pass + + def __len__(self) -> int: + return 0 def test_empty_class_init_py(benchmark): benchmark(EmptyClassPy) +def test_method_call_py(benchmark): + obj = EmptyClassPy() + assert benchmark(obj.method) == pyclasses.EmptyClass().method() + + +def test_proto_call_py(benchmark): + obj = EmptyClassPy() + assert benchmark(len, obj) == len(pyclasses.EmptyClass()) + + def test_iter(): i = pyclasses.PyClassIter() assert next(i) == 1 From 9d932c1061c06000c9424c6542f7c3f266d32836 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sat, 30 Mar 2024 22:10:21 +0000 Subject: [PATCH 121/936] add `#[inline]` hints on many `Bound` and `Borrowed` methods (#4024) --- newsfragments/4024.changed.md | 1 + src/instance.rs | 25 +++++++++++++++++++++++++ src/pycell.rs | 1 + 3 files changed, 27 insertions(+) create mode 100644 newsfragments/4024.changed.md diff --git a/newsfragments/4024.changed.md b/newsfragments/4024.changed.md new file mode 100644 index 00000000000..65afd8f0d0d --- /dev/null +++ b/newsfragments/4024.changed.md @@ -0,0 +1 @@ +Add `#[inline]` hints on many `Bound` and `Borrowed` methods. diff --git a/src/instance.rs b/src/instance.rs index 5f054d0d2d4..88a550ffd30 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -103,6 +103,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] pub unsafe fn from_owned_ptr(py: Python<'py>, ptr: *mut ffi::PyObject) -> Self { Self(py, ManuallyDrop::new(Py::from_owned_ptr(py, ptr))) } @@ -113,6 +114,7 @@ impl<'py> Bound<'py, PyAny> { /// /// - `ptr` must be a valid pointer to a Python object, or null /// - `ptr` must be an owned Python reference, as the `Bound<'py, PyAny>` will assume ownership + #[inline] pub unsafe fn from_owned_ptr_or_opt(py: Python<'py>, ptr: *mut ffi::PyObject) -> Option { Py::from_owned_ptr_or_opt(py, ptr).map(|obj| Self(py, ManuallyDrop::new(obj))) } @@ -124,6 +126,7 @@ impl<'py> Bound<'py, PyAny> { /// /// - `ptr` must be a valid pointer to a Python object, or null /// - `ptr` must be an owned Python reference, as the `Bound<'py, PyAny>` will assume ownership + #[inline] pub unsafe fn from_owned_ptr_or_err( py: Python<'py>, ptr: *mut ffi::PyObject, @@ -137,6 +140,7 @@ impl<'py> Bound<'py, PyAny> { /// # Safety /// /// - `ptr` must be a valid pointer to a Python object + #[inline] pub unsafe fn from_borrowed_ptr(py: Python<'py>, ptr: *mut ffi::PyObject) -> Self { Self(py, ManuallyDrop::new(Py::from_borrowed_ptr(py, ptr))) } @@ -147,6 +151,7 @@ impl<'py> Bound<'py, PyAny> { /// # Safety /// /// - `ptr` must be a valid pointer to a Python object, or null + #[inline] pub unsafe fn from_borrowed_ptr_or_opt( py: Python<'py>, ptr: *mut ffi::PyObject, @@ -160,6 +165,7 @@ impl<'py> Bound<'py, PyAny> { /// # Safety /// /// - `ptr` must be a valid pointer to a Python object, or null + #[inline] pub unsafe fn from_borrowed_ptr_or_err( py: Python<'py>, ptr: *mut ffi::PyObject, @@ -235,6 +241,7 @@ where /// /// Panics if the value is currently mutably borrowed. For a non-panicking variant, use /// [`try_borrow`](#method.try_borrow). + #[inline] pub fn borrow(&self) -> PyRef<'py, T> { PyRef::borrow(self) } @@ -268,6 +275,7 @@ where /// # Panics /// Panics if the value is currently borrowed. For a non-panicking variant, use /// [`try_borrow_mut`](#method.try_borrow_mut). + #[inline] pub fn borrow_mut(&self) -> PyRefMut<'py, T> where T: PyClass, @@ -282,6 +290,7 @@ where /// This is the non-panicking variant of [`borrow`](#method.borrow). /// /// For frozen classes, the simpler [`get`][Self::get] is available. + #[inline] pub fn try_borrow(&self) -> Result, PyBorrowError> { PyRef::try_borrow(self) } @@ -291,6 +300,7 @@ where /// The borrow lasts while the returned [`PyRefMut`] exists. /// /// This is the non-panicking variant of [`borrow_mut`](#method.borrow_mut). + #[inline] pub fn try_borrow_mut(&self) -> Result, PyBorrowMutError> where T: PyClass, @@ -321,6 +331,7 @@ where /// py_counter.get().value.fetch_add(1, Ordering::Relaxed); /// }); /// ``` + #[inline] pub fn get(&self) -> &T where T: PyClass + Sync, @@ -328,6 +339,7 @@ where self.1.get() } + #[inline] pub(crate) fn get_class_object(&self) -> &PyClassObject { self.1.get_class_object() } @@ -503,6 +515,7 @@ impl<'py, T> Bound<'py, T> { } unsafe impl AsPyPointer for Bound<'_, T> { + #[inline] fn as_ptr(&self) -> *mut ffi::PyObject { self.1.as_ptr() } @@ -559,6 +572,7 @@ impl<'a, 'py> Borrowed<'a, 'py, PyAny> { /// - similar to `std::slice::from_raw_parts`, the lifetime `'a` is completely defined by /// the caller and it is the caller's responsibility to ensure that the reference this is /// derived from is valid for the lifetime `'a`. + #[inline] pub unsafe fn from_ptr(py: Python<'py>, ptr: *mut ffi::PyObject) -> Self { Self( NonNull::new(ptr).unwrap_or_else(|| crate::err::panic_after_error(py)), @@ -578,6 +592,7 @@ impl<'a, 'py> Borrowed<'a, 'py, PyAny> { /// - similar to `std::slice::from_raw_parts`, the lifetime `'a` is completely defined by /// the caller and it is the caller's responsibility to ensure that the reference this is /// derived from is valid for the lifetime `'a`. + #[inline] pub unsafe fn from_ptr_or_opt(py: Python<'py>, ptr: *mut ffi::PyObject) -> Option { NonNull::new(ptr).map(|ptr| Self(ptr, PhantomData, py)) } @@ -594,6 +609,7 @@ impl<'a, 'py> Borrowed<'a, 'py, PyAny> { /// - similar to `std::slice::from_raw_parts`, the lifetime `'a` is completely defined by /// the caller and it is the caller's responsibility to ensure that the reference this is /// derived from is valid for the lifetime `'a`. + #[inline] pub unsafe fn from_ptr_or_err(py: Python<'py>, ptr: *mut ffi::PyObject) -> PyResult { NonNull::new(ptr).map_or_else( || Err(PyErr::fetch(py)), @@ -605,6 +621,7 @@ impl<'a, 'py> Borrowed<'a, 'py, PyAny> { /// This is similar to `std::slice::from_raw_parts`, the lifetime `'a` is completely defined by /// the caller and it's the caller's responsibility to ensure that the reference this is /// derived from is valid for the lifetime `'a`. + #[inline] pub(crate) unsafe fn from_ptr_unchecked(py: Python<'py>, ptr: *mut ffi::PyObject) -> Self { Self(NonNull::new_unchecked(ptr), PhantomData, py) } @@ -627,6 +644,7 @@ impl<'a, 'py> Borrowed<'a, 'py, PyAny> { /// /// # Safety /// Callers must ensure that the type is valid or risk type confusion. + #[inline] pub(crate) unsafe fn downcast_unchecked(self) -> Borrowed<'a, 'py, T> { Borrowed(self.0, PhantomData, self.2) } @@ -634,6 +652,7 @@ impl<'a, 'py> Borrowed<'a, 'py, PyAny> { impl<'a, 'py, T> From<&'a Bound<'py, T>> for Borrowed<'a, 'py, T> { /// Create borrow on a Bound + #[inline] fn from(instance: &'a Bound<'py, T>) -> Self { instance.as_borrowed() } @@ -1118,6 +1137,7 @@ where /// /// Panics if the value is currently mutably borrowed. For a non-panicking variant, use /// [`try_borrow`](#method.try_borrow). + #[inline] pub fn borrow<'py>(&'py self, py: Python<'py>) -> PyRef<'py, T> { self.bind(py).borrow() } @@ -1154,6 +1174,7 @@ where /// # Panics /// Panics if the value is currently borrowed. For a non-panicking variant, use /// [`try_borrow_mut`](#method.try_borrow_mut). + #[inline] pub fn borrow_mut<'py>(&'py self, py: Python<'py>) -> PyRefMut<'py, T> where T: PyClass, @@ -1171,6 +1192,7 @@ where /// /// Equivalent to `self.as_ref(py).borrow_mut()` - /// see [`PyCell::try_borrow`](crate::pycell::PyCell::try_borrow). + #[inline] pub fn try_borrow<'py>(&'py self, py: Python<'py>) -> Result, PyBorrowError> { self.bind(py).try_borrow() } @@ -1183,6 +1205,7 @@ where /// /// Equivalent to `self.as_ref(py).try_borrow_mut()` - /// see [`PyCell::try_borrow_mut`](crate::pycell::PyCell::try_borrow_mut). + #[inline] pub fn try_borrow_mut<'py>( &'py self, py: Python<'py>, @@ -1216,6 +1239,7 @@ where /// /// cell.get().value.fetch_add(1, Ordering::Relaxed); /// ``` + #[inline] pub fn get(&self) -> &T where T: PyClass + Sync, @@ -1225,6 +1249,7 @@ where } /// Get a view on the underlying `PyClass` contents. + #[inline] pub(crate) fn get_class_object(&self) -> &PyClassObject { let class_object = self.as_ptr().cast::>(); // Safety: Bound is known to contain an object which is laid out in memory as a diff --git a/src/pycell.rs b/src/pycell.rs index ccc55a756d6..a649ad02412 100644 --- a/src/pycell.rs +++ b/src/pycell.rs @@ -847,6 +847,7 @@ impl<'py, T: PyClass> PyRefMut<'py, T> { self.inner.clone().into_ptr() } + #[inline] pub(crate) fn borrow(obj: &Bound<'py, T>) -> Self { Self::try_borrow(obj).expect("Already borrowed") } From cff4aa3cc81aa14cfee3fa45c0faea191f38be7e Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sat, 30 Mar 2024 23:26:34 +0000 Subject: [PATCH 122/936] ci: defer test-debug to the merge queue (#4023) --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 63f6c31a599..12db5867f90 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -439,6 +439,7 @@ jobs: run: nox -s test-emscripten test-debug: + if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || github.event_name != 'pull_request' }} needs: [fmt] runs-on: ubuntu-latest steps: From 3af9a1f4e06216ad7b14a401d3cfcdc66eed12d7 Mon Sep 17 00:00:00 2001 From: Weijie Guo Date: Sun, 31 Mar 2024 16:03:38 +0800 Subject: [PATCH 123/936] docs: fix example in types.md (#4028) --- 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 a402e52090a..cd5e1052920 100644 --- a/guide/src/types.md +++ b/guide/src/types.md @@ -78,7 +78,7 @@ Or, without the type annotations: use pyo3::prelude::*; use pyo3::types::PyList; -# fn example(py: Python<'_>) -> PyResult<()> { +fn example(py: Python<'_>) -> PyResult<()> { let x = PyList::empty_bound(py); x.append(1)?; let y = x.clone(); From 336b1c982b081aff45468ac64eac374cd79f3683 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sun, 31 Mar 2024 20:55:31 +0100 Subject: [PATCH 124/936] add `import_exception_bound!` macro (#4027) * add `import_exception_bound!` macro * newsfragment and tidy up --- newsfragments/4027.added.md | 1 + src/exceptions.rs | 113 +++++++++++++++++++++++------------- src/impl_.rs | 1 + src/impl_/exceptions.rs | 28 +++++++++ src/sync.rs | 8 ++- 5 files changed, 108 insertions(+), 43 deletions(-) create mode 100644 newsfragments/4027.added.md create mode 100644 src/impl_/exceptions.rs diff --git a/newsfragments/4027.added.md b/newsfragments/4027.added.md new file mode 100644 index 00000000000..ccf32952da2 --- /dev/null +++ b/newsfragments/4027.added.md @@ -0,0 +1 @@ +Add `import_exception_bound!` macro to import exception types without generating GIL Ref functionality for them. diff --git a/src/exceptions.rs b/src/exceptions.rs index 66b5b57dc0e..c650d7af079 100644 --- a/src/exceptions.rs +++ b/src/exceptions.rs @@ -29,18 +29,7 @@ macro_rules! impl_exception_boilerplate { } } - impl $name { - /// Creates a new [`PyErr`] of this type. - /// - /// [`PyErr`]: https://docs.rs/pyo3/latest/pyo3/struct.PyErr.html "PyErr in pyo3" - #[inline] - pub fn new_err(args: A) -> $crate::PyErr - where - A: $crate::PyErrArguments + ::std::marker::Send + ::std::marker::Sync + 'static, - { - $crate::PyErr::new::<$name, A>(args) - } - } + $crate::impl_exception_boilerplate_bound!($name); impl ::std::error::Error for $name { fn source(&self) -> ::std::option::Option<&(dyn ::std::error::Error + 'static)> { @@ -59,6 +48,25 @@ macro_rules! impl_exception_boilerplate { }; } +#[doc(hidden)] +#[macro_export] +macro_rules! impl_exception_boilerplate_bound { + ($name: ident) => { + impl $name { + /// Creates a new [`PyErr`] of this type. + /// + /// [`PyErr`]: https://docs.rs/pyo3/latest/pyo3/struct.PyErr.html "PyErr in pyo3" + #[inline] + pub fn new_err(args: A) -> $crate::PyErr + where + A: $crate::PyErrArguments + ::std::marker::Send + ::std::marker::Sync + 'static, + { + $crate::PyErr::new::<$name, A>(args) + } + } + }; +} + /// Defines a Rust type for an exception defined in Python code. /// /// # Syntax @@ -105,34 +113,57 @@ macro_rules! import_exception { impl $name { fn type_object_raw(py: $crate::Python<'_>) -> *mut $crate::ffi::PyTypeObject { - use $crate::sync::GILOnceCell; - use $crate::prelude::PyTracebackMethods; - use $crate::prelude::PyAnyMethods; - static TYPE_OBJECT: GILOnceCell<$crate::Py<$crate::types::PyType>> = - GILOnceCell::new(); + use $crate::types::PyTypeMethods; + static TYPE_OBJECT: $crate::impl_::exceptions::ImportedExceptionTypeObject = + $crate::impl_::exceptions::ImportedExceptionTypeObject::new(stringify!($module), stringify!($name)); + TYPE_OBJECT.get(py).as_type_ptr() + } + } + }; +} - TYPE_OBJECT - .get_or_init(py, || { - let imp = py - .import_bound(stringify!($module)) - .unwrap_or_else(|err| { - let traceback = err - .traceback_bound(py) - .map(|tb| tb.format().expect("raised exception will have a traceback")) - .unwrap_or_default(); - ::std::panic!("Can not import module {}: {}\n{}", stringify!($module), err, traceback); - }); - let cls = imp.getattr(stringify!($name)).expect(concat!( - "Can not load exception class: ", - stringify!($module), - ".", - stringify!($name) - )); - - cls.extract() - .expect("Imported exception should be a type object") - }) - .as_ptr() as *mut _ +/// Variant of [`import_exception`](crate::import_exception) that does not emit code needed to +/// use the imported exception type as a GIL Ref. +/// +/// This is useful only during migration as a way to avoid generating needless code. +#[macro_export] +macro_rules! import_exception_bound { + ($module: expr, $name: ident) => { + /// A Rust type representing an exception defined in Python code. + /// + /// This type was created by the [`pyo3::import_exception_bound!`] macro - see its documentation + /// for more information. + /// + /// [`pyo3::import_exception_bound!`]: https://docs.rs/pyo3/latest/pyo3/macro.import_exception.html "import_exception in pyo3" + #[repr(transparent)] + #[allow(non_camel_case_types)] // E.g. `socket.herror` + pub struct $name($crate::PyAny); + + $crate::impl_exception_boilerplate_bound!($name); + + // FIXME remove this: was necessary while `PyTypeInfo` requires `HasPyGilRef`, + // should change in 0.22. + unsafe impl $crate::type_object::HasPyGilRef for $name { + type AsRefTarget = $crate::PyAny; + } + + $crate::pyobject_native_type_info!( + $name, + $name::type_object_raw, + ::std::option::Option::Some(stringify!($module)) + ); + + impl $crate::types::DerefToPyAny for $name {} + + impl $name { + fn type_object_raw(py: $crate::Python<'_>) -> *mut $crate::ffi::PyTypeObject { + use $crate::types::PyTypeMethods; + static TYPE_OBJECT: $crate::impl_::exceptions::ImportedExceptionTypeObject = + $crate::impl_::exceptions::ImportedExceptionTypeObject::new( + stringify!($module), + stringify!($name), + ); + TYPE_OBJECT.get(py).as_type_ptr() } } }; @@ -849,8 +880,8 @@ mod tests { use crate::types::{IntoPyDict, PyDict}; use crate::{PyErr, PyNativeType}; - import_exception!(socket, gaierror); - import_exception!(email.errors, MessageError); + import_exception_bound!(socket, gaierror); + import_exception_bound!(email.errors, MessageError); #[test] fn test_check_exception() { diff --git a/src/impl_.rs b/src/impl_.rs index ea71b257c0e..71ba397cb94 100644 --- a/src/impl_.rs +++ b/src/impl_.rs @@ -9,6 +9,7 @@ #[cfg(feature = "experimental-async")] pub mod coroutine; pub mod deprecations; +pub mod exceptions; pub mod extract_argument; pub mod freelist; pub mod frompyobject; diff --git a/src/impl_/exceptions.rs b/src/impl_/exceptions.rs new file mode 100644 index 00000000000..eafac1edfa2 --- /dev/null +++ b/src/impl_/exceptions.rs @@ -0,0 +1,28 @@ +use crate::{sync::GILOnceCell, types::PyType, Bound, Py, Python}; + +pub struct ImportedExceptionTypeObject { + imported_value: GILOnceCell>, + module: &'static str, + name: &'static str, +} + +impl ImportedExceptionTypeObject { + pub const fn new(module: &'static str, name: &'static str) -> Self { + Self { + imported_value: GILOnceCell::new(), + module, + name, + } + } + + pub fn get<'py>(&self, py: Python<'py>) -> &Bound<'py, PyType> { + self.imported_value + .get_or_try_init_type_ref(py, self.module, self.name) + .unwrap_or_else(|e| { + panic!( + "failed to import exception {}.{}: {}", + self.module, self.name, e + ) + }) + } +} diff --git a/src/sync.rs b/src/sync.rs index 5af4940461d..856ba84d1e3 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -201,13 +201,17 @@ impl GILOnceCell> { /// /// This is a shorthand method for `get_or_init` which imports the type from Python on init. pub(crate) fn get_or_try_init_type_ref<'py>( - &'py self, + &self, py: Python<'py>, module_name: &str, attr_name: &str, ) -> PyResult<&Bound<'py, PyType>> { self.get_or_try_init(py, || { - py.import_bound(module_name)?.getattr(attr_name)?.extract() + let type_object = py + .import_bound(module_name)? + .getattr(attr_name)? + .downcast_into()?; + Ok(type_object.unbind()) }) .map(|ty| ty.bind(py)) } From 63ba371db021985e01818694cc0987ee213fd4b9 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Mon, 1 Apr 2024 09:10:40 +0200 Subject: [PATCH 125/936] added various std traits for `PyBackedStr` and `PyBackedBytes` (#4020) * added various std traits for `PyBackedStr` and `PyBackedBytes` * add newsfragment * add tests --- newsfragments/4020.added.md | 1 + src/pybacked.rs | 263 +++++++++++++++++++++++++++++++++++- 2 files changed, 261 insertions(+), 3 deletions(-) create mode 100644 newsfragments/4020.added.md diff --git a/newsfragments/4020.added.md b/newsfragments/4020.added.md new file mode 100644 index 00000000000..53d0d6b8fd3 --- /dev/null +++ b/newsfragments/4020.added.md @@ -0,0 +1 @@ +Adds `Clone`, `Debug`, `PartialEq`, `Eq`, `PartialOrd`, `Ord` and `Hash` implementation for `PyBackedBytes` and `PyBackedStr`, and `Display` for `PyBackedStr`. diff --git a/src/pybacked.rs b/src/pybacked.rs index 01b9c2a6f00..e0bacb86144 100644 --- a/src/pybacked.rs +++ b/src/pybacked.rs @@ -1,6 +1,6 @@ //! Contains types for working with Python objects that own the underlying data. -use std::{ops::Deref, ptr::NonNull}; +use std::{ops::Deref, ptr::NonNull, sync::Arc}; use crate::{ types::{ @@ -13,6 +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)] pub struct PyBackedStr { #[allow(dead_code)] // only held so that the storage is not dropped storage: Py, @@ -44,6 +45,14 @@ impl AsRef<[u8]> for PyBackedStr { unsafe impl Send for PyBackedStr {} unsafe impl Sync for PyBackedStr {} +impl std::fmt::Display for PyBackedStr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.deref().fmt(f) + } +} + +impl_traits!(PyBackedStr, str); + impl TryFrom> for PyBackedStr { type Error = PyErr; fn try_from(py_string: Bound<'_, PyString>) -> Result { @@ -79,6 +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)] pub struct PyBackedBytes { #[allow(dead_code)] // only held so that the storage is not dropped storage: PyBackedBytesStorage, @@ -86,9 +96,10 @@ pub struct PyBackedBytes { } #[allow(dead_code)] +#[derive(Clone)] enum PyBackedBytesStorage { Python(Py), - Rust(Box<[u8]>), + Rust(Arc<[u8]>), } impl Deref for PyBackedBytes { @@ -110,6 +121,32 @@ impl AsRef<[u8]> for PyBackedBytes { unsafe impl Send for PyBackedBytes {} unsafe impl Sync for PyBackedBytes {} +impl PartialEq<[u8; N]> for PyBackedBytes { + fn eq(&self, other: &[u8; N]) -> bool { + self.deref() == other + } +} + +impl PartialEq for [u8; N] { + fn eq(&self, other: &PyBackedBytes) -> bool { + self == other.deref() + } +} + +impl PartialEq<&[u8; N]> for PyBackedBytes { + fn eq(&self, other: &&[u8; N]) -> bool { + self.deref() == *other + } +} + +impl PartialEq for &[u8; N] { + fn eq(&self, other: &PyBackedBytes) -> bool { + self == &other.deref() + } +} + +impl_traits!(PyBackedBytes, [u8]); + impl From> for PyBackedBytes { fn from(py_bytes: Bound<'_, PyBytes>) -> Self { let b = py_bytes.as_bytes(); @@ -123,7 +160,7 @@ impl From> for PyBackedBytes { impl From> for PyBackedBytes { fn from(py_bytearray: Bound<'_, PyByteArray>) -> Self { - let s = py_bytearray.to_vec().into_boxed_slice(); + let s = Arc::<[u8]>::from(py_bytearray.to_vec()); let data = NonNull::from(s.as_ref()); Self { storage: PyBackedBytesStorage::Rust(s), @@ -144,10 +181,85 @@ impl FromPyObject<'_> for PyBackedBytes { } } +macro_rules! impl_traits { + ($slf:ty, $equiv:ty) => { + impl std::fmt::Debug for $slf { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.deref().fmt(f) + } + } + + impl PartialEq for $slf { + fn eq(&self, other: &Self) -> bool { + self.deref() == other.deref() + } + } + + impl PartialEq<$equiv> for $slf { + fn eq(&self, other: &$equiv) -> bool { + self.deref() == other + } + } + + impl PartialEq<&$equiv> for $slf { + fn eq(&self, other: &&$equiv) -> bool { + self.deref() == *other + } + } + + impl PartialEq<$slf> for $equiv { + fn eq(&self, other: &$slf) -> bool { + self == other.deref() + } + } + + impl PartialEq<$slf> for &$equiv { + fn eq(&self, other: &$slf) -> bool { + self == &other.deref() + } + } + + impl Eq for $slf {} + + impl PartialOrd for $slf { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } + } + + impl PartialOrd<$equiv> for $slf { + fn partial_cmp(&self, other: &$equiv) -> Option { + self.deref().partial_cmp(other) + } + } + + impl PartialOrd<$slf> for $equiv { + fn partial_cmp(&self, other: &$slf) -> Option { + self.partial_cmp(other.deref()) + } + } + + impl Ord for $slf { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.deref().cmp(other.deref()) + } + } + + impl std::hash::Hash for $slf { + fn hash(&self, state: &mut H) { + self.deref().hash(state) + } + } + }; +} +use impl_traits; + #[cfg(test)] mod test { use super::*; use crate::Python; + use std::collections::hash_map::DefaultHasher; + use std::hash::{Hash, Hasher}; #[test] fn py_backed_str_empty() { @@ -223,4 +335,149 @@ mod test { is_send::(); is_sync::(); } + + #[test] + fn test_backed_str_clone() { + Python::with_gil(|py| { + let s1: PyBackedStr = PyString::new_bound(py, "hello").try_into().unwrap(); + let s2 = s1.clone(); + assert_eq!(s1, s2); + + drop(s1); + assert_eq!(s2, "hello"); + }); + } + + #[test] + fn test_backed_str_eq() { + Python::with_gil(|py| { + let s1: PyBackedStr = PyString::new_bound(py, "hello").try_into().unwrap(); + let s2: PyBackedStr = PyString::new_bound(py, "hello").try_into().unwrap(); + assert_eq!(s1, "hello"); + assert_eq!(s1, s2); + + let s3: PyBackedStr = PyString::new_bound(py, "abcde").try_into().unwrap(); + assert_eq!("abcde", s3); + assert_ne!(s1, s3); + }); + } + + #[test] + fn test_backed_str_hash() { + Python::with_gil(|py| { + let h = { + let mut hasher = DefaultHasher::new(); + "abcde".hash(&mut hasher); + hasher.finish() + }; + + let s1: PyBackedStr = PyString::new_bound(py, "abcde").try_into().unwrap(); + let h1 = { + let mut hasher = DefaultHasher::new(); + s1.hash(&mut hasher); + hasher.finish() + }; + + assert_eq!(h, h1); + }); + } + + #[test] + fn test_backed_str_ord() { + Python::with_gil(|py| { + let mut a = vec!["a", "c", "d", "b", "f", "g", "e"]; + let mut b = a + .iter() + .map(|s| PyString::new_bound(py, s).try_into().unwrap()) + .collect::>(); + + a.sort(); + b.sort(); + + assert_eq!(a, b); + }) + } + + #[test] + fn test_backed_bytes_from_bytes_clone() { + Python::with_gil(|py| { + let b1: PyBackedBytes = PyBytes::new_bound(py, b"abcde").into(); + let b2 = b1.clone(); + assert_eq!(b1, b2); + + drop(b1); + assert_eq!(b2, b"abcde"); + }); + } + + #[test] + fn test_backed_bytes_from_bytearray_clone() { + Python::with_gil(|py| { + let b1: PyBackedBytes = PyByteArray::new_bound(py, b"abcde").into(); + let b2 = b1.clone(); + assert_eq!(b1, b2); + + drop(b1); + assert_eq!(b2, b"abcde"); + }); + } + + #[test] + fn test_backed_bytes_eq() { + Python::with_gil(|py| { + let b1: PyBackedBytes = PyBytes::new_bound(py, b"abcde").into(); + let b2: PyBackedBytes = PyByteArray::new_bound(py, b"abcde").into(); + + assert_eq!(b1, b"abcde"); + assert_eq!(b1, b2); + + let b3: PyBackedBytes = PyBytes::new_bound(py, b"hello").into(); + assert_eq!(b"hello", b3); + assert_ne!(b1, b3); + }); + } + + #[test] + fn test_backed_bytes_hash() { + Python::with_gil(|py| { + let h = { + let mut hasher = DefaultHasher::new(); + b"abcde".hash(&mut hasher); + hasher.finish() + }; + + let b1: PyBackedBytes = PyBytes::new_bound(py, b"abcde").into(); + let h1 = { + let mut hasher = DefaultHasher::new(); + b1.hash(&mut hasher); + hasher.finish() + }; + + let b2: PyBackedBytes = PyByteArray::new_bound(py, b"abcde").into(); + let h2 = { + let mut hasher = DefaultHasher::new(); + b2.hash(&mut hasher); + hasher.finish() + }; + + assert_eq!(h, h1); + assert_eq!(h, h2); + }); + } + + #[test] + fn test_backed_bytes_ord() { + Python::with_gil(|py| { + let mut a = vec![b"a", b"c", b"d", b"b", b"f", b"g", b"e"]; + let mut b = a + .iter() + .map(|&b| PyBytes::new_bound(py, b).into()) + .collect::>(); + + a.sort(); + b.sort(); + + assert_eq!(a, b); + }) + } } From 8f87b8636df73ef7d1690bd848389382280aa335 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Mon, 1 Apr 2024 14:10:18 +0200 Subject: [PATCH 126/936] refactor `#[setter]` argument extraction (#4002) --- pyo3-macros-backend/src/method.rs | 4 ++ pyo3-macros-backend/src/params.rs | 39 +++++++----- pyo3-macros-backend/src/pymethod.rs | 97 +++++++++++++++++------------ src/impl_/extract_argument.rs | 4 +- tests/ui/static_ref.stderr | 23 +++---- 5 files changed, 97 insertions(+), 70 deletions(-) diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index f4fdb19377a..155c554025d 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -63,6 +63,10 @@ impl<'a> FnArg<'a> { } } } + + 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 { diff --git a/pyo3-macros-backend/src/params.rs b/pyo3-macros-backend/src/params.rs index cab28698b71..fa50d260986 100644 --- a/pyo3-macros-backend/src/params.rs +++ b/pyo3-macros-backend/src/params.rs @@ -73,7 +73,7 @@ pub fn is_forwarded_args(signature: &FunctionSignature<'_>) -> bool { ) } -fn check_arg_for_gil_refs( +pub(crate) fn check_arg_for_gil_refs( tokens: TokenStream, gil_refs_checker: syn::Ident, ctx: &Ctx, @@ -120,7 +120,11 @@ pub fn impl_arg_params( .iter() .enumerate() .map(|(i, arg)| { - impl_arg_param(arg, i, &mut 0, &args_array, holders, ctx).map(|tokens| { + 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()), @@ -161,14 +165,20 @@ pub fn impl_arg_params( let num_params = positional_parameter_names.len() + keyword_only_parameters.len(); - let mut option_pos = 0; + let mut option_pos = 0usize; let param_conversion = spec .signature .arguments .iter() .enumerate() .map(|(i, arg)| { - impl_arg_param(arg, i, &mut option_pos, &args_array, holders, ctx).map(|tokens| { + 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) }) }) @@ -234,11 +244,10 @@ pub fn impl_arg_params( /// Re option_pos: The option slice doesn't contain the py: Python argument, so the argument /// index and the index in option diverge when using py: Python -fn impl_arg_param( +pub(crate) fn impl_arg_param( arg: &FnArg<'_>, - pos: usize, - option_pos: &mut usize, - args_array: &syn::Ident, + from_py_with: syn::Ident, + arg_value: TokenStream, // expected type: Option<&'a Bound<'py, PyAny>> holders: &mut Holders, ctx: &Ctx, ) -> Result { @@ -291,9 +300,6 @@ fn impl_arg_param( }); } - let arg_value = quote_arg_span!(#args_array[#option_pos]); - *option_pos += 1; - let mut default = arg.default.as_ref().map(|expr| quote!(#expr)); // Option arguments have special treatment: the default should be specified _without_ the @@ -312,11 +318,10 @@ fn impl_arg_param( .map(|attr| &attr.value) .is_some() { - let from_py_with = syn::Ident::new(&format!("from_py_with_{}", pos), Span::call_site()); if let Some(default) = default { quote_arg_span! { #pyo3_path::impl_::extract_argument::from_py_with_with_default( - #arg_value.as_deref(), + #arg_value, #name_str, #from_py_with as fn(_) -> _, #[allow(clippy::redundant_closure)] @@ -328,7 +333,7 @@ fn impl_arg_param( } else { quote_arg_span! { #pyo3_path::impl_::extract_argument::from_py_with( - &#pyo3_path::impl_::extract_argument::unwrap_required_argument(#arg_value), + #pyo3_path::impl_::extract_argument::unwrap_required_argument(#arg_value), #name_str, #from_py_with as fn(_) -> _, )? @@ -338,7 +343,7 @@ fn impl_arg_param( let holder = holders.push_holder(arg.name.span()); quote_arg_span! { #pyo3_path::impl_::extract_argument::extract_optional_argument( - #arg_value.as_deref(), + #arg_value, &mut #holder, #name_str, #[allow(clippy::redundant_closure)] @@ -351,7 +356,7 @@ fn impl_arg_param( let holder = holders.push_holder(arg.name.span()); quote_arg_span! { #pyo3_path::impl_::extract_argument::extract_argument_with_default( - #arg_value.as_deref(), + #arg_value, &mut #holder, #name_str, #[allow(clippy::redundant_closure)] @@ -364,7 +369,7 @@ fn impl_arg_param( let holder = holders.push_holder(arg.name.span()); quote_arg_span! { #pyo3_path::impl_::extract_argument::extract_argument( - &#pyo3_path::impl_::extract_argument::unwrap_required_argument(#arg_value), + #pyo3_path::impl_::extract_argument::unwrap_required_argument(#arg_value), &mut #holder, #name_str )? diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index 22802a01177..ee7d3d7aaee 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -2,7 +2,7 @@ use std::borrow::Cow; use crate::attributes::{NameAttribute, RenamingRule}; use crate::method::{CallingConvention, ExtractErrorMode}; -use crate::params::Holders; +use crate::params::{check_arg_for_gil_refs, impl_arg_param, Holders}; use crate::utils::Ctx; use crate::utils::PythonDoc; use crate::{ @@ -586,48 +586,63 @@ pub fn impl_py_setter_def( } }; - // TODO: rework this to make use of `impl_::params::impl_arg_param` which - // handles all these cases already. - let extract = if let PropertyType::Function { spec, .. } = &property_type { - Some(spec) - } else { - None - } - .and_then(|spec| { - let (_, args) = split_off_python_arg(&spec.signature.arguments); - let value_arg = &args[0]; - let from_py_with = &value_arg.attrs.from_py_with.as_ref()?.value; - let name = value_arg.name.to_string(); - - Some(quote_spanned! { from_py_with.span() => - let e = #pyo3_path::impl_::deprecations::GilRefs::new(); - let from_py_with = #pyo3_path::impl_::deprecations::inspect_fn(#from_py_with, &e); - e.from_py_with_arg(); - let _val = #pyo3_path::impl_::extract_argument::from_py_with( - &_value.into(), - #name, - from_py_with as fn(_) -> _, - )?; - }) - }) - .unwrap_or_else(|| { - let (span, name) = match &property_type { - PropertyType::Descriptor { field, .. } => (field.ty.span(), field.ident.as_ref().map(|i|i.to_string()).unwrap_or_default()), - PropertyType::Function { spec, .. } => { - let (_, args) = split_off_python_arg(&spec.signature.arguments); - (args[0].ty.span(), args[0].name.to_string()) - } - }; + let extract = match &property_type { + PropertyType::Function { spec, .. } => { + let (_, args) = split_off_python_arg(&spec.signature.arguments); + let value_arg = &args[0]; + let (from_py_with, ident) = if let Some(from_py_with) = + &value_arg.attrs.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 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 extract = impl_arg_param( + &args[0], + 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, + ) + })?; + quote! { + #from_py_with + let _val = #extract; + } + } + PropertyType::Descriptor { field, .. } => { + let span = field.ty.span(); + let name = field + .ident + .as_ref() + .map(|i| i.to_string()) + .unwrap_or_default(); + + let holder = holders.push_holder(span); + let gil_refs_checker = holders.push_gil_refs_checker(span); + quote! { + let _val = #pyo3_path::impl_::deprecations::inspect_type( + #pyo3_path::impl_::extract_argument::extract_argument(_value.into(), &mut #holder, #name)?, + &#gil_refs_checker + ); + } } - }); + }; let mut cfg_attrs = TokenStream::new(); if let PropertyType::Descriptor { field, .. } = &property_type { diff --git a/src/impl_/extract_argument.rs b/src/impl_/extract_argument.rs index 4dcef02c5e6..485b8645086 100644 --- a/src/impl_/extract_argument.rs +++ b/src/impl_/extract_argument.rs @@ -223,7 +223,9 @@ pub fn argument_extraction_error(py: Python<'_>, arg_name: &str, error: PyErr) - /// `argument` must not be `None` #[doc(hidden)] #[inline] -pub unsafe fn unwrap_required_argument(argument: Option>) -> PyArg<'_> { +pub unsafe fn unwrap_required_argument<'a, 'py>( + argument: Option<&'a Bound<'py, PyAny>>, +) -> &'a Bound<'py, PyAny> { match argument { Some(value) => value, #[cfg(debug_assertions)] diff --git a/tests/ui/static_ref.stderr b/tests/ui/static_ref.stderr index 50b054f6245..6004c4037e5 100644 --- a/tests/ui/static_ref.stderr +++ b/tests/ui/static_ref.stderr @@ -9,6 +9,18 @@ error: lifetime may not live long enough | = note: this error originates in the attribute macro `pyfunction` (in Nightly builds, run with -Z macro-backtrace for more info) +error[E0597]: `output[_]` does not live long enough + --> tests/ui/static_ref.rs:4:1 + | +4 | #[pyfunction] + | ^^^^^^^^^^^^- + | | | + | | `output[_]` dropped here while still borrowed + | borrowed value does not live long enough + | argument requires that `output[_]` is borrowed for `'static` + | + = note: this error originates in the attribute macro `pyfunction` (in Nightly builds, run with -Z macro-backtrace for more info) + error[E0597]: `holder_0` does not live long enough --> tests/ui/static_ref.rs:5:15 | @@ -21,17 +33,6 @@ error[E0597]: `holder_0` does not live long enough 5 | fn static_ref(list: &'static Bound<'_, PyList>) -> usize { | ^^^^^^^ borrowed value does not live long enough -error[E0716]: temporary value dropped while borrowed - --> tests/ui/static_ref.rs:5:21 - | -4 | #[pyfunction] - | ------------- - | | | - | | temporary value is freed at the end of this statement - | argument requires that borrow lasts for `'static` -5 | fn static_ref(list: &'static Bound<'_, PyList>) -> usize { - | ^ creates a temporary value which is freed while still in use - error: lifetime may not live long enough --> tests/ui/static_ref.rs:9:1 | From 8cabd2619c349ffede8e141c09e540c0c83adb79 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Mon, 1 Apr 2024 16:18:57 +0100 Subject: [PATCH 127/936] docs: updates to guide for PyO3 0.21 feedback (#4031) * docs: add notes on smart pointer conversions * guide: add more notes on `.extract::<&str>()` to migration guide --- guide/src/migration.md | 16 +++++++++++++++- guide/src/types.md | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/guide/src/migration.md b/guide/src/migration.md index 69cf8922255..d855f69d396 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -246,14 +246,26 @@ Because the new `Bound` API brings ownership out of the PyO3 framework and in - `Bound::iter_borrowed` is slightly more efficient than `Bound::iter`. The default iteration of `Bound` cannot return borrowed references because Rust does not (yet) have "lending iterators". Similarly `Bound::get_borrowed_item` is more efficient than `Bound::get_item` for the same reason. - `&Bound` does not implement `FromPyObject` (although it might be possible to do this in the future once the GIL Refs API is completely removed). Use `bound_any.downcast::()` instead of `bound_any.extract::<&Bound>()`. - `Bound::to_str` now borrows from the `Bound` rather than from the `'py` lifetime, so code will need to store the smart pointer as a value in some cases where previously `&PyString` was just used as a temporary. (There are some more details relating to this in [the section below](#deactivating-the-gil-refs-feature).) +- `.extract::<&str>()` now borrows from the source Python object. The simplest way to update is to change to `.extract::()`, which retains ownership of the Python reference. See more information [in the section on deactivating the `gil-refs` feature](#deactivating-the-gil-refs-feature). -To convert between `&PyAny` and `&Bound` you can use the `as_borrowed()` method: +To convert between `&PyAny` and `&Bound` use the `as_borrowed()` method: ```rust,ignore let gil_ref: &PyAny = ...; let bound: &Bound = &gil_ref.as_borrowed(); ``` +To convert between `Py` and `Bound` use the `bind()` / `into_bound()` methods, and `as_unbound()` / `unbind()` to go back from `Bound` to `Py`. + +```rust,ignore +let obj: Py = ...; +let bound: &Bound<'py, PyList> = obj.bind(py); +let bound: Bound<'py, PyList> = obj.into_bound(py); + +let obj: &Py = bound.as_unbound(); +let obj: Py = bound.unbind(); +``` +
⚠️ Warning: dangling pointer trap 💣 @@ -325,6 +337,8 @@ There is just one case of code that changes upon disabling these features: `From To make PyO3's core functionality continue to work while the GIL Refs API is in the process of being removed, disabling the `gil-refs` feature moves the implementations of `FromPyObject` for `&str`, `Cow<'_, str>`, `&[u8]`, `Cow<'_, u8>` to a new temporary trait `FromPyObjectBound`. This trait is the expected future form of `FromPyObject` and has an additional lifetime `'a` to enable these types to borrow data from Python objects. +PyO3 0.21 has introduced the [`PyBackedStr`]({{#PYO3_DOCS_URL}}/pyo3/pybacked/struct.PyBackedStr.html) and [`PyBackedBytes`]({{#PYO3_DOCS_URL}}/pyo3/pybacked/struct.PyBackedBytes.html) types to help with this case. The easiest way to avoid lifetime challenges from extracting `&str` is to use these. For more complex types like `Vec<&str>`, is now impossible to extract directly from a Python object and `Vec` is the recommended upgrade path. + A key thing to note here is because extracting to these types now ties them to the input lifetime, some extremely common patterns may need to be split into multiple Rust lines. For example, the following snippet of calling `.extract::<&str>()` directly on the result of `.getattr()` needs to be adjusted when deactivating the `gil-refs-migration` feature. Before: diff --git a/guide/src/types.md b/guide/src/types.md index cd5e1052920..4c63d175991 100644 --- a/guide/src/types.md +++ b/guide/src/types.md @@ -160,6 +160,45 @@ for i in 0..=2 { # Python::with_gil(example).unwrap(); ``` +### Casting between smart pointer types + +To convert between `Py` and `Bound<'py, T>` use the `bind()` / `into_bound()` methods. Use the `as_unbound()` / `unbind()` methods to go back from `Bound<'py, T>` to `Py`. + +```rust,ignore +let obj: Py = ...; +let bound: &Bound<'py, PyAny> = obj.bind(py); +let bound: Bound<'py, PyAny> = obj.into_bound(py); + +let obj: &Py = bound.as_unbound(); +let obj: Py = bound.unbind(); +``` + +To convert between `Bound<'py, T>` and `Borrowed<'a, 'py, T>` use the `as_borrowed()` method. `Borrowed<'a, 'py, T>` has a deref coercion to `Bound<'py, T>`. Use the `to_owned()` method to increment the Python reference count and to create a new `Bound<'py, T>` from the `Borrowed<'a, 'py, T>`. + +```rust,ignore +let bound: Bound<'py, PyAny> = ...; +let borrowed: Borrowed<'_, 'py, PyAny> = bound.as_borrowed(); + +// deref coercion +let bound: &Bound<'py, PyAny> = &borrowed; + +// create a new Bound by increase the Python reference count +let bound: Bound<'py, PyAny> = borrowed.to_owned(); +``` + +To convert between `Py` and `Borrowed<'a, 'py, T>` use the `bind_borrowed()` method. Use either `as_unbound()` or `.to_owned().unbind()` to go back to `Py` from `Borrowed<'a, 'py, T>`, via `Bound<'py, T>`. + +```rust,ignore +let obj: Py = ...; +let borrowed: Borrowed<'_, 'py, PyAny> = bound.as_borrowed(); + +// via deref coercion to Bound and then using Bound::as_unbound +let obj: &Py = borrowed.as_unbound(); + +// via a new Bound by increasing the Python reference count, and unbind it +let obj: Py = borrowed.to_owned().unbind(). +``` + ## Concrete Python types In all of `Py`, `Bound<'py, T>`, and `Borrowed<'a, 'py, T>`, the type parameter `T` denotes the type of the Python object referred to by the smart pointer. From c1f11fb4bddc836f820e0db3db514b4742b8a3e7 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Mon, 1 Apr 2024 19:51:58 +0100 Subject: [PATCH 128/936] release: 0.21.1 (#4032) --- CHANGELOG.md | 24 +++++++++++++++++++ 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/3995.fixed.md | 1 - newsfragments/3998.changed.md | 1 - newsfragments/3998.fixed.md | 1 - newsfragments/4007.fixed.md | 1 - newsfragments/4009.fixed.md | 1 - newsfragments/4012.fixed.md | 1 - newsfragments/4015.fixed.md | 1 - newsfragments/4020.added.md | 1 - newsfragments/4024.changed.md | 1 - newsfragments/4027.added.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 +- 23 files changed, 43 insertions(+), 29 deletions(-) delete mode 100644 newsfragments/3995.fixed.md delete mode 100644 newsfragments/3998.changed.md delete mode 100644 newsfragments/3998.fixed.md delete mode 100644 newsfragments/4007.fixed.md delete mode 100644 newsfragments/4009.fixed.md delete mode 100644 newsfragments/4012.fixed.md delete mode 100644 newsfragments/4015.fixed.md delete mode 100644 newsfragments/4020.added.md delete mode 100644 newsfragments/4024.changed.md delete mode 100644 newsfragments/4027.added.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 047a4b21e1a..6f4ce218021 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,28 @@ To see unreleased changes, please see the [CHANGELOG on the main branch guide](h +## [0.21.1] - 2024-04-01 + +### Added + +- Implement `Send` and `Sync` for `PyBackedStr` and `PyBackedBytes`. [#4007](https://github.com/PyO3/pyo3/pull/4007) +- Implement `Clone`, `Debug`, `PartialEq`, `Eq`, `PartialOrd`, `Ord` and `Hash` implementation for `PyBackedBytes` and `PyBackedStr`, and `Display` for `PyBackedStr`. [#4020](https://github.com/PyO3/pyo3/pull/4020) +- Add `import_exception_bound!` macro to import exception types without generating GIL Ref functionality for them. [#4027](https://github.com/PyO3/pyo3/pull/4027) + +### Changed + +- Emit deprecation warning for uses of GIL Refs as `#[setter]` function arguments. [#3998](https://github.com/PyO3/pyo3/pull/3998) +- Add `#[inline]` hints on many `Bound` and `Borrowed` methods. [#4024](https://github.com/PyO3/pyo3/pull/4024) + +### Fixed + +- Handle `#[pyo3(from_py_with = "")]` in `#[setter]` methods [#3995](https://github.com/PyO3/pyo3/pull/3995) +- Allow extraction of `&Bound` in `#[setter]` methods. [#3998](https://github.com/PyO3/pyo3/pull/3998) +- Fix some uncovered code blocks emitted by `#[pymodule]`, `#[pyfunction]` and `#[pyclass]` macros. [#4009](https://github.com/PyO3/pyo3/pull/4009) +- Fix typo in the panic message when a class referenced in `pyo3::import_exception!` does not exist. [#4012](https://github.com/PyO3/pyo3/pull/4012) +- Fix compile error when using an async `#[pymethod]` with a receiver and additional arguments. [#4015](https://github.com/PyO3/pyo3/pull/4015) + + ## [0.21.0] - 2024-03-25 ### Added @@ -1709,6 +1731,8 @@ Yanked - Initial release +[Unreleased]: https://github.com/pyo3/pyo3/compare/v0.21.1...HEAD +[0.21.1]: https://github.com/pyo3/pyo3/compare/v0.21.0...v0.21.1 [0.21.0]: https://github.com/pyo3/pyo3/compare/v0.20.3...v0.21.0 [0.21.0-beta.0]: https://github.com/pyo3/pyo3/compare/v0.20.3...v0.21.0-beta.0 [0.20.3]: https://github.com/pyo3/pyo3/compare/v0.20.2...v0.20.3 diff --git a/Cargo.toml b/Cargo.toml index dfbeb2d601d..fdd1fa9d29a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3" -version = "0.21.0" +version = "0.21.1" 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.0" } +pyo3-ffi = { path = "pyo3-ffi", version = "=0.21.1" } # support crates for macros feature -pyo3-macros = { path = "pyo3-macros", version = "=0.21.0", optional = true } +pyo3-macros = { path = "pyo3-macros", version = "=0.21.1", 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.0", features = ["resolve-config"] } +pyo3-build-config = { path = "pyo3-build-config", version = "=0.21.1", features = ["resolve-config"] } [features] default = ["macros"] diff --git a/README.md b/README.md index 7d79f028dcd..8e7e2f75bca 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ name = "string_sum" crate-type = ["cdylib"] [dependencies] -pyo3 = { version = "0.21.0", features = ["extension-module"] } +pyo3 = { version = "0.21.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.21.0" +version = "0.21.1" features = ["auto-initialize"] ``` diff --git a/examples/decorator/.template/pre-script.rhai b/examples/decorator/.template/pre-script.rhai index cebb06c0f61..1dc689ef6d4 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.0"); +variable::set("PYO3_VERSION", "0.21.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 cebb06c0f61..1dc689ef6d4 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.0"); +variable::set("PYO3_VERSION", "0.21.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 4b290f7f7e0..78adb883f43 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.0"); +variable::set("PYO3_VERSION", "0.21.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 fbbe7ed0e7c..212f62f76fe 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.0"); +variable::set("PYO3_VERSION", "0.21.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 cebb06c0f61..1dc689ef6d4 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.0"); +variable::set("PYO3_VERSION", "0.21.1"); file::rename(".template/Cargo.toml", "Cargo.toml"); file::rename(".template/pyproject.toml", "pyproject.toml"); file::delete(".template"); diff --git a/newsfragments/3995.fixed.md b/newsfragments/3995.fixed.md deleted file mode 100644 index e47a71b790d..00000000000 --- a/newsfragments/3995.fixed.md +++ /dev/null @@ -1 +0,0 @@ -handle `#[pyo3(from_py_with = "")]` in `#[setter]` methods \ No newline at end of file diff --git a/newsfragments/3998.changed.md b/newsfragments/3998.changed.md deleted file mode 100644 index c02c6546c95..00000000000 --- a/newsfragments/3998.changed.md +++ /dev/null @@ -1 +0,0 @@ -Warn on uses of GIL Refs for `#[setter]` function arguments. \ No newline at end of file diff --git a/newsfragments/3998.fixed.md b/newsfragments/3998.fixed.md deleted file mode 100644 index 11f5d006ec0..00000000000 --- a/newsfragments/3998.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Allow extraction of `&Bound` in `#[setter]` methods. \ No newline at end of file diff --git a/newsfragments/4007.fixed.md b/newsfragments/4007.fixed.md deleted file mode 100644 index ff905fb4e94..00000000000 --- a/newsfragments/4007.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Implement `Send` and `Sync` for `PyBackedStr` and `PyBackedBytes`. diff --git a/newsfragments/4009.fixed.md b/newsfragments/4009.fixed.md deleted file mode 100644 index a5d378e12ad..00000000000 --- a/newsfragments/4009.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fix some uncovered code blocks emitted by `#[pymodule]`, `#[pyfunction]` and `#[pyclass]` macros. diff --git a/newsfragments/4012.fixed.md b/newsfragments/4012.fixed.md deleted file mode 100644 index 352ec928487..00000000000 --- a/newsfragments/4012.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fixed the error message when a class referenced in `pyo3::import_exception!` does not exist diff --git a/newsfragments/4015.fixed.md b/newsfragments/4015.fixed.md deleted file mode 100644 index a8f4f636c76..00000000000 --- a/newsfragments/4015.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fix the bug that an async `#[pymethod]` with receiver can't have any other args. \ No newline at end of file diff --git a/newsfragments/4020.added.md b/newsfragments/4020.added.md deleted file mode 100644 index 53d0d6b8fd3..00000000000 --- a/newsfragments/4020.added.md +++ /dev/null @@ -1 +0,0 @@ -Adds `Clone`, `Debug`, `PartialEq`, `Eq`, `PartialOrd`, `Ord` and `Hash` implementation for `PyBackedBytes` and `PyBackedStr`, and `Display` for `PyBackedStr`. diff --git a/newsfragments/4024.changed.md b/newsfragments/4024.changed.md deleted file mode 100644 index 65afd8f0d0d..00000000000 --- a/newsfragments/4024.changed.md +++ /dev/null @@ -1 +0,0 @@ -Add `#[inline]` hints on many `Bound` and `Borrowed` methods. diff --git a/newsfragments/4027.added.md b/newsfragments/4027.added.md deleted file mode 100644 index ccf32952da2..00000000000 --- a/newsfragments/4027.added.md +++ /dev/null @@ -1 +0,0 @@ -Add `import_exception_bound!` macro to import exception types without generating GIL Ref functionality for them. diff --git a/pyo3-build-config/Cargo.toml b/pyo3-build-config/Cargo.toml index f19ded99697..1eb269c2132 100644 --- a/pyo3-build-config/Cargo.toml +++ b/pyo3-build-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-build-config" -version = "0.21.0" +version = "0.21.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 091e5f898ef..64753976fab 100644 --- a/pyo3-ffi/Cargo.toml +++ b/pyo3-ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-ffi" -version = "0.21.0" +version = "0.21.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.21.0", features = ["resolve-config"] } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.21.1", features = ["resolve-config"] } [lints] workspace = true diff --git a/pyo3-macros-backend/Cargo.toml b/pyo3-macros-backend/Cargo.toml index 822dc372b47..6ca4eeade8c 100644 --- a/pyo3-macros-backend/Cargo.toml +++ b/pyo3-macros-backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-macros-backend" -version = "0.21.0" +version = "0.21.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.4" proc-macro2 = { version = "1", default-features = false } -pyo3-build-config = { path = "../pyo3-build-config", version = "=0.21.0", features = ["resolve-config"] } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.21.1", features = ["resolve-config"] } quote = { version = "1", default-features = false } [dependencies.syn] diff --git a/pyo3-macros/Cargo.toml b/pyo3-macros/Cargo.toml index a70ccb0ae7b..39a4b9198c6 100644 --- a/pyo3-macros/Cargo.toml +++ b/pyo3-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-macros" -version = "0.21.0" +version = "0.21.1" 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.0" } +pyo3-macros-backend = { path = "../pyo3-macros-backend", version = "=0.21.1" } [lints] workspace = true diff --git a/pyproject.toml b/pyproject.toml index 1a3cfa4aaad..d474753ccd1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ [tool.towncrier] filename = "CHANGELOG.md" -version = "0.21.0" +version = "0.21.1" start_string = "\n" template = ".towncrier.template.md" title_format = "## [{version}] - {project_date}" From a4aea230ed6087a5fccc415d6456c536645e0df1 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Tue, 2 Apr 2024 18:43:51 +0100 Subject: [PATCH 129/936] 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 130/936] 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 131/936] 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 132/936] 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 133/936] 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 134/936] 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 135/936] 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 136/936] 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 137/936] 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 138/936] 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 139/936] 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 140/936] 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 141/936] 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 142/936] 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 143/936] 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 144/936] 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 145/936] 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 146/936] 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 147/936] 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 148/936] 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 149/936] 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 150/936] 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 151/936] 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 152/936] 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 153/936] 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 154/936] 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 155/936] 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 156/936] 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 157/936] 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 158/936] 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 159/936] 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 160/936] 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 161/936] 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 162/936] 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 163/936] 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 164/936] 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 165/936] 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 166/936] 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 167/936] 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 168/936] 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 169/936] 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 170/936] 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 171/936] 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 172/936] 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 173/936] 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 174/936] 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 175/936] 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 176/936] 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 177/936] 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 178/936] 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 179/936] 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 180/936] 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 181/936] 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 182/936] 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 183/936] 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 184/936] 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 185/936] 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 186/936] 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 187/936] 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 188/936] 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 189/936] 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 190/936] 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 191/936] 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 192/936] 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 193/936] 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 194/936] 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 195/936] 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 196/936] 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 197/936] 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 198/936] 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 199/936] 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 200/936] 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 201/936] 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 202/936] 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 203/936] 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 204/936] 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 205/936] 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 206/936] 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 207/936] 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 208/936] 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 209/936] 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 210/936] 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 211/936] 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 212/936] 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 213/936] 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 214/936] 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 215/936] 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 216/936] 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 217/936] 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 218/936] 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 219/936] 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 220/936] 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 221/936] 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 222/936] 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 223/936] 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 224/936] 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 225/936] 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 226/936] 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 227/936] 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 228/936] 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 229/936] 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 230/936] 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 231/936] 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 232/936] 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 233/936] 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 234/936] 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 235/936] 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 236/936] 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 237/936] 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 238/936] 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 239/936] 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 240/936] 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 241/936] 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 242/936] 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 243/936] 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 244/936] 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 245/936] 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 246/936] 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 247/936] 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 248/936] 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 249/936] 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 250/936] 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 251/936] 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 252/936] 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 253/936] 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 254/936] 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 255/936] 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 256/936] 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 257/936] 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 258/936] 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 259/936] 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 260/936] 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 261/936] 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 262/936] 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 263/936] 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 264/936] 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 265/936] 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 266/936] 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 267/936] 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 268/936] 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 269/936] 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 270/936] 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 271/936] 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 272/936] 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 273/936] 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 274/936] 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 275/936] 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 276/936] 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 277/936] 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 278/936] 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 279/936] 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 280/936] 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%7Bpyo3_version%7D": 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 281/936] 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 282/936] 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 283/936] 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 284/936] 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 285/936] 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 286/936] 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 287/936] 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 288/936] 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 289/936] 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 290/936] 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 291/936] 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 292/936] 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 293/936] 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 294/936] 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 295/936] 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 296/936] 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 297/936] 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 298/936] 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 299/936] 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 300/936] 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 301/936] 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 302/936] 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 303/936] 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 304/936] 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 305/936] 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 306/936] 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 307/936] 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 308/936] 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 309/936] 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 310/936] 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 311/936] 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 312/936] 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 313/936] 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 314/936] 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 315/936] 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 316/936] 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 317/936] 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 318/936] 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 319/936] 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 320/936] 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 321/936] 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 322/936] 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 323/936] 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 324/936] 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 325/936] 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 326/936] 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 327/936] 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 328/936] 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 329/936] 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 330/936] 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 331/936] 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 332/936] 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 333/936] 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 334/936] 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 335/936] 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 336/936] 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 337/936] 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 338/936] 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 339/936] 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 340/936] 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 341/936] 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 342/936] 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 343/936] 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 344/936] 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 345/936] 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 346/936] 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 347/936] 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 348/936] 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 349/936] 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 350/936] 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 351/936] 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 352/936] 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 353/936] `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 354/936] 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 355/936] 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 356/936] 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 357/936] 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 358/936] 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 359/936] 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 360/936] 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 361/936] 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 362/936] 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 363/936] 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 364/936] 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 365/936] 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 366/936] 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 367/936] 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 368/936] 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 369/936] 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 370/936] 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 371/936] 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 372/936] 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 373/936] 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 374/936] 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 375/936] 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 376/936] 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 377/936] 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 378/936] 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 379/936] 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 380/936] 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 381/936] 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 382/936] 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 383/936] 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 384/936] 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 385/936] 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 386/936] 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 387/936] 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 388/936] 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 389/936] 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 390/936] 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 391/936] 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 392/936] 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 393/936] 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 394/936] 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 395/936] 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 396/936] 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 397/936] 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 398/936] 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 399/936] 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 400/936] 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 401/936] 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 402/936] 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 403/936] 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 404/936] 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 405/936] 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 406/936] 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 407/936] 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 408/936] 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 409/936] 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 410/936] 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 411/936] 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 412/936] 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 413/936] 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 414/936] 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 415/936] 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 416/936] 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 417/936] 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 418/936] 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 419/936] 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 420/936] 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 421/936] 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 458/936] 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 459/936] 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 460/936] 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 461/936] 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 462/936] 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 463/936] 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 464/936] 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 465/936] 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 466/936] 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 467/936] 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 468/936] 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 469/936] 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 470/936] 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 471/936] 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 472/936] 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 473/936] 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 474/936] 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 475/936] 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 476/936] 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 477/936] 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 478/936] 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 479/936] 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 480/936] 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 481/936] 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 482/936] 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 483/936] 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 484/936] 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 485/936] 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 486/936] 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 487/936] 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 488/936] 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 489/936] 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 490/936] 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 491/936] 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 492/936] 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 493/936] 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 494/936] 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 495/936] 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 496/936] 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 zcmeFac|29?`#-!p&e5ry%F!_;lui;ElX<9wB#J@^p>!f+hK#$@smL~+63SGPBuWx8 z7PfiJkXh!CA@lrPYufbr{+{3G^*n$5p6Atmy^hvid)@cC?`wEp@9VnnwT{n84VB+E zvTr1jNWUFZRnj7n81Etf{YnpKTE0?Z;hzn*s%ITYBtahJzjZFL3Qi;vH|dztq0=tm z!>#K!=*n}W7{Qi@arn&z6Gw^L}%MrH!JW2lb$GIOe zAd!B^;D-zlZ1`aZKV&y5?8)n?Y0?mOMaWfl90wlZJsxBf0x{mpGxbZ+JHwWXSOT% zcmRHi%xgD&TCT5ubK!?Yy#rMKs>u|EWpg1N>rv|Zw6~^LJVwJj57L@XK8R+(zpVdo zI)UByrTXOVT@#Yd64TwyehcHviC;gpj!gHe(^|N&|1FArd2*osIW;+^J~T>h8zWz@~&S{g4zq)ufE{lv)c%t`HYl4xj{na}xS*7|Ekjoz@jlAF_;Pqz{yth2lwB4nC`4#os`8^Q4nKl_SxgeToRd8aoh{5VOm-tzQ!HktKQ=diq1v?13CO3Xo^wUE7hxZFKSEyX zE1R^ZU`_?eI;k)F+uJ?`Y_YylRKI=3%ptR-G3C6Tdg^n#rBt18Nh5x_UbnN^Dcf^h zClK`+ujxs0a+gO%0HPRm&mv=qwQ<%4JM1HJ=Y_O%o!S1wCcOBQ&^3M?s zycR^hUJphJ&9e?D=(v_TzspSKVo7TCt#8RhdU@l1O|&xnMM&hh`l4O$A;WZgp@V7g z3vC#}PY63^RYf$NbAYW#zrI#GCk?r4qO$_azhvG(zC9DgMNz|rLV`>A&Z~u~qraA7 z9SPf3Mp#p8g#zwHj(g3`-pxiPtY^@{FZT<*5jC`&=P_=0aQQtgJ~{mMIhW_Y^<4KW zQBnd?`)+xewG~th!&WpHR(}Gg-RI6#zi<=SLX1|?>zc!F=oj+!WSH4elgA=PJ{>}- zUBmG=y!Kabve2yWHto0l!Y1e3<+49XC{?YBXiG9FmTkOr;JzB}ZDawFu$TIwTmfKA zi&LX30x(o+`PKcNGeca36RAP9+ViH*&-ih1Egb?|JGn;jM~FHcxl5F4&N(-yS!B9f z88P%Pe8OAX-MTMvSe`jvHINYYMZXn{3%sgUn>GyApMq;~w_&yRjqrIR5~d>Pw7IBB zJL4r|PC#KLA`5=G81Kd^E}yV3pR-`g8~&L;PAJ`!xv!`Tov6q7bnLNHY`2=ruU@${ zGPCj&1kzh}H`an^L=yfv(((ZN@mLk?jIjGt{d$*CznF3ND^Dix<;*p;KAQb#%t_6z zS~=soaq>)xkq@GYih0|Ws=FmV{j|B!dfa`m)n*8@f|KPfA-pjn@`eEk?A^iXId$yw z&*<|llEoMiD-xssknyo1^jjLOY4q)^c+4dgJ0fJ-DAU82zOWqyd3DcgB}_65Je+R5 zlr?XkGD*1(u3pf^&$MPHfuV%Qc_2ogEY>P=6FSIpT2}qSR_y>Vw#*$px!*#gAx2Ij z{M3HhU0pQ^!R5FvEe2hpxT#MKmQ_f)JvVZtpRD^9du(RGjKNKB6VZa)KHkTPeen~)npY?uCQx?qc6O$Z-$KrW z?22QC*1qKBGus|&pL<#`NPX2Oe4cIh+c@XU1osWh+=cn3$>xglNnxdBpB(-6 zFa4f-TiWPCue-2sSA9X^SLb?d`u94gb!m>FhB}c4+37xAitT?T`YQ16Hzq$HTHIz3 z6Fjq3CStuppy`>V!jFTw+EfwV!GtXwYhv+Ogo;*K31Qv;PbM2ohVm}i2egV zQ;y*VZHu1v>8Jfl#KcLiRVAmfFZ7R__EP7y?o0DH%d-VvKV^R*jkEqvKuoTx#UqaG zWsxkj84v8|oyFR)di(WW9{i{EK@@-CduusI`pJR@{r%FLdh6JZn7KLZbEM%riHAxk zlP)<`=XgY*<()=wRj0>kc11ur7_t= z3%zaFqs>d9z*J&E*p*`OgdBPK;GN{0d$E{5&fgjiVNi z!llgwDqz&f;jxv(j7TQlk`)+oL7T)AFf@L*DvctLwIe-GQro`5*b1?DF>3R4-|4}h zX{~N~)8zdakvC#*$qug!0-VaCWR~DSGeUU!d-vc7qc!RJg7|sz>BMbaCTX0h zsAW`MJ>sWh;@b-c1n$Q)&1zj`E@Nb@IlI?uQF)54@e21y>w=Bl(DYKe z0y<49!y58)+pBsu~{v%mSDr=;jA(slOy-6nTI*m2{L zr}zKpO0}%G&LA;1upF>`l-d~O#SdtG!XlZY<)JpnjCXEnwB2oM$Ghnim$|+LqvM2_ zSk>t9a)A^uD23#S{A}e2k(oS zi5}@G1^@qPQXP<>K3f^Nt%R8p5exX9Bz5=&UjmypX3==FesHet8Lx&67t@eGn`K}t}~teqJhNA@-(%0 zZFz0DG1Xo?(kkN4%A|9y_m)wwd(s<-`^oF>bi|MI|3ZNII*-dp*VQ);?#H6s(_b3| zTrMms%D;$)!0=*#GNWcohKh+QFz76&L^cGnXwsN9%(vTxSw6Y7Nk1W0qw`nr4nx4G z{S!6ZQP!4;%n`qI|C;|S<>W!D_a~oUxS2lQm7!-`sujm!kgDZ9R8_@#m58+5jp6JV zAa{N`ct7Qtj(gn!fs5M<)893!MjYClYVc>wuSZ5q;v%Kj`Nx8B`LR?ME*kPVmiibr z%QCIcbi8qem}F0Fcl~8YAx8{EdJcg^xIE@gABfuPIk>VX<#5(i%8HQVo}Lddzd`MQ zxXMme5WD)tfTArcBP{XS#X-5t3B0Cn5un{r_xdnR55MG`%sg`^e<>{J+LjvY;4ZJl zQQj_v<+1G9v1OHbcmM?Zbm}4=`hp1~c9aOGgl%nw_E(uTR3>~(%d3L@7cD!kjhQ}vyO^0_PH!43Q#0ZFLcw*y z(|t79xvO_&VJY2V^pNICTcE`H%qc`S7GvJ+Fgka~eqIE8>iBlo!Bm4;Dq}07Z%-R7 z{^h=~U=vqK))X%_6O9tyM_{O8BrCQTz~l))Tl%YmY??wAbeVbch);tbO(6a$cu&+G zWWCsLSy9IpUft%l#q>*8fSih7yuOb2cA;?JCS?)FjdGKfZd=ofx6yhnDkt zPWCP7xlR|wixEQR=6tD>;D&=>bRz`m#lo8%zWg#%voS(ZceG=R=%?Kp<2VC?t^ZtZ zT#09nQ@fcy)weV|mgC{9oDbLpunWHb2yW~()oYK)Lpv@oPhxp^rim0b#py-ai-LXlb?gX6O|cyqQ1b+{`y(n7siB`$HESl~+zE3VSZ zZMkz6U@ZT>4c{ln98viCJezVU7~bWxfY)->R@=_GavcG`TFmsCsM4~i=G9#G)mj`~ zX_RTV3EZ^3?5tr^d|V}KIvCMt){O9SnSy&L&Rk$GlgLGkS&)N(ocgjXcg=L?E`-Es z#Vy{_ymetYr46o4W@UNp15x$fD8o3lf??jQEn{VD?x zQyP1CactBxNcyITL=q@@wv6ctMe#eBw=28p{~QjO(~uSF#dKV5oR3R=6aR6 zG+x`k2L;*=FaCDVne&6&B<~L`h#i@b>~^^?wEXE=IV-re_e7gd9UDVlly$q0%q;;` z^VTABd1SbH?`F+L=LjHI{SXj|+L+0+#H+kr5G-z8?&$*9HOd4m?HW69B=ZdW^CGq) z2b4Ce+>Wt|l{v3|kz%DF7A$>T@Uf`;>M%Mu5lBch47oV4VTAjjd-31 ze_n8M#xMy<1speWlptjM=lN$?GBU_8{{(`ONn>8VBoI)@Ii3PHvP!I{lq}xglpxOY zFJYLw$6ZqO+m7$R#$V(JRAuGs7hpCT!~9;dj9y~}A!vlwxL$R-Y~%Q7?)XBy*g(pB zU3X-Fa-G*~8SmrzMPQutFXrG%1YLlD_?)_C)4l~vL)-4_iaR(yaW}Jzv!3Ss=r^-b zpg5463{oOtgovOX-=BLcH}@u0I!yZdIyh%)cI|4Uu@6uml%w7F8A$Hql!08iC5g+U z6>e8NlX_DBzV`&Ea}apU6Vkm#ba;D~qqj~Q#MwxJ0V z{f4gDuwPME@c@<#@Hl~408 z%lM~M9+r~!g)EG+*GyDJqGd1p00A#1(czqEzb1QuRCLw$h1Icn=mM_;WXiBrv$hh@ z)#eRU1Qh~41GeM=jV!NX+so6H;rlV3iS-cG+~3CXis));XVx=*IrUwLG-l88nklC! z2<~^C{*qkBN0(BYSYSTwLij-gVBmJIX}5SW)hF^(E{pG(jPC$x0s>@qIZX}MVzCcs z5wT!kY@UPLlocWJZ0eX@nvKfUIu;>%6@pFC|8yf(VXT6vII_djau0;OIySl)6=V57 zP*dsK&Q)-GMnNqipLO1MQ!Wdl0s0N82C**v*|*1ymp|v%@p0rEuhcW0igJ~OIPuMV z=(KzPdVpw?;`-M}TBw8&@(Y*brazh8YR1(|V)Z^eh#k+$0+kX`kjS~g4Az5Ozkg!C z2Ytf*KTJLNjzjDnMv|4I-RttZz7>8+_H66*Y-Fn+fO*ex_s3aYu?Y|!WN!bUsAWBr`UOh9n44DV(b^tdHO4TxR-{U zvt6^|j(~zt_0iM(zpUoG4F7yIMQ_m402l+Q`!gNSYe9{_oTk5ECUrbQ(f>66&2-m{ z^ORjhfUKLHvaG)~JrDjnS=heEP`JF~OT3wGAve zz1)*WN5o}kdLZ3y8=xzCyHpPOn0;wzxyjYvQfQr6Xpqq3Kz_)V&}5mx50bN1mvKX+ z)WJs~Ltur`A~Ph?ad~U?zbq5`#P6!S&bh~Sv&)(FE?f9q#=k|}=y>3dNAI?+=yAsD zZ})u1i_U<9+j{Lec>lNSwxZ~0S$7d?p(H~#i-FG(s|7Y=g+(Sa(Ho6t1>#ms@gJ!T zC}=1qq4>ZMdpUM1q%qWABpG9rI2z}*`mXT?@n<^bZwW7+*Sc8!xA$!CD<6@t_Xb~d zR;vZVAHwNTl;D;C@&XJ_%Yq^G_(KcGDI+0~c#|AnXaz-z8#}K;ozbX z-)Muf5&e7mM_?lDDIep-SQ#U3zMe5QvjPm|m$~(;wrDXC2^4cBkKtZ-l|?g7gOv^B zls^7Jif#+XF<`Bge;-=Mc@E(5M%%lc3F{<3i7zYKpa#pm&P;Uf)wk1su0kVEsShSY zV30>`z}pe*k2va;jfluw>Y7rv;B4B`k20c${x05^T z-I5S!)x7`6dDJlA&m@sM+P^0qT;rr7q_Jp*&Ptc!XNr~;qPIZ501=9UFQGhFGwDebFI{%@RFocXe4pJb*=vsyo=@Lg~#F{OUQV~*Gqo)JbBp4=kvg%y;K~5)z3gwy! z(Go!aPo5X|InoC_1C}ST(2r4f4@fIUaIpA5B0B<#HaL5Apfe<~U)2`yg?pRvoo z94iwtuXgc8zIwrE)LtlYaruM$g2Lk9)!bwez!W6JAmTI_^QUEEoi-p)d7q!(pWwpEfGIOGNG`5ZOZRaw(afh zjR5ovd!&Ip!njbDHx^GR>(vGbYfKXNE1`r!z-Uw^x^(@(hZQ9Lwcl`X1?qO7Cc6wr zg!#M>x&TFbT_99&?pQ>SA+Ryjg4~~i9w8D#$x56SBHOLDUCSawR=r6BTQ&$(w@40$ zA_=lk02PO$nHNZx^sYetPlMbcqM*yZAQJqwg0K5>Pdnl#OtYaPVKr&BiEGHv&ywe{ z2qdb>&datB{2WXKg{O@HCJ1#QMP2sb+uriB3aK%^Xwb6AV}TWgqG`Jayi{}Qk;Va< zvB`DbV?^g&^2Q)ei`P~iBQ5Et<;@Awl4pUy;dH1U1)+Y!b=nqrK`6+xT0&3gMKBn{Vfn>23y@CwNhHON9&5q@svE}tX&DSo@F=`+ZplrQ`a zTo1yw7=N4EYL)g@_(x15yHA00)^5o_Xv&B<^)ktnRebx=?h_0*L9f6u;>GBp27#5` zIzIt88O^Lmf*BMLVSMFx8i^E*q*3gH5AG7wwU`F3Lx4bB@TpyNYct~NKXEhx+yPp~ zi_Jg3|A+GO3QtS|zJdW&afr#mJE~Z^FTFiAxy+863z;3p)!D7AVP%uLPD4fu ztb8q1tg3^l5LNw3#s{G2g76`w4budhNorV?E z%>kV?BJ51DA}v&lBK3_+v*Lyx0i`k2ON_1Lws_|or_D>qQTR!C7d`UDq_a6u8`3qD zV2o6yTSY3SoH}6iz8FeVrj}P+RG6W10TsWyP-E`;_A7&Y?|blRv|is+jRbuHut-Nr z@d!fKc=o_m5kXiS9Pe;{^)@O@ErKMMIx{?3Egyg^E%GXMK+WRF!l&|0W}@zqmt#9c z(4oL+@_2B2$;*>#+83)~LPkUdHQj^v&)w(z2ZAibvO$J{3}DP?Ad|cgLNoKMwB(Gr z0F%)dL@jo6m6~8R+kFO2W*AegBy)0P z@Z4yfN8{PO+eO`erkYDW9|3ByI@3#%8U~nBM3Kd+pMQV%O5gWJ+k7>{VTPDZ2*;1M zLD)j;Rt9YvF66ONWZuCHwhKjy?a2?Ss0v1BKxo9ISTgZ9gHQPW;mcM+YO%*Nujy=MsY-wia5+VDUmR?|%V)?H~} zXZn5Kw5=N|{ckXTr2BaGfYW=M=iskU9|C-><2z!r5EfszLHJ~SsZf!@x2x}$P2xD~ z)jVBIs-e7LKNVU|XC^wS)@aL32(hShHw$`9H(_3ORA0Ug7J|{$Y=%U_c39I9Dm~oO zonZzzIKg?Fk^xYOP!ciwn5Mn#cOO~kL_S{=P`rudCSiHhRf>}VY$3?aHn(THEVSK@ zceqvhvFz+=eouc=_ZO&>LB7bb&6jL27}mvwpmJbkH527P>h{)IE`rVbjfGT_Ya@jB&?zPX zDjaoCH-1Pdsn$f(A3}x<8+b#pw%!P#{^`gQ0C|2W75nuHII4}D(|f26seP^)sv9~B z^;@1p^D;yZO;YLEhG7?7%{j>k>FYbM0;0A&>~`L{^bE-FRc1p0dTuW;w;>RqdJbK0fMvmqH%pY{9i(qVrJ-4#HUbt@bJozx$0CP_ zk>`Yd#5T1t{Bwv+1BxlWh&c88V*E5=RRPTtNqH8)ZdSy3`3OZbtKu_APY8!JKRP0(A~J&9?eaRK&A7Z2zu<% zcy4c;hKUk{a4eNG)@IS+1^mrpLGJ&}u_cs&FOfBvAV8g)@M3=SD2B&75q7Mgz=ROn zd*%=l(6NxnJW{O5pZ_ouee(9Z1X9WZr>{9f0{69fh5tsmg|n=XgE6Krm4hvN{mZw8 zi?Dr*PU#%gB}VKnkCWBOwz~+FDc1q)AYr(S+RqbkF=pU*5z-M0WV#=Gb__T8l>}}A zO7&)G2eBuHTqPnOmw<=SroGCkT`e_}$odr+JEh_oY%$6A+Q~&87XuWa+TWM%4uWpX zJpOW7V3Yff)7V?dYUrSG6eocqU>FDojX})dO9J7*B$wR~>^Mu;Nt%iF)RbzsGZEf2 z)^uz12{SsgBe8ALNHO{pQO{7B`7nceseJo76G_1PPx=w+5q1Jp^Wp?>xC2NPkdR5w zJ*Fb`c=8OATII2>B?caL=ZghD)8*N;Kvj4DcK?`-&_yQdDMWiIagMk*=oeP~h+jfD z*s`)M;<}OAMNHhPCJ}0q96WLTjlPcXp8S(~YqR7)Z9HfVJakI92lGIpKAJzpjN298 zOm#QTSx=662U)wc?;OXD7ihL50=+=;t4b@pDQe7#+{Y&3cZf}E2$C46?c^*k8lY$) z74QP|1u)Jsx~}sS5@mPoLe_ndVruJL(R#ZHKjVTnFIa_umD<`#EN^|EOlUlXhNB$DS1Lh4;5_kA;Z zpt!~im*%{ixN8_t3iVIZx1YN?NRs{#-Xd;(>-R5LQi50*qtExG^q`T(SOWXC`j|$QScxR)ihtIm)6&bL&Hnr-S-o-tRW$as+%4 zybtr6T!h-IiThGT#Z0%Q+sqnGGZofhh)an?{A!*5*HR5RAjaK8Rk@Cjy)-k=%)3bT zh0uvkIB)_*nbdWLY?Q!gG6#{&R5`L}#A%R`s_M~1E$^43xqpBz$5eBl!j74d4_{!P zNq313A8^+w{&l2I34j|&bqdNPIy7mkd5W6}#P3Hrq>DN1jX+|B@dE4#b{KtcvzT%* zh80#SA^CiqiR^qb9wL!?j#!MpSv7)PySl#D+BajbvD<)pkk(P~iz>;CTd5|G1kU;^nrx0rCMMk(KM!?Fhd+Jmx}_ z0{f+qgb|LYwWZo?QkUX&L})pk>4hJ^`apB1wIXmwfJN7iY2xG4bJrjqvzhG}N$_^p6PRAs6py&PHGNA@TwdTP$f zN*KwqK;;=lW3#U^3NA7^b00JTt?*AEzMpI0I`B83%}7P7VGLH-3t+cYxh6MnJO+Aj zIVBr5xD-TLwI7QhY+iBK7i)BvZr-nmQNwnStEzpv`#1HbFE3Kgvq2#P*akG+%J{!D zk-bmQ5;^%i)DZ1p`%hz3R7qiI`O5`!y}tQn^#cwK)JtwibrrhEE7FO4utvDMFyVHF zLdZnwuHmD73#K|C+LbwdF+t;SfBim#>yyT~X_2^w1Rw*(Crwh^WgUbZ=ZP4?t}LOi z;*a&&B5e}S=_Mj-Kj;>|n~aUt!RVU9Sn@>M2D%iYtyyV2O2;Mt#+m-vsE5}~@BfRp_VShjL#9^|YE9*OKRG<;V;qd5XEu z@7c@0ButBeTS5sDSLbH>oqHyH)c9D|OQ*V+B{8}=-`B2sq?r*7AW;2qdgR5ENJ2Gv z(oC4=%1CXkkJnV2L_w7 ze>YXy%GPtL$?=56`y4LkV)LDgF{|0S;j^H+=4Kst7-@8r`iX7sIqUJ3tissN?J>5G zcW+8o$Hzw2FzHvAW*uzw?w<)zN*zA6%h=l0-fR%bWZiF`Td}vz&%oP~ZVWt9i?x@W zLJ@7?kCpyhn?E$?{^sTCU|sprp=)NglO?Xo2?oBtLSAe*t__Ae`O4X3uq zr_7XaSJVxW#+Fw34T%OePdQOF#ETEkxm-yKa#ik-^mw~nsQlcWj~4IZ;{roRcYu*C zJIq^2ED!mP7Cr30udPXF&g8cqEi{2A8woFz+YWBuBFTEl=;1@A*ySTCC100VphQuZxc9{xatzdU`DG{%xAB#WU3$v9 zDIS5OF%sw~-2E?u;mccMBZ(X#Z(q|Z$DxLY@hV7Xiu>yA1(!!u0-i<&@!3Qe`{%lk zFG+eledxMzsMGnSwWb{6YiGW7^f7ne%s3uzfiyxsV`Tey)I1c`I@-@_$nA7^x@V(^ zm$^V+fdA2$NKLuh;s@l;s$$KrMr$J`c51(j7Qv6ItHPI&eoUgjwq#39{Zo#Jso9Vx zvO}S3m%ehU#T|4GuS(H5*gf>#i(CyKMJ@+lZ2IDIXT8T=<)oBQ>f|C_FBzcT5CFdh-lSM{A&8d7L3FwG;>>A)m3fH&+8UP>c3)x-|XpP z&$nC_ScHTo}PIrtUMgr^x4Cab&Q{}7Ue|hxz96JP61M(rH9n86JLI!W8$YuKh6Ih+K zwVK@ZfHP_t;KPO63D0nj?{x#qH0cDNSHicjl1TI9kKh3$62lYfefPl)?uS53X6seD zdycm}Mh1ENr3j(lpF#C@-wLnDvk*o$DU3l4eXYy6lTUR|oY-c%T&!2lwZr#TZd~N) z{r&D+z^)ixVfxua+tODZgX>0BGkvUX;(Ls_C}ejY*6PV>e7tKKw0=NFX~KO*jni++ z*CR7woNCB(Q6@h=ZY3rP*tgGv-IMMKB}6L>7~qKMH$%Yumo6+W2naz!6%L97o#3v)(t{@QB8VAw_N#m3s&X{oH zkL=OK{RfnLm|cDxh}~&eF%HENqzm+89*LFwg*(gJqooNynvm2rnb9Nk%3=l_9V?jx_a@UL0+5kp=QVC-P7E_0l-lw{zp^kz zQ@0zC2?D!T6)_*A@yH?hDYO$koBG-#@w|cOwV7^)Du^-zhw2vc&)RMy1a$FgBM^&n zc`*)%e5d%n1c3n4*p5#SdjVc~vO5-GHE!rj$M-HP+NEVQn7*Zh-)sVxu|G?nU}10U z_IO^8_y%T{2;fWFAM$KMs@L6gbbFpfCw0K7G+7&iQ2%DeGGtF-XC#{TwD%YycZ z-u3hPz`H=G;|W{}?BLLMN}x@(F8kDNZDjc9r}*bIgin)5u8Mfo94FRE4<%qgD(qP~e{z#}?5+U?FzDyRlj6jW@Ne<2*vyer(Y&Y2{KW|8k=@HHOk7Bg z6vnv=dM0`l_j}@Z9PB>qLSq&|GO>fZxy_^fCx z;DBu>26$`F1v0K)sHho=qa}Jp^$ZSm@QoaKa=%O^!JNmU!}Hd5f*yZAZv^8JB7=VB z@fd}Csb-@7LSuNGw5Nu#3z5Hy%EXiF@I@|0-{}cq6`hA9JqTHnCMjP^4x`ixZR&9X zBT48N{0}$J>I7)#MTsZG;miP=rurIC9I#v?i>hb$Aw4B-7ya^oqaUR2YO&`RR2~p_ z;r!<#3vsMi^}SRZ|L+GyzWxzGIHD!;FIjwz(k3aqd$7a`y<}2+jip(fR`WkV3|IEo zIH9$@pvT|MoUi`%>N9UMWhFG1h>rU%Pz)HX%7oVAe>bFSrg%6++9-DXj`33Di>q4d z9oH%si5B{QI1%)Aa^HcEqVZZ)65xYe*y*`C5n`M)1f!jR142yb zIk~t~RdKY^Qgz6xZ;&I!1Zi+(YrOgct*`$snbZ4a+6m0EeHorkYwB7%iC1ZmQ{)_+ z3DSOt0|FQs?p6F5P_XuwVnIwztb0G+-M%V$GF@%m0#guQhdN!bwwPV1eR7wrUcz!sz_M?2A}aVuy1lKNt1Tk_)j z%$lX8LYL9RsHc&HB&#P@l|ha?kaA(CeWCp}-`>3qX)0gGoqBv@(=%Rka5=uDPQ@0c zPmd@R#C9yo>byH1-%lhUlRu-#WpV~mB6tkg+Iqg$tN-iGA%revO=$8XB*FCr%G*TE zW8J12gudN$NuT!nWVoPg1T_Y_4Y4 zivv+gOU8@0KfOACq}4Eklb_4TpMP{wT5zeaKe_gte%d*#e-gV&>3V4lK8(!+CRB#7zP#$B)DSx0RZrocU8QUpEX&8NgOiYDpTYU6Gus>b{-pE-*zA4|3bfwf&%# z_e1{1?k&D6LP`je6HC`Og~bl=;Ffp`!qMbZ(GW&o9!)jRQ8m11i^Y^lmP27eGgX`s{=H?R)UKOi zHQ8^r9%rd4tSxj{ZS2#ly#+CbhJeuWIHY1Xn-L_r*s7Z4=qaVKIE=bdyKGvxD?4&k z-nnbfPEwkuFV?-4P?(wTx|x#JExJn#`3XM4E~p2Ykh9#=1@}0tM{-c88zBkKSv?=7 zxTHcbz+ISA%5^^E@r~!5+z~|hAFCDW9m3pD?EM|l3?)(m3N?^GeoD*ShqsE(8h0Js z5XLP^B#<|bV3W^Fnm#$)a_)p&%&2!6I-0Z7jzM9Cn=Br?{k5}38a;)CL{vD6RH*PH zW%(&e4lmaAXywJ4F9xRMxB`G zQOuE8x6PS;@ec~q{7KBWzlsX-1+P>~{~f>QtN>~(#jn6A0jAn2n6 z{Eb?gor2per}j1srtK2rGI-}A&ny(~6YJY=y+R1-g&a15XOPWO8a*a8dsFPf_;qax ze6$<83aXN*KH7r0=$&pnl0C@So9xq)Yy8E?59vhZB6C%gU+{?5hnx6V0#8^Y8?+Ra zXKqXlWAUbmoCcAtjEpkYxo=%Q4}VQB2Z4g^23m9D1DM>2O(zFIk)g5gZu<`3CL)q1Jtk@~gb*%ceAi<`5O(p44~>t8Nbda=`DR1Q}0 zF;};I%G&M7HMii0X~sRnM*cNx?rc#_9Xx2Mm!OiY69C%j@VqkqAva&K z_y(fHo;Qz{o5>7+RcwpVDvgbN&z_-e*jE>}rMn>B5~>r*gV*FNU0-h%;z}xVX^TN~ zU)J52klL4*Y6~aQx=Y|_iJ-E&_s@^}{1>(P(SZ8UE; zS4GRBKCt?DC$9NOmnafm#=lKW0eU<^m#FggxmcNtc#a3e#9IW4%$f3WUd!46E|as= z3>XyUTaI_AsL;G-87rY1U8}mzDdue#1~r-Sh$S9~xmxIook{TGREI|4%*sV+Fqpdl z`34rtfd1;>LCZQOeci?7YR-I9gagj%&($IBM6O2{IIeR}o_5=daD(JxPs68Bnj60n zGY+Sw(BOlFR{djgJ10ubm`-s%^d6rc@%A1N6fEdMmELxIlMhX0T@nR2Plqr5-i zf|TIL2YgiMO=M&Not55^LVdZnvKc(cf4k&=D;GE?%i;?<0=J;`LdFD1o$R63F%9sX zy;BBdwr<6^``AnHgvs(7%9UC-BCv*pf@GKF7kMZK!7WO#n9h*y=~o6d0++ctSD-$a zlt}O>%o=nmh`Wv$KY#)*aFfIqzSy)uM6;?(Sj?2aH`YFu4)Yk0e+;5cr$9=|79Cb|+ zd$0f%ErGE-BtW?gE!r=3*7KKii$Va-N%7hZg?8Q+bWCI1ms=j5YH6p;P8j5}(7H** zg5p>z9^;rd{9u*rbK(|Y%=U2Dj} z?>3PJ{tzaS&hVmDWCx{s6`#~sJcDm}+z~xUMm@s>IEnG%sOm1O+qCvK}h!nknxAZ=IaMn9M z?B;Dcv4{vi_}jLm+(odO$l$xkq3|y2q#5FI;UYb2PbvF}G7;x-dj{9rHBDy&DyNm- zq%}Ntu2ze{Q<6L;?AdOfCpm2a4x8YKVN~E88SzXrk6TtE;5~Hx)-&8jQrF;_MxMQQ zBPd^DwbX@zP3?NsCZ!W^cZ)`R^<$lU7te$%!3BF&lkj1uhjiFBw>(w-@R5QlFXWA8 zlte2*uA8P(8w>3r8qcJ0`Io`unHurNV`tG_J|_QI9h@Jx{!))`rOork=kD#xc%%J1 zw1`Q`X8f9%I zt?bm^`#ifIXXfsJTU}~QsA;p=orHTUs|P;P;kWd}LrI(>;wf9T#Wt-GxKlQJM8wp0 z!{oq*X=-gf+4aHxApQo8yngf7cSW*K-vRMrl3^o|t8)(Bl}BbwX&Ts$)$EijOPH4G zHsoy*#_xz)(zK4abj$Xdc75OWL;QAl;LaPz9ye7kHySd+=11m9lWg-@nX4Ivm!cWH z`c9$9G{I1uvL8sJ58Z&_eZn?v@$~8B8@QsvrWP(~!mK-Qmh^o?jJT=xvXau9wC6QV z?cUQ3`jeaWeTGmoB9U%|V0n^=M57xnCzoR*OCKZZHjLFGcH|nr&6i*IapPY{GmZQz zD_!3V)z|%nH$GB`1~p1F>OIyA=*`JB_bPhL-o9Z^iLLkczGQP|)fRpy?C43aIYKlk z(G|)aXcyob2cL$hAp$N8=C5h%@W*a7F#IA;16Qfbmv8|%Bs0Eg8Ys$))g`tHW?e;e z3WA0kvQBOWlZ7!NCO?4>(?}9i>ECNCZ+A$E26a-44{pjBvg_SK3!)aV25{OzWDOiX zWR?&3^eU7Uu!pGfg7n?9?@F&@U5DS&fkjB=<;Od?--6NKFp_Y@*2+kELAYWrrHSL) z18tu?HOdHq_9B2V;A7kF^r9kyH(IV$N@N74lv5HCu6F08h+V~k&7xBBWT{%*k! z%ZEU$ThQ&pWX9CLu*C67JerUyr- z(Qt2ax^2ZgP)Wzk_>&ky^Z4j03kg{y@TaR;M2vpklrFO2Ah7T$mUDs(iMIs&92a)w~GRIBRCPJ?K}uC}?CzdTDJG1=!N>B?f z#zZ0ehru^878Ar)gglp#_f3#o7-*vBz;qd!4!U&SJX%yz0?&40^Pr@6aD+4&EC@Fn zSk}l~ip4_;IS8M2aiX$xgUOvFKu=Dd7S}+%IX~K7i zgUS3$3-bex`iKFXO;8$*4kmcXEq}~l#4;-m;k=FHauMQQpP-R{hHpqrX6}-$;l(vm zel%8pmsitM0ev6i*zX>HcFBx}tDnQLw$3xhOB$5kaUw$G>Kea*nVHS7EOP_ znh4=BSS&N~P^(lH7cVcbRvh}1Y%p+dAn~>gKna0@g4~Kg(iQrDf>PE9tuq#rmDzw! z4A&|aM3#142J57d*Q!05q_AWjo%Va0>G`q#JgcKu_X z;o;%57b_MSfrznW4MAZTJvbqfJ7N;f3$-&vDoE5%fNlinP{P4}fVCFQB{X(F>{#;L zc4zd71|}&Jin$h=c7B2MZVm4wDK}ualbrtT5TMSdgm&){Paa3?iB%F$Ifhips}@HR zW~PxU@N)`DyFJ`getv0^<}rhwOyT;l)tvS1LwWYM>pp2!B3pb(=d3Y<7O-TqI6@uy7R zMMm3nRx<*_sJyMXWm~7zXhvYuBgx)GqdsEN!-*cOcoL4ogYa%VMz&n}jVMEqs<ltPMCICGOMo2gA+p3syw|<|AkX)2;)(6N1bQkpNx;~MJM!SWB(7K1J&6;s> zT>lTlQ;*`0#SO3XPaL&sTP)jvP-7pCWGwSVa?cbUAGi$yjsn<^M#8Aj6k9J(62%0P zkm|vZ0EjGRBVD}+7v;c7gvkLEpGaMOuC(Ag_B)M#aw$RY*@J#a6#$o$Eyh|&jEu>R z>;+csF?h&kT@Q2`=ivWCRFXew_#m2xMzlfz5QPI+I`qAV;tCXdpo*3O7;dkL@_{F@ zX>=oOqGHrj;VHaFju*rCF@`KBBql)7ca68@1>%X}cQ}UQU*`rC*hx_4r{$rrhd&Hg zM{8kO&5NboNa%8c|9b*{8v|OKH7aCC{?4>O>GavwghD*?l?D;jn7FjI4#eU)3+a|2 z%mrA~c;aZAH*lT8y7KMucaiI0XF5e4Z!x@@PLR!>U^E2EZo_A6lpyCk^zxzfCbWgg zixXIMyH#|t+nOL1@KR>TZlQDPoMoe#UGI(J$kI66vRDnm>X0erjiuKQ&|dfFpRDdA ziWm4QE0wYPoWQ+GSO??BTdv@VB8lXWyMN8l8z4Ca?ajAE1{?`lQL=o9TVP29@&5oY z`N!O0E;Q3cDrYKtyWMncYH>wwF#d*ZlP4E2sLj8SChpyorGf{=yc&MAtc8Fkf--ov zhM6BL9}`9+Kd+Kca@?4Vu?+GUc4RYz&hYH3;+4jkoWA^AO4Fz19kCV8yyE|huQ!3K zaqZs6HxAxo>Yy|#Vlp#3y~N{Gf-aKZ{$pCu$TxxZlz2En*Y@%q7Z#XZQeS`k8-rb9UIt7K$W1S zAQKHKrA9XtuNKPYn8sm~($KX@_h0SV_1|Ip!@bGwc;#IX8%x#>heV0Y~Ha zehZxhnV{Ci8|Wvy7c+*=@D^+j8P=r#0y96Y8c=j!Ex|q!&U8 zw81^Fu{#1Q5hYA+U$@H;hphM9J#K+&VV8W(Y9Pg#1+0(Q^&vz81 z;mMIc`YSBxnl-`IOItM(?T0>o9Op^^mfbN?`hF(Kk7$O=0-NlyG`Qc+iGakESxKW94szCL2oZ&-UY{O1m3qJQj@QXB1+Qgk%^X2j=9WADVw< zJ3jgVuf_-1;M&hT;jd)r)Hn#M4!aS5y_mOXrNjLT6C^0~Ux08T?LI?OGDsGMSZwIL z2ad?Ksay~1yD*Z`;Ulvh2a@eMzX1sg`#qz%u2;&)KqRprE260i4tv7rzC_T^1=FxB zT)FG8u*Ajo1Rt-+liXNFXC-4UHj3JceOPoM>qI|>Bx@Wu=n2RDTAVXo@fa?WjUO-YucC{hgwBTlxAknI0cFs9rL+b$NL%d%E^Bp)=LuowIy}?? zo4}GGvxd#I6m9F)uDWh$c#)+sUf2UlGwULCNLbfV0K`M*$#gD7q;lY}cM_N0#&sx2 z5=0;0AmA3bv=2$Pkq#c%DGzZx+L%8_@tE~u(EkbaMZ)vLuas%Phy!-#ej1imhFOL` zC|u}L{ypm}vYEfB6PKR8m1}7-_Y#t&VRNCiDz?qW4^GI!{(*2L6o7>a0s&kWv2v-m zuj6zC{!QL4IwNwpe(ilQN_F5<^k4xVN+TUfT6DUJ>ykwe9VdLYQ~TI?y4mMCJS-W7 z1jF1G54Y-@{RME@DhD^bh|XI>025W-pX+ zK0-u+_SANP4z=a^8ShWn6OWDcFTCiHU(Rw1lOn|<+fZG3$Axr)6Uva^AkT-HN7*aQ zAv8|gT!51n3@a`|yWMinhMFi;uPdIk(=nyANV_sza^fX+FkH;A z^q6vqf%CW{X23#kpvu7MoFYG)WZ}H{QB-jZ`&wT%0uO7Q35z50kzR#!?&AN|>|oZ( zEi?x$`U^X_V*E~?nCMpdTt}()!?$z?ClW1ZdV<;(AoD6k)gdT07;r96h^m$17H*}- z8ZajVa7R2HwWS=01y=IK28&RLU5(MX)&UuLjMZy3%TEwM2B%jxsqP?8rA zp4$2oGEXL8DIa~%1pE>u#z&iD_Ej60}OE<0>XpHr^r<8otn1q}@}11Pv+xV7YX zrLWHsuA%MoONnqDF2ZZ-F}qQz2Ie8slLj*mR4l&AxUWpU8?v_Og0nnN6v14&_XvRE zv-IB0#-``ptBMzEgzJ3#xN?@0xGqZXbUPdAxHW+`o8zr?$Mc%_Z5pg^b|srHHgHwI zd9v=nS7wc8Tp=CC$A;`bnYH4;zud>wt{fq#sk*YvMLEcW)zBO)?&CDLa{w3ZI22P= zREqslB2^<+`C+q$C1gb#rwU!ESkmY4Hx`zRX~Sdn2i~Q-@)wsJEBe~b(nt@1V?TWW z7sFk_R2)usd^GJ=>u`d5>u8-R`Y1%R@G9&cn<9<*2{$)BRn?!BBZx`2qIN!+D_mJh zMd=}ykI$|Qx4Bu>PeqI^*?(I5C-Xmlta^HyUGKu-4aH1Sv6CWiPEq?gB@r_$%1e7Moi^zz8+N^s4#MT)y--oy;1izlhPljckn$u zx?8WSyL8+1a?Hy*t$oBtRZ~6G+4Vn$=4C?kDy;4eE#wlSJ253lV*MC&%N;hb5Y#zOTojhQZy+f;otfmB&+bb1zIKH|EXyzHt<=3nm(KE@t`rTT#L$IX zLzI^y($1+d>LNrDY2ysx6W9Pl2cNNGlnE9|&JJfpKEVG8H19`ryRye9RqoZkjJq+8 z58cnu2AFWKi0)Iu+DBm~Z`Mp{tpCY?37zssXcJ(GB~j7r_cx6)H>iB3tJ{$5Z_?*_ z65`zOL2xkCOamvPL?QfjU5+H&M;^9%rTQWznz%JSaVINR?o=wNEMJYr=%Vqm(!l+M zsWm@UHiFd_nkB2JU>0=qgCfs4dea=dJXbBRtR_TDx62$7UMx$X-F6k-fRDV_?z;;m z1O9OF3f@yof7T;MK%6F?Nr3nFSiTiVDVP|0HvM{@7Hq$mVJ`_U(ITz?$@&P^rgZF0ysdyi7Te|E# zQ13(wGV0s#g58F;f4?=zC)dQ$8~(gmsZk@%q?dQkK3$@v1f6-4N+^v`AGp71`U@BZ ze9lTczIH=8RatSoSzKwaz+n$Tl`O@$YgU6u#e?(bOjA7RMnTxnesH~+1`lKSaua4% z`w#xIs;@M2P$;Df3uw*;wf)g`CF(!WVE;DFS-1>=y#k=r-amnj5zQ^VdyLpf(7q-R z#Wm}Eok==zzeauB7asunk3RNDTUn@{ZPL7nfGj54gT*CCpyWJ}xI*0$X_cWsjYu*kouq9=c&?Uu{PgR*1PI(7pOX6+U0*2xgR87tfq!m+z3$>|Q4Ca6EP zCcT`IOo#ys~2l|Nb|j^Wk7lHI|6LAr-=Ad@}%z~$8LLdy#a3D zz&Zf`GwGi@9GMNxcbpVUWy_W3Eac%jSlGg%trGbqCJT>wmRQT9+4Y-gizwjtqV>=u z?A5hS(zb9ai!Jo+v$Uhw7cO~ZJ50vwOJWHH`3LP6>n$V@DMX*n!+bB@Q3A+&Doa0? zyFIF{WvE}z>6Wg!!{oDBO2dm%g%62NOETCGm$_q}YjLINh77Gr6_8Q6eGLLx^>weA z=CWOTbLC{lbey~PN_R7>`T7Gd0Cppq@*EA^*x6s}zJ@_{T57;;9FOwC>Vh z+=-*$-5|S2@BD?I3v!^@?mqD{!BSlpjfG1|7Bwl2vmeTPY_il;@XV383M1ZDz6BFK zzBJoXx3lAj$GpRXT`v07dSiX@XFt>r6}T9P$WMJjV?}Q-5pQp`#^(qc_C^oij2uRV zU-CAwXRjqFm)nOEpT6z+A45gNTO6}D;zdsg9BM?%Sz%lB$MIDq;=B{Nae{@Orp!^k zmM%ka$Tn$+&xo&h>A$34!%s6Ub;`WyOBBo04NX*od2_X-#pY^<#V)myk{HF< zO3bgK{cx(s_)>O3tz@IFZz&dd(RY;}`bOp?!ljwu_(pNgTjiyZ3#F}V+opq+))faU z9qe#;JNl@M#pcwv7FPzs&8mXRztLNx<5jrc3X91&o>}bd*Dmf@9Ep>#%@d}HQ2ChN zQnXg5IvksM!rH;9cfnKae#KpS+Ih1tosS=vDg6HNe8G?RvV{pwQ4L3~o;-eBZ|SLd z?K$n^OH6m$e;gzfg=lUXUx9aphnbh81uvz!d8{mcC?{3>xl?|W*3&A0(Lq?i$|jiU z{zw}i_#V$$D}ZGk0m&b7QX2D?t|WCh?9*S=lCvvx+>`mHPs7x+I{}34CE6BG|KY7pYrZ`M53@m}=3W~Zby$piV4Kzc*T$96ptP6_S=zA5 z;)HOm<4r9L`3M6FFl8QSTy0i@z52y>o{G@;tDPnEyiKRij7px&!h&@O1B1uQR2If5 z?aMW55wBMl38gIZ$<22vq#5=6qqFF?BI9a*BnTH;$s4gH#ht73-%MNv#?5!aZ}Kms zS2?~P)mAH>_fhQrZpu&se?N;!KWKaQOyk#3zr)FMI@4o&g+=cke75ZQH`RkdyY$Vc zV@A!ryFE6p>RcR=P!B`!e)>XYHx9*drSdFF%a=|bnsbX%!Ufx+wN@FRTM!v$EB&Oq z3k0?(VN$As*~GN7OcwQ|40X~hEmky2>czgBH|Mv9fjr9+5yVA zL<)u`x^XjWtAM_~RuCpCc$T3q*T_pVGPtD5Hc!L(ml{tjmW#gxPj~I6dVD$toizui zl!z%6zsTCr=Abp`eMe|MR1FbYUdeL##r}lt8!B_#xI7E>XUk_t+NWuY%EIqr+BEsB z)$@s)GBuChm#Eu)SJUyOY+`D#U~(T5o35(ZaSg~iVu7M>cV(i(W-_seDsKOH}M_JyNxNszdU z3}(Ap;g0fpj*}7ZS>KFEZ!ND6cj$4tIl*(E@&+u1HHw3c#Gjyy@hu`>lP5`VfwZ!1 zWe!NoiryI}d572gX+~*Eja1^#>FvN|v;a>7nYjWM*{^nhMxf_&RN9toe`(<5UeLga zT%fXRwQ`;eyL|=AR)d1;nsBmiS#6eXiZAR6H~b%?=lNcq%h_OY#@&rs-fcNb;{`kv z>SF7b&LUq4c*H~t3A=LIQX?O{WzHyRWOrqqIdn~!TVpl&dQYUnb?eU+$!WKoU@dl-BX87@?Z<4X!YnYTy!HBH!4 z)>}9Wa$acX+!=8(;qkyF-3t?f-_vz_M&UIQ{R%(dT+}HFx5hgX`l<{x@ne$Bv7%&A~;_@rSqc z=i+4ry;ngtK`&H!FOU*jNZ*6DP|C){C`{WaadNa}i&4@xcX8EM46czFZsj-jAd#AM+E?#jb;}7q9C||$aJ6T1VG-2A9R}>on_sHp%tswRzd^+->eZK&^RzEQspGqlhI5G~n2eEt)3^+)B(5##l*s z5Usa8b1I5wU#RzlghrJ2fz3TM zBMLZ#asT{Y$f#z!)O^r#cXb&t%c@dQ;VjDg{e>RBl_|B70T1A>D_~mLPG@wX^Wn&(d0> z`Njyk3*nT{pMZ>dJhEnLwAGI4YeyG3x#ptA)Xlw z`h+<+r+8ehEXsz;IdgXNbIS73jOYwTc)paM6Y0m;b&)Q@z{3VteVle=C@8-8o8c>R zagoZV9bsAQBC9*wGWyrWs`5%Ktv`qAdu06@kv{FTu4*r%_Pn@{nw#mC6hLe2zo6s6 z14J%RZ|kado{m@z5t7TtCKF+-v5m=D`1N; zh+I0=t*kjQ==K1xqBs~h?ANQFW!4_BIsq}~0YZgbN8=E**DqxdTRce&IbTRUsT`Uk3WaUSSO?V4WVbrDjX}dIFmAUS zt)s=z^VEqH&8*U%ncg0%yF3%KA1D2RyJ!y-7?ELZv4d+hpy9+22cs#o>YM%L)zwXb zQfn*Ak>^k(1xmZZ7mg5f$|E>ns|j?ESW`Ocz1eS*&#j42+R9)?eD9dc6?(p5+zCNN z(DYqOEvwbL$c~%)xIeeDg=O{QF;JcSI;2N1>#8=EEPXwKq8ogO@38XU7x&0(qv*yO z0?{K3a8?GK%XZylaHb2-GwaGs)JnkF=v|Xln91>;zsu*{yyNA|`=h=n7Mct*)RgJS~dl^VBeueCVPQv$m&*)qFXt*lP7hlO%FMh713kx(joIHTR;h#xYXruTi z&C5i&k2PIwnOWE!O9PjAy_MVi|EwjDZK(JBh98)6tr#L@21^1!AjEIk*am43#v_RS zE#R%-p9+em+JlTI5r;>y$1i1?c#^K z%q4lZR6za+rv>H)Y;I3pzFv+GKI3kZ(6}AVL%mTDAoM5mqsepG_ZxovLeMVjltf~?uZmyn}^X|ZDWD|LpWagH^R zV!@pGm#)!|rI17;+?|^-jzeB8?6h2|v();umQ{Tl&}NKy-g~l zv}~8paWe$9qNdw&*-j%|syzhHc{_LJoQ0BnG!fk0`>mz?LWv6)Q9#P)mI>>brG9a4 z;kXq3!PEk&!J(m^?KN=<*RNk!4ws2&pi@@;!}fue7*TGXurc=A-#lGp!8iGMB&t9c*Kv%Jjhy4<^f@tTJp z!6dx%yMiE?)FoGOX=pRo#Ho0(3Y!$axmxoqE-o%;qno_Q#C&#z&C2pY#`K&lAJ6rp z$&fb}y$zmTSvmz6EYzFt#(1oKK@o{jr@#JSSh+R$hi9#y0Vngu9m`R(ZSVAG)Jhx| zh{m0qrlUMfFZ{4>t2M{I+6HJml0*_B4GtPnyjiVFciI;hdu(5bA7&VYf2vP9Z<)6= zUsO@^QoFQ9O}?#P*6Jxrk=*^lJxdGZ#DH_(RH7wZX7$eFi{iPjY%L+8ZWx%5@wFI2AeR>(Dc=ktYPMS@ruTJjGt0E#I`{Jd=#XFpB zk`}A|x;t;Lp*DVvJ!4=1%Pez^A?-jZCoN)$OVZ0Y$1mDh4*qevI6%p5Crvw~(Va3Z zFDI{_(6lkd=+68{kmP`PHng-C8os;Jm_t%Lq@O1N!@VuwVsL0^Xt1c+IrHXh!R4zp zaoME$q}o#$B?7DrJHra@SN5(Mv3>*Cy99nC;bq?EP;tGP3CnkakBH%9K|K!dcQBp& zP2PssoBi!zvgNG~7)+yn7;MAk#rTATQ=0YZ+^XR$NYcO3j;5pO^YX1y~rhb?%PWMtPMS0*a`PdkT>D#xi_J3;39J-8&v5rjuqcPVz zU@I#NSa?;6F7rXR$(Z<;FO~G_rLSGPX7p}x4voA+=k4`I${x#-2V7JA${?a2$E-8j zZ&Mklrt0qt#{GAuxz7C)jK~nuQV8lN@cuS_e9)LWHC#+1!4$tT{JXXu63{zYO%TLRK?2k_!q2_tWP+?XFN)5_M#bd23EO$C zQS$t)!I$#yP{z9kBd;62H1A-Bn~(?ri3mu5{l+IJV|LnI;uRAY$8CFV+s3{szli(B zyy|$ILv!~}vrQ9BUF-^x8Lmy5e9{%T&Uk`!QNdZL(tSjR7}%XS8Mo@Y>qirrrGh)xoYU zH$F90EnbIB6vz3nliuA~dq!6`Oltj}_wk#Ee1*~Banjk+~vKR0d#_86YE6|+Gu?X;$gMHeq1SUk|1*6Z}iT+ablIAy8 z6Et@1fdP7`Pm3S>6EGHp`R>Yirvg%SaNLYOj&UenLpu2wSXC(VBXG9WZOM7i`hVDOa2}H$a^r77fl{%a^s7*9f1*R?j{?3}h-b3I^vLI^0d3SMvV3ok!K%v?19`peiCv3VWpg&h@CE z98mTS6JY|LZ(XwmE^N;F1T|1p!$7HYzTLJ5*#e$g`2bhJ5|7#W$zK^SBfi00p_9%D z!@Uw2D22Y+2W=HT<<9km^*}^;R!{)$Zd{37%D`BN0g%N$S9hGJ z(XY`&7LFH+ZU^TX;#Du6{fgiXp^vah#mR`HA;NrXzDVo0itn*K`tEzw)R+>1-2O0G z;)&F%Tw`Thr`F{XFfQ>WghJzrB#6qcPyI9PTR#sI|&BQW9w z4PKiQ#)N^5OMF5YA;Rl!R%%jS0P*L zxYRD5vD;A*#pQSSVyM(zAye$43D2qrWozIBk8p4H9}8QbCpPieMNwAX&}CI-co&sUuGo~Tg0cqY^2b12iwo2wca4nFm^h3pt97!V$S zax0`FAUyha0*g8-R7dZ_YJaOLKS9}YuP)X7@1CxnI7)z1ly{f5s)`OMevr}Z!MsnQ z8w4humTsmL0LBgVu?9Zzdem_da?u5-|D z0^uGW3H|4w)Pdz9<0c}RtR1i!WOkY)ol7SPR6fGo!`FZZly-aLopQF$pasr;cKxp#z*K!Xt} zr;#Jr#Lrc0%yRmYC4DWx44g?cQ6F(8MIC;y zL9JNe@(E+!KTzHX5KUNMgdP9glOIY|d=AXWv3^JmZ}w6^ttCFi z6j$|s=wjKImHe$4;?Gl}*}cou|ASbhT9PZQ9V1pdQ0pM0{i)oauFTnVgZMP(#>Z)! z`Ne;?_axq4{D>ljE5DW4c`ZMf#PN4>)IvB+3_KW9!}%d|^6XS15w&uUxw0zE?<)w}FrZQD#%2~aqILB(G8}qpsxWq-{S{E*FD!g%xfeAM} zJp1!+PG8Y9Z?P3ZRF`;OO7^D7;2-=&!0LO_VwE&Z)hSX^Z@blied8X#HemwE$iQW? zbBEh&|Fn8X(aO>vzYgG6GTSohMU5&H5YjqnZN_pBjs}Y!v|VQm9xi86)d9I}bmtpPT;-)@)x0$CSv&N7!CGtN zWA=Qd0`+#-@*ZHRPLr8vjK~Fe_?v%%=R!otrNk|T-Z>SV22Y42N3-{$hgR_xe7Pkfcmo*7MiMFFvCb|k@% zQZ)RqmUS{32q;FKl9N>;jW#VL8>yF+E1E?C%15 zG(@wtE0He|9I@k=$a`kq_w}UNuiy*1jk^LnTqJB)=SKtMgymb@W03C<}o zlm^GOI4vDQorvzot!I^)xWm*nn2q+#?v*@w$OF!re(^%Yry^KzFU;}q6d;g8I^A$$ zF~gY*hGk=v2W59%Zqx@;T1Y>Fo)mY^_>MfDK+h&`#0~_B{$m&;R)UD0e$fSay4zvp zrH1L3^_;r7BG=5!ko_YO1T=4${72W2gw7a1_bzW3B3RaR2=vr=>$;YO;iPwCOEJNV zvF;@0nh_T}ZuBZES1fh}JfNpm`-h%>Tj&Py09==W20!qgK4e8Sh!Yyg!B$rAp82qz z1H@a|6#lnaRaG1{4o-S5{T&K*U;JHM#n98g$YLmTx)|hfRHMhd_!BC9tuJig+`M^n zg2upJ_>1~2#|FIwNsLH224f=7qm=hScXQbIEjwB^C^9qDT+U5FD-5eOEKl8#L+=a_R zXVOZ~@~HO#8X?^ppu**vK|id|L%xPGBVHAVhL0l+oSKS*Z;U+_ghivLq*3$9@|X~= zeiT;IF$tJUMxt6?^ktc}`HU+qa9w>0WGF{hBi6fiC+~bU__jrZ{jjkzyDPe1%|H8C_Lh{Z!U)q_2D_rsf&|-zo>UT0yt-%v#NI^xqQ-8lPO~cXn%mZm?3Hnq z4pIAMqr6kDzunVO?~ms`COa|E(i%vOV#S1b^=jXUV9%whD6a<(jM9R$?;moQtX5pg zBHq}m$W1CW0Ht%4`Z>j_*@@}4Yu7MUnYgcy8m5*r1+94}|*_%lGbl?XS6Wv&MY(%g*4U6FCtNbElU^ zX1Fm9s`=byL9Mj0xI{^QbZ}T%=UDyXT#O%(Hstam8W{=FweVJxt{<2^ERQXY~k7iWL zKOj@2{D-4!eV|al>DhDl0Cuoh{JF7kT z2k*=2xSoXV79Sr)s-0-ayy$N^L@R%3;T6-sh0)t(j{Um29dq^zjCKCdk-*_R_0T)%bGvSWL>n1w2S)3(+-tR51$KoKUP zu_W-lFS6-3GVks-<`QOkA6O9~EzapkYxk0y8~3_MX;|{q8K(!OEd+U+jCb=L>q$f`uX?A7!=ZU zpri}OajeYQ2#$waABw)QQ?{o{lRz+`d{3C8_Ey0n?KN>1MYrrfe8EZkrhGQfkLx>e zmyLdgahTw>$m|Q83~`oiBD-9hQ2VE5e2z^&#dNe#-gcedYkX{s0JYhL5ELAZtF0GD z;2nSMn5koTRAmzQM9sCEH?9u;^ZLr6%o8)yvT5c$eY9B+#Acylo7+-_4$^eU%2IYl zd?$Qax_T+HlV6U33h*e+!wXFTq>_egy@UNo2#t4iGMGy`!y3Qq|4i(|TNt%4@rcTE z-tAksd=8&jR){(WpI1IQD z*b5FcR-fCm)q|vV+tA?MB=Cj7g2Q5m{G3~N|6`@IFN!uxisgvdzW3IBGo3QvYNgiO zB1N;$K74-nKgsOtwwcMmPQ3dVaJ#?q;#9vYL?kR+eD&h29q0@~vZtgjuvw zaka|~mfEd>c8)$5l*$oVUQjDwbek$O?XI{tmtfPGn6GtE!mQ| zPI|e_^00p1tUR{DPTkclJzdnb@Wla)_QfSF)fQ4d`hR*U*Pje~EAC&5U$=1wzsT|9 zaY;ED2V5uZ3*ijV&SN_Pk_uj}#3Tp{#q5?B*wLPgjvXjU%1X4_=g7?Qz;8E2+>Dmzx3{&-Q0~;p(%;r`ALm zNHHAZD9+{si3au@E~=# z3AH=g(AJRc>#{fs4#zi>qgE-3_7RS+TIXst8X74sCB-%}GGd(a^un90EJaI8%S~HA zIK8bz;pV6K7P9!57FYxmKBjmi%-8-Ia#mw#VV*!ZPXokn24({066_lXt4=u8ysQon z^PY~=j!BqirkL=){N!ch{mGe(*iHB02sx7KKcZ3gloXm)J0&S4MB-!T#d#=&t&c5 z%;wK|JrS2e>JGaLETp|-wV-UITu$Nb$dp0k1dW6*mjgFmMylY|MyJ+lrlrch#*l!> zy;){vEMa@R(M-FOWpO>Ae5cFuu)v8PMYox^(6a1xwl*7(Rq)=mzi*NvCQ(5W8I1GLEz*q1d*^5V_1o?rho>OLQ zV03Vi-`CE<@WQ@+Ie7^dxa)}k z-x#9^oDB73t&=&{8-R_YKJL+t8Mygd6$-7Tf>=d;$Ey1w}C z@a(inb4{v?jO|f{s}K?zGE2J~86v&Wtl%>e5Qwcb;F|>7^x2*d{F}yO7bsJx_l`Yid3rzUunwb4)Yc^O3$1FlIw11Z z{z2$8nQtOPQO^PHz_Aj&yK=+juF&1Pca8a;Hn%N!sIdg>PM@FV@GG1wB+=)GoGB(`|$USOVnFJadIqaPsO*!PpBP8ab z>?sLdB^*h1HKTTytoWnljFJ#_dV0 zYi!i1FnF~KqEq!uVH%&~TU}t8#aSy;@+I5X=biD9;x36<{9;_coi7w7RU4 zmZ!c{jEU@2izs$lbiQ$FaVE<7fbEI85Q#a2JC!Mw%`_3`>77xC^v_{wig%98t=7Zje0hYR)z#B`dO=pS| zxLgEq^K8YrfL6CnR5-iR1(%TZXnta;LtKwN9yqvKeTm*A-rMdpCdzxvWOKfi9EMdE zF+#)(+cl;TY>*EI=QT2}ud*$9XLAua8aN5emv7y@y=jTxPJ+LLWn059_Nc=a;A7*5P!Gd{7E7K~-(4_)AFS zgKTF*leWBL)tU=l#rNulfW;L~4yoQkL=i|tckBx;FOSE-$N;bjWOjT>>jvi$e=M^i zBsM?o>Ow?O)U5t^nvVC`1OONAem*{N>UmoV0P{?e5zykh2!52fyIxU7CZiQTcw zc@w7$SAN?GW3mW0e=SN%Xzc#T^>)_aTcrR%V0Dc}6{3y+PP64vE^^!gVzH_rbvu#< zJx15h6#GRrvu@`xprLXDE_|n?w*^V;tOB+!hHP0pEZb+PyXQyzTa!-o$L9o6qYi2vi5K)Bz#yZtd)Ei+-{ z5ZfEiBvB6V`2e=Wm{s^z+JP;RL!Nj0bN6(M!?h44$9`J)kqA@O#Y-|v&A_w9SDkB} zATkD}dIS~ZAH+z?7G&Of_;CmurpA{ho_*1Sga}R^sb}E;d6BqwXk=un<%gd})8HCz^yuhH|}l;b=6VtOi#Pi089G=rsZ$hURQj$tXOjCy|fH z^Z=4YP0PRj1T&C73q#r?dgzT~f~K#?i$SKjcmZJ0o!2eHKLzwKY@yU@x_6Bikm?jCE$Yp#QgaZP~p6zZBP z{0MOJ_!Fd7Gxc_Tp1@Aa*_Y|IvOHVNBj*o(Ufa~fiu^#h^Vnt}N2hz3aWYh~`UNEn z)1$Pm{4mnskZE87pjRliTxXK&RJt5k z(UWg1m#gk*$yP6QM$S?-q)FYq<@iF0;8;9mCj%hpyymt4PW@V+3*hppp?GBHR8VA= zt4hy$SERr<3BGc3a~8J!gDD|BU3lHPb!SbBM4QB|u+lL>JwsLPvtNydtl-T!p6p1ow<*HE0AkNgW#m~DccJ7=yb0sCD8ck(cswQ8_$B!qI&ZmP>F~xyA zrAjsvbG*R^0uuAga{sLa4KknIK<|5G(Bdz!#qK7_5QL!>ldt_Uz?Z>+AM~|e2Df%B zsdDN`j+3ytR6u2s`{L=u`Jfx}yRTXHCAyZdQw&8%!yTBkTH%;wU#qCJJkk|Jo54MM zp758IJgUzsG!G3+^{6W!N}XkcU#Q?;m>%&;b)9;dARK?>VgyuaJ9A~0Yte#k;rO`v zSUosFWB(XuFmU{Xm;Ma2c$XMC(42V?CAq(_%;Zn)-S99iW1t034$V*fN7+WDwY?ft zK5(n-{2uYUM}`_ixW5COMoVqlw2394E0@U&+}P?Oq+492s>&r;%1P$4o%EJx??}#k z*KrXZsQJ>l}!{u0z< z_3pu6tq-3#!vIpJ$IDy$U;tyfzE&lVndeQ;`ufiEiZoO>psn-)Ek(;q)F2W`5VC^W z1DX%p+uN~NwW`5Ap3^H)9<%Kj()jVLqlPF7@LTerrZJpkU-y^NIT)#~RK)~Uix zJY_~kiTJ+neUD)XrRDO4eYs{;Ib2}fG6(z`A<gvCBLA|0++w- zlnS^l#^MVn^B@(KAD@6XL|>S)%TgX18W|cB=UtPWYSs4GwUYBuW+g#?&4~|vwQXZ$UumceEUD*Oh1P!`8C)toBY*B8@wy* z(4GoANNogh?E$Rgukz*=(h=J~TFJNjmu2N9K(!Yp^ZlUQ%f)I44_W6jzk$ePxj35; zamLYD0%zu0*pYOXi@(KKl?YmUkVWIR z_Vn7>^^oqE*MQ$twKZ&8>3FQv;{hm4S#;(=U5CtfW1tv= zs|HPcW6*s}go60$tgDD?mwebBmEhfw#!{ygcTnWzb42Xqj&wYLe2k#!Urx`9&FNkIr_zc<+)F>X{2HVwXxAia& zq6tfJMQ4pehH75j{Pvky3+%sX2*r2KZNN_3D~sN5upP;c1v%-W8$PSPg@+o7fp2<0 zeToX3#-g(V{XpKdx(8TEi(}_nzM%RhC>iB=t&H?;FcO!nH3{~as|iISZ}Vg1maVHS zOUGtJ@Pd>{W6;7pMA?A4<^C!KRNmVZy+K(oUJ3e4V`zkU0_Z^5JvGv#pT)P2*ab~T zeB^BH1JjZB&Du=D=2tfAV<8mMt*jt_D|wH zX~}9t4+i@fF~*{P+cPT(B_GENlK0}$Eo-l;Hu&|rF^mBbnzDwo*vsbUk*|t~Xx5X| zb07nUb`iP^nDK`!iH3)pIWhw^Vfnl7dux<6k`$)qkhZ$lA zRnG3h8x#O5X3Qhy9iS$OJ>bR9Vh{jc5wt-8MEz=sR8d;zuo9dF@?`ALvFO#d?P9{> zM=YCO8=vq!X3Q#U2jwGWuXk<`#apufuW(W zv>yNKumC1uyWs^;xnSpF^y+-;r}#`(uzA(DGySWGR)L*EZ?zLt?dqAiIZPxXhrKdi zx88Wq9?p(@=@TbTm`ny=)=1EV!*Oe_&Bls@nK6N0>xI-pQ1MZXk)Xwo=l4E83us5~ zqHs}UJFu|02qx?UCQL}FiW+J3m%6=4LPRE93c$Ma9h?AxX3O*OZ^WqsO z&R~6)d?OFX{Yu&i6?;&n2`>)k?$`#8iEc(4zQ|^i<4gPlutb!6t-8R$1oa%&l^B_u zn^XLh>^RssVee6m#q2mcK=lS;K`s2Y50i|wFC7t3eawQD{is#Z-dLAKQLK_LD3`tg z|3(l`0lxYKX`Bzgy!x9p@k{5s3G|$a^mLSReSJ7{9}P`K9*vdlNE$tbRZxi`n0J`H z8YzIw65&V4S0}6e7$0%iLpnD-))Cd)_#Bp`!%jpfh$}s&wGF@1=fMMdf$C?SF5osw zM6=heOpSt3z~iD%n>Xc(QYe(Orbj+(*~|b%lh+bZxdXTRKO=YJw~6diU-fD#m|shppiaeg9>FdXH#2-_N>D z@H$jR?BqiQn*~`F+_bNrJJfrY=eU7^0Vo2`D-G9q`}$JSs;gnsnHW%J@I1caV$F*{ zhY}K0+*BcJC5%0xk*_D$Tf=d$4#QT(q&zcKA>m`WNy<`5ewQv^V7FS9xkp-B8f@Hd z0wxLFPrA=g2z%|~|5B{NmpX?KdCQmOTWVZaqU*@^dKuHNL%nTo69bL$bC}#Cy14) zh4S+YP(f8cfrtpS(KjL+5~cXRcs7^m#o_gRP;RvFsy;gqcN~DMA}|ZrN;0Uyf-50- zt)0xG6*iR7n~+h0HPSuB`dZ!rBqZT$DF~g%=&Ku^_ZQ%3tG6B%WUbczYT-Z(7syW8 z&%tM0X4;rZBvOJ#unFGCVUVqn9Tg`~`>@=ON4h@-=4bp85}uM;Cr@H#NJzm1r|dgV z@@mk6+ISP2jW9+*l0a$fEn0)6?p_rU6Wg&c5B)Fl#x4G#b*dj0sY090BfJ36iZXOi z#kIXa{AXNt{*V1fU=2)@Ng>#VCIW9vO3Q0|jfwGbEAzHosNvOIMiqqB6~Slj3Kh9a z{5vieZTrH3)?Ul1e7DpdC>wzgVm+yh!a}W> z2v$@kvm7k;Rs)w|K5w(a!+QeB>Q!;M>)mX;EPv;W+?cdwuf zVD1`|3LJmD!?4P;#*HZ(%qNRfK8cEzqVUW#<4iM0uA-f<|2;iP`N_L&-!okY;DESr z*3{}Qr`sYxIN{*|#VSTbN6y+1~X zX@J`H7Fdk8^yJsegMaMmp-Fm!p|4#yksmi4q?Hb#aEVuT@hf@IlZ-GM;VO$)HfCjK z_i9^b9DEPtAfS_)uo7iB${pFnR2iO27@ewe!5?6Pt@XEBD}QSaXE@OnxfW)pBdk=~ zSp!*j?o^3*@9y9{Gs+uy8sKM_#+C>@q%zXE8u2_m!~=hYPkJbZ(RU`c7owBsAsap4 ze;x2J<$Qvsr)^uCppeibse1=mcJB0Pdy5w4|9@;<30RM5*Z<4lMVJ(oHj+vu?fW*P z1(hNRmC~MemG=4@gZ5CgFN)GWE!sDHDpV)}oKmTE-?Gkn^6ZF*Y5M84)xz5E;t zIQ*Ye9Bs1K54U)^gCT2z5Z`0gdFJHoC#x8(3v?%6L{);fGw@hx=49 z@$v8+1^9+evk}lO0;hSxkMVf6W*eNM8XxQ%-`&GK&|%VIG~ont%^4`)yw)qS!3Wm_z&54nqrD&~lQG9)!Qelkhh80G*hc>0bC z z{_t!aAPi6D`&diR-w~$vdY%UP{9C8r|PxHNnYzNeo7*^P58toTVVY;2**yr;c0f#d+pE! z%4wK(@-Kg#37!4)9Qv8V@e%Z!a?>F<)DBRjJa1}hWUWdK#KQae^XE6S&BGx-p0jI* z{KKR9cdYD!F5EZF9hp$#9WnrzF=`#5srSZ?f>8BtJS(TQHI~wF1#uQq_dEqlwj|+VjJN`0%;8P_nJpsIt?=kl1X?87c4stR;E=a)<5+0*? z$6EHg8MvfJ+tpVm<}fNB5*nzE_g|Rc-GA^mtXnSEE^BQGK?xCj1gpQF>zijHr#yf< z0_&4z-kmox4OF3j4PwGUK^JTLwRY!RE&Ck;y=TAuBb9a2FX4wRdGGtwHoP}eu7_xq3*hm7CyJMDr!CpfYl{B8akhI>e=O<$Jh;;|Ed9z{O&Tyj-*sb zKqN$DAKSHIGuhL`B~gBf{QBgcMDw}{+VhB>9Ss2Uj0hol$0=yR0}Os;_VC zl@I}lkTGkyCSc~NtNML<&sofm?W&Bz%V5r^Nun8-K&^#?PI#h1?$#inX3;lcZKV44 zZmdLP;Gqi7@`&7RC{DNnUK&uFFNlFoqua{)G)OfyT@suKKNT!0Xzq=|7h&aNp!*Q> z?!`(L0oL&!!|<1sDpno+bJaBu)?}kA3ROXF&V9aypA*RqX@C6)(w%sUn$>?1O54bZ zq^s>Xk6c-gNAs%ZFthTYY==0i{PRaC9r1EjQ3iL>#!aZzhi%n9k-S|*2!$<)P$}u? zxkciL%D>6 zsDOxSSQ=+oT3nQcw0!1y_2ZDlfgw87*28s$@w(t)$^i5OG2r|4!qcA3-mhOn{r07V za#Qy`qVmrTLxn<5 zZKOpNU^|&$A~oiBVm~CN79Z1cnbVkX)`sjT{DAg}0&&(0xrdG%(Q-6p+RN66l0=Kj=3VFt;KY;1S}Xxp5JUmcwIGly-t z+tyh=iZC_US}8U_k+V*IVnFx%;&C4@Hc3h$W^1tx@10%3H@b0Ae(V=w&G_V^)0r7p zxq7MqhitqC95$$V+k)-5J}zJ&XSOozwm#UlE`?9`zS0ZV9i`o{{C4xjY*m|&8-JR{ zKo|4k74PnYue-f$$>(%q*Jrv)6kcZ^jQYY*vW)MV1zZN#EHM}Ms zGrhF1hoDVbXm80n<&?yNc{#4V9XDFRA+y>;&5B*4==ETc*HH@Lui&Zz{NvwqURGZ~ zdLX?TkKPopD1@tgs=e#jQ@_Jl$pV4%@30{Sj$kdQ?~c-5@&2W!8cHDEKs&<0U{egA z^aUQjW2?U2QVe|zkY_E)!A!btQk(@}QTnVju2ZBMs|_IlCHGVtGR>I#E}jToRXp~x z5ocj6tQ$O)%04EV2p>t_Q zH(;67a1zX((x;l!YtZe!#dSTL^Cpx(MqrYPMXTMO(NW0!k> z=q3jI(eMM{C(L;7}S*`}SpW?=(xcu|$m`}5la!$AsQ@%5^ zbpcuVBSX(swZ^y(H2r|Gp@l8#H1i&&%ptEY^hl$z>*gj4#6iY)wx5Mv+$?Vp56$m? ztM+vJSy5)S-_ZsQfAMPpNO_=%odrlLih%e=EH;;VomhFJ;tS1`?~^iiyz<(c8j%)U zbuAWRSPLaW4#6yOJVrnblAlpgNw>|F_uAWq7pkIdy5)d}NqrpG^%4g@sr8b ziy1>$LY=uzCwC;ra@V%xuu4!qT>8?k9uAVDHz#o(z9}k*qhKs6Xx^z6-_jgyWW2RW zx-xRvclNnXr88s{ywZX44tbx`gz2frzSL+T4*FOpIf5nqP*Sb{h>jC>yu^bTWMY<5 z>NV=h`--l20AnWhJ~_p&L&E`X1pM*WSPVay2a;xdE!VGK!EJk3T)BQ=8QK_6Q%;PD zFLA)|KKZv^Wtsw%7uS*<^)HNC23!gqv^6f!2KLj{Ll9mQ+k9V}w`=M!f%}vKo zQ1t~{L5#-trws|sOvBe7j{z||8LXsnjT{NHAz{x%i3bv9{d-v1D}nXcvGy_an1-B* zIo#+OV(y&Da=aU^6T-6z!(U&Sguri^tgHEb&{_->hQh0=*zv#BeaEOycQ7@t7T+6e z;(-*2sNIp$rN|MyEpb>(%O2H+F9DEE;re7jKxd@Ix8tgahDiq4hwu0_K-aDNpDal@&o}jdb7khQ1$~|4+r+@_oPJM zzm4++Vf>u=91_m8bXT9(;$G1nBLScN{i${HHLX8J-_f;9+pbDB>mfU zjcZUjrv0(&zM^i^U^T=eds6J)@jwTN&)*%UUWWMO4icJs|h5*Yt(P{TC;{sO-Hzp$k@r+t5t zkM%?)?7^;9QMjv5OJLVn2txa0j}x!ib-z5s9VZOi#7c6WmOFFlf&Snz{O6_7&#x;9 z$nJO@f2>gm>#zS7sJFtk!ONv*_+eS)wX30=z(P@Y)b+qL50GqC?3N(mSsS4p zOzMHkRUeig3d_+AM}$7c>c@JH((ToEt!=`h0RmCWyXuGI;<|+8U`W0+3Z7Upox)`_ z>}^)_Lcxb4q^_=^2X5BxF&xX>&Vvy7KqQe#{Kth6FJWbL6j70zYqEgAGMW2wpue%4s2=ql0BH?FEq|1i>-G`kw@GAtj3=nDJs~VmJ;m4? zNz*_{1uhBLHwX)`qg51cKl|*N#uT&`y5wg2O?@Ux7RZBGSmRhM92`%C}(9X z79K)2Hj#|l3gAia;e?;fDLw#HifkE#W%f(r1^g*XY4;oyGx_wWy)2%TH4#6<_?OAe=7)WKaUfbtO6X1})b2?<|_RVH# zGYOqIrE&(h2OgA14wt$3E#Os)Jr#NHd86N+1y$}#N&<_FTRfSBm~4@skLT`*KurTo zPN>K+d2>f#;4lLn%+P`(D)3tntOXUGDFWw^hHNwJil}8YI^rEQI#Ysw4AvuBifnY0 z#s?F7L_63+KwjtmDi&me$m`gbXblG;>uI!veEiuvQyPXbP2L|e%UMmNcf{N3<1$Ix00*B%$=!Pr}1 z7kj?uvC~A`K0NiGzK`q6mC%KUBEYlC7`K>5K?P~5w>HWv`D<}51MdW-{HAEbNU7la zm@@t_(i`VeJpc!wqdEt|x7-m~3pyP@++8t_IpGxCYASzBw}ob6hwN~5sKc0kR-JLd z2(01<&MLKwkKLA}MBO|$mK*E@R0J^E-7~uP{g5KG+uCtZQXT+E|NRVP!goZcPP=(7 z1lbnwOaKi$76jpH(QejVsbNsW0)DQFJsSC9EaE=842Gh+48P<}B*Cz@XBR-7AI6-t zrB|vDki5F3Xs1X!V)PU3?XUiA! zq8Ix{haGzshTYvM7Z>F0rYnUDd%}jQf_T~ITeh;n7G1F(qLbWhX^!mDhEGO+os$;8 zWQA*yId7O@htZC@?`9!I?`A{#PbeuxdPGLnkZYpee-!>8>Q~Ir9i3VB-E7M>JSkDc z1oc}mzkcz%wRl6f=+S&FiDTV`r;BMlsC~uP)+z=!A{;h1wyuEnx=ndqPP0ht6=#xX zhqDZart(5d^L?uiLxu%op>BZ==6VBU+Tm=M7j0^2GX= zt*z%rQ5VgU$D!gGj1f6@H@+oSqp-n}{y}jU!}$Vdwhlki@IQj5uFpxuRyUH0XQM%Z_upE`h6ZfP$_CCO zFDk!{<4WP*_HgUI1#QnPj+Rmjt_ts>5z^fH{^`P621rPMN&@z-HTglH)jQbfz zOkWD;n|f7~x>}Y~BWI#B`p1}&CiK(Ve==AmC{f`g8dalhrb5=pNZ?yn&;n2Gsbf`zfhgIn7T_D;s~ z2?|DDp#xLwoovx1aazLZ7IxqDP5Rp4W?SaMP17yd1+>X=Bjjw=$NFVv{pwuqkPS zp9OW`-+OnJUzJy3?r3R3PRP5Oa7MqeMb~O$6{;->XT~Br)%#y1(xBgu=(f^(UXbf` z5;9o*=qF>|vR>K5FRO6Yt1(WFIhB9z$-Y%Qklv5W z=t6Ealr(l~l#mj*TD&XYEF@Y513Jf`fG~m5w06O1Ab1$X{$~t6uP3xE%pmUf%sYq> zIo7YQ32%g&x~MeOu7$t5S0Kcea8K9fTVaKfXG4ulF%7~HNJR++?3hE~a=?wm;-mb~ z{sH-PBsiWuQ-Ny(p}nJAj4Ilo3!cdHXEis*aB4+}?CUE)TH`u?cvdX zcYsm@qQaf__VZ@jYW!^ypja#2UC`^%m9+9a-o%@g-LEmGY)x}5T7Sy~3wwyXJXku> z-yd-!tamdEZ}{3B)+OAZHA-c&vSSzc5>7G7dXQpzt}TWLp+{u0m&g5nAhoJq3r~<*cZi z1k4X|MAn45j`aEsro{==CW9gu{+f0@RUV1JgwnLc@go*ATcN(OH=1QP{@6NmKVwL4 zCDS>Ko{ny5NsK|2!N_y;Trv7mFhNOF2n-TGV}4zMWGm0%fd*d&BY;w=4~~jjp*W(}=YTo`Y`Rfo2uglT5B z#dKZ!0xGH_{xJ_uBh9!v&3yq=pBK2&0^?h_y2R_CoSM-B8dV`ZviQ{^Ru-j(rEeyY%B}^N)54-j+{}Pq~ z#~`*fBNvUtbPwy@^MxW5f!#NA2JBQB!pIlq11mD0v|qg;d`?#GNyUbEsQP)I^f^x} zH&opUm9{*+?Tqi)N?`#>@|1xLtP z)*$r&rpmZ__FdvxzqbbyV7ySl+qAp)^mz5t%ql)Not1hH@V|UHroIl+e+neMZqb4d zCeKAYY-teO7c*VS-tc~_-G1Qh*73s0EpZ6Au5u@fRPm}>t$rWbQ|LOF>3rQS%E9{{ zg3ru%Zpr97TYet1A9y*e^DdPgt54@D|6$e@!Wg=`DHya~C<5N*nnEj9@11nFaE)tp zCpB!s;8(4ut%)Hhv3+%%MK^e2SZXLoK?@zqM5qj3k@7(9IN9t%g z!GWe#8pE>T)MelbwOT%5kst%)X4;x@VMif7ZM5~hX3yNPj;@-Mrhv!~LF(L0=`~s-V+vW76M_hkA&5XZmq+urs{Z!cjMG}0Xy{E}5`uV}-%}Rt zhgP8WUg4VjkXl*mR?-;^5&8AMRHoj1y30iDHrUz1T(fi@KI0Hl#%kBAl9cVC)H@&l{G_82X5S`15%_r7d;Klt3`?lUWu2Hw0Rh@$gW+WIPxfLarD&zQ4ys_j3bhNq5ZU9Z2$2Z=@>l_o z)KIz=BrV}GzS)*n2-^ItWd4W?(``zsCwrND>cn@?216Sd!+H7#dCEITL>8a6g;IcLEomvw zA>C7vE@MvIL-;a2Mdu;8=@nTXIJJ3;R(#fAjMiRjlI{_5v6Ti@{krn^QMN6ywUG&zma`=jXg=iiXR>40L@;mL1Vf71BUwGeY?-IG)3st-W{{q_Q#!QVPKCwCU%EFaSnex*AbGJbe*G18iJkBL8BEU#ZO^=pmjqIC)1rwcx-ijC)LaH>-H zKG+ro9-yJ0CtG+AIN9Mvcy-uatC?53pZ0hksjn`zsEt5(Zi1vS(Oj)RQo6YaqmHTo<&Y*9W=a%o8luv={xy_cpx^!1V^hF?-)(8VBd6QxC7p&|3l-VPkuKQE$OtgJ zboitP%PCpLx^fa8VXhIy;0V|$OWmdAz!+>CK`%id7@%g(@8hQS|67Y zO4<-=cK%wOTFzS!g_RHYXG2bOy<2TqW}IZer=$%j9)D{wh0`K5McTzF1u`5_;FdPO zCraCSqT_-Uq{qfc%P)fbf{`Wr*{4MPX*1=>xG0`}3Zsl?1Q5YUv|H$$XVdL0zXf|r zJUUl9{h&OY=19foBA5^OtVujOyA=78I>P0@P2~0tyr|zOO*`({c3LZv0dk}eF+dwU zkl35c@rzB=-u{p}#BG;)%-vZ};ELm}AaCv&hQ%tZ9D^&)o4$xok@_m!!haO#wVPS2 zsAhuoKG>+)r*5t%H^-d9xrOw4Ph?FBQ6o}ADJ99vG?v5mu=4L?bH}X8s`BhdX32eX zmH%w%6=L5U$>4=$6zI1WeXzGz*Mg7#JTjw<1Ok+zv_8Ut6>J6>91O6Ta{4_v$e&tH z-B+B4KpWY9yl0+tDRS^?{;=BVgnxqRdxE(ngkeL7p=TeTXYd9D6t4okfBE#`ez7i&* z;FKh!Li=_@e9vusciQisCMxS_XeAw5dIrU%%=+o zQMncwI;~FxKaF0Cb3!tToSsa}v^T7T$Xm@{>t*VqOWT`|-#Ami>RySwB9XLt{jl$(0GJJPjnEym@ zRr<2M37SWv9JK>bnu}e;_dYef`DsPSvA-6|IT?J|Z?wqherjqGhJZgWO5}D3FM`ue zXG<)WL$`+em%mlzff!8R+mYh~T}X}@L@5wJi4>n#MyDbBv=?%1-``aZT^mM$!k&|@ zeCN{_e5{Z%Z`SYuDrm30A*5558+Hw=Je&FhU(usS}v5H}#X)i>Pm zCCuaPL9uo^DjHe+s2ODO*BcBCLFMh0dV5F-qj0!zFu;@79#` zOOJo~JlU=Whydc}8VokR!7g?Rne01UNFA9E*CTfmSWl`)jCm^c=1ID?a6`|T%T+CL zdHwHO_aV0f;hhI`yMH`@I629lISZ7jY4{wMJ%=l6b>Np9Mc=bU>$Nultfo|sd@vUw0^zbi(?c)*Aa8YGOa~xyMpQs3~k`M z)~DV>nG4sYZy`f6kob+6>TiK;20jj5fmo-hR9Yk_fMuMBFVuF>356f>XY(n~b97{E zVNXszL|QwoSuBVVKqUU<4dfQ(Ko|9c{Xpsprj`VhwEP(Rc>n7PrP97-$Lk@}UQ!PhoRRsSdNAP|uF^ zgmuBj>^}su5plr@1x`W*18F0YuB|MqB%wRR7KkTFGej_i^-}Q(e`Ca|@99f&!2NWwb zA!a}o45}jY1D}Tlcqn$aj1aLCCtIU$e})l>Qxfg}_yg@)?27 zQ!KL?zOTVLI<*xROJ;4o69kvyhr<98^jmj#JgP67MiD)pOCVY=ehS~1T|WFNox<;R zQ3@7R%=lLHFZa-Ki>w=9E>0j&lQXa0~a|9{1$yb*sn+C56r9a0n9Bg1;P>;(KxpzaxkhY!%|oCw(4?cSJ)-s zTl)%}w>)~<-rGU5(XhZ?+CYHMLI^AKXhq@+Ifyg)ri#Yq<`2%W!dCsZL*G5v%}$`m zS5)^aTNu{@PAV+s-)@Z&?UqT3+vgJ6u$UhbYuO`V?IbtRB@PQHzYb26kK;BqI+1@O zyppw%t_A4{C%p?;;*ix5vz^?tICI~{v71mf@#BO`9PSxcl_E=pn5Hy20<}8>n;e9kOwM9=o=ceS<05{5wHCP1!!Ug$>(kNdMAE;sM26|DSpq_N4e-hR9b3uFAZeC| z!CUK;y+2=`70v?crmJl^l6jR{yXJjzFg;}M+E%Z_=tS0w=;f$JB5flV8u0CA_O?P` zjn$MtgR%?)S6~o=0KWpp#jf0F*U*li@PJY~4t1#FA|Zr5{+I30CIuW*{^`q}W|Wj# zpNUi|M@U;(`Bz}ZXZ_qVxH@hq0dnGE7T5}mu%GRNr1Zs)IVibH5ew>sg$^YSD0FZZ zL2)2!vp)yel|7+4r2M0jaMq__Doi#q@vq1|TR%K+ z_r@0#6Xb~f-LMZQA%3de~@D~u13iZ)l{B< zHwW-xM7ibS56+pQ7<^y9mOwqK@hEt@t?Y$StwD`nj-Im~g1#v&?~)Utr~KRGc6J4SIoU}|hpi9RcXxNEtp&R6RY=;g4L$#1;#!PwO<@7R=laPufTQASRMU>D z(s&0nlaa;)2NWT;=LKVj9i=zv^lfUS>pe_JfOS$}-4siLmhrSw?}ovBAiL(N{f*7c zeB{-VJzH8@(9}YXTj_cVp&gx_x=T~tvmfEt_eQ0PDJ|bx!hp$7$-M-y7)3LKkJ4qP82kKC)-j6g+HZ%%`i0r;xm0GS3B{1|*Yl$&Y%J*?u3C8uev>0-n zl(i6h>>{i`$)NL(PfYa6dT*lJoS@p!(sKO$`}gF^Fn6M}b6_+s(ih!%f!UE}+I!1A zLi-=iW(~%tEPo6x?}?W`^52NZ=`V5uo)#(tJA%O zr+tevVMYkr9417Lmh4%Zba!w||ZQ6s$j{ONDF!OT4LormK z?A1B-{`ZTEpyt=00)e~efs;H+eA_PhmQqeTo}*u18;DowE9iC`?z)-%Ko%`$X@h08 zzXb{%4NKlM009XlIbyIRJXn>n`0tb#KIa)$R6fbupAk5SvVa!WvE0GJ=gwJm=h<3S z*9yv+nN9u@7|t-j!?oDObVT7#aG)ONomZN%j1Y-o@^*LzwG&Xs8T6}NmlGo@jTs{i zvH_3_pr)U43%Bm`IF9NGN6F8=KPH`$&dfFIBL+dsF%^3$8d{n&l{bF$x=(mA90P2D zPU9mO+Vn7bY5f9&RfgD)?272L3*eiA)E)6fIh2w=bJt^A2g)7+Td#esx~1Oxi41#; zRr?@R1jD30I0a8%4ENi!;D?c`Bcs~oR#Afv<9uOfkU~7^=k?gd#l^PoeUE07EnL|1 z^7D~%P5>5j{)ihBRHlG0^DjZX#z8GtejJq-B%-q9)QxspU7nIRyyJ6`aDlRiSzVHs z^Y-j0LU!r4uKUpY4Xd$`XB}qWAYoYfY3paFba}>GQ~lJP|| z52Q*RaoYhq&3 z?ywI;fF0c6-80fGg^pv&#NM`W~yKTy%R^1^zr8Ny1XKCmUe^FG3P ziw=eO5JVf;MomX)nTsvh#TCr8u*BF-p^`wg2-KAk2iIn zA`JLX1bJPdS_Ux-9PoZtAwb4gd7Z4*o$D8==wJWx$8tpylBo0_ z5-zV>eM!qHr|SEThlgai|YY|Q@(LKXm8M`M_GOZ_S{&3p87||Z__R7fZtA0j7W>K0|FT!rTK6Wbc zSo*bGl2e7+y9>80ZB0#HGF*~q^{W^-e}7PCZk@eV@biL$i3#;T|C3ighOSf zg?6jiIlEYZ0z}UiYfD!1TMO4nNh7>ovfn6eEn0jZBxI#CmK4p)#q}$?swb9r7g}zy zI+AA$=nlu;YIsoi_2NmNltPw)*A=ex`K~sAxApJU+49e-`6(>j-uEk6>e7)41`apy zNnF3DpKFZCEj%64DH_eDvTXwXz)S#J`*L1h-g_+V{3WB`E#511SlE6yA-=#)VaP;l z?e==$)_UY=|Icsg*e(YvFq;=|EH2j=N|5h6Oz(|uCtgy{tP9&vofxI_u0Iz-5xjo@ zntvTL8vC|C?fz%gG|M|Kv@0v^BchJXwnz29dD-z-4EQPd#lw8~E4x?^)KAcR(siS( zBH8^1GtvbM8HKZc`I{np{pdWc7~*|H5>qaPBZzoOwUxhIE_H2VIj)CtV11??^Ko3U zm?8vzt1sd^n{tm9ktS?koGSF_do#~&epA|n(xyvB_wIS3U2Te2i7{zSVWl__?;pjw zu+<+qQ1Vr)eE2vN0v}NRKw8c9g4v*=Vrg@{X89unKefVcdp{qg!=*}xg{5~78jaMp z_D4EudAH_AdS^UhiO!pfxvJS3Bytc|_X3-&D)kntu++#j2}$VR(pW&(36!7D#nqK1 zC*JYp9X1(oQW8G~lbWuCU%!v0R3Fq@{+(9Lp6f8ZSdhp*&o3WNKMuO7c5KV>3UFe| zUwcG3{3RVY57Tr1r=skZv$gI7R7D!xangt0Yti%XlUMBLOJQhD39E>tv%r<2#rp-b zpNf5Sw`NSGHOQK68GI7FBbUI4yyIxL^|?AfYl7B~-_7bYC0Kul+UB~!8?)Y5xy@V(r|k83ehi`minl6p^%XUu051t13L!7OE(vnD z4=r|Lg-<0xB3aY9LVuZI&9n)4=`PW0u}d)3_f}&9(9=>f4u%S`OK}&{rsX7gn0fzz z-heuz>G}=LDEZ@AS30pQ^!+U1jB^a65AR@ zsn`QG5X;f3$Lx*vVL(38!uPB{i}dC<;ZHoJVdM88oo;K8q~WXz|A&0|Zsn%U&WuNC z`PtwSE>3`eQ;9jU_AGzxEl+c^7hp_rAItNTRb>vv0$*9hEPY446QKjk)!kTPRr@-I z@;Iy9)(4Ho|GLYjB1c3Y#b(V5IH$10}jlzBD@>zjQ2_;@zB-kEuGwX@gv_Ybc{ z>?=ffA38YP%)+_L-px8S)#s&risvz}UixAY%jH0;p^4O^YTU1oX1J|&?BW6r5KR7M#rzF>5u7$>|@;6d^qOM zqO@p!86lqy?Q#Bwj_Yu#kHA(Q(<^d(DRmsPezA(7*M_hE372|r=9%8Iv4v@M52NSS z%V7c~;Y`f@WAcXF`D9$wsPrLMDNL`?ed3Vq0Ek*ZK%(ejxNLY?N|l+Wc9O0E()BxM zTT0iKCE+{iB!B@??*lmY2o*0BiEx=f;gd4AfQ1|H4e{lBneX41`7KTn|Y z+kApNgEC)irh+?z%i0oc+rG<}H=A-toWCZIbU83)727k~9zH!1OEW$&D;HbV^qCxl zJ0L9ZjyO$r@+P{bRjQ{RD`#SEjO0H-!3?WZpyI~p3s$v(@v*U%SS*_RgdgCCQ*;EX zgW?c!_ijw5mcHaC*Bzd(mxG(yV{bgI*Cw+okTtVjTZG%;6npp9mp%Mc4EA^RCaA!{ zSHWDILwB@jBGtd60*iB28}EaSDt-MoP!#eq&A)U~k9nQKW_wub(wOtC zO+n65AgUd+1~W|SmMgkx)v6_Ts+U1k&ERxnTL5;9oSkoO<+6TT)184)MW{a?S9D5{ zm_xeVWl$8#Z~N3!JzALe*%Guf=^O?Wx|Z9w$aX&(?icOM!X`SisAl%`^yCb-sGr7- z$V~IT*wVnMdl0=@wmTj^To$85>vOJGJVNC0L9Vy@zl&{FX5JJ^g$_JYpVi-INj|Va z%yN}fY}(n|*7CYvk~H*AU{Qv0$6-@-hKfb;P>dmy^~S++#lU zU)ALX8d!YGO8Rn}gx8-4w>FmsO7zmQQ^JK*=!v;LKA2i&f++zHukj5|aI!jI4&`F| zV|nSx!8qNma(-_6@5&fsNJWM*0SD*)E@bd>nzGzyM4&R0FJ5nK+wAu+GBR?%kXmq* zoaKr_cxf>e6_vZXd13QA($dbSYfB(_U_rfeYCC(qT3{#LH4T2V3V%+|Bo z-XkQ66b$}Y;}tuzQp9>9mIt+3GnEtAbgorJek;kWXVW0bai-bb{br8kU9qj+EvJ4) ziOK}kM4frmQRZasuwc*3;s}XUa8;xTWoULWHBE%jam+`YRY$mL@I_)j02G?%yQuU{ zidVdrtEa=DaPE3gf`R~oPPQNDo*NtCD3{@0j-|+=tFA4@mG4-88=^ecEphoA0K4a* z{vI+1@9A!aDP#($zP&<1O#d0;xnqZ{602}YqwFc11{v~~enlHj4a$Nb>m~2Tg@q{4 z`a(r}otN@_s;wJ+7647XxtZJcjJxFKXwK5`tp5dQa!)vx^tB_)2@ zkk#HczIlDVm3&Zgjv}rleN+}hyoPNOTU)e-VvR*U&+)-~(eLBI1`)2md8@T)Z0a^! zn$N)CIQTM%NB{CiM$T8Tl(xm;*0L8${1BJ{pI7q~>8QL!GnLk=vwWte7Ie#w(h+YI zr?5+%1ny}@{X~kH7o--9%!|)qHLdH%V<5^T1Ymb1lz&SxPVj`o(b)`RA6N|gBuaSJ zF*IpWp0H2LWoZE6MDRaHk6XpVyyH>i^Zc^P%USeNI}dsuOEYeaEtS#mMg2Iy%E!dTL9UJ|W3oQg3-$+8NjacLlq+Ma zPxAjNRD#3}zeC-g9&w!nLcak?S*~1+Vq^{=c%zgV@e3=eT zdp!%^S`UrRm#|E7+xG*gVfyI_aOQH;udS@sYXzZ(^FWqGFTFhKf4%FHLXEHA1tqt+ z?lk!DlgT(Hq*6tjQhUw^Y{R>%rdVW_qJE#~U6%TGWC>6FE@4-AJu6{Do|{yc8j6nT zv}7VSwS+Y>WbM&S;K@uoGSAu`h|&AhT1>yxZ3i_o7j?=Qu6R~3z{Px^KiGPvdVcT9 z9fGSzMDD9EKbHeyLN7I~&cq_tfZVSiN85_66iMmDA)^l;?&^iW%Efo!e(ziJq#6BY zRuk#((Gr^g=R6Bz_4D=^ihiN zP8D190vxNG@?TCtxg4W{m6K4kTtbB)?|rl29Py<^cT$3Kt^tSe@zjF3%VVUn?a))E z>;8B$4u{IS0$%O68X3(62TO>GPQKmAAeskRbOMvg%g?goe~3l{slanc5sRHzFMu~s zGwhzD;v~i8%MQC=!u=N<+I(kx98HMj#mx&4?Mp&&|!PjlxUxtaq%5EsGVtQ>ZognY)0SGlyQu zetHiQ1^cMn4?z(p0B9Jf1TOjjR(Qzy(P^Kafnp+>))Vm80Qu$xg^=0lYlhFHpfOpO zki{Kj%Fc4d{@X*sHs;(l)(WFVM;b9_=emQaC1f%uf%h?*< zdZ_Q?wuRs{+H%pkt0?Lea`o5$L=hKFu{HGDmikQ18Rk21-mZd1(wVL9aD;n^pBA|) zbc{jN&7aM8A|)JaY6MZ0cIT>4)^t1U zKP(fwwS9ftW&D?28%$aBCsQeo$or!d5gn`E)X(dORpLs5^1xTN<&I(nZ_0}|k|+Hz z;*3yr8N#91o^(d$+A0~zfB9iRbEc>sZ5#WL!$r#jaJ0O+^&p z*Acn-A>gBRbTmiu%Bp_8&{4o#ePbTpdF=Hzb%|UBruaHo+)QPHLMc{tI4rr8bL82> zJ@n6a5m-;C6q8*wx%e=9VPT=nXMRXVA`-H~x?>-j)hZq(Vs2aNR>$(U&&yT1#Q=en zuL1;G3$zNx2`S|_2Ua%Q))t&}bGWeVH1Pn+ZxYkf|Hq8YY#MB7j*)oQJV|vEpUR&u zYNGyMg=oLE6S%&+xb2o(y1wjqph{AD7yd;H4P6W$Uq#ACeDUCajhJ(eU#7IiQtvf2 z=*=Bx0Vm_w%BNbko}~Z?R~8Oz+mUPJ`DMQ0gv(<#jfz+hQlKx7 z(wsnEC8k+7Vin_2rZ!8cqyG$zZ*dbHh_x@oiMTI^fzjY7p0$Ze(wsoxP&-qD+hOjT z)?1)ch`rx`Kt%5_zg{}4=I(fQ)@#r<&}yK-VOz5tT?Y;!q8gLDgFh^#Gf_dev6!^l zM*1JM+W#*KN#Dp>Q?t7R#iGiQbd>VuDL!j~g5{Ae#@9HuY+N#XxlUo6^dW+_UUN6a zRbElsJu(TeU!OW)SSPKfh9z_5TwMijaOSchPAcH>mhPwX{WZ6i0!dg6z?;st)GwC* zf9C5ba!ik%-Tna)L>}w1Z=d}OAHk4lCIh8ZUx4X1GozTd4LFxTtn2CM2j*i3Jll|i zw;%AadXoyBNxkD0k0^P)^g^vvN(SwJc)-4+eA#x=z*hlng>|Mm`LM7zrxnZEoyHoL z>Ba(J0Vl}Viia$klR0Sqr*3@|DWx_HZ?alSfVf;+0R>1;BNjJclQ=8NS(M7v=+|Um zr#UZtdT++P&@$LyIT?nLCEl-bXyk)&g*b;J+x$ORj4-OW<#w8f7ZicSpeBe+ZWV~- zlAw<7GnqGYmr<&TpqxJC+-EcakQinx|FZ3cqH{O0@b^4N@5wNCAf<%MKOmmU*xe|u z=&_6eMnM*Gz`^C~!oe6;`GugYfhqarQk01X{u{k33f9jtI zMR*8-+U+C@@v4CR_hvwQ_$P6OBD2WZx&29#x!H11V!up)aaaH0HK=hvjaunyD~RW; zjsab5f{CMWzSBXg;gA^6SgJFs!4sXIcz%i`Wv^~pTA*MbqcqRdp(*2mdRJ4Jhq5)P zL9a1>bsr(h2Ie6#nzpB(nY!~HM9n+iqkR@|os#T@?q#$m%GZjr_!+di9ntMm-6;5q z*q*$uzu;+7)S^H0^Ou9LPq+w{9kjRQoJU$^`TVEzaI{=#p6wfJ!Or8_ zPv=3(uhDs2#X^(*P-`95+`oj?zHsfc{a`#~?9pjNBa}Mxa41{9zqgmW0U&D0MQ+wl zG9Bsq@?C}Z0MJmW09`ZicVndYle}5v5~B1px~+tu&5#ElJRBo_UGeLBvTYE_4zv{^ zvIL&}2lnhDiO%OpRb^3}4VU2Nbd;aSbLp0Q3w=RTCCi&0C8qlBQ_DuFU|_iwmXx~+ zv*0ck!2`}1AsSLt+7J-T;AUb*>lTljb zRKHru2a_D(%~5~uBu0pt2myDlz6#>+4*yIb$adr3SEa{uRmhqdl8V>jN`l6OxhR0A zvBqTiUfh*Pzf>^;Z!%)<1J`9!h`TWB_QbdNc>CwZ7&d5F0ufQk87ehZI*<+@w=oy) zx+Bow0Z@q4eyTtbIF8KC{)dS=jUBP?5^aqEtd*Gp@c_khH4Aj&jZG5^-(0Xz)`y`D63$AY!4x74pfyVlTWZ>=p2llEeSe7 zB}R!N{oK}86jc$UGZ)jc`@fCOgGFHFek6CK4B~lNFau!b4q;;X41D9eJRTa9jln1j z&$gT8RdxmMiGB|LU-@&E~esd2{Q zbV4YA%a3!rcVh`VsHp_H5qVpMa4`(DQ@pg06r?&owV@9}QY8f`2jKAmx=#68sgg5x z@`&3W1!ZGEK?KSxU9c!Z*C}EDzAl#yF~^Wnwd{yUqcW>-y<9NaaE3iml|r zk}JnhJ*3YeMq}(>k8j!&@bHWmB@7s0LMcj^lzU9yAeo3~z#JMlEAZSKkH- zYFVx4J9(_?=@!td?JL@KVP(7JUvWUQpG%02gbXvf-$;!9YhVp#i~yA~Oo%* zM}e7Kys*$}rtbp^_E4gqPjU6)`nd2^0<2QYrC++BM-nC}5D!@Yq|kMdqBQNVsJT-T zME;MhuMWsM+tz*^M>z&E3P^(^3aChzATcAM0s2{|}jNWf#&IM~?jqz(ZsLTf_ge}pt4rxAllko|NVYDO$ETOEl4k&$nU><0ja zro93N_qTao)da<37|$spXd-Sffb&3NCY-MWXDShNf3(*J4d?E+JyGy7&xMD}d_WSi zVM#wV>}6-qS}k1ScO&B82%w255`=aO>~5Yv2XE=Z!&iJ7(h+*dyM50OrIXW?2tp-_ z@!!wuta-2wRM8pW3-(ATLRD32#U5_Gz&56=R>02e}^xiFeVxScOy?!feIG7U7-?SyiSq`Eo`AwI8TIf8as5T2JoLJOQkj`madFA=`f zfEFn@I1y76hft0}@z%6fBk@Se%h$$#tEu|5j|#T499#7ccY}-}QhhL=!L5qCGJZG}j<}$-5q42NPVr{@8?X+4G3!N;*nfLJbeysCM1t^y#u{SUAK> z=ufBd2P>eqM>$960l+s-&3uUtk{vIgIDmfi?(4nc5b@*uE!2Y{MAbv(VRS;zpdL}Q z=78%B{u<9@01{lef_o!)?m!75eLX1tpm8T0&0~(Be9VHJuq~H{NsH?G z)1FY{i?2FOY5S_7(1c4CQAFhngn0dWNCMIhI87=X-Xpr=Pq*$Gk8 zU?SVqpprP4aR4y9igPSicH@JfvJbjQ4Nqq=+KtK^vYLMd85$YE$>nf%;3nxJC``LU_3#wBG|MHa}kA5xB$130O+nkJ40 zl-9Iu-~`!0Zp1+C){Nj)lxxQIAGTAUrm``lvFT#vLrG^eTrB6jxIGZ4-}K*_z>3K| z&*i{7pv*TS4Ad$p+sI0g;6Yhq2qxPzoN}?i4mJnpqi!Z783NZ{;t64YJYeOS5t60o zOOc-F$}YLQ6|gPB+a{zx$)<*T`92zfkCL#!sw~}Gr*DKE3{oRzqR0`eAR#E;nKLV7 zEG#UH=Vc?eB&|{W3MDW+;8yyR_Y4^T0t`b%s;I5^2{0dAFYzP~Lxpz}*mgoj0<9qE zKUCNbJt_})xUI*V;oF)^`p-mxZuliL^u&UgB$V!hn6@fNjykuy!2au!=!BZ)3fAfS zYzZ(k@C4GOKkyYsicO>+fcG%gQ1rPJDOVT73-j2IwLu#Mx>&A(OJQP6;%BT-V*^Dd z(P4XGIn_Ql2|CO>L)ktIOp^5GyQBVaP=urVk%Y{NW zhBW3oGTDuigq$mg7}&R-7DP%8!3Kvqd58ZPw%wflNUH$rUOdf9aCIG>ob-euR2e^E z=(XjD8c2#r=|yo|2}zLEIgVQn zt1w;y#iu&$V8Hr z>;Z`YLC7QFi=>GD^&&k`rYusVSl9Q_DA1=GC`I}ry@Efqyon~9D=O{wVRT15 zk(*v*)Ch;|m=F%5(my^MPLJvjEC2&VxD^9&aT=tde)(8r@QUMkJ#x@`SH(2#hW;4D zc~Q?{C=*0|iExT;6=n zoyW^ga`YOk$jXUsBJvHxFR=rPiY!5P?teo%NX)$L9F%u(p`-sCMjrAW(doTe{a0}%9k0;OH#mr9Oz%x<9Iro+X5p{iTzJ|-OnXa3qFu`;%5kMPAW{9nRfvIgaw3Xr8v!|IUv`p`5s zj|=ipJA{l7a^*mU@FAsbtRm1(u9E*i)J&zGg=$iYb9sy5cv};diOiBB_?G^2sO0Dq zp+>hRDE7xnX(92DLEv7P5w}OsJ5!1cWOlbM7znTa+~Xxjhm5~yTxwgPf=v&dM93NB zu$dl#**4H7!V1|^a`Da9&Yx)FnTrX|yb+Ed!9SOn5l?vOK)8TGv9tMsEUh+)q3ujLs)GAr5a||z@*VKav;CIDom@PDJd!K zMUHFK%>=)Qq9}u-&f^;Ba+bsLKxsiPze5O$nn|F`oXPb=R4NY9sm*!C3WcGtfrq0j zzQ7DAmowFw7t|efNxP7JwsHKlOLV4aZJKWA&_zus>y7`;c3qT20Z|v1zOii;KjsJR z1op*g!YF-Gx~sSn?5n1hRjgRxC%&Ha#>#99Wm<=OJ$UO3^$y6&4t$k?-L`)nga(r0 zu1$E1f}DUO_$T^puA=xm>c(75QfCBLj3W~+o&Ry35>hzm*#Vt@<&Zo^x6r?{+e&w1 zm#7(RUFYWF;;P~IOK^a1+Tnh+4S&TI1!Q`)iHWV^vpDfW0u~_&IqmW~ zkv0rwact;HPnJPe`iUUSLZC>4lp@uS0tYcBDDU^NwJ9bnF8x5tlDVxcc3L7u&cfjH z6(9d!lFu>rHLWUdRu4cG8CZ&i|9n-C(b{0NY6CvMV_n!a%MT4bQ0GAUNMXdLvaOFT zmY@IB1P9X)-!!5tlRbM5u(lQ~sjyFt0X(nI*N1)P;4Y(0#!uIx+78kK7kBJ*A42+o zskW7#A4V%GK4xiE(QKaI%7(j*K6J4IHev%scn@tSQqZ-E)a|>`w)PwX$B}`m_Co2F zu$&40UM-Jh$4*E$;&z6}Nn}rPT*yoV+6rkf4#46^`?|uS;Wy!ZO-g8xMA0Wy%KLJd$ZvX>#=K{3-dMUuFNnv4`5Ah*Y^ zZrKcF28n|~Joi3IZAFmM3BZEVKDK5>ci2OVy3gxf-q@f#3{t1INg`MEi~Q)e`7=)i zbzSUbIIg24Ux6izN`)UyN3Ca(f1@CTjYFC9rXov3LuF6mRj{kgZ(R?^up~C$ag!`H9*M<1ej~BDuxu*Vg?nk61+2%4?<^cM2qAB|SN^uWPhQINfJR z_uf{d`NcyI^<`H>Eq~b6>G8a@79;d*8U5@9=o@)VV`<@Cl$v3x-;=}C1RHyndYre8 zH7lAf()m3g%iL-q^0Ie_rRN;6(Iv0nDps~+z^c1F1C($Ea zm)p~LZQI+n?!er*%BkK$5x94joTnD>_rX_PykWYGcd`i06kAgvW2AhmR#lm()knU@ z*iMssV+Jw2p~3NnY4^#{C&WLD@PjZg^eAf9ehDVX!t}r?liaM?H;e(bLj&1n#f}e+ z*aUa$_m^64X&*!JH}t5TdL}*i=>e=HhZO428d=3A$&9;dv6C6az8hqKtrxXe)R|PB zhsl_IP4L4%MJ04ge;hpMBlb!VY~*?SWJR5ncosZ4vgxyI_`Ox}(*64mo$MN}uh;Rs zcKomM!(wJa++CR)b|=I8GDHtJuYJXF8B@Z|*>pAY+Ik{%;|9JFq*>>UHWKn=@2^ky zJz?p{<~b0tbp65o{33dS}8A50u+k{O9ztw?QRGI4{w^jB5cTD@mWp$=L3f^s`hHSUc?r^?w;36bjqvlrN_%Xv=5?c; zWhAQD2TJdKv@>n%1FUwDv61>y@Hu+(*S|A4XUN;|Mkmp(k0Q$2AA5H7irD6luLwz8 zZpY@Pt-fri8nQ5-$TA^UJeNyNON(E$C`#w$?L)ri_IUd#&Dr(vDf8tnGMO~4m-sIm z7!X&1r%=+fh;T+Mt9?PB|$#`qtm| z9q=CS>}(l1xy9k=Df6vRI7v-~`p$B@m3}k@+J$ZZfihXG4$3vA0+GGB4=^E_X?kaO zEveVW_FsH(i9jzCOPcTG7ubBP&vUHw9vvGCdNixe>O$j4r*6(Oh z*z^C%qd=;yo5CX{_Y7jU5*GvnZQ85X%3>x!TD%kpo^1`v1gm@04i=4XFpwv9QS^$C7g-qRW!+#u+|%&jL?(G&?Sf zydJT(wXIC<%90-q^7DQsEDWIBQB4X(6p$p}LaPM#4YAR}!%!2X+)XkNojU2hLDtLg zq+0?bbCuo%woSP#oST>EK<*b9qm61YRApT*w6@m_Bjc;KY@q(==TFmZcwW_+u-5zF6n3~iOW8l zqvGgItoGjbsR!GQKCQ`5VGs!(!@rk`@( zReBGuv-yr%0sGbNbY86J)yRpL9LJ(R)0&(V(md}~o7dj=mZ!x4bVcZm?S?+!vtNOm5hha*|{!O8r>Bg{7RWJ^>nJS;60Am zB`#oCc#E2K?~Y|@boqVD%bsaJm_L)tTAjIa8~rDGF^(_mQUx?Q9Cd%ZRth2EpQ8UrrC&3vnjGt&V5xL-7LWkVTxAB~*F6u{2t z#Ds@})12>xlm>es9|O)Sbx=Tow|O5KWRXa?NY}@}_f&-7vWnm1#&w7RzQ(g1mVPtl z&JU^f3t{jh=v9Y&o#s80f{hI`9P6>|QHclP#Th%#eb-*^X~L?Ze|sZg^=+GTToki_ zaW^&K#|4p?MA*u@Jk@1}`Co|?H6x8jp(ZQ(T)$9s%?S2>!V_iOuT%hT!U>GpRwp~U>-*i@i5dhR|t4RIovd5IK^k0N~k03t*eJ^O_@ z|IN>f4m=*Rsre5&Ss()sJcQ^1NBuCQzsLvfLbv8SYa{ej@lue$bUr+0tJ>=2I&qoI z8hDoI>Fzb$6GPG!*}n~Xu@+Mx zF1WIJ4zYBXy)Uq7t(0O*#0!wLFM@Y(v>W=2U3YrgJ*a>^Ub+W&04T+e0*%-!Z^OgG zbLQu9S^`i9iHOT-W%E(m%ma~u@7YY=IyaA@R!GGiw1+u8TDpj`e2!m+#dK%oucDqO zjmXP%er~QHX>YnLQ!j8k;?@_X#>U3n(Eq6ci;Q={31xpepPE*LvB-#XZ=%|xqP zeUeWb4GzJoj-PhBoUq;YXY?t3iua><09(;*w%;H0YHWk{lGsTazym_uelt>v!J$XJ zTL82aPd+?o-|E+#9dDcQ`g!nSgsgCQtYut7h*mv+jEs0+_E0D6#Ab>E0*I6nXub2e zU%I~B?wG45w!-!jBv!hR9sdUHxI%Lul`%n?lz@ISz*kTG^o2BZwcB1H5MS;23PgLJ zbQGkWu>RQa3U0PuWX3aDu$LN~d%R^$jb($V0ov^p`S9#&^IfDe?w8>*5r6o_TSm(9 zv)?!jJOz^h?TZM@kHnL&q%C)cM{%F!L6!SRy;rM|bw2QbrD6_t!vY^H`}|pPJoIH- zW8+QYPZaPgdIpfuHj)qm_gCH+b9Y>t@yyOE!qaQnlz~~DIIL-jdrDkRU=H!^hL{j- z*Yr4I_TMcvieI0V)<1d_{3HXNfUzrUIJ_RaSltPmZ@Y_hWnd_We~tdzebD!lu}xOo zul`ibf9atF&GldWNQocv{1O)szi3gHXgXoe5d<0xE6`g>o zCn8<|T?DQVj1I=ALQwMl`7eYB@mj@OJRPfQhym!);eS^SkHhfWk7zBE@q7iWf~Jaw zJ4Q;M)oZ7VpWR&;FIt=NA*BUJA}A!ZRBJSMon#yTpWd zy869%T3)MDep&=;>h>@BF(D)P?ldIKHqQ*3R?{5d4H!rXKyC%$_$qjIG%hw+bE~`6aefv~Y5)4Q<{N zF!|csJLgvEQ)C#k-Ih=%3_?9NX=~y3IxFs!m{F3rx}Kt<19Q=~0HneR+osNccuwya z{~aBcC9p|DbSR&teRqs9b1|GFQS|Ro~*O=$hMqL!RgF4k(F?(g^*++EB|MB$#nvm zr((J%{Lg|uf3H`(AW@huWWMIj@UK+;W>}vcASIQ8Urka=J8IgO#tfmr__2Q0_nFNo zTO0Y?M!D`Q{+BQibGmmN(uJaTWxn07T@ob$UULU#4*hFeml0b8wgg^eIc-tJ+I(oB z6Q(a81J{C<-$?kh2L_KiV0YnEi zSVVqwNSx3E@{3wo`gazzEU2L%uxi1-bq)A|#O+^MUCHC%#xGwiI1pRVaOLb36?dAC z!JY9JL^s1ev@hDHM%SNzlbd@+z^s=(M9|FpbckSOp7lh`k5zacF>7pKaCUQf5OnNJ zblYy03h}KenJ7mUJcHn5)z(HgXGL#YK-kuOX%*|T_73V#WJOyfmG0*EN88tmoKx&r z_CDHpF(WRdk_+)i=S%r)X&_tR$8dAYMMYqulg{5$*bdL(Rv?zgD8W(V4%n@oS4$e< zb#LpI*0|@(&)wbhQ&UrE_vVKU^XF=yDDN?mTI}?9{WLhB(r09qo3K_M3VlEgASfrd z5O{vce2Ww7$wN=idQSy$3)ycn2AA4{TKGHS5)!=A21ZwAR8$%~o6gOwFF&hrH=i2~ zF}HdDy*D6MpSpn3#{gnfd-zsJl%t-b8|S;%aO8J|8DOL>4IZ zW{gZ`W%_ajtSc0P&LnbI)Kk!z3e65Dy6xRTV7bcD z^+%$(wQ8Mu2E6G4^Zgsl8@XY)THN|id^#J+&d-0PylqC4-YB32MN)wl&-%m|PC4nr zT40y-xhEnVKAhcY$MCo7=vm!buhH6)9`}6f#3#Y4E3HDIKw2y)&3o1JD^n4p!P7o3 zoj+%u?t?EvyzQ!4z6iJhd=G9_OHA&-E^t+R`?rmWpKTpBXh6aoGebmao`D~HNjX*J2!^1b(R90Au^3LreB+`Ff>g& zfY;!sc;1U&LQl0xZ|I+iWQ;?K|@cGSikwfLm%p;YzATdJT`X9pgzx<8!_&c*q zedMy>c^3%zw;T8MRO#)baSJNEMn`DUfi@V($@GW|(a@RNbHi6&Q_D{0xMBALH9iyK z{@7V+Jv)}#`%*YcFY~>bTf%tV+pvfkaSF(w&25lGsZ|vVip_KG-#K0xm1TPv)KGFn?> zpi#$XvYkwBthb0_x1#tbgfvJal&2)$tP=&~g(Ln`I9fZmP~V#-*}0?C&|EkKJTs*SX!o`Pdz zskbG|hCSEUI)+!`$&W%f4_^cCxMAq2_`-xvwJcfySHcAa_XLNb5U%y_D-$uC2y%^$0Mt(bLjf7RV)Pxg)Shjmx>)A|R z;7_pJ32SR3l`~>#$E~HS@7(S?q|!39v2qodJ6te?Bi{}zgl^Fi0*n5+<5oL9*0>2IUj@%HSSFhRUsp3T(cvTg#J~vp(X6vSCUuZ+!}9 z3OEhn6?Z)3>L7JT9kRpa_DKgwGJKaOEj~Q`WCH8MPM%BRGGFQ+(of0ea zk?^9NnHS7(hKsGKWrr$G^U;h=1KMqA2&z%_hxxbUwL+UjX95>PYd1 zAtcjK`axpX@*^`7_ZdbQ3@3ddnBU$>Bdb8vTDs%Jx83lie?Xki7EZe|+b{PgnyH#2 z@#|MB8~5!LVeWIrF;5|Wy?2Oz$Shxl$L zxftbLPAaL(nn3(;B7ALx9Gua`8nDX{lqlRwR8Gb8^iRF^K_`QZ9RWwFKFPKf&a6G@ zE?F@TVj}(YZHkrxg|86w+zW6|U?dG?k{Q<_)Zz>(X6_opy($5N^g}>2O!Kkxt1L z0SAl;ic^iJzrVj_JTtJ&la8~~E3+Yj{HQz{sF?dTBW<00i}~psO2zm+*6QP7Wn3Gm z!A9#slGj?q)q)tD0ndLYc+9$ zl8RSWa#z&7YxV$&jQn1f%tv+j!~=6E2sK*ZBn##TGSIL{@EdZ7^vb;qt~hg$_ozjo zC>Bw#fwZh_Z7zE2_535%OgK~VJ>QUhJ6viSb7v%tH^O!z4gE;B^-iqw`W)=`VEkJNq)J^vpmx5;h<62Y@kHWTumRO@Nzc@4N=_T_Cw4UY8UobVlftZJ zwiC$M16xsC@#+`vbRk*&8Wrc+RWxFZaK+_c?&J4%JlL4MXw~^r*awQ*B_?5UE?vFd zgo@akvvVlcguEzBI|JXa2Yi>EQ^Vp>q*}>QwV1R}iHQ zq~<*uK2Cd4bI0@MnZDjb#X0qMY>;)ng~(201`}X*`Z%YNn{g8ivJBJ#;ljR#xxjo2 zT@(o#cIAlOBRz{9VU z9m(H2b`Q70N=Qo98Q#8qCIHRix_swOoO^0tzECR%JUaS~aJSl`)IhhlxC0*T9Elg) zyy*yW6iUTL);VHyNP(95i>)8|i~ry?fcYVjl0*0Hj{=^7T_Vs0lYuvDikE`}vbK}E zMAdv)2;t9Qa!Msm%}QG9>JFyhoBuM_XgRx_!E9cdX$`H@){t*@k$nl;St@>(p(hpc z{J%XfTc8Ubuocf|!+fw`p(y{)UeQ;uw)SO$Q9Ae99K0Zd{kN8frIjaw)!v!PvUgY+ zvFA>=8s_Q9H-9qoa-~sX_bgL0=d)&kclK4JWnF#FJmZJoZ`KyWN-~#=jaKEsc1sJ6 z+`&wEmYbaAzdrr)=VnDptzujAB+aYn<#GQ@t7(yg>dqsuRLterro~eF@=~!FddWo4 zp9T__HfSUmUC(yylk8RL(ub$uWW(T=kc5C$F}HOwfOmqIKu6sE*LWWS#{6Tva`+)> zzt(NIo!DZld+gT&@$Fpjch+(Ku>f6SJGh3t9c^{_1g~Q3uG^?INcD1F|4SWABXe|o z;;1q&&_PPJwk}I24HnxtRv;JfPABkC$C-Ulk6OP8Hj7_<1RnX7v%1_)`bTHkC}U+B z%AF@pl$ottoNI2aLz(cqjlm%agKjghN0JbDxp={7ejv+73q~UWo`09=TT$~2=fAR? zQ5V`mE$`6@pZ^+N402C!F&b$rS%KM$d4_j1$%C$u9z4h$ymX(6@G4%7+AX_y>ys=_MrtyZoUg&e+n9LB3T|s{?m1CR6@Dj>r`)@3k3zW@CO2@v zk|SCE@%-6h1AC~8q-k%i{p4x27@ANm&$(;7ME#9X9Bg56U*kB zhZD_?K*kge4Lax5HC1d;zjgOzih1t`C(m}xvPNNd_U?#vD)4z-gn&yqM zM<5oPna~by#p$c3!zhV-xWsSyWBu`2j|o~#r-r=`G`O$jZN@g!95-s6Tpy>AywuHEl^4m$s0UvnNMu86PRU`x zirx^aP8ToS(P%!*Z{5rL`h5%R<%?IIrsmvlA4lkW{ zYG2-HP5tE2CFXPuQ{0lzT4d$pw!QA3Dz&_#GQDPPTIKV_FN2G&^+!Rvr#i%fP)e>W z$PCe`jzRnD6m5`0SX=A!3_OVIJb=b`4}zQknimU4Alpq)E<+7@8ZH53xOh=>8MxBf zHHA`4==YYqU1{no=K06{?Oju-tg?&+zcM{(xc;S|E=^#oROoZgu}fF&i_ZMxs5N-d z^&~<7i70k&=LAeBg^^CE9)&${ue~XRLR^$s1iS7-Ie)*8XSu(ZHFC=p(NlAsqjm#o z0B;w}H=f2U7u#^42Tl^ZsC(^s>I<$p^eqj;QUccAF>hDbqP%ZS;`4m(pjW(-$;uFE@-Ae*|G?q4MU+X{ESjJq95E}v|q z^C9oFdHHZxJz1S6WyA8Z+k}_GU@6G`fLaLLp&h}MJPt)rCot{7uy9OdKsICzyjP2HdWT=Ss^g}0`&C$Evc9TPK z;v%%Eg6y8WDnS=gWX-0(Bt|84t}*{HJNO_7`6Cpm%%?8`$Ja>`qK&%usGcB~48=ZV z=17iUceuc`583#f%z=vPC}N@SB#ua_pG_5aQY7y?wMA75Jg&L>3WI_}rTl3L{Xhsz zwKfL9QKRVXQn3w-boiMw)0N58G_G_$MHIO~3yrFRW`}AnmeyVKYJ49DGj{!w0Drh` zhbjzL+EXiDPMCV-t*A=gx_SD1|e(my5N`+UlR z1U(=H6Esp^QK4aGT(=LRO%McE0rn zmG!)O)SX*i3L(3-zl5D#y@fe&ft6!3A3V$kRd;Y*x*>c0AnJa?nVZ){MMLMSfh@oE z+1aoUaCrfGP{4Fp@JV$yHbD-KP}AKWZ^^w(#mcb~Iy_LFv$>qe?QA7<<95N>`YS5+ z_IoS?ocL~Cxi?Kp6%}KCi7zw7ZP!GWB&1LgH1l*k1@*5m>@~6?$Q6Lw>)qTGWWFWI z$7zC_`yE_C(ug9t36kNyhLh-(9jsh|oQ_1UdU1l-zXq9Oxb=0>e>uC2pBOWScG?O9 zE-AF%Qw0D0~2RvLnD==**nAa9gg+UhLf}P!U>j zPity|&(`|X2judBhU9EKgLUS8H`-7>!)5Q?;bfitTN_nx!?bK|_)^3-@4OEaK5WLv z?(W-?>1&f*tq9&E*Mteg)g%pp7GnGF9VS(E8=r=t1$MxWe)@kBi6C0IQ2;FWe|^jM z9e^IlKnm7Yi*WjL7fJ;Z22nd}97SS4y}LK7H)K4h0}zPmYH6UFqRU?Mfi#EEdl^Dw zZMRQ+8z7&tl980u>M0OXR$-jstqacQy2Z~4A!XC$Z4dX*T7AI%Q?}GLQt)k=Qta~; ze7s9ysAgeNG9`Oty%?^++Rgn^v3g_&JN)>Hr6z^rQbz5k46`G@WQ2{qrJ-OC)C$V$Yzeno%P=tQjPwLAWd7|*Y!r|phy?JvtM&b#YC8w3_9;}NhyVgN z#T~yW)`hO2%*NTWG_XRdZ;*wVRb667S*AfVPXAIx#hrCA0`|y zxjHZNY_LL?vvR%ht6lW$99-b_8PU2M1?+dm4ZvE(&>4VRRscmo7g%19*#_f{SfuA? zz#YC6CWFW81D|%9LtPtnap^2CP2qc0maKwbQTpc_fZ z>9!_%DOd9N%=+S(+U%({@RaKs(4pvVwlx%ty^(8EYM7hu(t((Ip&31Ed56Lw-C3V~ zoyxJH+5bck)_c67HS|rt7meZOSzhGT&1|Vn58;7P_?t_s!9=4e{~3^&$7Bjo(Ks zZ|VGt5EnanjTYAp1B;K{-g$Qn;R8e~sQ1uc!xogN_^6KbvYrv4*8p7P%#X8l5Fa35 zLj>nL=l{goiWR@8mJ&2@EO!-F>Joz#g%xqJ3^voXWwfdS67opt`?W&Pht2Y~gfVzc z_!QHc#Cn(0k$Md1S)07{u8=ZOQf>?U)>-lm{G4Rzz)+fYk3UKU&|deKA*vS+4oD>S zAo7ixJ%>{Y1mFu0m`X^ZP#D1fQo+3=SnXqeJzG}R$)AB8Y(tDdnA%RIDh2--{kxPp zM56PBQNi%PF@oQEM)rz|!9aR6Q&{@Mj@#%*?imS|Q-+v%JoO(^YT^?7sb8cnkEQ1y z{jWJnN&>hJgi%E-r~1%~hInruF66f|<${|TD+CO(<0oxjqM6xM7+ zh)#?#uV1UsN01F%G-~AoY>SdhBR6A2ZJ59kwY(8Gg{UsD6-Cf>uBgciO$@2Q4L8)! zfgyfu2iR>f$%(lzPiEG6QuXkV2=@-{ydU;==T)d^Z>k z5?D5K=P=u{>v$Zo&kqF$C`|e3p%IkAeO2faVqPE)xUs<+1!2@oPYEc*u_pL8y%|R5 zIspW@I8aJWn;sqK)ihNre?{N4DC4=64A@9|dvc$x24lqf5T1A#cf`Oa!s|mZ&80B& z>XftA?51Z|FW1qF(2>w`Mtsf)Ih-jazcIN-lss-5Lb*A1a*-mu1iY52vzys{6$0?E znAS{)Er^3j*qDhwew|8x0EKbDFy(@QHoe!pLFeN%VF#OfgRev7t7x@S6=nM)KYE_E zJc%f%b;aXAF#n`a#S9@}Y>w9R1BNC|k3&Ul$7G<$*z!1Z*AcFn~7G= z;9SnsU;t4Dq3dQ(ggR7mqvDa%Kt6w?qtUML=FQOf8qYP)jsPu6-i!rbMG`qGqyW!Q ztPfp%(zjaeooC?tmCdGWjQ-1s4iF7Dl$yq4YamWp?=&B53^*#7AqK&m=J!FA*c;g^xN|4@2y3PO(OYGPn>~p1 z$L`?ztJl#{>zfa~Cy2u9r;S_%l#2_#Mm|$i1VgyMr~f<-qbOBGpBO4ml=!I?T3wxV zUeH_bzdPjQE}|&B`LzM265iv*+2xz{jN)|XoI{{SrhhA3gbsOl?l!uOjx*-(v&POb zDMQ*wFN7oHAzVofL`d7LV_PdgQ5FBZ#OTP2P6xiq&Nh5D>lp?09!t z7$>H|4!ub$p6Ep9Av_2o1~V-|gl*$bFGs zkM#xfwatUoUtT!CU+)orn6ZAoMEscJkHOi%YD4z5(D`-NLCbG5BMbUcaVC9-nPctW zaPi==w`z~E1@d=xhkNDc;Sm??arTQzZp9*Y+#Ex(kl?DVDvfZEo$S6IhKRrZ_<#Xt z`Q%VXGmVGzf)%%oPpJ3Qit51MSppna1Av!nZTOjT?2fyy;C~fbYaUd}cT&I7l8PVCp8C5dCI$UNZsn7>3-~Y%>J7N-)v3-i zn^KsFjEy}mp(Z`YR>5SwMYRFyi2iOquyIRlwa6dABfGLMYpl+C7j!}$j*b~n>vo*Y zH4HjkAxmc?6Mr^Dcu#+TpMJVhSJCOt-uuq)Cp(!A3QBQr-SOKy2%A-x`EOk;hu?qg z8gg`8zy{UlT=Y4WUwIhIpVJtHf-t}(J3>i9KW)o!%|P}Nez&N0_p}$~!Gp75gSu|A zF43ly{^QG?u}}g9sHMsOVY|W>1}l5h;xc{0yz=jpN#DAE@=~TXkas)fpacy*;|_-G zNk^`6fkFW}MQTQiHWL>q({EM^-x?OBO8rHZ{JC|Y3^aeDt@GeYh9kFmkvM{>cdp&d zt*ruhEsB4z>H!ngz3SAYvQOocUT5!p;x08rf_e4?bT~6bS5H0C&JfdZjR0966rwcacjPfc|{Kk5p# z&(qWSr~pxtuF})Y`0G{u2WOEc-sOtY*;=b0H+nuMI_+p{v)+K>QZy`wJv2LQKQWl$ zoCn=D1VU~1(4epRN;Qx{*&AKuRD&tKk6r|YyDOW7L0pq^Ju60xHfz#*VW~jS!h`{A zMOb3jJ6F%erHUuL(}umX-fXqhoX`}!^7);(%nj`wcpQ zv!|-cJyPW=cv)Ei-#xm$Pq)lwDwuhJybutwTT+SJpSZ;d?_|{u^E4?pa@!rXi8xFD z$cs)lC*}+YhCkl)j?XZ|l5}m#hLTF(A#$o!5Q9S4u6F~J^QGK(CnZ3)$vc8q5jek_ zM26SX%{X!&o_hlsJ%m~p32k5N6iwB{ znR{>Px#=&Y&p*Q@?W|<2)SBAqwfIuuLvV4e1OQ4~19P5<)sS&du6z zY9X7FI1U0KNgHE~aAsoy)AQ&shKvzepxG>tCH6>M#GBC&Y<4Hweg zd6lNmP6zso-<@~(=6HHm%PAXg+&op*u1l_Tmj9KoZh4iQLU}9i7Hz%)qxXjjFK&&d zP(a{3{RE4~clx$T#Uq7CZT=MsTlxB3(APgm)fXi}$x&R)PS+wo^+LRhihN`Z)maF8 zh_+F@T zGZys@cP9);PKvEaBG7S64pWNLfPrlNPrqFkc{Aj_(U@+uWmL*jdJZ|D`rp(Jf{{}T zEvj8<$(BsXWF{U6PFGfvw$rHsl=u`*cjoMvI!$R_nS~oMJSs%MQqOy_z0Bv7>R^5toQ30ujm%9wHqdPRJiqqu|G~ZonC#GfZWE_ux_=kFvv!dh zday=2Q01#T`=uYqj`oA8_R9>s^&bc*WF^zbuq}e zpt}IdE+wR=YW0`_j!n!8#m5D#r6eVt-f@~!mdph(O?vDr>}f0*WB~Vc72K0x!hf5S z_737{xP-;X?enint>vLD_S*;Q7xyXqTn?V2*m2OBe+av%zxFC9R4m(a z#@6>6=*7h9OawW5^{|pvY7bUWr`!fB=VDD5Z$lC?uPq{pN;*pSd2>@J>Y!P-q8`#gMxC4G&E`#t zZVBj5I-l~%ijuU-H{iuAk{M7&O#1?osCv^MVEtH|_u(pDcjSC&QyO?{TVu<+hY0-! znl!IgYE$=@>zTs@aV~`v&uS}^WJEvUJk~K3QChVipSHOF3v;yVuLx$!)H19^>9=iy zV4}Fre&gsP%=d$x1NOWDG(zb9?(NWZ5uI;VKIR%g4F1IYk6y@P-6F{>-nC0a<9AZR z_YBaI0nN4Xftdq0_)hMuea;Qmf0=gUx>(FAw5jG?oM;I1M12IhQ5m^aSuqJLbxoeE*zy-JZc?1QV2oyfUi9g;Tyw1cjE7eE*uVpaFz zQ4hKKtvHG>hF3}?3@dVr(~me=9UB*z_B57vL}?PWm*6yWJDoib{p!-sk_V@mW+`~Z7T z#cfXjvthrturfcpXu#y5Qa)>Uucwvm)tVrs_Dc1YA*2wGB8s3(2Ha9A%Mu2EqWI zUGLs{g-*MzcN7`6r?b}ZRP2FAsMqxpER?Rk@KV(h3dePLR^%hK!@ay4%G!K9D?ujj zv#&@ZoH<5tdFxqI@&(>t5nRgqaj2pn!5R=!_v4DO=zMD0+PAON@Kmdm{}c2b49f$u zqiCSnWZeDopG?Tnu+{+}88&Vw!j^bAjrM@ypDgfxpEpn@(VZMl zLf*)|Z2TkBM*6jdHQ@g}WtC!%VB2oG)A@@s@m!2gzP=7TL5a}q44_-6r6b5!(mzXXm0I^y5s(mk=%EDCd- z1m`)xQ6lDO54hT z1sT$RYw@n75=T;Q9svgNbtQr&djr;9tP>>V9tPC-Z3; z8IU0ni}3`{cmD~aIa!Xhn)h7v=wuupO!rG-6Uk2Twzp}kXd@%5#I6$lQP!2pubjp{ z+aYhiHE)1{?y@DkTu^)5$x%dYyTIrPFaXgVbe6t50>E$BUDWn&e~C))3Ch7q14v` zy|B@1OXb+BnmEkA?s}6F18GbcHy678!7cFYSjL}b?4Z*Ok63kE(Li`$(Q=t8 zvL{Z@iU_?D;KXh?ezj}PGV}TA-~AvF6_;S^tGYU9)#ZJ@`fAk9kpU47E?&4 z>@P8TgHG*-m!`yw!9%uxsR#Pq^$sxr%i_pDCT}>x8=H-+xCAYD!U=Ip^mxo-Rm7Bs z+x!MXEtd&gH}a1IHMjewDCX3y>Z#e4@J2w@A06$6iD9BAhN(y44h4Lp(?9Mq1CB?n z)rQU1JK`>^etCKN(`Ayju;oZr4Khu_yA{gni)N(2z3flw2^8L={TDDlm@Ql22Jtb{ zYyhmIZJjTO$9a0%UZ=nP0rEMq5N);LC-Us(;$so+4dTMI22U?Gb|~G3VTo$3h#fv6 z<0}simZKjp@)IWUf6JhrFxUtwgp}kK^fmpedCMKL-1d-0*>0vL zBAtI1iH?#B!GzLDPw=&PgRXWXib8?grxxACW^(5t2Vh&mq#l9*rN*;i;|K_9U_-6>m8#qa{ z#j~dmUX=qNafS%>F(Cm2+ZL3fc^T^uw;7mj%oW^htg#c()OY*k4;u_t&DPGoI=7;x ztjw5goAY@WK`k8JPZ+TG@Se=sFxJX}%e^>TyzDm&9GnBep z{n{e)sk+}K|2qPF$jiR-`#j{}AT@CWl1^?vO^OeYGcbsw;-{DA$N8n&cE}UZPlmfj zi9RueKmsLFfHKePoKx3#`2HYeu9l7Br}V{R@_@OUl2 z1)(k%Va`ZVUXFpSiNi>BT8^LX^7B_y)0)sIv9C)PPKtml=S~wVm-VosF)iN*ly|YImRN6K?QAhzHnQm4q{dIL~E>kmrFe@qJJVn$ty4`>THctASb{ zuk!z5h$W$2pRdv|ZG`!|x-Qqxrj_wOuaN0&J4EJeVR=R6egheyLkxNJSpl3YIrD$d}1J@{PCD{wt~aRV7OKJN$4wjcqwt zUMtl5D<|!4eRIS7WgVm^K!mv4a~j|6e|Ho{c)M1SEtZ2mfJ5c1;pruZi3i}=kuP5l z1t{?+U~PRU+9E=O98|fE0K{5YTT*Xg+2m9Y7;kgb^1P>V-MwF7Gj_>b2H=O-s>o?b z@BgqQ=e`yyukpL(1oMoMTdL!I+2$5KY(v&TMM4avTJ#W_ni)cF($slO4P}_XZxWMU zN8CV1aZcyz3z$rtB(zQDQGk=qzkqTBN8bw5``1%g8sID&BQ6k3PKq&&Tf`FhPB7+nQA|SnYEJ%~?(3=Gn5KyY12-16z zjx3?76c1LlW+>w5mXO z{<}O}bAnnS0KBa(RF%039hH}Z>C#7J6CzC8d0zTKsZK36HmHYGGli^EOGTMrX1~|S zz-*$0DJJZb5&-I!hPZvc5SEVn8mcTjBKd*LL0Q~b{y@7nXSAy>+}VsPe~}E>8NtIH z#cz&Xx+H_aXDKV)|1O{44WQb$x{oW>wFh4HGA^B#E?)UUj6UGb1JD%!UFv1PKH+c1 zeU+}B{dvz78Tb2O#jxK>iyM%~f3MSv;aGX7FqrZDD&CUY`_bJGwQ9PuM_CIp-ezu< z`Q*Oxqki%Sd)ID)KxYCl3Df;M% z__EK_CJ{vjM(p5*VhoIyXWTZ_7y#ZHFU%Q3&CGJTj3I>(QQqHT2)U~GCO4O_bawdo z*@3|ahI;#-Jq8CF;a1BODEtYz&eCgb_F?`-RYOfoh{8{S z;i1>;I0>8T(zq)4%=gJlmKKRVFbhW@t^j`XtC|Dki0a_F9Yo+`{M%>8GFajfL39*q zUDC?XqXsynCG!KqmALdTlpKtX%CBt?z8Z$Gk)o=ja`C)0*0?mvdWVoe(VDsu*1LpK zZa`t`1&{@0)NlaO50# zi0Bz`SRzWsTylVgQK>SUbO8fykyschumBq_b-Qu4wK<4LyU%xouH4?h-2!WnFCYRaDgsK?^_7QP=;4VYMUfS%kFi`l$?XT@+77MIFzm z(GyYc1tDKx1QOe4&8abkQjb$8$HJF~o~D^R0>U37w-i)o$Oql?c#o3iuoDs7uwy{1 zDlQR?dJN_+y7VicGs!bk$2yAeqVwX)F5TZ`Px5Jb@7s9v`s*{#@jiwFkQ*oKA6oc8 z{{aicQv(>S6PqQc7NT0|HNP`$wRp+Hw%u zx>aM&(48rL20#m8J1Ly)_>Ix^LEKv6b zrmUANzm>Q)uW#gAYbD$1YUv8iP}j3R{2nBx9K!r5IxLa9c)(8`cIHgjTRKxHTpb zCdN&T>pG|OHmq8icf5xBWdqLO+}Ww+i*OGwi3oKubLjWbj&?1^9UbaybEE0L8#@&v zF{IxCeP9N_IN%1+c0H~6RbJcdnL^j81@V9Lukz_ywF%k`cZ%X{If2#?7Mo7`pyH?x zJ6~&_u$`qRicO*Wh;W(MxzV`2b(RwimBdbc2%$I)#mBXo!04ctInNfK{tIPLbT!Oj ze(Ri_749NXLWR)5r%EevJ|>2ZXH=q>f%;4cT}ti|JWyGoKfA#Ct3SwH(B+vx*_uHbf-e$GjL-ct#3oGvO*SGN=U`$5U)*K_tpw!u zbE8SV&h~9gPaqEnp8RJLe81}`qb{?NWeGp%0jl~(N014k*8g%}`2~o{;A|(@AHynV z27vs)&uFcUh#D5<-e_+L?s5(IZMhi=y%}OBmz(Z{H5LTH{=WCm5$iBooQ5yTQzJq% z3d6Lpn?YkYX?3#lS?N&r_PWrwxy;cp8H-xNu-~Yb^d*7SKXShV@V`h2C}jQBdDS+j zW0DyuhzvSKmh##Bw}?@W?c%~Pt%0JkeyEqfYm}%PSUhPFnI$Xo!M6BPT~W+c1*jAjhD{r zK%tf;m)psTFk7ZwFL_Ou1YP9~R1%Qf3=U$1KVV?!JVHZwth;*Kq!;^a@hVxHo#*{Vk_&I1@3zCK>(d!X0wtn+ zw*6$V7(|2+m;jML{o`iS{R+AX05}Q`TmrzRu?fs~2QZM>UBYT~7+8w71NzlvbY%w0 z7hL+=;8$S~we;D-3xAgMi=NKDE<=D|du`CXMNwUObyLKBoV>M?z9yyHh3>vL2G;+|Rb9d6U zzg49VEl2oE#*c)G2}1+N<^+m#tHlQ-V4#<=X*&X-4OiabII!Twz{pi13YVB$_f==L zxv#xh;|u2;+m=>ca{A9byI^Fj!j>tphYScwWZV}oc4WUftqEt){}|Z*vSVkY!?lLz zt8O#XH()0+O=c7pfc0ieJF^8V+ASMunP7lSWO z;}c@^Mv-`=62Yl$#pW)+2F=}~e|S#_6t;k|x!yJSk#r`fr}kKzq@H4LD2qS3M>tQ=oHUTtFD z{t?&l2%zRZ$5o3}qbWu_I%ob?1-!(X?e^MrHIDc-i=lTR} z(1O82xVY>(sh>yGXDeF?XSJQH9PFW4>GByF?m4Tv&PU>aHcJyKk3nFmBWzrfUG%Bk7?!!=nHTkfa5KXeiMlU05J49ks*$qaP6pHv6_8euuh|xLP1_jU@5)D{dQB9X;PHC=&vaN0Ir6Bg5bfJ6e5QhPvs_1m6Q4!Aj5%+Rg~(0RoGV@N zgT6SQ3h?0;UzQBs%kElutBiH8*B6*VI|CgOmpOU^bR~eQfUH7hn3;;Y2)GbmN)3x$ z;{y3%=zD^t&-In#wzDq(S%Xo>JC%k=CvvSK;-s2xB&coT|9AG=e`7Y_Ehmf+xqIjO z#;s-00X&P5EZo8^@)Bq>AEU~vxVIBpv+s@FEvXY-gCIY&PEJ{LVOK~DAj%mU$ps(3Vtts9 zO z?C5;l;5#BwCXde{Iu!H}VJVLpt|wiF;-vb%{t?f>TE{*b_@ z;R6nZj&O&p!3M+(U#%TS90>q&;bL0uO~9i6%{cZhf>bM{REIq9t3#v_g26nl2LLtT z@mQD&ixKW64z}`qCd>ZMEN_`@FJDf2##05~2s$&hC&1Ss*$-ce&B^vPow^`zFlV7UeYi68^7q#w7lhRxI-+4XEQr@w!TDDN%ys3P% z3X)ci7mFR`nI^u+?iwjmIw^JwK|8}3AimGc#T){r7gZu2hS*W%b6czPhp~cH6&=Hy{t!W(gb%Yu8L9q+l-aYi6Vu)X|oNb_o6hEw?E& zUJ+Lbu?zu0o&0|@KAEYMuPdf-?AVP=5Gx1_Oeucy`(c?LKYH|xv-Ul|Su1g}NY;9) z&*~IC5&9K~41c0UP<94jM0$2iwi4`iWWR@?DwL`SKLKZaP+J*;*Xi2uuVJ0yM{j|2 zdd7v~G|mknHh9W&m&*J{y+S2T|9lYuk?Jp;)Wvfu%0|mCWawKazvX`IaWOQ%#pZm3 z^CnzPwoX0iNtz$K9XcmR97J?TJB51HP*TxNRls_7B6%BxNd}a-s#D;uNTVV)>HH5!WSQifpEsX5l}Mt>dAJBe}-&8{)n@DNByvjS+7HuzS-BIK3$epHVkvP zS-*HjFaMau%l&>J$A!}s)z6gnmqKR{w!L(RBHaw=JW;1>^PvYL9+gJWPX^9I0@MH? z$wE8u%Lz%)@TT=+c?2XCV)D7Mle2MdzCam>jdN9@h>w&aSO|F1(R5FCva&ZnfmC1` z%9-5z#w@?`Vmx0bpWA85WLkE!f$EhZB^|cr{dj=-e^6rGQ0u>zvnKTLtB1t!dCPn_ z?Fbi=3RVMBQ$#HP_S*DM@{0*QQ~rKc6e<_!Gqj>=IbsV5u9GAEu!p>Dls!eeM_V>R zQBYu4zFsMazvmfIrAJx9@(fk*5yW#XcP9a93yu8wBUr?#p+p>_kp=M2=P-*&Z}cjS zrg*-Ags_9n1(#l;%$z|F7(Gg4o#D@W0aPfehhL8P*%Y`<=Ow)|A_X@}5BIOX3y--P@TLuEBAE+k_!@c3F%(_PSzN zY`A<&rA{tE?k+ufDPoSeBd-L`2q_n+1$p;Z`1!r(Dpb*6`rU)5Qu!@{r%Qp74^$;s zcm)zy2CJexn6cY1A})|6U6FHwT*K)$-p1ROlUeXO%+_`?Slw`3vkWEZ47! zK@n&54T^E&PHW4)c6?7uAQ5YW{PCk`*&q^Iu-NvQuh6?%7nu!m5j>U=pB;LDr6-Sm zi(-vk?my+1qR;5Q#2b=O3KtQMS9`YlWeh*V#cKz4OMyQDwKB;A)rhT4B(A~8w>-$enNcM6%f7G`1 z+7}D57S0{2y5V7n#F+?jq^BLy6LIX&ZN4LcwR>a~L6>q#ZtfydlOeko!d7a)i@H)m zj3{26oot1Sn;@sTvqV6U*!bW$yk^0e6ZY!fF*Jm_%H45ojlSVFoHDk#ycp?q^Moh- zy|%1iGV<->7mH7G27-hFDK0gu5&=QWTmat#orwaHCurDU$)}E=k_gIeeo&JSXo3~? z^ao?HDsH68p>|$DMSh<$``duI71C1gclRYpjQHMQ&>0L7i<;+C*!_uDs+~9eRr22i zbQrb(_<+@bAQdCj12EaC!5tmO5Ju)mFVq6%b5=&@EkHO+;X9?q5_#q)TxVXrRi%Y! z&t&N{T6g;vE1a%NBctSQq3beW?!Hs3Ba=72rY5F18mjT~yT3jjD81+tJtcoU0rdfv zmyX0)D61oxbSgbmM0c==xnWji;s(-K(fRC(c%DsNScwVw?PESzbUQ4d8scTtn(E(6 zc-}Km{T)rY+}$Hrqt7yT85bT}C02Dkno@4Ec68vV;w|1Cb1$Li=ZA%7`%4(&s=c6b zR#~TVk}o(#iJhj-2LcUp)iBjUhB0^2x-)-a7gKT{0R|0Z07!&{rOP9|@o>H17j*U! zj!P9pMs90jsa}X4F;#=TSy&vJKO(>5bfsjOwdKfTiNUT2=cAqcR@})6EVK^=r+5j> zy)t8#{!?GV&75>J9glLVwG_|h77fhkOI}oIZEld{EWgY7cx$Dt{))=u7ic58I7H!V zgAYaN!;*tV4#LqZpMKnvFA)P-HAW0bVZ3nWpH#%TH>GO{iXC~L);N+zP;vr$FVr3W zZ+J!Fh|9gfTfT(PM>4OPFBc39I6C|Oyrp{Mb;`k>LCKrxTN{g42RYZ~B55>kk#37I zi3$Bc_J?I0M#w)sKzu1;2DuwENbZBw6{+l_R>`p>jp;o9lHF`>lV2M(pb@iPN;b%- zcj9X&)7s~bV0?eF%77kobW&ovSB;JNdw}8&I!Kz+a8_*9w(AX0|Itip0h8AxmdDlI z50U%|FRZjUHI!#LG;{~T$tG{nXxN^=>0xUlR9+e1JHc(`@S;zuT`{gNBrIS z@%FDhGGlAR0JeYyi54^MnDLOb$+c|zwMOf`vk?kC|^ z#Quliy3?CJo|3gVZ9bpy#P-wlt)hIR?2=>o_VN~Q4u5sF&41GQ&Aee-wRLz)oA4@+vAY_B{Y>&RQJulPO_r{li8M8~QBH>TR**5L}llGQo_{6e98R`3uo3W0a zf1YgJ>Lltw{>KwVva*h=w`(Fs0WBkhQqkZPjG>2{20gJ9h^>3P5~u2 z@tJ4g$48RxFp3DAh@oky*O|4d3;x{7(wZRl@)OZgF7c4EF8h4UDVuct$i?vq4)S7} zGH2_?qkbc#CNb55A*}>lO3!fLQIQvp{-llj$w8{UlZ!zkod?`qXIA;wbBKAm$PwpU zWsM>y(C8`<4ujJ#xcY3F=HE1&tbyQ-E2hCr2aaq%L8;Mk3Ssd#eVqE(_f3BY40M_< zzaizna);%l>te6$;xtj|hzJko*ENDcDC?5pL&BxXE&B!ugTs--GC$`SeB=o<`i4k!_@{@!`1Yfyi7KLJV~X!b{6y!!S`AKvlshV?HRh!G>}1tVGHD zZ#!G}jbAo4_h~?UFnT7spGIC8;deioI((}!=en+$g8Kr|j3MOh8@%YQHa5}etK`SY zpZJ%OMeTnQO>K-|9EM?2w9^{4880o`b(AmgSq8NiTsX&mHDX;4Z7jOG!S^9s432!+(8)`PgR8y)zm{ zSWLrp3%V?n@-IfN`3HvEk7dm8MI0Y#gps);a{z-mYgI90r+iS?XWlLHU%_8pdSuf3rMrUX5B0moQ@pW)J zIq*;er_OEoUrT*@AsBVXA%WFwyd}vhkRbO(+Pve{U+hXWCzL)6-gfC~EBJ}W?I~4I z`6}j|5uFy%3XB1+{5joEp_S#A!YUuYTHqHRuX_ml2QA33V*viNMu>0Jw4swRf|1ME zOkE)>k%;(Vc%p8qe0uiyu&*rl5P65+aNalTxaZ5QLMLWFZegD3vwB0gRDXt5itN>_ zmR2p#85sGRbZ(olYFDhg&XTCB?KfHc=K{s1)l7ntCRdT8L;xdsD^8%McS~Z@Agu(y zZqbiV9(%WJZCb|>QF%AzsgZl4!of6Svrtql;(p-$d7~<$Q_#dj>5(#$6{6VR_<$7rrJbD(3)gG6;Sk0os<4l1Pl9ZLAPZ(JR0t}U9H zZfNc!X(dEms&k}q!WU}F9}yf@DX7LL^yJ)f%UGWABhY#8O+UuV zcg9T<*zz!&>+Bnx>wLA`_YC27oWkj1zupdW7R*I7ME=KjKkW3eBZ8vM8Pxehf$m2U{9t=;V=6!V z;PD#MFyL^_e^L@QR7wro?Dr>qJ{i__f>Zg8pZxooV463n{HC3Ei3t_k#+Ync6OCq7 zJ$=#%yOZDZ_K;_cogS7Jjvr;Y)sgsA;T=Ayr=`V_4!nXNCmukj-<9HElu@NW+IH24#Dp#&ml)}id9;{Ro=M2Flj3eC-9JhouSP%6IRBc*7eqe{Fwga1kdo` zJn~2u;Dwp9NaYz3JF2ENWHJsXu?t_*xhH*UaNCeU?>1GvL`#ItxvALpCJ5NC<`A}fFukKhKiM3@y-P1w zUQt$XFEvpy&9rCMy?tBU{>d#j_lHj(j2IsEkuul0)bWMZL_(D}69kjSJ^Kht=2B$3 zpz;Q}o6{v1o~QC#ZGw2Q?Y(gBNx^uBt0eqJLyNQBL&JSHMZ!zvAe>yLnUuS9LrwF{ z7f$~+OQ0$mzzRU`PC|U3etnA#d6L7DKd|=PT>$W!Ty+E zuf<&yjz4KknY85JV#Uv?(#|KPR+8r@zV3Yud>48+g!M}&9+$dwrL;1pjME5TbNc!G zc^ZG_=Bk?&i**9*8y)zzj?)6+*sGq-)45Mv30ZVZfE?!j{~K`r+0+mx8? zIff2{6LjlF40omUE|uPdr2F93VxO+G{@hgvcmP4f%I3EBvlB5_#Gx-iPv2S{?Zsa!`rnY|Kj4ts2h|y5@G;oQnp-XuZ@9_ zn-rtR>4fC%jV!e?I$yT-9gGJXnnzN@a%JZD+S>v+S2@*eo9TSkSSzE(xrdjO7nfrM zHH{25EYB8JEcSiK-n>tjOs8{4(2w{c+457hRp3DJIWo7-I{&tWD-*Kdg;o-q7hd>@ zy7)tI$OlM7T`{3EIfFd15oc~8+8)joM~zf4YKdQIJa@G3L-A>sz9%pO*QweF1jS$o z2i)gBJ|wstw1~JvA7EoNQy3_3JT>}=u(`7e#VDQPshIlJ z@0i{0P%L_+T`+~znr4-RQVJ9ImHBSLMW)gfIiM_lBDN*_Km#HhBnlr zqMpkP|DfKVdo^rnuD*s4m&UvviGtwqNS^*6f0VGcqbiNR?n*r!7DKn{c$O^;Z!dB1 zbAQ-!i^SaAR9zoFwITPUz1nJAG(o8zg@v-BfjxM7i`Q~*Ig>1JDV})>dDtUmU~)rH z>G<9xIn3+-9+V8LzPOJBTt*GAOcbK}n%eu~H?jBbIZ~6Kc*`QWjIHr|=s+re-SW>H zdiHMIaRt5Ea&P(eGszBHF6C<*MP2lh{Dj}Ho#0z|vT{rAQn9M3ZKua-JoymQrX#7! znbK$VBEwJXB;fst@bPL_TwF7HB1)Ltvm&{2X*5(RrltgWhE!rD?&(6J%n&1``oM8{ zQ!}7K+G?n?UgPaNq%MCRddLqLa3{e|7W$ z+zP%RF*z%{`4SVDu|cKh?&GH8B~r@lwEZ{P(`<1nCodtw&7iV(%ZGh7r$n1AvA7tt zHB=f4fT`YLqtT^{5mM%h?~knvmkbharW#PFTU_QXcF6&P>FrK@MTgi~n@;??+8==T zFt1y8Xopc}%v!;Moi8fDl27;DIJI*khg8nFnOz%UWEf1#{hs6P3+(#JfK321s}#oU z=!zK~ss(q1B{?@t7EPUB)WafU)Q#W3Q=-Lxz>{GY^jhIj2}z*6IaA)drge9v zgG427VNnP>@h`}2``EgU=MUw|-Fao=t|2dAu4IsvxUKLc`9<>djNgtCmlHQ5?x^mT z%S*QYwin*X;T8XqnNB)ZfN)I#+3iO0V7#D%xbL?&bXL1DpqY5`_X0!uE~QozuP(OM zh!+`C=X@*0jg2ab@9dBH`;?Hdh0J#j*m2W{JGIk-V&7ivYdDg|uRy!^tSF0xaT_?k zlabz2_ax&86@e-mzi~_FGKd|UuJhqc6?1ZB;N`c!f41f9vxSN$87dw`H^9O8=e_nC zHf%Q=Up675i7;rfmVzDz!x$ggk96rfCqRWmkZxgkWUs|;K#$zKbp~#Ui@GVHEu1?gKduaB z3{@hh>IWl&aYN)W;l zaW$mjtas!JKId7I@#%bHsxNy8p83&{N?70dcFnO{q^8IX-uXnxQbB)g@lk2htaj@$ z@oK;fm2GxhEhQI9#Y>>}3d%J$OR!t5cJXK@vKtXMh1){%u3RB zGn{m00N^~lzf(d#{ZFC#7p$)V1{&_&l1|#js&IXdXTyv8Sp8_~!P{r-lGsI=CG4bR z7*I_omIxNwAa{Net&C0 z0|v4Ks|4(T*ua1Ooc8jIIz4Zl=Ag=bh&_Pt0oSgx{3|w|q$f1I-LcpW_<_XBzTbrD zv3Qdizi|PVS`4M`7UD}39Q!9Xq}sxu6#ApnbRml+E;m2Jye+eb3eHp>w>>bN9gS!N z?4M`10mS`iR&htH=i`J#rp~wGV^TW`_~T{CfZ2c0b~)U9zTG!Im7h}yJcxTL<=m5~ zKn2&cM+60xv)LsZ9Pgoee9}`W?adjB2<2I&@yD#P6PcuP!ygW=HlJ8bM@E8qKSW-| zS>?ij5aj~(qv`D6QCv#QlZN4US?dHW--Ka&M=Gi=vm53BR>8xCU8V|#A%W)`EfwW% zdO6D#+?`Cx`vx;-r_QU%Z>!TX3JR7bU!XwSGCP-p=^6V!8%8K*g%>&X&ur9RbIuS; zG&W7!>$9P-(4>M-cnfPJZBH4~m*HwZhPEKVH6_KtdnY3{3u-2Gv)m6%Gu3fV8|kfw zpsk>~UhX`q^!E8}`%o{ZHHb?*XkiyXVP-CJG)9kpxmq6~W{%~+R1RtmrXTx|nKH3# zrM7Y7l&Q>8OKVRyI(Zl#)sa<6V%nY>=?l#XUoB;{jJnaNxd->-eZ#~kzwO!GdS zR?_GCN<-G0VN>RsX4YI_X*OPGW883#H6%V#)A9c)KvuSzEksQa$yr!~~eP{`QHm2j^qHR2PvE4SEY_)kQgb)Y$bqCuyfughB}WWbwb zMFf8la>sDVe^M&@4Zzei{&~0 z0#~lQ>X(2xEn&}-m%j$VeYD3JXdL#B+74D#@ z^x_vgs&1djlM}qcdR=9JW$Sz4v^}OTiDczs*FYxWJdT}q;7d^h@7qsvg%5x745dW{ zDgdEP7!Xp}oHbj|p1fw)WsfnWX=7uL}OK{Gj}smiYG?j1Y|2ZD;MmA|wAunr1u zx3BRfRPfdVs-~wk9CgjI%-0LMcH*iKAV8)z{S>_a;A=7BzEt(YVQ~LmbVtWDXKQeR ziTg9fF#F8$o&r|0;g-8ZqX?xy_zzy(eu(}XgV?vg_L^#)X^F2I*DbOd6@p*}v#t8B zUJjMF;rZWgAZzvDgdmZlaU0;uM=a53?4ri|wj%mYQhB>>;rFpU;ZD+jt)$nil6?wO8pb zvP%_!ZKq4Cds~kgnzXY){oRg_i7&Rke3VW`uMu|D8O;3g;yxtf_8kg{sTIVWhjVJ<*=lN>DlMUJ`S0X&}XEtA3{5(;Jt7=l5JF zq~}c83Zxm}hg#bnORd1j=Ih77bp-AlV8n5_Lm3b@=b}R8WWf-;Mu$#dwyPlke^&Y= zX7ZIAhO4@hskBt0&_4h)H(^Ek$eCfRD#aDQQTP~xs=gmGxdQtq<)E-?nl(EJ?;5U^ z*U9MJV-3C*4B^P-?P7}C3%tMN_o^JA5(kWqg2xrCCj?)Gy!LW5MY=E>ruK-=ZGpbj z#sz~qe4MEx%>jWNJI=-|PZ)OM&VM`GACv*hHn#FatPF}G8$n30I~nWu0h-o?5xWjP z%*LO2rmo$d0i`@eafjouClH*vCpVG3UuUg^XIgYr8=p^Nf#QbTa4VaXr~!d1>&l?F zXuKGm;^AC1Y)0D|BY^fBsq#9o5b6!%H2SObb=URzmM2$S%=0lHGcmYKi{reKYMy=G zD+h=@Q^?y8Y}aGej)8!Lw&2j{lq)5Css4%xQ`GR~Ki6=KZrWHzy`0J}2PHw`n%Uw1 z*bHCmaQE=H0)7BTUi~^{DA)!Q9Cw%%Dt!}%7^c2#O@@HL-1L+7ybYMEmX^-s^`Pf~ zV_^L|1syx^ajCH#j3Q6llQ--*G{AL$K{gTKe&yG6#lJ>1034^Er^0NDAE=qBcG2#( z2B3VHR%||1!hk7++6n9!8b>g}A-)t0hjSZSzM%;By-geFt8MdBA2W%h1_}6=6f8z! zOIJ-`hRdU_3?nAjY4$ca%r%MUU+Lvi$J=;!+NpGXj`l1}^|o+R^;g|~>HH=M@AT7s z^Kpx9^P%40Y_~Z(ZbT?$L0}kR)(#pS32y4((a-FJ3CcYGKRIoE?}Zi0Osn4CZ4%*H z+CTv}r>O~Tej9(G#Mne+)5_A#{-RH<+tximMIfXS^69jUwl=o?O~r3+L|n5lNs^0P zb*RC|+(hMWirBZSkcD9kz?mdZIY?V^mmREr3_o{tl zwIcg4kgs6!z1!|Le{4^>NZjkW#{}V>!?`St} z;+Lyu+S>PjWm7qos}^u7GUAj8hbKTt+tMf~6tm=xF@s&pc>=Fo6H?Zx$o=hA)l3`b zi_5k9Qu6_#Fc^;%o&)m*bASORmqW5^{#+t+XScr>q>L!1h4YG43MlwyQG;*y=KYLz zGZX{?NQat~qb_;gWre?HX5_UKo?TOjKZUvgaqQ{&q(o!ch?0Tp^yk=L0eD#$jScbM z&=k^Qb2dOtepVF`wn48Is8;a0XL1pld6nOBs9QL9nkYmNufvsnC-$0;DRDe)ZT32M ze6J4VHu_sx-6I{{>#&5J8I9Nyl#n&iNUfoN=T2w;S?(shkC{=M$Z>a!Yjf~5DG1so@ZP}OIDwJ~VgJudd`-49H z%bX++Hwksaym1mcfG>Xv-A~DQ&?&=(aX%Qo5xcyT6nkOI>1P$tRtth^iY| zn&owx##&|)GK7gIL~=gKK{tXJv*?ihccw6o$cY9P(f9str>;J0nBx<>rH4>+s^X&S z1!=jW3%)GcbaJu%J0uVh!b55<&iuW$u)gyb*rHB}F_{)fI{#fN@RJH1 zHB@M6RA~O>TAC%3eZKxDvZiM?(wCN=P&B$JM}n$&+j$2 zP)7`0!C`lf=*6nN$LkDLZ%?%fahV)A1a2a>%eww;4 z)t+sC2}lQ1`K4fKhqP?$&&oNSCQeJD&Kh*g@o{~tD{Qd@)C{@Qg5pG*r_TWxdOxTI zMn{g5Pj2yV10y*E9QAVDeFDiF-|;b>nPr(7OP7q(v}~qrHRl zXe&@@4lbzv(X_Br`qFq6*>hdJ93R%Rq2FWnH8p#!1M8Ha5<+M1uX#T5|4Psr8(u|8 zeiig2ti=G_Ix>^>eD`A6S{tF2QI(8{iNK060BgrPOQqgkzDKAs+;ovig-yP?zTP-X zfrW+}Y9j~`(|5dLT93s;TL)%aU~GqWLMj6kpzv)8amZLy*eq{eq=o%qz@daYStz)f zK1P#X`CpLPy+w$LZ6qHHa6DV?q23*we)t*`0ElV2=c*}iM$de>Cs`f)1C{5$*+<;r zJi^17wUstAy3Ks7nM}XMkX0v1ui5%mM)mjP9h@D{)Ur-wc>1(V>ez@fD8~+&Ua+Xc zS2pK}r9vG-s8UU`ynHNNE5%{yDBz)EbTTz;-bi?0Dh|l!u@xZ!thj&(q!>2T%^R<1uWcOGR_yUUhwYb19XGK%h2ChyQTp2A&;7!s%<1eX&3& zrYftcUT97!S({-(X!)2bIi0OUe;>~SsHC-QR&ueHSh~jlIP2{e)Hxe$-UPK%>#>_7 zBe4mA!2(u&8TND*S5gjSpF{3yafNHA337Ki8j5_tbGJ!4Zbx_W4atXShYQqse{k#ORzeT0X(|t{4^^6>3wVjEU^_xemj;gFbpkulTO{&a+(5Si|(g~ zOTI=UQaJGMg; zbx~34wBNks^k2O>w>cV1HQ_%ypLIiGJO4UdvNB*N$bdY>rxfVL*w$q}Wnl@)5KY$5 zHrytP@=fvm-N%1tFMjw@gL(Of|ILs=6PtiRLLd4KQqQ+M;q-}#$iy1OFiW>WWwx@y zMtqDZf~gcx(g+{~BiZHqcbsu9c9T#xJ0b-IcW-zK|JV+h0H@c7s*c$CNot+iEv;cx z%)=nd5l{yXyBfca(Vatyer*K@r?1n3nQ4oY@6_?_=-*K^8x`zV)@mh$W4#d(<^u&uc%@#3rL@q>=`7w62&_*=wGO zExOyF$)mt+5|I^sSMEHZ?S**@4I6L-3<**kloTb9L{zcwu6wu(X3ihrA4@! z4?r1FTUjdV3BF@){}_MwX7jS6(S$IAmd7-)q8o!z+ z%A=-ffB?NMI`Kwdpczth_2)B)G|-{c{^2ll+h!`6Z7_H886+_1w|nR79!eo1M$>;-s^qOvo9M2>rIsk6L+*M5$+!f-WRCvb6 zJqZabS3%4(65&`0$pN%JAo4MiVB@El%Zpopy3-MSiEm97?M#gdd;%X2>57X6mmEmk zr!bZG96UAimd^(DLE0S@s>X22Gx`^QgGGF*5D6|bp%l9R*ED5VM6lNhmEYQ7zBzg;SArJI^W3Fj)f-R%Q-)q#qc3^j8(rJ zNC&Ve4q!F{x+Ui zp2fRBqE32?@s{RkgP;D~;HXE7>mEBb(*ajBdKlVf2U1G3FW1JN3;Y}vMc5b1OR~QP zd>#Tj%1gi5wm%$Ij~)l+;iF}%V-xv9AcTovipVDw#tOUVOLZ-geUZFQIl1%mEe*iE zG!$BT01B|?7PE+}oI{7{9v=;g6wQc>lOVsSwfhaJ2W;X74PR})dzn+QsDSyr`ewm+ z^{mY}kCDb^+j}bPvJnh9K=a?}7>(^1?SsR)VIUq{-5BrWC<0AqkJlAc3`TDO7!1tg z_nK!um5RZR+`r2Ts>x#lXGBFn^?^#w!&0V?oMuYaUMnKz|NJ>xGnwx`u5V^)g04p> zPKM*1`?s`e8$APaQH2U(;Y*Tt(fW}m*=mAvTdSc3Aa~5ms<8!F9jzb5OH28z8S+Wv zESoPfNrPHIeCAQcgeue*P^=+b5X8cwMu9f~zp^NxHzT?;1`J+&hN_|Q;Iz5%xrN46 z6OeC8db*r;(fU>?Y~s6jr+4i|LKVW$&qTyog}R@U5&^;##@iv(;5G`;jU4kI4!jl|Nl?dkyTA>*VA&7r*6Y5X^vobP1twIwc4u{iz5mZMIIo$hnTG;IhlpWCa2ub`CtmQon zJmnHNHfZUMt!RI z--(fc6$NP2HMG>M-|PXwn@R%id#EcQxY!I`fnPJ8xf1?JbZw&v5LHKyOFh&N*UlnZ zJaQr|rAe)v)Qs@!i7~3QWnT80(Q5uNF2_3?g~%_Sy*UE2wFx= z`3)48ImwMcUn4ZNbc4UTJiB1o*a{SQuQXoYmw-Ag-y;w;v!GPwuBDdu>u6=sjE4bm z!7nwq{ypO(d|`C~)LtA%$^VLZ3hpO;iDq4>-XJp5@fh0GElG_FT#L)*->!bk;fiOr zuY&j}G+Et_v}~0`PzR}aHdnE1w&CldFSHtYG;s7%8h>Yty54eWtM2}Ml7=eCAVlA+ z_;=2(g2Lyg10&5woI#wSq<-rw5%>7>qGq53e>Z<#5j8Ho^H3ubFyi?auB(x^r@uQ> zL^r>Tga86)_h~O$*O8FK);z0difQ+on&u@|l+_J?{tmAXR$& z^51(p2n)4k$h*$j2N8e)q33PyNS|-FfC?|@IYVP#(moh)V^N{CtKrZRl6Pf)6-yDa z{BbR#tjN#F56eXKopX|icN(BZf+ASHS&H~TPv!Hni+9=b4YC( zA!j z8vIA2i&J3r+xljaw8IG11Vs=HhObdJ6XLo}U6%HVw6E1}2U++|%dS||T!@KYxI431 z;Bf!IO{@{ZV*(5`GrJ$B4tGOo@QI!J8BY~@j^_&>X92jh%^Rp>77*7BbUr@?4sK!h z34{?Ftg{1wZT+Xx`U@&8kmCaE!6dh!_eVDez|+|Sj6Rhmn?urt9h|m5YyB1tJPoTcy$2Hkmcy-YYH@Qa@nM3 zG@=uE_A?u((3aCz`)%hE6R5&40gnuX|1X0}m`#D)AML${$I|R*ej?}*R=&+8fUG4d zP50bCjb4w}p`e9=l~cmFLr;}CtWoBEdSaVm z#_1gE^>BkPJmp^(e(q@_SK3OU)iWDYaV`FWkKrttB}DL61Sa}RXivYIT04lEbcU73 z9g0D-yl^}po#iWT0x~Rv!m5XhlseLI(~(d3C6`D{)M|>IG6DbpZts8aQSit}PS&@K zfACS%tx@-+3tb{P)$mH_{0R_dw2V}80Rao{RB~G^Ce%FVc5wN-Da2)Qb_jJI#KX{9 z*Z^=)p?2JnPt<5Yr}cOHp#AmnA%?IbG&K7NYo(goBK1DDrbk^<{DpZul2C>k_9A2I zG}Tv0+3KYRCBWQlxgYU*e=y^wBh7)Xs>keG@rDu--c!KM^nxc7m7&O>g;3di8U;xr zWt#v7y}EUw#QvKd7kHFijOH0vt{PU&GjgXiwbPksveKy=<5cWUqIM$6FG9PCOYBQr zD%9yon;Qr0HV7TdxB)D<*6A{J)(j|)WswLQgmNHMrspXNEiO3`$!om{Ae%2QUE^#+ zX9^Oo0}fFF9`C>1yConx8aRR-c4RsI&$_kx+bYVV@7=5{-n^#E1M7jTB8!%#$XqOQs?EFG|8rJ`}9R8DH zw~2a7`Xe9nY4d<+@I~}AG+_Bnm4y6iwhEb{9yr!3py%H?0Tj=2{8#TSQ#RpWeIe3# zARy8IpWtH18Dy+DJDQ1fy{H8vzKu$<;!?x^$;sf=EiEl3GW*o~D5;{QpUO-YN@NhE zRTF_j3K`BJC7Se~;pAtM9zwfx_|@tfm<2%zHXHvHcx=eY3m109a3VQ6_>bf3mPmX`ESOF*GBVN%` zZKLe1;;DZCuXsZ}1Hd6cx}GavF>LDXGpIRH_LS4y1*yKoP~lR*-hDbywcR@iO8rYH zl=y+TY4ed!kTEI2bt|y={Rve7Ybo>L`%Bm4(Qf0E8kON@r1V_WTH4)d!m(ujwb|d= zJ@>C=x{wja}v>crEO4P@A(ZBqViMiB?4ips|FLAZ+k{?3eTzeXvSgf#rbpodAC*P zs{ew24WeEV7FC$g9vcB*=68>QwE72g@oLohZBn`~_BTOQYf=mQ44^+#!k_WXF7^JM zc_(1KLyIjOJ`oWHXu#c_BMP8j?7ZYc{6Pp)x@sH#uQM8g$ z+(g)>M;HrU*9b0rM>iLTpL_*dC4yo6nv?x&51=6lf#v9 zw0EXWzNK)CsWe+_u!ZtS&gx4aTKN8A`)nNHcC)H&NsQm#UtNIieK&N=L|13N-gbi) zixPj2x$b86@HBp_U3V^D(Tj}Hacad&?0FSy5hijo!dmsxwYK81sJa?yMNxP0J4M^* z)4@Y69|GeR$`x5Vvg~z0V4^qzeuM*b*R#{9@(9G$y{8DyP4`pbcNAyDe(l!lD~uwY zkx$REnv7cqc9MaFk|QZs*O|t z(fiBSRgXs;H#IO;^gk<-(U@ge5+j`G&}>G9Gp$MNdSBv+`oqx4J0|BivM1gYTLUgNvG@>ZA{37@RuB&X8^ZJDUsWAYtjG!^i(D zOdWBa)xZF<_nB9THc7_}0d1qU$3JMs>|MZ{=(bC`ETb}HSgEtD{^{iH)H(4W^nJJ( zxVo$@sBP372#X>xU#~} zh4oiHj;@dR>9FvH9ZF-gkuKEi<_!g}bluHIka8HN;m8XGmBQvAzCF<9fS(f;e_5`8 zS+#ECRD}?lmZ8Ly0Vhe8<9<{YCGi~F+M;=Q9kk+_gTPK|H$8x%c&>l??B{rN$Ugpf z7Ndi{MTWD_Oe%sk>cLKgUOig>RAU3=rtF-G;^Z?4q*OH3v%dqyZPmAJ_7R4jJL@oanRu=*qkfDbrR`lutlDPB><7aJI&kq~^Glg90UfWF?KN~A``6Oo`zzOH#e$7M zo5?`c+fjWuYZrKtt{yvV7=rM}*?m8{CN4fGi~)3-C};lE+6c1*Ptd2&DteJl4r+qm zgyY4Wth6NE-}72vIn#s7erW#YN-Y_JvZ*)kPNm|sT0G5>?inG-frb&UB=PWfh10b@ zQ#EgVkd&kQV6?ez(f8{+YBouLai*>d^HO9Hn%{(_2bHJw6NPPflq3ae_W4w9*O0^p zfD3A;*sbd@^A!GUL6pMB6q1_cTMr_h!U3f2VmI1Tv=-LRlAQVJbFR#)LIi3|SE+nd|Al&%>M)xQuhE6`~W82=b{xi@(=y=_qaLcRsG);wv!H!2s@yWzC zYVT7eE0x?P8Ch__;w>n)l)*l|Pn+kE#Jx6LmpWeGkkma;$3(iwG?UVlQB$UL!mMEb z!*F{OJcE8qSN$6wh&CaGDB2uHKJEuT{=!OS(vO8=yz3ihEqr&(Qj5!9!)f(BVlTn# z%ROwMuhp<)uy4}gwyu5oui?B}TI`Iez7heJGr8fs>1Uggvw9EQ%oI21e5AF~$CX z&U`{GHmL%$=&JuX8FmX1tfB|O#Dz2C!o4l{&!Hh?X_hp<9!1uhm=WPV8PU#;RA#Bc z2Gu6CdOHt^4tl$NWjn4F2r zoOC}-%^r49r-?!6wo7*DncFZ;^wUi%=(1|Sm;%0h41v>JND-t+2Bjt0 z36P<~ie&vBnGgL+SPp@=^AZOi+QK@uHY%#u&-mfB)Yhyhu|UK5U$I%PDNp~}AJ77kEO6EE9tTJRCGGrDR-^^s@U;B9}=luWgI@i_tPJKQ5S$nN}-RoX!? zAr7F$Y}?2cxP(grs!hf9?-*@oAD2Z>x9^*ajVL8zv-wW>nVkib5?A$W*;hX?U6&}a z4y8;G4_#}i5#VYt4!&Wz^4=rf)*lf|qm;5m?bL7Eh$qN#&KakOoQYD%_F}(+jmh(+riMNuO<8bVpK3~9 zTS#_FSfB2to1@gICq!GS+~-)ep;t(&vRzzw%qVyvzG$uQHUobqN)#<+&X+Eh9e<>9 zA7!dBGGKa5?ztFlrx)vHo{}6c|>VIGiSkHEz+YTsX?x}ZxY!K%(Xcwse`tMGq%3DP zXk0o`=pSA}1Fw+tu9c?qEyj#&nBay#l^=LaZ1S~dkDvXKf>C$< znnkQHI<1zb39OdAK42jp)bT_whfH+w?OW;Le%UVOtll)4SbiY%n-22{JE8|%+YW;hIU3oO72qbv=AC%3$h9Z{WT{Eujtlt8^9p4Vf}UkGBz( zBROvr6cfvMdkiFb8@mSBjSab{MY*~03VDh8zD?+owI~f@82lrTKWIWl0 zBz$N2#r|Ej!$lX&uS$-)fe$mknYEIA`*nox%>zO=#h#r7%nZ0&Lu-JK4VDxzWXzHu zetqf}I~?6%af*?#vAR+9BEYT3fw?h8Ro{z6KF0G|1`{!hoOX0beW{d?lZKiI#h3ni zqv%kl5$rHUk37G#`b!uf5^w<`A^HS152(1oCgp`G4q6l&=MCU)6d6x8LdBy5#)-O9 z!Rj}}<0Gtno?%NrJ@-n4zPU%pqEYVS}&Q0V>%bYUA^qD3GwID5-qqE%2$U(BD9;bCY+&ZHlJWckd^Ptc}>szslkH&_Mx3PszE z8!DdqvTRq}9^M}`*ETVp3D-t649Z*y;#JYt@-TmV=OCF)PFv-=(*?qgjFb4dzWKHM zSKrPZmUy5($FEU-<9vhKV}$S&?@YRW*9~%|kka8Fa1EmJ^yyN?E$KoyF^YWk9CmXd zEXkSmRV~e2T_k_4b+mXf-_f3Cu#*Qj$IFqTPP5j%lH+{p1%sh>Ag_*n3ofG#3BqbqJfvQQ;=(xW)HEh_bxW)zGNBhViJn;)6`6n+1BCW#~e>;4d01fSC%KvP=&OxIELbMf1$>a>TQ}9{ri96-%qIkr zGHQztA96pD;U9R5bK=%)_XY%avs~wX`0a5MZSfU; zg}0~Xuj5)gwYzWH8efLo?$^2-jrgM;qns214J0usA6t+H2!tQ!bHk}??vDLbRJn`Y zoFEq%*|VBqYN=1N_}Wi#FsZV>n(z01GTV7^FVI2>pVlp{i)3n;$QGh_+ z^n9tN;9$`VX3-K>M{rr4>vgIcUwmdSKtKt>QQfY|)LjGryu&!gU2Vnk!X;-6qga7z z%(MDH7Z_GkeE9o|Qte%vjL%%JvF#n{QyP2WGu%seYPl*3zGepzf!99jaR!u&$#5rfBf!Veb4#QqD%KAP{095IOb^()~1E5(O z)}maXsnG*0`YZTjoRL9Rt*FQDvezn7(((RQrymY&#(nsGE60MltJ1srPYA(KR^{&L z5V(0=<>0+hxl?U%C}OB_H_!Yhn6@=!CJHR~eLc7uAOMzaVg9|^YET~huD+R#G^-{| z_CCIJFQ>YNYivH&-jb+hC;`j> zE%oQh)ZfkXKW{oupk9e#2+$v8b69$1)>@_KvnAdD| zk%9)au?7$_)%lW)93eQyop28=TT?i%70TjfvmA|&xj=1VEU=<##BFW6eamh`*-^!| z37e!lc_1eHtaSSNzSFJ=CN4M>1Ofpk?Q5nxcP-!&*i7%rka6oknhnBoYGpNHNBUXr zs7*yVj#ic&@&o)NaO)`8IQ){dv>htYS@KvuIlI8e9au+03^L8h5e|0(<_m4Bi=WMu zPvVT)E86}MUN_?dojL!SSkNa|W&(Kv)m)I>Y1w=!RS+>XWl?BeRAn?VZ zN<(~2`(i;rnqHpH$3yiZ#<_{cL%Slb^M5pNs|8`8`7>dMB9YT7R_6hAi&KQSe>c|U z#)Bs+((eDJKX-Do4%5sRKqV%7Qk2&NrXPzBU*ArBI{pQe6m8~$y zyzC8`>?RI9sF08AOg+h{c(+d+!}U{Xzvaf8)m!DpFxx)_FodbLqJJtTt@#E>urPLw zD;9XU)uL|v|FY{_DS8YU6(IUHV~lY zsA_xtca!j_)=z;NVtW2AX{Q-`0DpZ`ZZZ!2X9GYik_hlar$+O;fB-@9W z>syJE$(6(H>EVfx!U_I$Op2*5{elYQUM+i;B{d|=6tS*Lgufs11e)6XD@ek2Fv#YD zM)SFJzq!3=&3z1lK9bTb+?>Ft!z6M7_~D=5qAA@@?YThvSmLSqo7gwnQ52~)62;t& zLM$@v^r!)z+RHZaEBvOMj_~ZWlMnt@Xy297`nXkgX!biL56ga?;{DcwP=1M6kMah; zl~#^!xJSdwshOgyUly?pf)@* zmm(WF$$GyBa#mocjfu)zmI|BDiCnRh1Qp5qEu{(6({5RK-&Ocvj$Ck{Rdsed@ke3! z<<^^Cm1h>VFWI*;9F|Zt{c!@NRjwU7amBVECrB9QT!d3ed7I2MQeb^1%{67HZIsF#Or2PaHIOo{2Y_ezsaU^Z_^{ zfFAKmUHG>!u$hB zQosQS)V(Ia2~{-wl^pMdYg@6~F(6$GVXfRd$&(R@0#v3=*M0_uuJSj1kX%n0;axTQ zD)@l8Q8_h}Z$?~uPpJ(hd>6*n@7o8-->tVeMWD3dy72&b*kVtSoTUB=dIClie%(;9|iYfpzy?R z1Yk5@QP{5`UUSU{M0zZ+C90wO(Z8mNoud-<-MG^Xhr3%AHNCW)9648OSN@>f!E6{h zJu_|Zu+Y{T{X!sX+3ma=9FK!mnl&}GT!19|3{>?yBhW-xnomHQ3|QdiaUX*QLC0mN zDV$l~1O+vwCJSp}6*||q?1?At?%SbC-aLZrsg#7i=rBD9u9#J7((@RHSlii>p0@)wt z|7xfE^6{|d7%uwx)m#;`{LfI19u!=h{VWlD7`tav)YZhXuAl`*tl)t8CP;=EMsi<4 zSx9`Z)W{&1vzIJQVCfjKd#CsE`C=5Uc-HY*r(aXTCW zjFYb(N~8i?9c*7JT&6Zbs3cdau{uP8;t-XNr^4mlxjec7|MvSWGwv9(CKs)%s7$OD z@I9C*fXiR}S{A4&E8q_`xzT5e!&56q(j;4Ui?(sXV;k$Ea-Tkg_l*OBKFX(#Nwjiv zzdh-X_pI9&V>IzZrbW*#v@V?1rjUzE;78PT^!x3CnQqQ5iynIPRC~B63{i~ zl}{@A!fk>ur^>AJ8MLU>DHX_Gx{p)ef1(F3jw{==u{?|=A|Hl9NyXHrSU>#q2&58t zSlB<%+LbVF(?gzw6Dw3c^RQR|b_M-p%YU`pNkPsw3KjqGaLj3|+oQ&KYI5rYl=IE$ z-gLfWv@wJt)bM96+{!WW!~64%Ak=9p;m!igzXOC1ZTljKc%s)`czIX_xs!YkmOTYs znA#m~$AT^Jg*N|>e)h-GY{mu|J;$ycBj zn5^V1nXLzXL{TV(H&nlfcdkqLUy9r@= zy#LN6oEePA3^zM%MDu}mh-S>`&-QIq3Y_oXhW001Bu*SLh?ugpIoB#^D6xcX$0T#C z)Wy{&pcck`s&o@Cvb6HXlge?qSA28jNx%2`TnAv<2ePYtrvzu7;-9j3mB(ZL30!3$ zUq*DwQzMw3QI2y$1@3aV1f|uY+AV0U6Tx&lOod3VrD!gywO~s9!NK1&Ihhz^h8t@QTW5@Yt#IV?$?7}5TiZJ-uKsA@!U&sfuC^gwIrskYl13CPI zplR^7uz&V4^duD7KTO7#<^F)FhS4??$n1*wiIN(j*kE-EGtc<9Xpd!K1NhT!!S+jc z^q3uWF)%#Z3rP{*6GYfjJ#?#P5&5JT4rltP*E;U3g3!|p;QnZKZWrRY;m@NH$18~>D!ciVk( z-z{~qJd{P1#bD0E0LEWT@4{VyaKqUDRgxz?{r_Ys^7!tih`Mjf}3Q#o!&wGwU z?26i2=|Q07)V&1da-}%u9$nS z*|M@id4(c$%i~rj@k49>K(cB30Qb8?gLSF3L5ARiKi%FLYfZRIATL(@>wv<;K?<1T zx7>aQ3Kn!xsq?uq!Vc<*&hQF9sCS9PAM|MyoC@%G^Ml~OzgY@oihY|Zkn>F~H85+~ zebekr84?ly)&N(yih>}G3N48sOE#Mog7klOXl56Y zNyFt5Y6$@sInY6uOBFN~$0B7Ypyv}K52XfDBSqnSC!nQs{Jt#{3#`BKqL-_{MveK; zph8%baUpXI^r%v6&(8Q3wHpS*?W^8tyB$^tStI4K+4#!b0m|~G`T|MDWVlf?W)FQ0gxTp6nh8gB$68;a#>-5YcM#gim)TW5}C#2KgIiRDbG3H z9QyLIdv%fI2*YhQKki^=+QFN(%NL4d7bPw@Q8E9h<8|&(5-EXPtJzbEXJjqftYNYA zC0ZaBP;5vQ->yjE)p|NvB&^fr9$cw`b&u3M%DPvPDhKY1l&~)~)fF|>u~55V3@Pa` zm8ao!B-}!WT<78VEG$p$GXf-U^*y;6=_Hy zc>AeN-i}T%s_B@oWa=Z3PTqxyM9CRT)@-<)gm@8^8bvadT)yQlN}BD~s{iiS_3m+b z%?2dn7W+9pA`#y>9R{hNvkbM9B#u$fjy@gRk5vZ}S62I@^A3VQ$n7j?YcNSl>_;cq zRY=1C1wH)NxPcv?RL+aQfiO-)Q=H>LLchic+CR*sLRNz(K@)DLHt~46+TSOSG($^^ zm}lrVOBw43(!r}^jML{5sJX(4jKv)r5aRfCnHVcBN2M78SMC&T^3}@U^?leS zT0aQrY_64guY>0Ttk!n1>3tHJ zT*sl(mFwRW4RG!S&DquV53d9#dZKnS6!oGv|F^Rs*V5_IdVj?|ww%)~{CZPfP7RTe zc7W%x2~AbWsls{|AC-Cxz69;11y)mR;3<1II&nX+6;;T^4sw2?ULalwQF2r{2rO$Kz zTqCDTUW|c>IX41SAC}L>E<;BUw|*TE&a$T~k%Ott6h6cPlngE#!z2wYY4`=&-*BfH z1@ghLBL`F;qIOk|;#i^AcJl$Z#dT|3&T7;=*5mHobGajPw7gCTYGm+muFcte$k#9l zMTdN6fj`k>E%1o%VtLVUrfR34p27VyXPpglMlR% z1`C?|*B^{ELf%&d76J&1SjhBBIS%G!aV<2ke_O_dD}Pe~Ef4E>ivT!I(|02oe;79D zN^^Tm^)6G%QCX2spL=51djz|ZmP2!{cn1ie1>9Fi#za1?8nVmUX!I%g#>UfOBIi$2 z1dG{sMt%VKL~o7SI(0?5S&QI|>SfFI(CDZdWXJ!{W2 zTps6piOnrUJ54l$WUq>b3Ll4FGyZua{ayq_FO~gT7;z>Qt^mQ>0;i&Z|3ZL*lcKo! zuQa1Z3iDPV)FP&eNj0>a2vU5QPe`f0xc(>#A+4@hmm2}eE6u!soFxJzFOTb~7mYhX z0NXmG;rBSoz?2ewQX5=V-bC$mJj$hQcVYr{qJjkLTZ3Dty0N;_P&wc784NDH4uS#{ zE&;`+Pjw6nrie34YEjUl_vV;GIT{yM@;h=2ztnkET_85J1L5@J35S+@@F(;d+J{>~ ztuW}_bx*p69nOTmLFl$Ndu*(Q&iVKVj4_WB?7W~ucssgx=4ZHhjI0s+1YVcOkrv8$X57=-_O3}~AeYc;2Xz1zxKPn2x* zTySy`&~no_e7n)FkaIoFB0#G+9?iUif))1u0H^qTXLHO1rT&Av+7$x&Hy{4GJnLK3 z9VJ%}ry7S5opW0JDSe-e{KhS+-(n_Vhuq$^Yf{8+HW-naRucO}WhMJQ$w~s{FpR#V zREUkh{~A|vo{&XOO}zyHC4K|37Q@0wb~Q}Eqedmj!i+ZQ8K2FNRw;KNJWL4@dzqrt z_ecsHQfsOWd0x0mSs+nDJK>xdyZBtVtw{43YPdb-n}SaJXd7Fy*G0K7wF?@_`NHBk zWM9F{u){quKGx-X^_ayo&cf_IsKCJ@QzS4~6pdyUpt{7xLt%_YFx_m3H+!G>v}wMS zT4JhhhPeead;RQ~+i`;Ho#Sb9LO-1hiB^k!vucom#sn%X`^jba|rH(9#a> zFeG%&%VZJIaEq4$CxJTO6+{TV@*4LF+`)a%OMEtg@;Gpv`;S2wf`F1~n*Z6{b|`f4 zq8QGMCA6w?3!`8cOYLPvsTQ&)Zp$(~0iy+AA{(YxZHF7kU}DtJHYsM%HWWbx;Cb=E zobT-dT^Spl5wzn`oa>^AdDa1KNORa;QkJd1NPii5P!X!WN0M_;v?AB4|n{JsV z&0e8)P5)V_b0|No66S=EF(-Q;M-92P@0*EZsdy3vVQg~GWR?(;ci7kRh3$7=cf6-* z8$&BMrJ0gZL=Twq+bdV*uJ!7?2S1DHNo(H-kfp<}mW6#MaWt zg7_MMbjz!Mp$lsH1X^`5L&1ZlON&|I6(Ep)A42#oyWp%PK@33iH4rt)<;&cMMf_mC z{igT}Oh->)1>dw9mo;e3zzeLHv4HKF8P@0KlNv4 zn2a%taIBic;<-2%YIBT0Sv0>676fGw8a9aX!cpK z2_c}nPu;q9=eKRxbQX>4?_oa*RnjZlHM(uLwh*oO17?H|NR=bJY~6jTyCFV}!x2WQ zMSWoq<+V7br=YUnCQP71#cNygz=wd@m)Ai@P~Fl32`F#Aq&hV5V!yQ_n2A=N=zB*WwqHlnu=?BO2{S2 zHPQ?oZ)dt;c8D?&8uX(+P%xMkUI>Q|#+{TDI5YEI)2L~pvkP@AB_A@IpFpJpM`%}k z1&mNot6kF8@dAYv0Nk-}C7?C^tl*tsiz|;C-a`u;EA{Z>Up_AT3e0^#8YcvgLEZY? zPX50g?*b&H(Ackkw1XQ(J|JPB_~vhMRrL0Q^z%yt>VJ1-B+FQhzrbce@*`nFroLEC zg@dVTboRDV-Kw2!b28d$K4Y)p79oo+2fGjMzQv<(#A$81tzc<<#0c*6-vf<`X0u$V z0gLNcXr=@0=j>Q%rs2jDHmFfc$`~;;s)efS@Y0GyGL&N z>Z`|BW;+t76SnJciOxyLu3Q^&WxB1emI=puHkB(FoNtQ9q(jkz9c$ss%`mrvX;tRC zLAvR2>eaX^hNAuKW-*Kd`9mkKd(LR0q0sZ|l5dTEM#kTI*c42{ZPZX+>7@|3pIITW zGrq*1T&~N_C7)SyH3WXaSHexOnW`V*@CzWU=DHtP0$ERQwPannc)3#uEZNzt zf|$Be?k-&|qJOfyR<5j}lE0>sWXG~L*W%^tQRs~xO z%Kxn)ly}0B*TU>=f#UNX4&4YRhjaG{?p5IGL#tguf_=&BYj#sDE^AFJo&SUg@?BkT zae4)H;Wl-`45!mn#KJadH=td{$&E>mZY z%RtFf-1bFp3e2|fs?{3%lW-hPx^iRx;>H(eirhoO$M!Qh*?-kkjm4L%9$M-juoCZ^=#rTd;R0V0HDayi&Y2-6Is$KZh@fuk8^M0CpvBM5uBlH! zg8koe^7=nFJ{_(-Q!ckQZNd+Z!Sgx*W(I$))4eF-7pD0-(A~suPgbTImVgg#O)Q#54(sL*lIzg7Fc;*z z77L#X9+mKpR*1$?#Bt$y@0X6HI`+eqFZ}Kv2#cwPDa{gzf;WH@&OsaQmOb`Rcugtw zfK@`<6hy&PUgOjA568!?zt;p~HoJ3AY{T1XV8E;QR&{G`pNMQHJ$x8=E#8`N$qC%; z5htAi14D2?%-S9JQgUlTIH8njtb0eEdvf>#$7?J`i1k-k|9(q+Vh`#?ic_Mels&tl6BmsZ8m?C%U zhG}#yu6eae>gn`MYV>(}C*J8g^lL#E>0J=E3N#$ej<5AssB@~1O9y{4 z(Ul@q=~;=K;hgX3mI$8O>Cn@5uwb>c2~5hD07mh4no(?u2S^otQb}RvT6ffr|lVU#lPM+Y&?Y9TL@l=|tosPEQ*}JTSV_Bvw z<0_ic!MkvxRCbF6uNDYJJxm-$f(Lw6tiXql>2TFE=C%&$tTiq!Sm%TY3~-O1F?~^+ zghxSdstkLwb<1uAyvRQGE(K3?V%RaS7iLVzh{Y7yTkRrDKc2(q=CoFHwLlw0Y0J02TW zJZXLLFQHZYA5XJTwDnq!HZP)dH`*+L1=q!`8IK)hyzdjMbtc@*RQqqJj=T5x@|L7p zT9t3fEa2+zAnWA{D#@SRj;sF*<>Ani;~q})ki=HLJ`+O#E~9-gVS>qXEph!5gx#y^ z+z|hwg&X!O7CI~BVR`el!($v#DEM#f>3CQ^OGqYSj)5G?GPL%SMK3(vLld_F?g9v? z6)xOSjH-wrj5HlJs3p5P$O zhy7M;TFvJ-X8|sg4{$`+lHmS&R7Y6>Z=rB9Rib5{vRu6_!-SR9 zPUZ3wIC1o-?edFM_8#O?y{Z+umJJQf3`I`sx*MrFRbJCA@8LLP9mxJa+&cOr`irii z4dycsI)h-M_o+I)_oB5PKVIT9zh&$`efEP#j~;C;ptf^6q@8e{UZ=E;c8udh5(!T0 zT#Zj_xzkF`>Qt4LxO{X9X_7|;Zj6O5`vmJD$3v$Pl&7-mOOfIXOoQe{4j<|fV2rF# zh*fYVookp1EiX=5z42{H3vH?J3Ds}ZfT_WCV886G&cDDrwt8oFY^k+O*HqLbhucx9 zec$@*cexe6tjzMBf*fw)6)|DfjPWm4tw$ji@JDgDtdAv4Wf*jz{5-pq{a0d*Ke@TM zxVZhwl*E}^5cMVM$JRu);{rABbd38uuKFs+wgMDT#DViY{k0Y}{N(}q8h0=E_;cj{ z3}BwtjfTB_M4#9?FRw8JH}eczGazVn!z9}E?->(iJt6r$FQw?)&gQf230gl6PL9iMfxzq&pPDKksU2z^SbCRL&I`sA;c=ET#amD zfc@)hPE+mErcNAO#yKbX&Hg&1qh83-Hnk0B8ci|Zc_^~t>kYOVECcC{8iP0Ktd_wP zGhiyq+^9W3E|!E&ciuQdBO(J3);Zgz_`2pfBH~YmV%AdC8+~$q z@h5zO+nMxD#b94JiTE+^bF<{Q`cqqovSv7UXdRQ(#xfXnxJIP>>~#88wDMQRudR%k zjeEJqe)Nw`@X9`>eaeX+gd9;-e;|2vj7%@qC)=(16UkP=a#F!1hY2v^t**L$C85aZ z6n;;=vw%R}I}O$6kuDOCb@bn*EXua5WP{KZE7)_$LOeT7XwvuAXhydE)MtPR4rCwm zoBvLRg_oJ}$U3-sYxfGCH?A}Eu&^=$lh;9g9pDM>@QblE?;Sdvs1YIwMnmJC>(=*` zr2M)@vV(!oX(QGN>=+hPJpDwqZ#pveoXGgnT3;X&6;6xI-QMW}dm;V;tAakt%%gp( z<%qrL^cJon7L{!{vsT^*|4{)I7bnI~(Y#=a_Qck6xIAGqUbR zf%9S5_sjP3sOPV>s%S_VwvTfdx>VLw_CCcY*Ojl7I%%Z!50GY+EER^I6J&(*kA&L!t(AgGY-`_iDW%Tj zM?gggHM~{HM2|=S@HgkWtyJ!s*--y3?GxkIf2pE9e48{Lg<~-9?^Y7} z{U$t0IAFyiAM$8tT=6-D!##xn)!cx8%nD1x1u<$1U2#m&aKuxLawT9r8t42*DY>@1 zLO=9@m0hoq&}9D;PV=5ML>o$cXR>uf9C%Q@7rN0=d>-tzely{{r&f)t zjt&DSmS5ERItkW)!!H!oI2@bKfBmCAjF;T(1;ytM_$#DM^>P@~CRXn@U)9?P?D%Oj z5*!cfp<~;7-UIV+7qU2o-t!JEH7;yF9d<>MGw)rgryShvnr;jvk(jx^lclmij2-}D zf;a{W2w=_6fgr`g)EMbu=Z<^luIhocErtez*OIs6+U21h1X?U!pmH6mo0JCe5Vx9T zCTU20qL<&Ezrfg>6M#&urbql*XuA(5ooSPtqcD2A^!3Uw)$pe1d54ZbVX6bqYh|AB z#8^5-66_(pl_OuS?KEsIjpD}UvaJiM3oT|F>H4k5vt)xLNJR-icJT~k^Z zdf`ypr>G{F&or5UEc~3CLkJ^+@x&&OC_NYR=fp2~j?!UKurWu8vITG1Q+);4V-v#- z8|OoE!FUyqYf^~)teMkUt}ZByoM|%C6jAGxk`Y<2 zWUM#NndY1(!42`cB73XjwQ8*IYr`vMJ99Xfu8wYx5czsh?{N3Yfw3~BD^jf44Tigro;V}(X;kb zOU<5O4HYwPqeqZ_P=_nQH+;qZCrfP^Y)DxW+!AiD3S}kob_?SVw8_At@O!0MfrRV{ zvVwPM?`*upe`|EEF25y1*lc(Y@Ugdca|Umh=Ng*mL`kZ2EmJ2>)?LX=VS!XCs+za( zYieQ4Nx&ab@{OEt^4Xu9WdJDdgRo5&B)BBm&{5z6r|1mK%WDr}^*1K!gjdWt=qo{{ zL|BS5CzBCgRzyUw7I#?rPnG7JX@!yfyC|yCg*J>$>RwU*{OYn~s!kXf8_}#%qSMvm zl04vxwKi6YhU(zo_o4p!mOYb)Nkvdu7PBFN!}VAlfau{)5)olql6hbX+i>+QwFS03 zwW~`8NOj26C%U%ETTZ&bE1*$b-6fG4ZT&ZTB38{v#-_ncjMn9+&GpURULJx)^zb0$ zpSVuuiG1txnBqf(2WsV%i9{6j+cgeV!#nR|2>&inQxBtQ{ zr}H@t`AtX3bMp9mP~dmY1uqkg6ofpluilzi6p_$_47 zZomPJp48J@=Wf0AU{Kn1_ZPY5bLqt@^mo&r1)prK?P8!B;ye86PV<8Ycl&B<1S5b} zAraB}CfAl29Dl^bTqC;q5PsO8>>fMbTJS(mE$YAKSG1+_(9VybIsH%kUiXcLp z=i2L6ba2KH5utWab-=4i0wi6U_|sjHHrMlIwV@#ZAPlzsaEz40zLTL2T)-uJfgF16 za(r+Wuv4@{V5V^fWRXzfyKR8B3&@Shygs2`Lf~5kygb|&g5Ziml`>$<=fSryEYY)C z(K9*7JK^5h`v%z`m%w?5c{^Xz*@lz% zKY#x(kbSTaIk6EU+ubXQ$8>Z_afO|*Z2$8`oEky-f<*YKncE6Ku=WGFe}dV-97+%x zfES=3xG*;9qBV*9nZ9t$$2Qa~9)2CJlqIwq{q4n47Q7E<{oZBSvA`&GLL8FPbPUgW zO4Iav%shc=F;eF7FtEU<-%Vc815-xcMf%-zzSskF7<1}OVT2!HYz)gIIyaJUVXwfj z@d_ntKc+|;xEF$zPpLmgy3XEoUsxM@w%GHnMy#wY;e55>EO^K^mKVrUqV;$7XA&$aaw?*c6;FOK@HEe~21=eP; zSKK%j$P;VFB(hJ;&dGYUKhouXwPv7O>9pDzHXo`t7ndusR5QXY#vQR5*>oE>>B}6& zTUN5@F;%ws5t$ml*FF;b;*@bWG9tV%e!MxtV!Txx!kEGoIHdOMtCJ|C(;39VaJYNK zy4RU`c9AonF20rKol2m`q9mLs4%>#_wp^42jLnI{cr|i(OhY1grzpJul$=yU$?jkB zL9T1!pY#yOOS5RT8ux<4fk6>Z3RFD+H(tbyVUAw;KE^F@>A}AR1VHkQ3qqv_gUWP; z&46~0MUp|aQuc}Iu2L#_HeJqmBrU_KQpDUUH9Z^F!bFRX=I^e}l5-y%d$e*(gLu29^-#wZz}+7j!)O4sgmEA}{L+{K_HdjzR+^|$`iaiSd? zS((E0$F6?P>D7$Y6Qdw#(nuTv1>{h3qjAES*4EjD;zabc6;$G`HBb5T)l!xPyl_FA zs{b+W%YQbNMh=)a#1V>BD3DBNUqx%rtRcU~rTm*`FprDx+o{zR2ag=8>bgh zL$3ii&G)6r>feS}wgi4ic)|%p*?L|3Z!B3nehTEt>(gK`CY7PM=AG^cWpICC#sEBq zC)>Cu>f8M6%&UrDNWwE9+miBrucEP9hbvSmneB20kDY@qsR4&3FRwR2!wsNhl*d{1 z;b5b0s{aTij{X%bDZke82W>8-lbbb3Lx(0Yw(0M`I%*!lJK=QZDw7Rla>(9W6CcmU zPXF~gzn8^%DA-G_DFZx^I*+}v=pa-Y(w>$@wO_xYo4;W=Z5>dUKRDNmX62u$ICE$Le z$HN|})BAItC+SRp9PJ-ByAkDmkfu4+HJH8*{90iozx6p+O_O@OjSdPj?9r2S<&R(V5gx?PLuD%UNnh7y8CA}v)e&;zpO=#jA99tZW09Yrmi z%a5Y&FjDjGL%?Cn|?F_JH2V*2hpy2l@Isz#8{5dUsh;+a%wy`Qw9pP2Z zmmz;6W*O*9cJLAXT(K8rS+N6F38d$bv}(FqXPaAm~efJxI$k@a) ztvXobXaDhk69b|F_D@wzR!5f`Jhhxj9U1K*33V@ho1^?3s`RL;0u%z3+DZ@**VJ~| zdqa->0YVVz5Sj|+iT!PEo<|$8Z+d_Pms0t!)`Rn#4e_PrC1*f$TDn{z*I9)?kJ3uD z(Id_rP_IFISpKp-m+*{U^87=N6Gs1-=>U(L&}4WLz&>C0z{cV7<$Mdr$x$PMPVP^N z`TFW4iV9f8Wz;a<3+w274R3$sYw|kkf$X^Ic$V>458tJoxRmy{(ID;YCGJ4VRriX7 zM5E7Kz-Us{;z2mgz8A_ENLO1|4uZweqxZWcIR)f$*KDpDC{uv`3$3Hv>6JeHP(P!t z{rfpB9CbbjYnjl#M#y{cU$qw%0zJoaN_cV9VFWEo3Z>usL+BufnU89NW@(^Kr4Z^~s7RC7R~`9j&MO+*h}9pK z6eTz%I&2Be!GG#;s;yan`cLtUGuV)xv?w~47aq~JFMn>LQ_D~ec>%^J3Jh-rc7=tL zV?embolG$Vh$yn|Qnxy5U8AqTCqI*jx)q9%^JK}Pm`@PMl6{T3T1D1Yw8)EVxQ+v( z70vs1L9zoNnH8WJ32&m3LC`3$9=amt2qLIA5gwHF%5)ERppdMxOr3m225A^;)gBO) zq{qCWvDL9wiQ-$Qd-CDDMdmDlO8MJAJ?D$Q6DW#s9%V1d--8abnpl_wZSB$KN;W1D zAe3BfozhO~cL-0i?2Q2mcd*~uVX8=sIS;8{FP=<-pD+@k(^1uN7pRo*>1c2soYuh{ zh1)Z+tu)#YAV7o=EzyIX-)<;OuR!UAWH{8Ur8c02|2wD>j^F1Rf5dV)j(_rVGQA9W zi~&@zj_y|SJ*rktBLwKJzZzNm>0$+yEP!*-f(?5;w0%54F6++h$s^&@|ILD6o%jAo z1R!N@OmW|G<@3pS3ZRMh2v!Bsa1%f1B30xS^?QYd$Q6=r33IQ=FZIsO4UJt`X*~16 zA?Tn`CpO$S{WbpY5|fupZl^6Cn_*DfL+n9~nHLkO&*uG&Ej37PUkl= z;?X(z>-kv#;Cj|>n<;ZCn1=wJj0(njw(4Y(ac}PSm4r`Wq;RAC)X7|?yO59e`pr<% z3MX`!iVp+ByKHJM`5WWdl2ih7g!d`1NsBw&Sz9u9Po-^d_PrBOx3^CBL(VB9Vk2zTfktfRvD0atr0_&8$mviQi^F3sm%!J4jJ&|V^3)a!XF^cN zRwmrV-tp|8Sv5$;1+yRH%V)#ykuEC+p)xCOsMZ4{fdgo+ zM!`THswWuIxO{I~vFXrY1ToC#Zjft@*|&Xje?hek*CO*TVH#y0>*0V1jD)nLh6)d^ zkmGu3Hgch=mekXZk@l2oD6Js(##%z$$$?0x+@lGKjnJ!+@$122RHvjv3WX0S1P8f} zc2>`Qrwy9|QU42tSrHe{*Q}AH8L3P@u(D_s|NLmf`FSIf{ly-1iyy5~mk0y8f2l`? zL~QA$`DwRg1L~`r@4(XBqkz)sw~ru;gA&1%^U>~I^jTV@74q}=S2pc}DhjpnN6BVx z1PB=MkgB{5rvu?H8M>HM&&q-&xA7hSxBkr*%{6!Nt^KbG43zcU$W+kqupi<|2ppH)tLgjjk+jNk{n;zhkD9`X--_A_T9$eD5BwIq2Wb`+6pY9 z-dk0n0U()&ECwU7Sw0@NjTrX+Qv%q3%(z2`dTljEWBP#=o3jAGo@cvnx?@5}!nV(x zqK4qTt>tjGh!O~N;N?s+g$uLQAaO&{hSsvtJ`(+k9UlOb-1&{& z&h*+^q_{BtjRCrr`9l#%?09xl9&%Ga11&r$q7q>fyQQAN;K!!x@I{B;9TRx9Ev{3w zI99i}tTf9L6CNctT!)n6XFC(+%COcb`9cf0tx}m@(%xz)(@`%YJrS6gE*X8qe3EuT z)qG^_ciy-+!h(kt;8@M}%V8txbZ-`Qb(v9!LH8w&n1oBkK=ERR0UlzncT5^&l6v{K zLECWlcAF1HO>zj%g2fka?we>uJ)E=1fe$zCz|~JfpXL>P{z=ZWm8~cdZh)W`6l+1RV^1+jPz36xEH2qfJ{j9&cJQP?vFeA^^+0+R5Ca;*M5nZg6_|yzJ(MVb^`1h0dWH1g2C`Jvv7^zBH_a`BO*BaqkQ?R zmD$yqy8ugsKDbE3DsZN4Xvjp6gApVUo>!?S_tJOmz-8C`L1l=Q4PDRP1JnW#OFTO# zGn-ga2BH1m`b-az5@i@Lgm+eip%L-A;3o|V-~0t!g3L`WMDf^+0{-STkaXq)=0F9f zBz~718hACMCEqQET#7;Y`aPT!770iQ1-d>`GI=(X%*lfw#vXET)VfO~8DZfGRdece zuUW{k0m!{Q#n$A-?I;iMoIb?9ozG~iszR+33Qo8oC)m5}aW2)b1QR&EU?*aYFI`S{pUs*&2UEe@1OEzNG29?AP?~g&)bN8URJ0v~fTJ)rRP=seUe{ z6RR|oOj+(De16Py5{FQVymP()W%@m;Oj7XSf%I3P;lB0T&Nr~N<*<8;{<0}6KZlIw z1yLc++z}+O^Z9f1hU!fIIn&u6)MVivi}bVbh7nwIAAP6%D-?idA4FJu9mkUbvEpJv zslntwH6sL5OhoMoxwiE2jw}{N+d-^d_QqnmEgDNHU|$8I@F&^<{lcmGM-lA2k4QoK zDQO(2)V~T0(9~srK$#riWrcI~uILCfmbpF5711(43-A^zY?f^sPNn`&gXrin5n8WT z6J8ir?q*_Gzw*{*BnL}@8;r^XNGT#1>T27i4UtLX`?)0jPD2MDjo0yHqnIg3E%AX*X{!a#$PKLoms%P$V+i;!# zBW9%Z3@~n<{|=REZJY7R`szzEsk`wV9l&b3SGQ3vgunHQ-e|%$I^63@ zT}#dsqjg^q)PIDy&q15nznxEym~6^oz*5g3EgtlZO+&7FID`n9?JWgfOwYp$L?Cz) zK43e^UJkP28+J{)K*XZ;x8dSZTmq|VupH8wrRI&Qy?45mR5!_^-1^u_oX7CjelF-` zBfgGI6M#)LXwz=b<~Owl-e&i&oO@y84I^JMxtD*LM{_T(v;o_kTfTxq6Y{AHgm-t; z(tDeEABM2uL&*k)BzeEe{|{Sl0+!SIy^rsN4kC^WNrfDlDh*1ddW{)NQaTkyC_{== zXnGw7(Zr!rX%-qZP${WiLxrX{BBexWPJ=X0zx&zy4WIA-f3E9X$3c5P!&>*c@3q$R zY>so4JDlGWo%71as$ZT1&!VCE+51Yl?iX||iMNhVMy6Ar1DX0q=r_VDTb(P!_|Fx| z%kxtOk?0pFdzNE}-r3-1X2{3wlu8Dq!A_4#%2V7eBm+ZSm!w6I3z{6qxL=T@jcu#* zAWIM(aU?l+#{mXf(K6;|hhgY_67w?ehLHU6Wz>{VfCsYui3A-u2{+|^Z9h>UsJ)Zc zDb>4Hq$|hWdL#l{SH1nI1NbbDH%^4B?v^`iW$+VJpmNG-^OSCi8KNlfVc;kNHg4qg`SZ$RJKOBg!KC} z0o?L$2r@5630Gt+InsYa?;;zOm)v9#%+Hc+YRGqzpLe~@ow)Grl%4+lq>N1Z+agGF z@f0d40~+_klaWLccL-ru!SO`36Q?Uem)X1(@9jYRvv#FD?BHkkaSS8ALjNLg&UjbC zQXXl5Yuzi2E)9tXnZ3)$J%noI?i}-z&czuT)~rS6&^_w?M&K@*?M52HS3;{Hlfv_E zEhKoL-`M)!nWE32%b%$#AoLwv1!jfsqs|F-Nho~#$U_IM#1o%-g5q5lX_dGIcbwd% zx^R!mjK4~KQZHIAJ|6gHm4IXT{S@~3Js*}P@Xe7q`6XN_Av^rS*yR=cu~)`4UijdC zd*ioBwV``dI33k5*QQJ>Try51t~h_<-hZymUbk?$?k>q^Uq8gIIyyPBwB)AxEH5!B z`(v9I^@WIN2$|iSv$MMXzs~Q@S+16wcsrodw^L{JcZm>(CfS59vjh0d?pw*+*l(LC z9GD_(j{jJFODowds%>!p#k0PXyDSQ`@gMl~XQkS$>WNkjmm4b6tGc#QmU3OkmK4qv z@j7cjPQv(q8@Ombu=^sZyeZx<&2s$EhWd*JPn;QTkui0<6f*bn+A1%k@h3m$3Q`W? z8Ilr#?vV|>sGr`E&c;g(tXg;GdwXxRt1~I7yF(~b%sfBY^*x*&$3)LQOWsCV&i6Cb znCYh@L4eZVBy*~%#;N>0 ze6905s_Uxk9;h6bUC>;*zc%CS6Jz6TLE|_}dmHGfe<=Ce%_P?sZ{JHV`}a%hCIXEDw=aJ6d%NmHIv`IZQc?sgd)%ujH^~<|5arlj`$y0|T{5 z@W0x6F6@KSkM*az+DN}j#hqBA%L-a#Yq#HXG+Qj*pYfcgt8bniP}%GH#fFAa<`^sc zPU$<%*+QCU&?e2f=+K#yj1KLZ%JTiOPj-6;md1+&@)>G<6YFdEu+w9EQ1%Ct#Ty3d zo^;$CpS0p{ucpY$hBpV-9B}u6W3$fZ`JLTAuzPsjDIviPGOp;_K|1Bx*j%a<*gx};n<^<86yR}X%~5Lm0qO*f-=rcGl;i%A;ooHa);Gi>=hRGd z+c4p8ft}ZyKm4caUbLgg_mjj`|G_oOZ=q)d+;Usnry;o3gwhRsp-(f0E1$8({)C^%~-|1?Ly4fe0tn3U_Ow!uXGb%=o)>CMb$0E=ZQ4GA=~xBPe>g8 zMR9Koe<=!fBmRy5naYSE?hSjfRl*sLu4;Je<;o`~q|u@$Byi~X-jP=$lMAWI_wK9D z0+K{H+iIK?Gv03-_Tx*g;@w!IGp_6FoK&I}>Pb`<-IZA~{bSa52@~0I?D40%8|Ie^ zvemwG%yn{8!U(IQ6oaB_B;_Q$J$JvQRX-@M~)ca2p?0SBe;!=ekf?6qtL~@4x zj+SC=&+)ZnrpJ$c=NdLZa-`#o&>AWs&ZCf%^P~9b&6IAFV=^}uEC5X3NcHI2Pabc7 z@gNl{m+u{NC8}mhK6Ts_=BqW_p7zqAi`3>Vm(J4Bka$>&))H=D``D$ltFOC>n| zDPzF=V}!Tk)EU&D_bj%asC#Ks^3Vu0L)`{r0z%-Tj}}w{uH{w0c?z z<8DuLI(6U5btwM=fGL;nF>Ci=GN#0C1JuXJ3W( ze{oX5?r{Q|D$_Mn+C-I>^Ha?&-}7>-zc-qbt?a5@mpdpZMA-!XTy5&=4MwcY`PA+F zJaK3l_M9@?GZfz8-~Zk7bX#SMdHlMV0W)C~zVs~Qu$P(&;+;>A+AQ-x3Q{y^AG-QX z0r~I-anfTD1$aE&&@+XWfB&<}JSutdeY%4y(4VyQDL(O@!UZT88xOYE006VG;vp`T zo<}7H2ChD1@NvG$5IJ5im#bWb)GlK`J%`o)EX0a%dqN7vri! z6CB;ZbYb%%4YUGN^Oi7_PBC_ERpII0SJz(Fwvjj`-HgDKxHI0hx88e7F%3-h!-KXV zm)t_cE*?Hw1UL-w;v_kIl`KuH9V!xWn}4n;e?4|O2N9p_C>r7~COKEx(81Rbm(l&5 z{HKMNAm$ygYxjuR>cOwqXwh4n|I&tlHt_cq&r|2ue!p<~t5AroybR8t)TAww3&UUNTHv^LL*EMa<&qJH+>)J_) z-->f5=biA;^XbFjcB?NqTj|9`BsHuKcZ=N!^lPu2x9#~zi_+uO;`Z2w9rt6n_ zjl&6b_0Dg&1^10&!UDU!oW822R1{ZLmi+CJ(p_=arJ(MatoRu@>iNd2`pXh+GUxsR z4{nkmUR@qlxa~z*ga8SIOHG?D%Sx>FCGd2(q)sOL!$XqAqBhmNZ6obw6niXy5+@2N zrBoeJmYmlZr_f+&=T!@8PFYjzce+JKPW1gqygBsr^6AfscOx9ytkHwR_9bxsJu>*^ z%w^=X!3}AH{VszyLW*&2M>ov+QjZzMx%htX@|i++y+~$?EkpqEW6xw_iqxDB2of%X zP3|X#v#oRR>j%-I6dz^qaf`Fc%td)}BhQOp*1GF9eDo*MZmo=|$AwoXp%pg2tGjFd zkvp9ZG%xp@=z3<@gm2I<!2kIHsGg|HPw2s$2vPOKFFiI;uHiR zdvaK?bT-}@tuP%3wlTBhI$jcBlvyHbooO#`(3D5!v#ZE=!K28jJr#gpr1t-` z0}to_a17~Bxo(AzinzHM-dvs=S={!bq~luZ*?sv1&TzzqF$VinoECr%Jhm4+>o$XG zW!2AW^`-~Ux^5IDtM&1YS3?srVaR6-8#F1`G+js}^J*?+xWcP%sy2&LG6v|a_&p{}w(V;?OV*0|&4{US1JK>La9L3zMv z({dzoieV7gC)yzV&PH^88bFqh_4cLJk{5%$NTG#pzF*=OVWoHJgcij%M#$K|@4?)> z-&AgNMuY~*UbAqj%6v*2W3h}rTeYNKFCRqReYHF`1QW{?7k$!_b>yvTaCF%=xrs=#WXq3gS{`dl(Wu?=rT_d+bNS*ob&jw>`eDd zdRpW)35l!QKfKj`iDOAKef5{g8EZ__%hmeb1k8f4_{4f(hFbL*rXEhO_~R&0jFV!C zCdM8xW*BaOp}O%&AzYXsZ5UPM5i=`?MA_KD^w#vYOjm6&a$t{>Q~WCmQ3>OJP6(a| zjkE$RH<#aAT`X-7hmEMvumkV5+)|Ohg`*`WA*z;t-?MQp^qXgoDaG0N_7~)snJ$&x(BUh4*i*l!=# zYl7X%OsF>{plNCO37<$3^<={I`wc`ay1Fh@oft_>j~~o#3J;c!gtLS` z{k2lL>ODg1wTip;Q*LgSA4{fLZ+}8gLGCZD%KUSW+IUG*$R~Iv7oKUp?*V2B6Jrp^ zY>%MRHxaaMqgyw|VrxguZc`5g+>W}VQb841VX?pCq;mq}oi*V-6`k3Yv?Y$EHrsu2 zbj=^JDKV36Pebs7D%K>nwGOJKCO1Blw2}XOMS)e7^4y-GoZW=kQV#c26{Zm#TW>me z2-8_zF&6c))Ag?pJn4$qy}-1|v(T}PWMMB?&e$#M5$Qz*7#iHC0nU#xuvma&N!?vB zd=Tf@9}I#S>Q(Zu>-d~CphIAs{Si=GegATnHz(B6&<{V?{MBVQ~Lo=_*Ao)_)YP%hU%&7;^l zi954I5M#(|M_&^Y_Zv93Pa=eYgqXfrSt9TJCkQQpz_LM*B(awmIJbv&N*D^2G7G90 zwSnI^Ce|rRnM8WB*YA1k)<8eZ+5)z$6E0nPjZlzY6%4r=tf=Nqg9B{<0ywa(haDs4 zTpi~YH~f^$q)xSMX#bPG@>hmYXmvMLHA6QQiM+hB=2*@B7;h9gTERpurk* z;@x*{P{1h-zU-4Vx5eVgrJ zl6e)zXRt>rk>fa+v z!f)z`EGR`CGsHwb1@sP_9!kGNg8xPXQiZ|@eGnluU6!g|F^)hw1G*>nF+38aWI6Jz zUjay8w`RB;8c+3(`wiE&SJ+iK_YB#B!x0bN4KS6JP@T!(JZ1L$%*g!mVnRGg%0vZJ zf#lanuFCER^c~FKwf7;B;L`Jcqqy_Ww4pHfpnHGf=~c|_76nJa2}Oc>KB4prCU&E| zA~IF6Kj@~a#-G@6+LSPb17sDj83zb-f*9T|{ z=ZCR+aA-savUt(V$u5rAM-ZE{J+0)d8YQ|Be}N`8#cii=VwxKJfdquD7x^-tjF(*d zxYLc!iLoZ#DH4im!d0=}&s)LqY zPegjku~mX*F;if10Kxo`_MxYW?4pQZ=?$Q{7czrr7Nf0o;%L6F4oO7*e1R27xek8a z#0#0Myy#znMozQ!>3sN!J_y$7qt!lObzPk0l=SB~dY{K~$6zU12R!5Hp8iz;Mp!7A z0AZR5(F{ko`6%x4T^5mpv^G%EOJWQlhiC0?>fHoBb!ZbN(aK>&0D`aAeWBsU=T1=p z20uq?#5rjt2MB#!g}W!Fo47hEeM*xjeBVr;;bEUs2yH1=UqKptDI<~`Ca4dFOu`l+ zU?>x_-lh}KZ`O*3CVO72yBtmB?2o=kVk&8hArO;uqI_lh|Ke>U*9LYCbI&9O^z%i& zc0z*L;3c%f8*SljG+wrWu&F6%u)akz!0sVJSJiuVh_h(OK(Fn?%EaFJNLT0f@d3ZHA71^`UznoSqv;}1nr|Exx(`+K3vFZsk@;ih$M$X{)OZYcEq+@vhCWiM=+Ro4(@nA8bAPi&ITAntbh!kl zS?xtsHgl-Y{G&{d ztijgNk*@j?BPW9A|Bk%yi0#`G-3?Fg73{%HYLvv%AK%;vj^Nq951#O*33#xv zW1`_|=87pS5;bS1k|XJ&~#-o;2Md8xx9>WY66B zOP{X$EidwO^lu0RNLJO3Rih|8o2(0Rak)gW&&5U{O=7f7Op~xwyq~rH-(mr?cLk`W zsT-sGh4lURJ?htF)8c31PM>et`#WqHdhmv94s0704&-34nS^JPWg)?tey;R3U+miI z7ecI)h&?bD0+P6>;0=g9Y6!GW`1e@HeG&)=tx|jM%LwXIAjl-QMg9g$9u$oT-TZ4` zZc577Y_gpl7TWxOZizlE7LJsD(U92M0#ro1sN+DOtBPr!+1*aT@B5b z$SQFj`%8zcAmA|=o`tAD6nTL7`rUN2a;Ip=u{IXoy%ayu@&hq zJ!W6OI}oYHUa5>Qfb@4R!X|xEoAylCFkdyS+dZSnBQSRaqq)Kz@Y!c02f*A$P0brDOD0y<9P z?G;L57RRcTiO{CrZ$lKvLJG%c*?b^db@g8|oQ6;8!mubf*Z!G>ev&bd-%p{)C4iO& zyk&LUBQS+7{NYCo?MUjYyCH0q;}0`B`->e?S`{Hr-R2Tj!e_aLt_tJfw|a~z)um-F!g5Eq}P6DtIy>I z(?H@7i=r~waSLy8MPkoMW3f}Qms%^m;W$P+5aW5F_P-{QGld}=GlrZLdda0tN0QX& zz;r575Nt^uu)9T1H%P^goCyI_^g+)8N=+fybzM^6g5fPBr58&j#&V4Ek*XWr^y6E9 z0fj0cf^-!?#FFtuq8kCy1D+sl3126Y0~s9o3z=dB#mV-|^S1vs)Jcl#K{~Zs;Y;Wj zieA63cK}&$qoz+omQ7z4)T_ag?AtT)3<~=MT26K>LCzO z3(A%R0`|qL48`1dkTTQRFOVA>QBrh2U;`6w*(? z{e4n?$lp5sQ(ln-?>Eqg=p@rT=RNe_An)7F@z@UH+R8ffk7D&Ls5v7QTuEtPn5z|a-0!znHb!Bli({8qTx$c=rU5mU$?50z^1_*L1QmUZsvrI&QOB% zuVA^;FJrAg+XhHT@CqY}X=njp%uzZT6qRSBf6_bf+ryGbXHHYqbplWe7KD@a@)k#B zSZj2>lKur`xx;iZ2FsIs)CTv87^;o0<70c0Oc+;?@jpd>@SRaUK;$$u39HLK zEPiq}wlU<=rMc&lcfoUrv_d}kapzv#9dwFun}8Q_{Fey+B&;d$)p_y3R6~rU9`#@c zO0kHu&{D(+6vK#c0>4Qg{O$@mKqa(3wr|;tU$jT@*8Lnh0!9U%C<&N3!Op5O!A>%x zh<U({3to3bNu9H+W6$pba(6Z>tDp5+IzXM zTlBqh}}B9<$QLo`0RLMqcQf%R|~K6cQY!!&v&CBU~p$*(ZYW3lH8yPG>UH6B~N zQ&rToNwsH*sYA{4YyLu+$M^O5RlmDXb!EiC#ni+#-geK~efKd4)mQ<_((pIdt5Jpk zK_n*GQA(7BwzF-61q!6#1By<&vD3vEYme;r0696a_&J(Nz&2?^1dwMQaB(7(hxiV)oUVEP=0h;iQg&s9Ow3E z(caB__>xj9eAi-?k)Ja25$mnZ^3-e?32Zo)mDn`WeJAB?i%jOUby8uClyzCR7Mrz?+D^YLqN% z(DXqfcN`F;b)X=Aqj4N*X^Ht7HJs&D)p+JiL6(lifM@Uh`{s#5t;{Z z=u_0pkr9uZJm#nca*tx!_vsQB==#Lqrjh2}s>dU_k6nrezWel+nx0-+_u$iMh2}k_ zZIc~H0sLVtooCRvBh5Ie=F!q~G*PfzgWM_TX=ADruI`{)Fv#6hB^{FJ_W;1M$CuKW z%Nf6dQ35gZvTx=KsD8A5A)(%GGT0G6tPfw+c|$!~TJ>%^kJ@|lViya*?nRskVL9pQ zXpH}bc+`6?x*Hf89D2^~+lT9_d>^QvYhvF15l;^4;(M%(+NjoLVaKI!4-o?L# z0P6A{x&S5vl@iKl*VWb$Dw!bjQ@DJdIoY)%D|?ojg`P#lf3kK#?$(nvrejU-XrB{e zHk0~n!K~e;W1|Llk`yA*NHpV}zbzfNEXfYVyY^(AX3t{ld|jhurVc}-I~9RK1aq_A z+N#>+ST!EYn&$e0W80CN)xW#A%thU9%4moUVLBA&kv6x{pte04oCGftqd^^}z~X;B za~Y8uWchaVv}!>>iUJuUVM$IG3C0Tyxdy4$c#Rs{9nlwIU9|t9wk5*2?Vlu-%j;dS zTubO!u#*Pp@N8br2#1C)bk?F#09Pfm&NtFzE@2Idh&kWj$dU@{F2Hkrs$`opc4gs9HUplogPG zqe3?$Gdc5T)tPl3;+^DAk+N(OHseRCQ;QgjAf@rN?Z+*GRVsEl(P<8yn%U(4(_63H zeA0b1%Ju`Bmu&%Q1f^rJg0m4!Z-NwVTD37ozKf)52tRwUL0JN?ws9o=xTGWr(s7l} zQ*p+_URx}5(Pr6J=X!-dYw%H&!uuD*&xq9nov*20n0)8xCCBr8?406X3AqoOz$Iq) zmWQlokd@j_Smkur&TVw!m0Z2ED0!!;7djF)kes+4WjWOXp?Iij@xfAt&!2Q0Xqy+v(a|D7D#06pQ^B(v2K+h;$E&IL|Skmi8=9!q@yRpf` zt|Kzewxhtc&*B7uFT07N?4G|VmLvm;30}zm4n9D?lko%W5hQ((38u!0eth~nAsq-x ztn&?Y=H%E^7}d+@oKC(uPYL~zY8`OF%fbHLa_tBtjbJ4IO<*f@+@6efO|*k$b*N3ENEZ`RtWxqwrz=}t)Q9%r z&hu4Xu8EC7B_f;2T2_^-&gAcTGHvAa&~UG*^2mm!g+|=2M;0}xTM@xQEN5_}rLx01 z=t5BaK-L+Ro9oT;HH?61i!2>QaU1yUUjyQe1jJdT3vz$YHy2e;@8S zayRN!_Nm*ks!b-BcUU=aIhGIq-1b4jXl1Cr`{#?Iiqrp%ij&X0oc$q55A?S z^*mAKwp9MmzDB{MsnsvKdsOP5>~(!={Ex)oSCy(Fs1xd2bxM4zyVsjLEdaWSvGVHT znrAGRFV(9lTY^7LO6bqNu*$AC41YQ?bhY9wq^z#>U6w4HKv`M0)JeQ32zpp1(r9m& zHK(dCKO~2Lowrtfe`L#CqitsfT3b-#aAWW^VbSY#&x_NKE>y#Xp#K~xnN`zeA@06A zxB_BDT1^1vtF*&^j(6YX`iBbCruQoo)VHWRmx-_fElZ9Lj~}-wDQ4hZ>fJU=`;B2v zVLi3IpW4rN_~;D1Ob}d_Vv>})Rd_t zvAy0bdJbmB>wGpmcs{~hcd4TsIzmQhFJb>p*_>9iIV zLX4BdoW6zjoRwoYwR5_2BW#C0mWf<=pEmq0)wN;5bNSdmieUfKV%u6lpl)YTF^4v5k>7keB9yzt)Q}*Mz8&#F; z`v#_)O|E?FpxD~zIA1N|@ToXgwblj|k*ws}+SBHTjd#gd6>5i1z>szQm9~wgz8d+7 z;e{0U_3;b`YkP!&ah<>BQmTy6EWQX4hn^03HGcNO$djh0CbzuAZLlu~UtDV$%T775 zwLY!)^<9@6LB%)DT0fau_4wPD-uG5QxpA$#AKF^V2PabO1w0GbGF~e;{HU2DJuBTn zwduJMWugyjbPnFd5IFHFurkY%S!DZniq9$3rph-BRj=K3Cl|j{{){oKM-LSO%JJyQ zc?rk%@PCYmDDeSnOiJsjtgIY(C`smGv_0ys?D2uu%I14=J&6qqbIN8-$u8;1n(lXG z)SQ(bQcc|~^CKTyCPrO2Q%00fy) z+3jP!QR}cCnf0rtcNbO-zx)K?++k06tWzv}N!MSTSjw6+maEkGEERmBI+$fJdM~Cf zRUMoeQ6?gNcy~`s$Ni7uUYY~>k80xfj(9!OYM%4q8VFaC3X@t{7VBD6sViO{M2P!? zvW~koy&-L$fuCNS7 zd6_uZ#e=;&E~@ddk2R;Kr}tQBk;x=o@!Gk&W%zm9i^pD~WFB*HH?zs4M>6Orux3ervSKT1nEXSLuWqsR7TSwr><9SFH z%-ekM>}u_~gNI7$wDC4r(n@1hSDDAh%clhyr@*!nE`G{zS~BeNJ^rBrTL~lH*!C^N zTQuGAfgWyc{ZOFkR@8MXztD1PP;-^n7uODl4z-ZgGnL~8Hatm8Do7C2h5wzkn-AQw z)N6h~Ad@oMfW7MPaauyaY^-Phe!z-($Ls3@z?OscWDIs{UC;T8@5vsr!299OWfNGU z0b;I8)%ZdWR=iUa@A)BCP-y8$UO8~Q|HNRa`iPwP?6a#=j-_4=S|1*FP7Dak5;d-> ztjy3hn^fihg)A2rP zMFD*C2FisHiA^O!LbG>{vy`m;mi}-$TS-xEc$fVcmY$>8K+Or;`eXLvcZc=l5bK}b zk?}>KW?4wT?y)kP`D&N--|LnOM+TE961x5VZ_u1S?C07U@}vcoT}hwDj+;({-sVv! zWe6@)29#I2(>?~9Ths6sWzle8FFm(x-V%8 zZ=%d5D7Spot79L{xtU7cdm{781BU<=lKkSiP=dw_sc#|FkF+N zyw`2uQm|XwWX;*;$|Lh=^M5h1!($wE;P2mTU&LwzWbyU7bi@M6@F0PkWh2g6vfr`K z-1l?3vQIziaHr>_hd*xHn@I3?1)a0X!S{v2ooDVF_CZN;%H$2Y0*~Z*K=a%!A9l&kwMJ6q1avt+&85|n9 z3N+#@1&J|l8mPJqsHMY7=+xma6{l`I64F8hQX9wCKV{w{Rb60S1#~ap5s^6?JBM9c z{KVC@+0_#pe$XOe{PCsIFM>i=i4&W5b0hC27Qb-!3F&Bs>kLz&tPbt5Co9fUcubZX z(mpo4DgVIW*RaHU+dOH|W8sRf!5xU;F|iRzOCFE62g9+tr)v8qY$X(WdY?c?+V@v2 zg6|F6JZJ4EyshV=gonUIac^l+nFCUZ+h`AggC}LCr#rgZqt6p>D^opb`lGFHCkb6K zUt1Y6Lb5hnHDVU&eFHC+4p-iut7UkJvtTa4&!=R7|J62 zH2txu=j+IVj+Q~oxiJ+YTm94?KJ5J?2lo{S{ubjD-9KjbZ`Ztqk*wi4c*Nd4FKLs$ zvje1s*+es+Q^#7akex?NDM3sr-L4jHo0rle%zpOa!I!mzcR!94rS97!sdixuA(Vn6 z?pf|teP0t(PgKf=pNuL^*v5CiIaRK1q)L*62?Q=H=KIGS{iQ;abGu#ptZ7@{2RAY# zto7%*D|()_dm1Lon$*15PQ`xg8v?2{;)qZexM?m+hP{JE{yM8XxLWPPKj|0&nLEOb zr_S-zsn$<23KF(GEvYM9Z`BIcNO3p5KPW(!o6P&MtQzW$Gj_$xyy(#C&S~k@qcs5c zhW1c9JB*J#6SQ0s6h#jd<=*aD;5ie&c*B`0DpUf4)?Dvf+m_c6;(!CEg6iVNN+vH0 zzo--bmz?sOQ58@A6*>$yA#^X27%|t}2n%C3w*$YPeC*xew7;IjhuD7*xjH&ay)0nM z&B}v86OuBB$QmkW_NAotS8~#YmAiR*$XpfXBib5GWGuzS63| z;AL~s>Y4GY(=_LF8PP0EF#{VJuH>)?99Mq_!#zJR1b3HdUx|11qzqXL3+{qU?!d{x z`bwdnD(66GC(FR^pmAbjei zf&mVjPV8oO+z*BLTImX%~U>O z%Y&cVT^8D^-Qz5E$-{jb{T@uG-dFs@cguZ0$wrw)rWm}TpzyHTsNXZ~2A?O=(h1%l z_xArTQc`Sb$QeaIEn?bzdIyi-Q(_mujaS+d6%e5_vcZ15RW(Q?tpIjwbVan> zNO?os48kREg`RzGFud^^(heU9iP-2YN2puAm!=OLvZbM%&WsnZT!Seb>zuAdrtm^K zsJ`0xk@{SWzfe1@fYxS38lB&WpV@CQZbz_){fnW765#+1W35&~3_s7oLE0KAA9gJD@uZ=k=WRK+n>a?1yz_w+;AUaL}9Y1hl091I#x%IPr^xCTsCHmT#1E31EX)MxFd% zG?+z=_v)!WOEixlh_a8}ouYXoh~i9aqE$sw+Ut_jO!Qt>cvEnL9XQent)j~<9=^3A zlPJSw%x-?NusOASOD*kJzWZFN@qIZP(-K*k1Hdzinx>nTIu)@R{+ocWdyOfZ(-q)7 zk&>=I7nbY!YKLWy5DzCTH`j($62cqENX7QxojRtBOrY4P^Y^kybW?0frlrhhXHsDY z>jr);<0MM%7|U3SK|Vxc?yR$*M(ky@ir4&c+J0l6e8mJRb{u0MZE1+0#k%E0PjKkC z(<|vXbedd)I58TkonRga%Tu5%e#lQjg;@vPC3>q@T@&1pLopIX(!*q;K6u z`u5*(DBVQDT~Bh%Kp|)jnX6G`GkQ z-(zCdF)8D|WY30DPB(m(iVKAi>0J_f0nZMgy>0n(fE-Q(K&03NVShFtzKL};t+gM| zc_4BlBc2b)Ob>jDb~ep=JGj zvRY9Ue#9*X@^w~AkhIxod`!4P#>c2x8&75v)~{a={8!`q95Q%vTOBaJ1&0St=Zv8Y zn1+hb`!~+^?{9ud1O1btsOJJ8kC$K*Si6-w7+g4rwex)a(E&J&8a5Mf-Bv8(HwmHL zkK#V#?dZXFcT;a(R9!%}VY)z8BhU}z|Ii`Rv#Lx~idz#^5fVi1)=EypJ(eWNXVvOk zXiKymt`aJZr8(lSr$($+oIB*0NuYqWo-8DBHC0=ATFe=mBRrG5VIcy^D*LflZqv4$ zlt|>uqvMXNR7>s#_mFK%FjT6qov9qb4wh(JAEpl1VNT!c7ZJmgHdqwI3|%aZ@~g!5QARj`N(5-NLV|U+!VB`w~h%Z(6;;=-p78&#C^*% zzr5Pg@$M7}Hxla+;E?}wEAtEXLYxLSI*7cH@Me13EhP!{qS-ZjWl!Ck_p3&*4j#S- zlfZ7I8OQd0)YWO~wUZ=!@Tne>avQvsA*Xp_A-%whuWrnk34ntUBy5WmZ>u_2-99|Q za2X+wL<*|$?_8a}A|QzShRD9@@Hi&Y)eiCbzf^tDC}* zD9f!tjQ>IOamoX4-tzK9f+XQEmhHMfbgJQGp0BPt-g>J*``oVFrIKY6tjK+K33C{G zkZykO*mDSkAt@4@wOm`wWih>BmVbJlEts|fA3GtNg$ybz8=fGIkH!z`$QrZ~SzlOa zIf*dycRWzvobkweJ((4j)c7#O7lu$er7MF9hu#Nm0437Ud`}EBjtl{S5(3VpkH!Y!?FUgF7#lhcUr}=HhT+_F zIcj0Q(YWIlA|Uf+1Xdmqo;;3dh0@?6FU{|)QRzo*Z)ut2>Vq-qFZCIKVQ7ScVjZ!y zcz`2@hqjH^Xehf--12VCjco4tahpa@f*ehFLYEqv$S7pOxit0pP09}>qqwIS-Au#%byVr zPV+Kif}~ZrkrxSPkmAS$N`iZ@G+bUim|VXEA^(D=+=7d55VQU_Wf#c>0*RyksleTkr6ThMCcG6riaI}XBq^lh$N>eAPPBo&|uDq$HFZq ztlAu@>@txZi;HAD5F}S(aGWhn$4k<`WRFlY@-=`2C1|YVpvo*IKeLqWa?HfD?&!vE zI9Ps9(8g|9e`EyHydXdHzL3;7U`r(A#xkE-V7+b5a9+qu92wz8t8Ii`1tjvD$wqAy-NbuPYF z%FJ4cu^P6wF0CPZSB^i(j-gre*u^)HdN>g)|6fpXx04w#ax-I@civ~!A2(I!$KBX~ z22mPo-sETgIK^;plQmJf17|}|BNFergO>N*_DG=Ej}d;I_&TU;!jrqah_--%gzYgA zB{S&J0dW5+@X{3fWju*MTc0JDIMfETtuBsc!p+mlH)mwj_$Hb+e$P2=@1m%7zK2j+ z1Lh1>=wYj&K|h|<39Y@Q>NIb!Yh5wnk`#N@z%x@~+v7<3XDnMtAH_R5N-R#;-VEs( zx?;Gy+JfYG?8mps5e6>In4w-1_{;li1GM235=%@ZA1Yn!fkGo=-R;v$M97}2NT-tk=zsnNf z(?sk3`#~p$g9^$-p6HZvyzo~-H}IaOjl>-l6p+fIeBL01NAiWeT-B7vmKfI6or^hQih$^5d zBzX``fjL^y+p-|ygiA!Xeta-}UgejN;p6sWB%#155oCtY7JW8*IiNfP-VHzEXOD%$%I`-EV(!54c<6;Lu@+`yb0!Rvj z3{g+bu9qzb*+PXjs*E4M_zr_~GxTXn7;)S#V zLt);kLf#T72Yvv}cO21MZUu|a?pOtO5(`H1IP5X1E*%dOPn|KhA0Hdl_{ctoY_LM= z#6A3KNRhA?F&9cLPQt`dngOMv6r~wki}9zcPHfXJ+-Zv+H~GIJ%a2r94s2sDd0PF< zjSr`%ZizhUiYbjQ*@YBB-Ms#j#f9k0cSmGe;^rI#w9SNA2dZ61EFV~PYV4Ibg2HE4 ztMM-m#bas;BJY=pBrGEK+uAWmQ3TNz* zIOqFpu73wRC()YdubHpSUP&^x_G9vPF-8|~lUc)r6qn&Vo|5P8a#2~NH!2wuj^L+- z-PeeOiomZmUSqAQqe6y`W46(ed7?FjwS_H$zEG*i7~?1(V!@5WY9I zLZrS*&bP#aN2M1rM;8{~dMfKe(wF4UBu4(AgMn0wxh^y$QM?Gl$&EHOS+V60lVY-# zIy%nJ?+HrR(j?GCpvZ{gBr^eAcnbivp{g^I5Uo4yd2;?6Gkj?of0c(+ZH*sNiNi(D z{%fp8A$b}%&@``*Jwp!Ro2G)}OT%capcI*lv{+#kXWKpzR&d9Rw!(RY70`SSw~H_y zryo^#D_YymFwvZcr0t9eJMEyEEw#XjHQsWm&*2Dp z&O5oK>l7SGU_CcuM^j@SaRugnh|sCvgf1qy+fI)8>A}bS+2aJBQq=OTUHJ!4%2@C5 zn(M)66odjpKGJ(ZCso*s7$UNGlr61Qpv|kjcqzFj?+VkUW22AU739ex^IaDY*~ZHM zN$MR6hVw@egL%fL-Qsh+zH?j2emx5aIsNG-3GSR3Bp#Zn5XhxvYJi8XNWd%GNywYT z&O;dIBs2M?+oH$>VN#3H#!wH@aEY0V$(o40?yqsMgni6w?zuxdgH-&3D8=~^@`~rP z?oY|>b5OFdNcd;O}{)_6~YJ4|quUF4|%dna{yp z7sDOxhaDbFH{3@HCMt9SvmrYxd4{r9o?eiI)mV=23~x<60_Z}Fovvmea#>)c)tCm; ziB22@T5}MBGLhI75@>^tyFfeLyzL{w=@Gk9ZVFNDG0dcvn~qLu8{=v*;J9R7Y0ADS z*&)IG7j;D@))H(PYj7FIZ_LsdU^$Gg7hp6sF_&4}(`@4kVHku3uiy< zB)c=(IdGlG6r4mSsXDSj!Ze~$rO2<-yqR@}#K&sK@9tOq|PSX1#uX1v1z-E$K$6;cR*H;@24=yAn82mKZ*`Gg6 zN_ePPUV!XUWVYoW^7Mqc@90=`VYbiwJjSx*B|67azRYc&33pK3`*r~*JRITb)jm=^ zNVMUfq#K0C!ndfunLO0;t;`Vid%R9~&j4N7t)QoK;OEGzP}`ZJTEd;tT?hk*_+6xh z*6u3W1{sa-XLtr;pM0~cyf{ZzEPZa&9|*lS=IHH`nuuVE^Nz{m#rhAcJ7`6_(1AX;NhHuAFjqvJWm8_ydCgAAR!+;J5X ziL6}TxvRz$ydU{OGZn@lkM3u1Yn~5HUD(M?7_!_t1-=ScFtozZCl?iZACgMQQQZ>A z%Reekfa3!jqMLuc{V?zC8BnH7&r-QIQNv?65A^9(wc^|pTy_}hViiH-^J6!z=3~e3 z?8=U38HbzRjbp5HGZS>IqG(#2upB~~(e+AZyDs{i90jpWges;QF@UcBE~~zEu@5aL zc7n2#E*vEL@Xx|1nhZ1Rd5mL%o~Z%H;l$VyFV?R-wF z*!AyFxPk4CE~+OQf}}GC%CqtcLpNuPO5^hjQ6TCEbTHp7F?+}JSZRvgjqg%pB%!m3 zezWj+O~tMh@M0+-SjHw37H3*GxW<0`_69nVE?f!#BoP8&8R>=njX4~|1XNBKO9+3+z@ zXd1wSxeK4v^UKfn*{4FQ+)#=p{(w_P4H;xJG?x=VXs+yDf+xb;ki=x9sa5s~Np36y z%1UG8+saU0PQd?A-p}i1^Nlw&jug)lp#rXFiJsf$ zT`(A?D-zdG)>qszcH%QP$jp_zryOTC%4|F1H;YqHrUJ#HwudqsY>S`WN7J(NV~o&3 zc(qi0qPEVOnsShDor8Nxk*$v zElCA!0cS|A@Z3LgU(sMcm+`GDON1sHKM|nHbCAJ5(bT?LxFnvhpYNau3j?0ADFUmn>LV07A(nAVL;Pi`XHV~Q-wlkZ=L=|lkKi_d;@%9*O6 z&2|_%lW#;qAEC8I)?#>`+WGYa2oWX^TI^skLSu&w?_>zoMCM9In@U2Hnf_ukeI%P{p@)+jG@JTqAfNfC zWb`GUP9$lGqp>Fs+s@7EEyJXvS0|}~2rpj6Y=oUCFIQ&31IhO|uYzFDRbui_rTJB! ztZ75<0!`hO{ZfGM7pIK;aCkU<@v5Rd6vu~gpg1CDc!!jpF#m=k3C0(N^f@2A&VV0ycIKJR;*bb32NfLvA8e)3=;Zy zBFo)owjkVIe&Zj)#pFSH5%N}F(k>LkRDcOjeHi&UF`vHu(x%`yMG;enTrZuJC;rWz z@pZ*)iXHPMVmeDV187H9t3}YDjO0GYY=^cca_;AI5Ti+kG2uvIrp-%cB$J|PLSZ?H zA)0aS#9|5QB05vS@UNd4=+Fr$5E1b@hOsb{>tH0jny;BYDM69cWFZEH8+;f$O}fC4 z7CVOSr-rYyc9EuG@9EOh?+vH!j^gbiQ!3-E{4G7pxe~L@J2&dN&_N6G$b>C($K!h@ z$t1o^r{WrZO0d|_pYqyrK@P%|H&Y@pO!;ki+3z5iq9nvo7$(E>Q4t(ZeLgZ}$J@tE}3&MQ<4uvUo9LUJtrHY5d76c`l3IyT=Xr=n*7Gh@tQ|~?fm=y&O9Qsa9rkLpSlIC*zt0!o&Gzg!YAneDLD!=C#;@PVrF<0CA}4U3uY0f3P8^PH zzmObA4s&R{RSOh)(UOjjz?sd89cmg*{XV=dN}++A++(LRw6o&j`P?FMa`=OO5U!Ly z{be%~!uX#(HDhP^`2fr_il)xzF57DyldBnrdz_!nI`zfE^{VPQ@dNodd=|L2iM&7b z5n5W#HcX-#)JD3Mz17x_#T7UgVow)65}7$guq|)0+qb%%r;ekgEbHy7k9*trs7D>; zu;QSXrMn}pTd$vwn^eZM`yZ-uRurFmR(4UFBW^Vdd0tn*-%)O)ZZ}@tQZrLifcmkB z*%oz`(}O1DUZy>EUC$JzOy#50YqiZ~%Qn?|JreRg^44Kc<-xTp#h<+oEBi%Uf7JHf ze5=D?;`{H_QklMw3>KK0R(5GnW)6L;aegJ}$jX`Fs~g#xT+v|XW~ae-&viEREz8;z z^h~`!_gziY^akDE7OcfEO6gnZG>W_XlS^Npp??;+uyXba35kD~cxkWy!nve8*VSq4 zomHw|#(#C*F;^_B+1uxcp2P4tbG3mqO$`IgK4jI4;f@ml2Il)LG;x(pTJjushY?#G zN6_FNmw}czn^RX1cDnn1XUE-3uCv#f;-Jwb$M)+p&dWP#WP1NWRWSE`n&nOr*Yv|7 zd9%{k{H#Aq>-UoT!+)H+|I}~)`t>(*?zZ;YMXWggP1}5B$f|#TEU8fpa;iz;3$V96 z>==5-wx(M`VLd;kR!YC~gL!uGWgWpIV}1l+mAo!p;CuX(XFWMRM>MsAo=q9*EH5?h ztiu7Uvt$h%*W;ol20ra??+WZ@NmE>9BhT#AvtJr%Hq~O~R+sF2{XBEUikL>b7g&>wV7e+?e^l?|VQ0`TLBy_xC&JInVQbzR&k;vbF)a{zkT(UZAEh?*5-ZK`_@lEs#-*>x!cTTn*eb%V73qL2x zu&d=Jh+)!d#C@W&)u%+A+P9Y zDb-hgrJ@BnU2<`(>Htke{ZqNf{dwt(ug_c^WvFOqYmm$>y=@m&++g{&R_v-=k4CVv zkNWkH#{e*lbwryOF}n0bq-=@c4urCLx^71}c5Ffe*A31n|HAiIPL=9zW@kIMno6*5jR?F>JhN>Wz5JLuo<;6k?XDxG7Wi7#b=n$t#?A z3x3GTvzh;9QRF7|6 zzxZsFbWHsx)N~q7PI#D-IdXXe%+{p^8C&)yMChS`Kda732JrcQy;?B-bx+uK&CC}= zgP-RPgg!Ca4C+BmE@2BM%~H-vfAS!;c<5T3Fm7~XLezMIc_7dGTzO?v4*_W7=wdCn!xEdM)$^^ z-25Bt8$@QLTDLdh3xvzJ5TF=%)<|lOD?SKiAlfGCSHP35oTmoP?xkGJFEz?+=NKLf zSL@AvgJoSeLd*LeBrjobHI>6egiB%TY??-8=GH|e+0N=y1K$NL*+ohQt^7~G+{xgF z&b*!vuP?^OB$;tKs$CWuWV?V5xfx5>`6-#7S5S7v2cr*U8k`w>!L>x`JNM*Dx&15T z_Nz~1byfOmoUq}g+dp4=)x4D(4&gf#a0#}`W;BMq=R1lc+&x7yMt8}9=`=XSh1>Jd z7UdgY54Hj!aVn3C@8N@XzNfou;i#i4tC`3oS->wg+FZZ=UpREF2e%E@nbH{FFTmJK9M{YgPerx_<29 zL$n?U#3q0bd~Hg>;-`(}C*O@2;W!ZsvlkVo3Cmo&rmgAGSbFg5nEm10QhwUfuuNz= z({Az8qwTX8W@WaJB%lp=jQ}!@5&(e1OZ>UrhV&|#BTX^Aif!REO(&j4MlkER^ zPn)%M&Pxq*Q^y7=cRA5md_vGmGW||Bh2%HtXJ^cXvRVU#!~H!xH$#YYUTtK*DQ9px znEw}?1DeFwt+?rv?KtG1M1 z_aE*?KJiLojNBg7Hn5UMzZ~3vQ{hfx(l1cBQb(Evg^ZdUJr1|8^TU{= z@*dMkIpzB+s!TRGv--hr3muM*TN5|{r+QF(xLV>KSACG!FT98zb0}bTqWFdE!T&l@ zc3)sz9w8vSj0zdV(k8m=ilf$ha^tTe+Is9i?U(Suc`RXLv9Po=nh&qfhahF@LBcr} zT_`QF%x?2k!Qnt`*-DW8e-h_a>eGFIxO#OnFK12$qR zwJ|>A#J|8eB=kl!BT$8h#BLl1()dt_^Ga(oT|GQa?s7Or3% z$SMv9z-=Dz)BSoaGo}5RIWrNHE2-hEC3P~>o1Aq@GQRiJ?FnNsaA8nGT*xmuG6s)j zXXTgm8a?;<&`Wa%r=#w)hemgG!g%0>NaM$v@ITk5w#Rtc?tnu;$&uTxjC9}yY4 zSV&MHOV~pEWw>&*m6_}GeNOKfvSK3G@=ipll&^n!Lr?&!+5ramOygJ$KA*c@2>eQ? z$#7q|>eA?5S-!c5ZK`#G;nh667Q%IBM*6oZLq~imra?xxh76h1q#~`*72G?~d5dvT9bC280Yt>cFV2K86RD zcAKbrJWsAKhQ0O*&=z*1oaQ1H&6ph@NEkwaR0e*XNO@5%aFRSVOnMOA(XX+Gp_w_gKOyz7z{* zaN(6amu&D^Yb>jmZVjjS=5y_O=n*B+ToAcW-C$UKDavLDYM*!k$p{?dh4u;PQLY#x z3R1G)q4r;3dSTC8>(eSOoa(_wI%p;I{y5zLtyWD5F55>e~&V4<*IbnI)<3TK%BvAaG9ZL zGymp`rSnn)-a+ZjGVh-f5;5wrQD2~OdcuJaWV5zcIo=zcxix`sE_?>Q&LFerETqiI zc31DPOLP7`7=c$JY`1%TQ0+yv%V^p?13jn~%X| zNV&se2b+$F`R|Yl&`+#GF!P?@-EG>V!NptJAJFs0XGhOl6W@dw-L|E|QRC09(+wXO;8Ssarpb;9L&kFm6C)ZtN19eXyaGo z>H>4Zr-DS_cBRj<1(Qwfy#$uHz5qP{CWWnak%JmbI3=_6P$d2x$7S-+F&Zmm+bEX_5QHSj0;7P+Ke~N4uE|)N7+=}k)GYgb3_`i{V;VuoJ6*5U2HARi*>^7}#kqqq|>=j6~&qh+=8V$7CMZx-c^TG)M^PLBwv3hU}wp3F3mUp8SyHm9@}eg>!9Xq z=TD0S$b?=jBOA-*mkCIWyHLFfx1BvcvlhS&q7u{|9GI8D%W&o>3l+kr^StnMqPLXB z=B==c*Rpoq02RQ24AoY(bpvTGjs-{PkWemHTt|{)*f%7_rW$Deg*IPR7vKyAY| z02*c^_zq>gm4U?@!>cVU;8eTP&5Cvc^WQys_sniG29NCLCH5w~a1AU~*2*K?6<~;v z`fH$e^Y3t65Ap_cH)h6RThpBvh$5!%V(mPrv?VMDKP^lioEk1q^WV20kZ!6eOM=zK zd&CGlMB^G&yz{6VE|*)X&&%Ka>G_ec#G21ZAeYR9gqa zz<~LZm$cSgEs9#`LxKd#3WUq7x%|;P%cg15376PmzO{;W5`L1KvxTOgPVH)6RGIvW zt1gg_v5jA(bZ4)ggGOb@|wLRC;nwk=_(kQ#L`qOsF^q{N{`X==H1aqA1$ej0`A#VtIshV|i# zw55-B_2C=hmytt#OY@%IC>&ZFgHSCK>T>JbCtxU(0{<@yo5dU5t(K_6%21Pn_3hRN zL8(F&5Q_FnpbE7i zNyd@#H7TAWN`cE(N&|r9wp-yH6dcQkW6MLn1W#R2Y#Z=A!OT?+GC8oUU}cCHi| z-g3x^Ae5lkwIOfgGsg$A0G9wLOr^Z6q@b2RkkRM%#4P+Oj%iS3SuCjKL8CrzK9BB8 z^2bvR2(w_%097iwJ`gEuWChYTE?UM<*)_{nTWsV|7)zK}W-utut! z$y@5nUjQsThV&oMWZ}Yv@wrEF=Z!7@xetPBU2}X0w0j(c7gp;z>jNzyhc*t&-EK_P zo>~i4lBJ&adz{!TDkLvQJdp@IrzlzZ$zq-XhX}8rC?Zo(Ty?06bl(9Y(6lkZI9i2wh3d)qk$53ql0^Y{Z~QG?}ZND!KvR^ZtIFRO2L zjrpF_79P&*v4n{UoGY4#y*E+=R5T`)(dW256|;c2WH{p%?P5_9*+3}NOAz}IoeXfX zrt0V_z>-ex9k5_xa=G08qffk)bK7HJv~2{7uo-ZL$Y-{k!fDV|`VRd{KZgq$Z0Il3 zs;)qU>Py{wJ937u;_BGX5C1=A7GI{auw9_SgZh+t0?OsI2@CIyU#rBff)9_w^?3iz1vk)pb7GA~kJ&@WvI7*yV4`O)UeZ#<= znwnn^1*8-?CcC&6UjPM|;SQ4!+73?CfyRg|8)i$t{V-Q>C7g;AZx(trWE7WYBVyQf z7A(9GvKq!v91;(B?Tg&w37HZ=d_gPv>O-FL+Eonjr2BN zj)(C)#FI~a_kzKh>TNW%EH=)2dJjfMI0+95DshltuR>Z)ydbU2?QrLmp zd&IX;R`fUF48Ag8qOBx%;|Fi2fw5`<1mxm)DWb<=k~7#AY_ zzF}*Y6)fMk_VB>Op>pY+bX8%j-U*^g9#?CaN?~R9yW_*@O6L>45Xebf{7F9J0*FY8rBR{fEEY`y5jfWA4=^DWBPAs9oliiQPYEQ%#&h`b1A}1d5%hy=Hn#ILf z2^k{I#&0H6!aC{?1dQ9enG4kFbRUA6Xqe*NtmlW!GZuc zl4mBEpU1Nm3t<|-f&F94V!l3jjOoB*#hxgOSdRV6RVwlSDL9|~h%)p5CwsQe1HR`ZnN*I6~OmPgGv-aU8^~D?qmn04@v0vRl4;z9B zfssJTzVf9yDQ)1-9$4~#yhu|>%{{I>s)cbdMJ`eF_7RidT=B8lsQzjZOf*6GLI9Hj zVBita#8?P~hc-ZZp1A9an@VH*A&jUy)m~~X7los~;YPE2l%1TYE`XDtfp_}Z-w;9k zzY;t9fQo7RfDaATrkGf%jm)`@3m821n5qD2W*?H2ynQe?FUSLUoFcJog`GJ1`usc+cObT6^vi`4(IZA3g+-2qWr~By zs;>&HT=g7K@H`TGcYfpzI_og77_jUGkOeom3^<9Yx==^`4rD$tssf=w>9!@Po;Z1f zu=4qo)BPt#nB*(rISOu?0iz^kpH^9XKW-gQXOiHYwP)i|@Jg%0To$sJFzX%A4S6INH$)2zxkEAIoN(gyrDb{g#^V zOx7cU0AdS4#$(tTZmGX1ibPhD^yeEKxP?9kbq0dyRI%yfbF^fr`&4kMjHxEzE4MG=O8eo#?^qKDfI5g~r3N67XhZ>8B^9b7ABggwUED!-z= zT=eTiD3s8LiN^$pu$?eYzzWtm^U~^MkGtWh?s1b#U>0<{+#n`w_T29N`}u(3?~!ud1`mS(LC44?Qet% z9RCAoefY^c3x5c_*k@_lAhiKvcuLtVIlu$Aka8*w^!>g~)X_zvh?lIgWOG?+*U|-H zd4RFyhi%dfGZ$d+Th4dlN(({I{O~-CfePLms^3R{KG;cXVA(^gW;fWcVBI1!o%Mgn9g&{yGh-J&+qKlb;wt;wgSxArTSaJI(&%Hop^>uLl>Z zoJS@B7-wL<0#l2KMFxEEKyR6(_*^W3a1 z<&R9J~#nIuH!BNn$Ee zI`X4$TIP#5H@@ktLR|hCys*b@#R|U`aaz55`ag-2h4NHk5jmmIS1W0HnnY zW}%^XAau7L*~xIX{}*B~N&xm9zE_sJ-${{kgz@`u|GGGt#YW7Mlv~15*D%Wkvt7m$ zp|BnR)*Mj527-3+@)ZnmC~^O7-ZU#BZQ}ZVtYiJ#!ik+yuOn7alfi5`Oul9@5}F3L zpwhjV^4!7m?43l;EZvF4pdeUC0)7GWM4b5y_FT|uB&J?#1VSpo)Q7PiRyF(p(c@Dg zI8ZC2mh44o&tPrP9W_+C*h7LB`8044LJ+@TXgkm+B4{~f0ofP{o9?F8 zz|$<`puTK)7MRyFRUeHL3rvrG{-a7YQEvgAx~(CZdGZNQlC(bZc;5s$KmhSI+SgT4{s-;O#TQV7qKOZgFd zK!GutARuJQsX9F$fsBfi&qnl;gBUoAQYOaht}$NDp!_?FJz>x7$jthfZgw_C4moEE zIZ!1`GRvRm1CZbtWo@&Rz{qbskh9FjVzE|Zno%8-~fC()i zSbLP3$x^sr3YHTprzg*1WB^mcbusV?!r`2t=u>`bFs*TY9_Z(oAGHm3ztZ3+r@#$bP6&&q9Yh z7vheB&X(%vgMw#xYPLbrQ|+sbp*DNefRcY+Onv;kN7v;l>==t+@kZ48Cq{RnEGs>S zv-VtM1`C*Ql?DgIP*@EyeTh2VT%HXnPe1`o{J88uXU5$JTrRJ1c>o*>(*Q>JEh?xf zUO+91G@tx}NurhKIu6T-ft6msx=b8p=b+NHcnGkAkZ5_sVKAQ1@+#bn*6&r6GvFLW zvjDY@G6m6%FxQr@4f;3&x&|W~)@=YGW}k*Q4?b<~%Z=K|gu%?CFa9gSL5YlR73fBt zn=NrFbB_aM9lXTp8_bt7@kT`V&OQBzxnz3VI;r}_LOzTW61v$B#&4y z2hM>2bAW$LYp^Jo+lt!p(-{Pr`n8p0VBO?DyD@$yWH+Y~Ti@o``h8CjJc;E+64YEc z0h{uQe1L-5iMih|m|7~}8XDuz1rTSjK#83XTv?hzx3P;K(}u623{aSHI(4JUc_@7$ zTjHh)lHg-83$<1YC_+gkJthgRxW{J$AdQa2KufWu#_CBlwPKPN&OHOR~;Hllg1*mc}u0?dwE;z9b zT+6ONCj|4rk!qo5lrVklt><`3QIPg2v3E;zfJm!1+*klDD)4z z58A=p#LDBC<1sZjpfb@dXzu^oDgx&Co=WFHqIu=Mi=p@Fg8+Zj4GyqjOuF|;?hfG98;&k=;#!kD5))B4Im|mbW&Ptx#JVkg! zSaew+b~k#|ColOP2atfpL+dKI7A(xR;<66}{?+4%$H5)n00x{9Anl^B5f{UK=)>|* zKfj39=<`F~Q#nE=Tv11`De3l}P6tk4QQ)0LW{h`^s;g6AAp%InQyRfPdDo2qBdX>G zVnF1oq(Dx_Qv24lLm_qI?etij=1z2q+ETUbC^$Dj2M9oK!3|X;ZUI2mG$}Ctf%U=l zU$H<4#K_MtUuQp1$I=*z;)47FQxOrwO8n-ZN%2|5zm_wB>1MqXDveJV{SA3T|M2uZx>y zo--ucTR1&BroNxciI4J|&d)LPi|}H_Bz|U&^V!HbX!tQ!z~K#OfE=uzq{k_U);;^Q zJk)gR+Y-UW+&<0aclKaC8`2q4`xg0H0m|e^Sh$E6kj!J+E!m0E&JoO_Y1*jWg(dXT zU@zeJg77rX#H$%z0^T$nH~qU;#`B~m@7Bhqt~X!Vf?wfmHMbmNzh!!rp7?q_BTM^p8u@co?rY z9!rA?5&faL&8T_1hjnUo zRUmm@SB}TF0z>)}PQVrSZv9TM(T(Sk<_FL4xvrRZn zQYkgJxT*_V?2AuG!9t;B(ZL4k%B^DpD3>G_5hhq^_3?~ISYN``F?{{qi$w47$hP>Q zx}m{4DDu(aFmgNqn*Hss9gMH|{aQO^<+S1LJe;*Ik2t-d`7FKphir6zn5x>;$W>2` zb*x9SCaoW*>Ly=<`QXFk%UyunDo&lzgkK|MNv0wB^^beWLeoST+XM=1#>Y}Ml#?uZ z{I3`o>qbn5Y3z!&Ip>7K1eN*>jU960rX5+*%nglD&$xbQFa(~=EFmbuFlxD7vuG}| zY*AWW)TR^@u{+|cvn{iA)WTG9cc8{pBdvsML40t?C)}do=1(6$LxDUO2*2*!6I1p* z^;h^;`m&ja4T<{$B#nK;5Bs2}7PdOa>xfM2xcAii=KJ^fmVKNQ^xPT}_v`~s&+ruG zc&&DqYWTNH<;0KVjyJ=g(b3HFT5dl8Swwv4(Dgk!vRP-R!ks3ngm$P%(k_k*sNFgt z;ISO)MEF!*poWV1o!^lQMKTYE!SEqoGNHsz16zeWt%cXn0$2QX?1g8SX#D*MA^II~ z9Nl5hsbpelx-T}(EXFJA#Kh=A_4Rn}01ZS?sa1qgnC%1pP7_>#{kbiqe>keCBz(?R zC6;_}oMYRRZ?+hef%pg8+&$MqY4QD~oy8Ss#IEQkVgPOYB-6ulGED-w7%MJIfD@Om;C=Xw`H#FTX`Ox5n3a$d6M)l!z$Dh-kfS=tNIC$KvVE#>{zaUa!WsreR;=LSc zaql5bCtemyggRY=(R*|j)fT!Faeei;P8AVoM9nLi`QRE4oaTzySHrlALF!swSZ7Wbt$oiu(i0Vb~5D&k7V%TL(itB=|X}GUY=Akdc zhTO=Py?|qaeUu3xw$)dgwEEX_vF;E`>S+{ep@Ur$&frjwR9^Y4YkIt55KMkS27I0; z(Cc?Hn(Sv(_vtpz+hYvOP5p{-8*}|h`5u}10{7*xlQf zupopbd>C9O6BBX3qf+hYjLb)?k-sRF4obB{Mx(*YmMym2b>fT0_;4%u7;odVoPNI8 zVArSkWGv(#Y3%vCG8eR=XruD4Ci}hp%M= zR7H=gPagn7!AE{s*I)|u>naw-vS)7%{jU$Tb+i~-8)2;g{WkYEwHY1;~W%@|y5fTS>Kjb@@vML!fH5M#UhfMH#@ zA9l~jsjJ&Yo@rcxV7bdDF?kyDDSYHLffew^j1S?cfJVI|I;1#W#F{l4Pzic+13g#02PAb0!BmNDh!nkmv<=0 zMmuE(#Yj&zHnaH|+4=t&!|N%<;TSPt8e8XF7v<`DO9=hszqvE$u;nBXF*g_otsW`Qnf1C&1e<~Ze z6VdN+=4oKWVuQPYX)$hC?rL!}tzF~UuQ4SL(3z)maI4z)FBX?6j6;7uCw5GHb_!}I zNCc83tb9Uwd;vJG$ARywylAa|9nPYdf)K$2L`&wp6;eBNg1%DZ z=BPcqa`Hot@{%yFBun6qFMIj}cM{8Odl?40V-rJSdNv98O4uyUZ$VwSP*a#+9ebf90I1F3CVLzk_LA?>yR>S8@7tr z6;YJc{w-`v*&W@)aMukmrrL#N>Cdg&8VIdeb|9|1M-#nnv|0yGE7jvz=3B~J?3RB| z4_`=(*6UFg4%@e7x>4gW@F!@B8*hT`5VmOmBkru6^GZxVDGVo#?TB{DLcWjkH^6N-so&gZ$_~nK1#sSB+9+Paz zuzkC{y(Xl<#Z?Rn3)>(_IF2IqfQqdZ3Csqc(IBJ|Y_+*qyjpnwWsY zwAMvAsAqnbgIm)|0P||kv1e%ao97(dhras~$J`5m zNnSu_sz2+v4c*6iBTv#B;)`kJE}-7&#)o?`!XQEa!1)la_UNxd;vAtjGckV`G7S8y zynTjxpNrG><&$Ms4{{pNbtehU9` zNX%o)PY&dFDluX+*69GgcuCWJfxHA){05^$um8KJMm$V^OJ1?Zsw~%rqfWduJ$U}V z-%Q5f&Bjhl`v(Yh7+Em-WWy4)>h-9_lHhd1=>a`22}pR)a1;^pj9>RBrlLYT@zzFgaOb`V| zZ*WUb8_>4&iFu7^nEo6{y<^X$%^?f2|B#i7QQIBJ$=yYyMI&UW9C7CrVz&Wt6yfyWo4&GD_cg1 ztMrEsv|PFhCRD3({N7+x=mH(ZAF0Q#Zs0(Sb4x}rwHm|6z#=$(k~C(!YTdvz>+aUG z1Gwl3ryPL<;}NRQz*fn&5P@bP??$o>YnGge6@>=G z87lH0J~X68lXj=|?q6kJ?Lg46o63^}`aS?9%#n&MxG}7~oAuUXl>bB} zJbpJTeltv0hM(h?_$NUwAqqmKUR?IC(0gFuZvFS538YZO6Qqlk-jGoPmpT5)M9Wt+wq)})}t8jzFzD8b#fbgr-jiBqag$td_)|=jHOa40y=p7AI~II zf=HF5GK41)EI#{UU5`%PDX}ZXb9ybp$M*xO>aFhI{jYpdp_&Et9&4}2B#t2$qRmIi zDVcF2$$@vd_n$kDCb0eQ2FOq0Dxf_9*;;SW^BP!7)VYr?h%wB6r#)b5+L$_Zlx#?9ba^ArxpMEiiiUk zj0(b|wF%{AaGwk}2cN1}d|pukTm}g}A+%APqByKKcngD4sV42&sHgnLDTUS(P`)wz!weTx#x!uu;V}F+5G8y`4<>2_ z?u!Un__yadjLGp)Ctxc6nN(;k^?k6B_cTN^C$~!f+md+71MHwcK|1{bupY>a(z(U)1j>9#?C7>KW92~*|Lp8~y-euJ1Mv<#3XNfE-lZhmrIbc@P4ZL5JYA@5{diUtvF5SmWoayk_78M`kROMFIt`qBH4ir)ai zm`T^Ndrd#g^>l1ZasPN%cpYG9O)^MVKLxZ!yD4=$4GcLm~!OfUSKi~b<+qJt3 zg8(V=;N6TrE4R3=A<@WKPP|T&#GL+lZS!fq4|7#qaPghvs(=CLUjtn_L*N)HpDBTg zG57Y~z1aU$sAytb4lY>-NKI?NFc6Q^d9qZ=^u#ncJo^vD&0&3icPzz2S6@z83H!nZ zd9>`%tr86HKb}E#pT0)Svk5>-rEmT8Kr8m0SRzis&lkN(Z;s)VkRl2c0PTsw@CD+} z5y0Q)ZVq-%T6#DeX1Reh|3AGj*SL#m2+p`r&y#9cNSc%u2eyvoLN)Bx_j%s53dlFo zP|~i39JdjFm%;rn(;+Hv-PtnQxg7%0Kcwuc`=|D|Ss#|1>%>*Wf200S@J)T2_%SH5 z;5ketK@0~DVuVH7dB`d^T=ST|J^WB<{b4AE*c`PEzKwnDEJ;N&i?GR2V5@+%V77TV`Ny~`*BzR|#c+7@vji*nCSsUcQ>lg-$eCh* z(*a426k1N4{S9&Ug#P##3+%9%ZWm@plgOaIaaOtgF}J{ya2MoHcBk&SNCtgXF$_8( zJ21@{v-DG2*+$Gv)Ev|(IIa<}#B-`M$G)&MIVIK&K(%Q0MfiM$h(#mCR2N>V~4 zNeS{F!cU3)qBcG3?P`ZB^ip1iIn;swVic(keFq?zc}h9xW(>mfPh?6qq114eru%)! znNYL2Oj%iYNO#sCmE+4OK`SkU(paf}C*WId5F`0E1&ORTDr32JZ^Oo8BQAxB(qn+h zfD6Hg<;R%=WQ`Ng>?G;)&iVCcy7f$N_beTA)ys6z!wtS4AFU&26G?*01_!&S|A9*b%5ub2L`8CSc@I&3ol32Lr^k$j5+ zV8%JMXB%p^g#sW!!=_I?EZ`LhKfE5&wZ7EOZ&=*#0TR&(BktJL5f{uS<4M zSeQh0I^rcI4zk}EW=s}KBcwF*Ldry_;`~F0ljmEZckSaVy^ z!^|X6v?zoU9CM8!r#!7$pTtEEo;ccpaleOE@~rIr$DlgZt>60*{}l&fE3({K{^UV* zA+Xp0q3)J)R2U$o@7w$Txe(FhZWU0-_12gO8ek8RBcjrQF_dRv^Dq`tGt7+S{bW)E zeR*p^<8F{i*uSEPkOiPbCYEyzAX3jdD*_oASx{mlwzM)i=Un+5NHHWE z!TaT3nY(xI0%yh_PT6G1(SB@?V&AB3@!{Dzw6V_9dRg9Vky`|k;LHAxY+DUZ19Wic z?Hl5kYnmz4W35%KJG?aC0hWe)DO_*j<|maXs(+26cSab8f{9dDxi>}Sx#$Bpd?8|L zD=fWmj)TJPONAvjzu}K=zZT~+5!*X?Xy~)|rMed+* zw|hR~_G@y3sRzuap){5p8#VT9esS%SxyA0<)b)dM9oL{esgrNM+*MPQe*)V~f};0p zIDe^7MgGE69v$DEV*+lD}5nVP%;5DZ6DdrQ>T) zwrA6gJIPu5`mVXi2iEj9B=WxxP8_cc(6aeD?Tm%^vx)M6Fp;N;AL4HOLIhAtX{tIv z-K^br#|?oZ8#Y(0?__V$ju<-_aRnWPrgGF?LFX>{cPWwbJ(3q)r|JB!og*E>RYa51 z8w*+Q-jxj(fl*&}6k3KD7-!qg=_%Y2khcMhE%P(N&vD)u9UPK%WgS{n3U9=MddKK^ zN4$<(|8~K1ySV+YQ@+Q#*yi$8^v^aMMT3}>8wPIY7PNHj?#VhMc47q-y)epYV_tFD zXHDzQbuHM713y+SC-Hq>Xq=rW6K@Z6?0(}7Sqy)ebYe~8zN$-LO(q}Ik<}lV?^4Yt zPD)y%qaYiCbNJr?RY0dLYulafFFgTyBA1U`PY`{O9#C7-wJ+-zH75!B*$d6Jx zCM`FB9{$;L+@Srr;#bi=#-At4OcJtdRkJ4UTby$~*7A2lXxE0n@RAM8mh9t*wAzl`XPLxKbbM&XU$ZeW3kshL$wGt*E5nEc=sTg7r3enG`-s9I9e#~S!Xd- zmdm(;{>mHVXN{arjMy7iu5}ugSK#(Hh%D2Baj(rWR6bkV+FyNib#00YWkxmwgieC>E4bY(Ffq%=W({c&qLV9;>j*k+^o2 z5jWCb1m%dR`U{E{Grn+g8!%l+5+L5AvHIxulc}3!X42`!6^wrhK?R}c1uNswpj_mJ zFH#DyAj|1jeS7TN0Cq6(QsJozB>14ZV{QE&SPhlD9HF+9)@!)JgAeC$aglih*h{4` z%gHer?>F_TEoxO)zqpj~Pf=ee5WLG}r#2_}=iT$P)8wTefa>9Mm5oQeRY?ff;MN^b zjtM>9uhBfV2tSk|P96NfI*@NA4EW7ZuL2ErGA-qf`t7=jf{%ILPOiIlBA;fk(Ro-( zbUG~Stfe^^3YR1+8G>w=Y7=i=Gi&#gt0K&H9aIm(E>)p=3#Q$aT5m<*2{HI^6l?k# z0vUlAqFvV?uv)A8uZZdLzQpr?! zIINdGo*28-p+430QnZrMdt{kVeJ)2`0?`~qrmDCeZvKO*pL7BJU?$|_P(EjX<#ue; zIswtr{?o_hPf*!M|2$qG0&DNC-&P!sS+A>ct~M{+?CUR&x!5_>93uUgOWi}~U zaI?<4pK_db?(xPHEx3Kd2E=oHZ+iM~cT}~Gp}E!3%aDaha+wKQI)H#O@=Uk09mL*qGAi}2i?tw2{{ zG3lh_P`8F+_jqytxaiQE@%>xAisx?1FK`f=-}WWo&YE=k+5A7NPbev_IjlIRyOWug zVV)&Zz2k3*7p%z}ljFagN#y!_;)b`|ON~Dt9H_mYvF&Kx3jGU(_S|B{nuhl-T{ru5 zZQ0qCCi&MF9``ykKQb|Dd9B8=4aPQAPq%E07%f(nKsUZuyz3YAY5YDunVtQ`x+pp8 zh1kNF6)RxPe5U2Ghc^SF17;!i(|POfkww&!GNKW9!*LB+{v2$aqJ9?~itSTWA$hsN zYpeKlNje{FJ9Y}G*u+XYm8#hIvF}-Tf&k6r+xw9GyK;Ozb+81z+$vIWqvXN&b2|~u zb@Aw}iV`qo@Siz(JTzCTa$r6`>@gQbHfc2HNUqNwfa5_K36I5=z#+n=i|<-R1nw>> z(aK1HeNBwz<+ky4y>@#Q)t98?zKU1k*@$QbnPZc7c>Owj==q!no~91;rA18mc14fm z=uRi%ja`M-OPTL%>;9Phc(r6_%t<)pj6Q3zifMAX<0{3H@XK@X(_Fv3FN2oD*YQW> zEJb-_;!k@jQu{v=CkCu~Pq=Hq9uZc4(cQRY*C7+(9v&7Gj(bDFZ!Zb!lKmJke?v(@ z%;%2>@yVdHh;O2Cco$0Xu2uT0z9g>6ix!S>_7wE1lSK zye=LM+z5)haKxw}7d8fXFyB8b$-5*ud3=$IDQtPD*XIX(^r?R}FNW+8L-OP@QGK1V zF&kLw&(yo(Ay*xbcM+O3Z=Np?>t2w2rBpFPB<#qzCqg?pS=zr#zG=I8x-bBQ#-&KZNpgdWN_{Ib|@hGm@hmVu@8RTuoV;N{UnhGqbsG^ zCuiTWrXap11tG`X`wIBAxY4-^S(m1XwxF2-#Wx**Mkp?9&dp+M z(w)HJVVKn#oabNvcu6ws%I@{*tw$9R`=*Pv8d%D2%i@qHOH)Ee?`&~VJ-xC2M%gOE zRxUKx`ATitKqKCEh(98D0l|;fRhpaP4RAssPhf|k$TsMz`Q$&T4=Sthi4{BI44+%N zpBb&UTMA?SdFi|j8&Vg+;58~EW?gZF`l-^z_=${_|92x%G`0yi4USfDBl@@JrX3$_ zmEWFpfGgAW^h@YFH9+W14*e^_Srl|v<;F11h zQYl!6gD*ju>G%M0=I3>5HH2IB0E4(t&CZMziLP?D!{-}}jGnf~JA|f3KY}BV0JMsE zkic@g1m$9?eVo%#WI}8_$eyj#p7QyK<@<>^0z~W{*tu3O;e5r#CS1+&d(Uv!k4L6G3?CM%Nd`avzv=na3*nW z-ca?~dm5e!j|Z#Z8%Iy~Y8rKHjQs?=Jd+^45{szD?IP1i(T%TmBa!*8j`+p^THA9| z(caYQZVRpCXZY|r_5{1KiK>IQiDyg+mrX z@0ea`i;kIJ0Kyc;hVQSa?f3A?@9PLQTsj21`d}*}AIEu4;amVE4cEq$^DC~Y_xj)P zLhK-bGA*3paAOhU9HF$bMjg-aSJm^Cj4>DoYu&KJ7T~?O91cp#$xL4g9s=idQok7{ z<*Gq`ZWx-6BA3}Ez*bHq6L8c@4qks<|M$4uf!>&-Zf$Lkj#HAH^Z zxliFV8aNPB82~SuH&N#Bc32aMvR8Q4sr zs%OI>2x9gcPkRftjEzLV4}&mlMg5O~vMH#O{|;ZnKb9mJ7aq*38=t&DJK}uqmfzv* zl>eG$n535rx2Z8ip4XL3NG|2~2lv^KHQQ}7WB@Dv=?6pEMuw&f0d{ImgLWp&j7JT?oU zg@+HpvKc(CE1M_Ok<5D1f#;kAVmX@2=>su>-CYP$+zbaBSqSmc-xH_maR}m1eKyg; zna6SdkbhSVd&dBTVR>h?Q31Xng&wsZ5Pfa9Ut5#jRyeC_N6V`~-JKZs38HtCU9P;w z@o-2XQ0-(KHjpDu?WbawzOoEbJ+%JeZ;%za(Eu$YWudV7mYJUmyd~}-Cwfjv4~e+U zfW-c%$&Zeek$fOOu=?QUhrj8)f%sh!x7CXaz3#*eiPM8p5=Kez2kW~vq;iZDhf>t7 zqXd!Lqw)iOxa%p96BZ$+Af%60XSm)JK08SI;ECf9SN)hud=rWNu9Q$$;g?*r+dn(L z(|DtoAc}nO-<)AW!6Ti1yh-=t|6%Jrz@kdJt>J@=;3y+zK@c1j(ydiYSN#BO+NvP*NL+k__Z1h#)yh4*#lix|zB6`}=v`XI_D>Q?Ta5_+HgdMB%vGUHmXKjEN*S?M#Eq$fy(vmxeMoqJZM@g0QPP=jW4myoZ+lpgABjo-y7j(w zZL32-wYi43@$eB#vL~_PBJv0#`LDHC7}R9*S&uaBe#z?8-?VAx$XoTx<_BCP6r+r` z88Gv6r!NO*yAm*VI|N+^J_v-Rm@FeE1SG@ZRcu#SmY|e8xHIh8gx%6^Q~T?hjAT-( zUALzA$minPioO*8n8{>*)ElO=#?Q+x_K>^^hkBj>;P}C;!PVfGva)27A1e0k6hB1B zrqENxLt?RZ4-@K4ha4Tx^kj`GtyURJUHptjdv+UBMb+SPd{r{pkVFgtg_?Ya;Sv}H zi`l$bYzM?86w`db?!m~l;;Qa*zSZ|GZ)r#sxpRP3(XfJ^Vxt7(evcvuV!0ipxapM?KbJpIA{qq-ROu-lX>8q`1_% zp6UY*!!6NcI>FEA$%Z5{-eUdyjNP<}?8CPe=~M889>cwX1SThl(ewD5Ahj%vyY#l7 z=6yG7*WKU*vr3&xSq=t-3@AR zKAiHGjNq=JIn-mbO#)QorQy%{N7^+z@25r&Udi9|D{k$@0SW+5Pz&*62D1@}lXXfP zKVVCoeYY0(vk+YXCa(rY!fZP31E>i&fnF{Q-}MB1+$|?=`0Q}k_U4q7zQ~c}eq~mV zR;?N9m%7dC$yA%C*h}KSeO#jq)u-=c=-C=tVsl18whMmzbzXOv0{hS2u;|eX!##=B zIP#2=%)E@r3uDzKulu|1OPIF*@U(A{9KE)FtUdM1W3HxGF`B+*QSo~n4a9T^*ce^p z=MAz?=T;M__9JBF@qtrhGDeLWI)r;_Hg+}+&#mif@)jP`!_*KSO=;dGvg{zv(**C@ z$UrJ*YyZ~+V=ANVo@OM43`M3ZN-glOlvejPw=!ret1EQr6R!3AajGK0;jy_l!(o$| zO;E1;xEz+}eT(3I{W+|Gxt(9yU8JC6sb|31E3X$zfgj5UiWS#-?r3;3=x5d1zPU~d zw*w6|vkD6{Xoz52azivvG0@XAZJ7X6t?u`~3vMlKJH_jOsq~Ufx6FrcwWwx&f&-b_$mbO%C zp+`2IqawMzfa!8L7l4q(<;tp#!-CIGd8qf6mTVZ?mmkE0x2arJKjsp{tz7(Bv}=`` zD1!cQYwE}~5~em#w+uirNmU`;zUp9))RB1B;YMM$@G8))E2bGV z-2%tMBic@}qGy&jk8F^Lt$r=KCABAReP6OwZ~XuzVhAf>-y(L7{*E)(7nn>CM+m$- z#y)iK1plL(g-hzrv25j64>#^7i7EhDzyO|0gbs&2n0v{b(%A6*j>{ZJ#j7c*)uUBL z!@YAy&kVE1zC{oA-Xr#=m3NMh-Nk)^_0rjth$XQ=f%l&>f{|7HNs0jXh6OpUz(82? z#XtSGue+`EJI*zIecRZ}*y!Z1kqa-fG~tR~7ikU3?qM)WmX7F6vl3V93fQ~vj%}V` zxFC#{irLr_R8dyY=qJeDgqVAg&z~ePQ?b_OiiA1JbB%X*RlxkxdL+lb*#8>qm%OQ> zPLH0%w5NqUxFs^Fw(Q@nLv76~Cnjo~1wu&-brqW;fFf)CX}69&oydV#UbuVTjxt%_ zVxV_zBzgDqhW3hOM}B-9Q(3ortobMyKRzA18SfLJiy&0v4W=@{PJFud$G?(x=zL9V zW*c>WZ=9MKSl`#C*^j4n}~i zGMoORw4v^u_b43}woy`Hh^FbL+^W}7J{L4Sl@hnJ0>aNAyKp;3z4^cvqBzN=)y*Gz9X1$UYin=i#aTbbdf3yMblG(e9WU7u za(Af?OFH=0?14%3kFa`C7o!X0bOmY=d*6iy_DZIkZaRWxceP7!%4VYa1xN0LC| zoz&*GBS+YY%Wa{K_ptJG0z^nf6uSRlDvWr;*x(0CT5|{e4 zL+ag>TghAdoE#^Bni~!qd~<@v>3WG(kuJ3godsoQ5oHNCA|EN{POdDZ5fOsy!^rPz z+Gvx~(1K^#w|q4A&PW;lRpN4N!>i)kro@#GQtr1swW{WMzgAuN1yuewO7^Sm>LhW1 z^jFdUe<3jm$9D*Y(*dluwj+|p+vNW1Ujg23*T%|>*^%~2d32HpDO$gv+OpnSGfTKp zIJ^rtbQqVV+OLXAKC{6_^FYi0vMBZp;>XwbCB~6^2iN~bN3})B+(`|<0Xt?ml^~az zdl!o#9bSq6men{bk*o5$K1~x7$jL6L5ZnoFpjpIo1I+_E>Ye`fCr)qRLekAlhRjVb zLIw>|w~^@&pXGw>bkpA{y93{G#SV4z*-=xl`29O&RA+BanT?FtBDL!XsjBVf*W9#D zZNTfB-zC)V9+GnG1RN&?j5;$=u_H|3O&fUMiQz1k7+A~L2CkAeGIh|b*YBk%p-=`$ zLPRSk^!rAv6Gf6oYCNBo+oHm~;g#_Pxh_u~yJY@1@9be%P$?`Di^7aZ3EtOGVc0fB$;WXhO zJ%xlpv2W5rUt^!I)b18ot}oqm5wQp1Btc1(dW^1tsl(4OKO=_~3m10%XnLEl6WNi0 zV9&nA;SwGh!>2UT+xJq``|n^%IFhaYtLTXKg=I^AjLN<$9yM*6K?(TL5K`kpOKCya z|M9}&*RN1N3Hgf09lAKfyV`*@AO5`}5 ze7w^~*10dVr%Nol>GG2rb(*6#F$(OKZg-(oAp#y<%-F5FE#e|9P6&8&oCuTKL%5H ztKk=zfz%@S4wbB9p0g!VPO`H{9!;fE2S)_$q4P}-nH`PpImP-n|BD>K_ zu$iC{Hf!KGe&rCNAAIGqeSR$JlV~ChzUAy}C~JFLkkiUH&t4{e%i)DCYv=eBK3Rw` zI&@B9c{f-PKZ()|Vz{mw=u3fDvH8!5JHhS|1Haz1TH7I zQ2}A7s@t5YKq6&(kg3=%68KtG^L+;m)H+hNl^f+!2a~GNDO?s0(Wd96#eEg!k0zv2 zt0TWNrw`vxAk2AieLlzZ->YjaLZ$K@lqe$f}yS_WG z)8EeJ-e_j}(sq$ADXP&K!o(`%10PZd31hJc> zY4LtJU8decnyd-N?ZbV7@*{}Qi>6$X@>$^)#vE`QufSdVIF^@rDz!!jV9TQdi2V6dFS4379m{9r3m zLJ#eX?oH+-pQJ0GvI||D`@g#g=E4_-Mu!MpxnWgRY-;!3@sIaOL*1s%D{*f8Ctu^i zgM|OsX#Q;3f&!XkdT$vEd{bi~Q$3Kay_Y|Ms%J{^iz-yX&hK2zq$Goa1F2iP7K=HK z$=%f)(}$AZ{yJ_h<#OS%uhPSp5(CNVOlXj|TrEh|_i8i|2h;RPM3>e2R6IF5_ZCiW zAwP7>ZSxJz2=EdU&o*i}zH{swi@6eoSHRJj!7Z%|R^0flW)w{gxClN4vlXKaJp-?q zhbg$dE9nWXz3(nbuFadkRE!W%KQQ)9j$SABjn1XUN$j4WLl;$3prja;R)0?Bah}LL z;+{QoNB3eQ!HlQ$J`Oy9XF*)bAtCEVS&w;~B~g2iE%cJ`JY^oOo{f*?b+3`YL^67* zN11_BCQ-+jGUy_1YeyK0N?_5{5r0Fiza`x)eWtst@T=%XGcdz1tYIzE)Td*b4&t9hV4wGTF)h@bGNWLoD z#J~x+#0PtE@8#*pv~kyP6*vf)4Voylqzde~G>T7Z66Icu<`DgBw(zmO?0hC9+^~L8 z;#ocVPMurF*7%*az>s#P>P-SBPHbd<{)i1JgdtEhL6tmqEwWs4q`%tJUBYD%B;{O_ zewxzQo+tPlyNj*|A*dW+QrPYJ`TNLsvunW$7*3{{SC!i=H*$$Z?mAHe8jzNkRL+K# z)E*Z)A!bOZn#{gSAWP(Big>xi`WU*m2V^L7@fI`7J52*jYek-qw^oxSe*Z z^h`(Tg)YsF?dWQSY+q;QUM@(OGAMJUwgj;#nnAGQ-fawU$PS^X}v)-%`4m z*q?dB-E_DyRB9orx}<$}xagx(88d8}`UqBI6*1%yQN?3Rvu?E9!+FiVtuVCkJj z+Y1C>A2Y){Coh9vSa+(LZO;`m4`E7ZdP6BuwL2Z!u(FM4z{4no0!c%H1}KjZ5Dtsz zXb~S#6Yrdro9gdN**Qhr^h`q+YsPHaPPq4E6dY)~2StWv;(ha;znN_UFx$%{eFs9N zRe9nb1UtFWtk?aX?itnH=f3RF+Ojyj_w^*UQAf8prm=n{opJT}@>f!_PFG1oGr0iN zF%$<}lP7U5C#WttM*wGeMcNk0u#6HZ$<*cs=~BI&lB{Pquc-&yM##;uB8M&I1*!K; zXN-qWG+?GgixGkU!ND4Ozt@JgjC<#nGvh z8*`$^;EHfantk{Sov*nSVao};l#IY>o@?yY+#2{*6f^Qt5%(tyG70AX$F~%ZI}RF zTni$Fvw}Yu_!G0+&By-s#qu;VJ`o3I(;0p(vM>?tIpw*;k=4A0iyqR-D)3f7!2+by za2eeX3$bAEM3IaCTs)ZZ%2;j%KStqv6ZH{wlHAZ`?IEc;?3Z(T3u>xSs&lB38_XsI zsilnvHG1?uB!S$2bv zS6@%xucA(Du5PQk8~lwNlvW&!YsiPtH3s77dM=wWHVs1c=^VkjMkIsGjH5>1z@;m ziQPY3Ht}>_`?CC9jVg134x&dT= zOjZzmdJlV)DI6ImoC8D9u=`iN0+;3dyF|vmhx&MBRS>WTs9Vov$S|S|mnqHVc?!MR zfwQ#?y9UWt4&p8BvEbyP-*0xMbgPW%=qxn5vMF0WWMSby);cyX1Z?&gUH-!+WW%q2 zZ2UjIaV`pSx*jd|`GS-CA?XQ{ex81-?LJui<2o}@;Y7iv^Dj?5GJGoVx4;~2*7qn| z-QV{--gUbG)JQRZhy@0AG7gf+X}JLgKtuBWAsX2pK}fOH}1c}0rS9Y7EyA#W-_Qa4bx z-=cb8pKbabr-tg38{{_{!l!2Ewm@;a7{$#l%-UFr2p%xov#{ki_I;b7vLXB%pGERZ zzNiP4AH3FJu3ai>yf;u~mT%d4TLEh9>N#67QX*1vO?=ATUzh%~q%&liz_8<< zkI9f5t!zdO@Naw!k3y^#V&&5l`y{X2__WV9TdGxc@Bsd3_^;P*mMW77E3$xb1b~e2 z+U6;H*AzWhAm44A0EwFRl%B2j$lA22Wy7$;t!&>GA@VD#jA>5HKQOO62#b4p@I}h? zKx{>f$P4}mU16yA9$7u!cw)=lpq~M9IG26j1xGRl#5sL$pwcaTtHxphCB&RwaNgkY zxqH=a(+mvS>F~C)NN;x*jj!aj$x!D6NRN z*HE{{CTQkUgv@a>GKm7d)JU0mK0jxUs4SUDOzK0_#Vs!sD7T=w@nq$#Oe4Wd`amD8 zn4$<7{I^3;`dsJ>n$n(lp`|{$dAR7hb+!HB_R*C2J_8kuK4p8h*Kdc6WxwlRnw!hV zS-@=j%ka%u`kZ`Kpr|$c9IamJ<)=`|v4t*np+SLfP3KkH$-ZiLTCQyR@tKeNy>@CV zi7%NyHlL$p^?0`ZL1n!_EzWI44dvj$R+fv&}EKfU6=`fFTo zg+TiD%Zj^1yEn>&@!@A``12cb zm*Zc7HbYGYvpnM%JqNwJI24&+Q+LE-^oOqPWbdD^1LK}1^`EuXEceYWH_iW6Or*Jw zI4Vxup10!;#KvAnGiBiP%#$k(vv9hO`M6co{O+u_`O4e%__uTl{pwb6+JS{TU zlQ<}EDBL!m;B2${YPvq`A0Oe^GY!R1NDizz)^Y4ci$_%Xx&B`SkB(%L7c@^O(zN*e zew(H}v}>omxj%tz^h$}fnpQeHnv4)T+NonI@BF=6&Yw8<_F{za#;AR-Z;~l&HeHK9 zJt0L8L{Tc!3AJY4snKXZ>CzRhH`ag3RzSon3Q|4) z^C0#%!3E|nYo+XRY)0*PdG$DC;$5KkrX_^jsH*WmuDYpefcHb)9RZxwJ6!IQAR{% zPn9kzhAi3Z=t6o|mfbsglH% zNO-5%OL@#Rg9~-PUHuiO7syhie{T}9R@d^wV8($L3if}e9}UV0dLL~Dq>0#mT8X#^ z{+8HuE)z%PB+m15YEIIzHK?Fzm z`t2*x2w?UCKpWsf6q8qHaP5YDzgX{$IEiC)HGsb!HbRs{!{@u1kS{!bxy+mijZ~j5 zmK37RbJmiiOTUGvN9F|P(Xb~X(zmWXy(~_+w(3VE#CcuLKeCi@G1t1sDBwgdn-@-W z5y04~>2_1hkI_^?u%&U}wFloMrs4@PI;p9N67iM>9z(&inFSq+Z?5@7^r9WM3j(4x z{qq9Uh-o z%L&wJvYZpM#Tb)NHRtjy28BaG9J_7jub)@nd8Gx%Q#V{X4Y_G7rL_vskDb*FuNJ`S z(z7dm(qWAq^i}Wm^kzxp+`O9QqCP)^3s+xX zK*LJ^!r0E2ligj3lGOVCw^%Vsl&ZTiR?+BK7)JOd?&JXv0i76^Xcz;lRR26gcH*}* z%W`;^oSO*jN-G)8C2s$@WJ;oE*7<{v*UhF^M4m=TFfp2mxVHh4xhB={q@A!yz}ClYl>y8q^OnpyBB9WyDt?!}x%d>?7SO z^_h`Jq!@*V6Y4?O&pc_x=h`J8lmjbpYooUorrr}dPGC%E0@`@F>HAI^w!I7fYc=Lv ztE;2KgoUe|e^o4ZWnufHh=ez7+Qjt)G`<*t?O+J0U_=sY2@k9_GDb$*9GuR!zE1#X zD_>eAQf&t=Ak2k@l>J<9{*apFfjjYba%r*s%E(=&{M5y#0PACIBp-k^Y+0*5pi?ne zpJs2J>KpiO4oO|g?NV+9zLkWYAE>CoWeV&nl=;|3B>NO#E_=c^Mb3U~Fnf3+f{CLq zD*^WXJ$1&LOzSmmCm#Y`w`*1EfYobM6Gqu{^|H$zHNUkoJ z5zR^xm4CI&dS!C-CX~hX!i^F;vz^8T%5zc{a+KPJUE>a**`8U32}a* zlxB5xk{19ykAZ@xM;aS>AVilgw|L77O&ub(^X*|gA<_Gn3Ioyrq9nh|`bvj)%rr$r#RDi$-UIt=4<%T`A=PrtZ|p95mQa5zDlx+2$s`YxW}=E zn9cdE5osS>4}jjr8VY(tDLt3CA$Q?I1_BG1w-qVM{t@G{*GS;@R}Q3sxnmEBbpU3@ zMP9xgPd{TeCr%ajBm{=9pgX%zl}vGZr%b~}biKIsU!2E$)weHyQ+P>c9?@9;)!cNN z^V4|px0s4?fcz2#jc3~KATka{gq#s;5}Cl&IK_@WF*`TmLhmLzG-AX5!Q&aJho+XFloi2o0cb_h2LEnNyUM-fgr(z!;4O!=1MSSpk{L15mUG z!y(zb!NV(*FRc?1U6qy>=3kQ1{kUaTnFSMiR3jM>K$&zLCHF^=D0%%T&kSV0uM>-} z^)Z)B2iTS_fB81^#B$qq^%8D6SduDXspO1siQlQ784VNoOk#KeHyK#-chrErV32^Dt6m zA>5ag4H&-k@`{}Qx4ksoYdi=EvyBizF^GmBcRm!AvKO(eYoL@n6N`10Y2yxi!Y6jF zZARP3mLo@B-U|GC?*3ES0y8Oz2TXMC|C>fdp2*Ac?k|AI-@ZVAzWXbU8ILpDGw#H2 zT^x;Te+Q_WBw_;9k;eo$ z{wF#XAAV_P)D4E2enMby=|1xEZt!ul*ojgP9~1LhA(L`1@b8<<{PLG0x&Tg`?(3`} zd~PbYMaPkX0Bg<>@a08L{s#w*J*KoLA1qEsR)5sv_N&D@v9mF7N%GiMx|B5e z%J%wJ&12N4?^zoy%$YOy-tWJ$-_ngFXp^$HKsYbfn$IMe0-Wp$|Adab0O1k7FX|(e ztgB|4nu!K!y1XI`syffL8~M0$Sk5I2##}P$KCb1UOcW5#43#m-K|v^jn1RxOZH0{#r z_WVHX=$QJ*NWSUO4>uZ&`Vi}PircT%)Z9_JyyyD%)7iN3m;^LPl1eGiOCpKodB0F; ztovmkiM8yv2O^@=c-9!A_42AGpdhsKQ@+cL$Oi+PSVQ4sgOYVfPxJnXJ(gemq_%v> zC@U{yODjo{@G>)#-oIgv5y!9{`@uxEm}ZHE;~#7*J-9iV4jwDNvW#{Tx z6vBhk`{sL)Qr+^N7_I=8`AY8vmjsTDn>w?Z0%O&L8LHc?urVZ+^3=Ynv z0n}mVC%tPsmR5T~<}X_nSig+NOjQ)*%UG!e^4|W*)V* z!DtfIbCXGS&og&3;{hf+h%gAZAYDxzI|!QSwa%ig2COS#G_yz@FLc(IY*3vEUuc;%xRb6!XuP2X4IfbYmHY=)aEM8HY?=Ba@@?J!7{h{X zra!kE*f->&@)1>6Jou~PrTn7-~9{cL)Au`nbzhX6Xbsqt~a% z7CEYsc^L`+Pov!6zh*Eockl@ON;e`Wux&8zPX36y$nw+8gEsD2$kIA%F<^O#SIaB` z4Voz|@N`^26`0SvW;EwF>OIqy!)t6_X##D}jtKU-NH!P$ z*cYWLL~6?@Wp4Ue8jB$}F(SR&)xU|s&40$Z&*?ZsLCc}P=3+RZN5`GzRws4W(Ts0K zeAIBf{SxIPl&W#SRRXHCuL@mfJYXJ}y+(A_yh#Go8|K|6GCkX3mW#@`ycey%WB6u+WQ?o5r$5k@@* zj4ZF7EySB#1tSrA{O^J>u7(WVd2?6KN8A3$y%5jY}K9WG~&brl@u>-+K#5 zowH33rjAzET~Z%1#xr);r`>t@#*Sqxu#pL%jXd+e!_@yZ?syy(#31BzvGrluBb}&UiUoWns|YEZLXqw{z2nNa?XvL0MjnQcTDy@DnoBL5G8`bcKXh^dZh`uhAv( zYV|maJ-;6nH^uxQhL%a(r-Rk-`rG;Wnn=|fRAL7!!dm`GksY+yKywe0eY~6YY`7tXz@h2cBy?5DCYg zb6BFOIbUiiZ3|R^j@wJjl+*;>GPc?>3#VkoiA>=~H2>?^0<|IldJnnm{Bcg2)yGO# zka}1usv6c+`w^wi>8A3JVTySLi^owaiqJ`r34&9k2C`t`?dkwe5~p(q z|F+#;?qu7ydzSeQ40#NxP8}}ML{+i?wxPWNd7)i}9N8B(?9 zn??ySui{`xjRTRWk@&t=pvx1o=iz4+Dnnlq&UZyFL-FsN(zDKwV~WauRwA)Ct>qXq zjf{@POysBErfpnHR)z-7=6{8d2tvIE2?WV!W!IpHeT_(flnu1s)cUaO6*YI1KUeq+ zc78SRivpS?*}Gk%pEAnd%#ie&9b7+HNPF!|9VTbPQ8fhdcws=~1gNOmV0rL|E$Tzt zh{bF$v}x`dX!$2f44B%ct~>)_`?+?_eL4!VqpVX=dfLThk~iptyDWPnx%Xb;0GKHm zg|-Bt2ndDHj9Sv)x-~1G8H6W}zHK0yT^y)S)Fp2ZKj(KOd`8 zvOC8C@gxFm-H%eRK0Jn_ak6M`-RG=${A4B=Vd;jjrgNdsXTGULd%|yh$EjOE^FQu( z&Dh7#=w?0J*e;UtJ6&nNPMP%k zJ?5&!A-ie#HL61&>_)p7-)~$F;{@W-wYys>JzA`>?vnD*YDJIa80GZT?(H5PN~5_o zNU(&Saj6MUkasRTAlksE=N@XYd!yQ}pco!ZzmTN*tvBvF0=Bc zUiiB(dFUZvs#LVVmP}KJ={)yeE&8EJ-1FGK!aP6nH5M@hWzTH#HUxXkX46$sW5f1q zZm%{A75WFzTyn0*y;Riu7#gVPfnK3`e~1)g;Y#mJ5LB;m`%ND;dGJh0{D4^_Ey*EF zk{x9}Ta2KylXVLW==9eJnGXu3)4dP3T&g6o&m&8=!(%ndl%Qgbnj%y)oYT?3^q4)7Oci>?m&LlylGolm6Lxp^`qFK`t%|=gG zSXJ(5B{Zl%<-n1MH_CDR&qa(V0ned%&=u4CSin&8vTXh{7)a?Pnl}VN`Wl64Hes>@ zrHdmFFJlIg)hmZL?(ZGBQ1&YxULW|yozQfB{-e@>yf6c~XA^}ycvd@$>_))*7c`!d zNk$VwiGYTt`3M~c?T^q>QJqd2lG<)nO_#=`d$q>$J40-un~Pmi*3AY^zi}~a-0WNS z^2{m|6Z=2K{_n)e!0p5+7QTB*I{D7W29itlNMRcdsmZLE*RRB@%LbMjCRULO$?n__ zz-YVHy9Lc~{EMh+PJi6h3ueZSCMAvc<4Ie=ov2`?IjC1Z-7UfRgVlMYu2{i4Z##zq z_2jW`zft3{zWxTmK4q6klC$*N8C|)tcT2)IZ=F+LptjR+P#SCWnIiSpnv?XF2+-Gf zwZT%YT1Ryy2~ss6rtAbhC<1CkcMN7p)V@uw&KSw9PS1>gW!Ewf91ARVw(nfywvx3W z!Aufd_CVIK}F70YxQV@ zmvb*;%3v=Ye?0RM5MjUF!&Hl%E_KhMWRu2$(K(kEZ1PbEw!fxCEEcwhUNP;v^ZP5Z zugPSPGzGMEs*{q?ZJGsfZqxZ1>N=?XQQNp4_PoK%dCH0slg#HtJYzk(7|%dIVr}DMT#o{;U??p z#vgl?oX13}=UI9!i8Bkb#>u6IGQ66kdxTjNzAB_TbKq z_xOI5Rw{Sy+%()hb2zsD}=26wb zil*Vj&bOV4z<>zsrL=39%j(k`vx2t4OP=$jIw6}Ra$T=^k*C51I;bhn$fG zi_t8&kq?1eQ&Y3{?L|SdM<`HR!R=_s6d`$PWE_k?1E1`b4t$V1E8lhJd`Kt z3PBRp5EK`?kOYILdSrDh&h;IWNjEB)?>IVKE#l+4nklJiMf?Z)3d<_U4kVt+YHo&?_Ws70jq2&Pwys9zTGeLg%~S;gcl2(8d`sbkjpcPR0y{nuAQ+ z_Il<_u9ccJ0*`5!Uf3%>>T>oE3|jdT!>>&3Q$*q8SZF~VTGZXieCH1$j&x1znN27D zr_dxt1Y~aE!FMcaPA zzY;u0`=hf`GR2orCJn(gx1&DfzF0bZ*==bN8Z+QK*fP>GOJp9?`OtdAtugCs`ttD_ zjF`ZoK=rxP4-*VCVUr2(M;9pbyJ4tzeq<<13>&Wr!00CiXF(0)K2Ybvt?@v~b{@z_E~P^xiCvX*g53lz4W~ zm}u-K5A_R%n96*?Y>YtjX>52Rz7{m@Yf1yR@uSk z{kN!W;s@$}=36vNoM%+tZ?6jl+F~P>?F1pZ`9gJ+Am=77DO`{Jd*g*8PAaPV#3AW6 z6EPj(!&}`f4+g4ZQ{zUOT%x|*hWsUZ#XNJOTH$5m`Ag3OM6TvgFF=TA`*6Kj=>~j% z_%EOZviY?OHx^@B*v$99ATV>c>-{||4m?0bT!Bj2%WZnyziI=c;q4tjt@S0cV9=cY zFRZ@0Wxnxd4H!>o)kgWFWXDUd#K>Q^0zzm^l_g6wUSKMIoQ+)0CCqJl0Z%58ej*59 z`$%KLUQ!`mt*~gL40OJ38NcDadEFyo1w%|TiIRB6)aWH-xp1n{&TPfgcmteOTi>fm zGT`Ny#>V@x=NGRIbW+CuKKcp$B>OYyZ3!Eh%-nua-wG<^&e}QAM-f3)c>?y{PcwKn z$$e<%OBy&(G~4yqRyQNE)s4`4eptiaC>qMiR_FgA|WR z5}K&)Bu#jNi3wxP2c_hDBH&LY=lX|AMP)mn=jn0*fqWf%xFS;IW}Xtv#)?<;eiu%R z9{hr9zYq6U1>vkF5FMec0vWQj{=>Etsq3U;RPd2oCELOj$}ubZ?c{sbEuQ^V-d-ro z_|o4!KsarE-)Q;{=|(YhBytBIv||@0W9ZdSxrn%j$r5@d(G>w~3cGnRk?OF<-_NbO zpg#ZSW z|B@IZpltYO>d+ao-RB&B<**%9X1FDGfs|bra&AQ}pg+$TTXS(XMHpSKpqw6Z^DC)w z7WFa!Rla}aTRkv3R{K9`zvt9W zTA(rA`tbJU>F@rYu>Z56z!{%Y_f{1jS~~JbEBNeOKh}+=!B=}%DC+05?tC=qpFIiJ zwLLPI|91ViNsY6%_-V)V&%LVJ>mk{r(Usv|JY|wfv;WGf&FM3|T~hO}oU}Q%bIy;I zR+5|K8u3b0zrTYyd2xWV)c1WiUYp_E>^BbMR@tbcZ5vd)S;GU(eo1ZgX$)JRtVr*y z{5U2v+8f%?ITDZDEk<{VnOZkL=Sj9*FL`sviM&-<wDwSJa*je(`%&!!lWsesH!sZT1Lk< zw?IC&B3Gu?QWu66T>VQ1FR(@2y4JRL)(qEBjVMpp@o`DGs3h@PTCtyr2-z`F8m{npW2Z>X$?*i-`Ahu31fa_f3D#;pDm+9yh`iW1 zK0S)PX^B~7|Dmx9v?{xLU2qBy#lC!5Byd;@$~4e$&Bt?2sI)zTeH@MWU2olhmsvl> zIVUvnG)wQ`=!ok;cGu~P@|=AgU9%~Z+5N-6QNzsEGz`XG+zlUu zt$VVDywx>tD{433r{+iW4aquJzuB_dkK=Lcz021_ILnA)Z^-7_$H#6pswQ;Yqv9gS zVL$7(!`V~Eirecx-N~;z+z^iAiYU(H@T;BW-?AZ%JlD8=Q}a=d=G^_rb#+}p2vH<+ zGATt_esHy?^pZ~Mq}$1)UdcMBn9|=DE%h;goGHSdEq|fX^V$KHjsI^yo{9%;`4Fvb z#?c3AIRC+a`){6K>B!-T3~D8LNs{^Oh!W|kZ6EtJM10D=WMBnOn4)4nJSo)i*zHvZ ztrR}Kwn)~h7+>JLc@}T`@U|*B@vUQ95I>IM``61S?hKbtaU9sSGw)4jS}JS)!K$4& zw1rxE<7|mHa0G*ChjKKDAvLeEk|f7=^hXev`Ojo3^pfM6tyQQ+x4*5g*!WalT7kPHBVf$=_!9>mJ*mR?;C&MTBwD)lBD39kEpANIb~ZlXh9E#yDlH zcQZXFtFdXnigpvrH*tkK&W#?O_t2+~cJmVZ!pDD!`ne}LPn#}-7huI!SC^90BRlA= z6~c`5=lH%)xn=>O#ZdJfDx;&-sn^K--~@X=N<7*P^84dN*Bdc1<~Cg_B$}C?U zz|VP;_3Ex>9mV$DBjiA1BlA9ifA^eCl%8>^uELS)ZzfRvVeP5T)A4oU@7Av1@m*hUkZ} z#aCophTM5L*|$e%H_@lh3(KZiH3}Z8d>jWtG9o7AFQ(Yu^dgB##AtHC;=c%%a0$qW zxxvL>Pnsb8h#(9*ZvV%ou$k;X?zFcMycyRgb2%iv(I0&T zZi&3q4*yqPcKc`7Wp*70a!UuDHl8#4I1oFg>*q7i`^QHtY1h?VYUu1E49f(YGKhtkve^2~!ZTCZj0 zeyk}n5XYWTF>+iZ^C^x_k`pQV+T$KhEL5mo*{zmh*B_W2v=$D;ylCv-{R5gaK8TBr zC|yIj#_@eStgQe@Km>C`i z_^IH39g^P@f`G&sBaJ3!0&~iixsMQ({vILr<=M_a{*_yGJoYW+=s!DVW31(R=_7+* zmLIOKr8<^#LuG=mgL!d)E}zLRu7@LH)LyF|{gcFzMPM4+bmY7+Ex=*bWFfKa)FD~P zUqw+iK4p!4`2H)L z@u3|d;=K>6%E&SI71}ALPV*@}@;WG6Yus8jHf10+d{D99Smn%1St(2>9Za4Ho zO6>n#;@}jg{Q&Z$R9A`&Q`@%@+d#c7?$%)Tduh2_<vV(i&kdHQV#>M#>1eO`W5aJ$ z2lt&!5H_N0nkSMkwN!azj!TrXp2CIJl3BdxB36^PlPA*FX!3&*x!((Y-M+Pwlzl$q z_xAnY!|h8h9RFu3#r_VN%57*Sh+s20=CAC->cqW8W7a)iRhPy&^1YQmKMo+Zmw{$! zCZSYl;9E;Yq#`siL^|f^6CF`P|5h7~6vm*QzAZ`ghW|aAKNj+D>wh6g>1wCE2qqjU zoy(EC~u zoVs*~OU|Jv>GaDOOIFwu=X;UI2Iw9j{sc&z%+2OGk%f#s&4#rTc-h>y?3HkEI+&Lj z?tHBA@yLWLbl+y;R)yXM6%t${nB+2NvyO-1B!2c9N87JNTDLAOV`=^|>)SO`~TDE9Wn?Y7a$#S>Ot`2lwAA zCpkxivx`R>i^QMuQ^!xAwRLt~4?yBhyo9pSJc>X-a|F-^{`%I7 zgmK0n!QBH1<56zgs_?`{ek#WRxUD`~am^xJ)Z67(FyOnH1cIDQ2DY#Mm5W^I!lyh?r=P}*U(F<49Ol=uX6yS5;~n0o~j8sd8ar@5Rx_#x#|8i67b;m zV!HU#+q%hd=9%J@-twS$TvyM$7XA9^)xE@b^5BSIHbnn!5DXJ-#bF#OEh^UtDo&x; zECxfi$ThzHit!@;;i9DOy-$LaC3D^^eojdrLwo}KJ&!!DGFZsMtm;-hr>Ng|8X2>U zK>{&*nsHZbH^qzJuq0jB0-3}mUO3quRbAA(5XoxHTcjPs-7>7JWgi`AkpaF}T0Mct z^bUP#YI3M({}sM$FqD|?Hfl%MigdN$37nhZ$9`p&50RtlEgv&klP)i*qbS`nq?}+( z$Ouog*nn&JU&Evo;8n0FLPZbq)wn&|aZz4Ydh^&Cn+Y*by5{F!=VKSjM@D%#@xxAF zukb1B5D}`#I600SgkNJu4CBaj1+C5`Ix>1dpr%~og%qSv z7PnyE#BTU7QcxZ2gLgoPGK5LoLs55XJ{_ID8Wj-|+WJ29Mtq+(>Y^)=(LHPoPDgwn zR|U{;jf6TqF>`Lxe}svmb9Lm%DP=1)5Pis$`JsORDOe~Bb<3+l4|oOPNT*6KlGD`` zbcA*jzH_9YyO$i-#LBW0!YO6p845;J%`5JZa|-xO6u!EA`@9w*1r|e6?EbcT#ZM(T zKj+&QIt89UN3YcR*O&9m1%R5NU_zM`&bs)8aJb1d`hZUb0ue6T-@w#NmW=CFv#!&J z#1Ik+p;zy_x(BQ>OqWC(iw+BdC3~a?$%#@Xzqs+8Hx@M1{&aQ5nNO-$kuE_6WNAQA zO~U^EpcWwXV2I71)agP4KSvO>hf7Tb4N})kq)g(7Q?xOg+s`RPMj5=Ez+T&+p+d#9 zAQgg?99E-9AQnk`Q_)taiFhHbERM2h8mx(oipnq@428$7-Uai^*#_y-Agnueh>jOL z^K%RvsigzsQRtNrBX}+d00~6B-?Xh>Pek2DN$j#;c}F-FiMg`I*qkndm3*`SSGMG9WiK`0GZX^o@f@S^TvYM&hsX90Lqe7 zWXFd*Yv1PHLB;SrcfvVH6z4O7E=h)J6Yo@u+~H&Yag?6U>+i~BIaC_~Ztiy>v^y3b zAD;l<9Vo!Ll4_S7%$M5CqhzH75YCQU=^LUtP_&gR5W8A`h5vq&n;B=@E3BS*Mm49{ zes~dNF%OTN4Saea((^z(NbEXa%|)7#M_f8w1Iwg4{s70>hNa+h<3HZB?F#^b99K?6-s@R4_m&0u~g3UGr_=N)_Hm0wDl?XRt-a{dB!I&1Huy5NhtC($nQ zP7}9z-oc~-k>K-yMGmymJ4~w|KZu9a;K+z>lzA9L1ab>KZGsi2*DVD|Y$E(gfRZ-T zH$*CED4f}&MJN~QkR8+AM%f25|793Hv_N@d`-!>Z#`AMW2XJ~A#rE8TSj5d;2H-wjt5#Bz zE3rp?1Be0LE?hXecICNgpwFPaA8aUD$F!QB*5Xv8-%V%lk))Kp_=`O`UQ}-vM93cE z(lVX2^`v0TPk~$aE`Wg%racju4A#p`&>|e2>Lt~seMtgrU+t+wK*%Z~(8QRkC+Y?; zYh44=d_Rz+H{6JyMpHGDC=za|nB`58hId{g#O;R}eyS%~gH)}9$rG*=f_rjCKI`|OKV z<TO$D9(0*kU7kI**~&fnRSulM=9N$49(+JLOBLw855_m zQOYOm6PAc<(RA>=Z$l3iV?x8N-V{=ep(37g{W6&t#Bq`&kdJ~-eL2BgJ$^|sf21(& zf8+v%uT19qjA|Ef86&EX>U$8uy5fvX6bK0DG=MH_3!;kXD1&kW+)g`ii&+|0R|#Q~w+ zQb@B9+2^47VDt755C_N`l%3UkJB5rGc^oXp?Ku0f&g{B)mRGWZcA?mIzt+9kHG_{jjV6l@JR- z*-VI-cxRbw`KcBg0$YSw%cxC(Nz!FvK&*fED}x9SJm;hhF9*V6Nk{Qis{Q{*)|Urz zwYA?L6t^PHMWNZyK#|IP8|W%SsH90rghnJ0pKiCAG@v3;5{lA5gA65n|2APMH zsSLlh_dcrc`|Gdwz4bX~pS_=FJ!`FJpM9D>wJ-7T&OMhZgS=aEt$B~d)lo?@M~}2O zrF;Vk|1e(j#X%v1@`PTHJ2QJi8^_1F;C^uTb{uHTS%#4h`*Nw9+KZbOleK{3Jem-X(`Yt4RdGfAg zr(iVdTiX|pkA8c%ou?so!@J~x>?Tjuc{VPhUJcIkXDpj>6iYvIFju&JTXLvaL+0F} z7^jEKtJjc0mA41=UX~~zXcLkLp8BqHlVwvzJ^Y!dT>8_l)F*qv0u)S~0QI~EnqL3E z4^qq5&8^G4qtw&#wi5je$rXb?_>zOYQX_kjVjGF$l{}$Ymaag=w>6K_F$eC3tA6cR z>Pvb6gX7m}?be+0t)cxz%euxFHv^4U4A<>$ob|D*=Vh|ypl=P^@$CVPuTEn&rA;y{ zPbd|i0UP~lXgBtzS&v{!!%cRpNa`1N=k++r*YdnurY2a$tL|^xMD534S58Kwq@^Uq z?(nAD{%PGFHp*`*+LLmd+zzcDucPbIhl|7BzEr1CVSTp3{PdmLHft1Tx#i$2A%T0Z z=vo>hV3^W`CQ04ZR08;+7(`!?6trAWew);R&20Du4hBf+e^ipq9PuQ@oxT>cSlKPA{c?t^QOHD zF(SI_gttNlQiZpT>SQkNAkSS?Mxq3Wq+pSJJ#j(p{ps;ou@eP%lmInTP_KNaro8(L zwQOeM%VVS<*LX(^#J{jsol_^m1!o3Z(xzYGM0b4uSGbqM4(_dttLD*@UP;uI<8vK)|JPUQXg-(1$?+sEacHKSm0gUAsl zU!FB@%14`+VKxNP3+PEAR8&8l*XC4|>1{0!S4*xS^tBHzMg9_hRNVL)KxMh*VBTl* zfSe-ndY_uMsq>hHdO{%bo&L({U#sQBMH@{cgek1q1V*(OQ5vUJCn|Vp!5AHWA-Mld zYs@ByZ!o;&sl7vkej%fo)k~)DKWpy&0IZ4|%M2$UewLPudea9ah8Cp(U~J@veXrf! z#f)w)wb=GPdT0lz%9{$~qP%uG7pD$RL#Ec3jcPq}To;XHwlp0V>a96i5U4!pB@K^* z>LoI1iGN!1w-zpSHUC4cCOppPvD)IDtFM@5j5V zB)H(-oyWCPX+zKP(Xpd~SDSy~tjbW{3c+qLdf~HE#HwjTS=!O2ZNa}IhYN+~K{}B! zy=WKj!@Xb}Z^RY02Vlt4<|}DzNn&CdnDb?AC%=&A@MLr$>A(=`#dDXC(}+OOn#F-2 zVd71S_@_izsiI0la*!Dv&8*}BHVb7q^%cw?CO_JAJ@Cz%t%sy`T<_EF4^2Xd!KRE# zInH`84ca&rN+r07AM0btzvp5myh>TW>--cUohyNFpkCk7yYQn|XWP0)Tvf`TQ8^|` zjQqx0#=TqO_EPcHSNiDiu91p4o2o*N1#%SatvOwqx=o*yS~_W*YQ?+RFAjd2$l5*F z-9pn(oo)Xa;b0X&ZnEHE1TYa1>G}!(TAwtaxW!ik-=Ksdw&HYnS9BbE98*fwxf6%y zw~!xkF{584uU9`WkH@)bhhy?BNn!~W_I5VrfvffAl_~<5ctcWgBZ!XbWBreIofc_0 ztC))dOWAKy=V8YPexWy>LI}ObizTFQDTx0tZ&K4ZT!dQwNM!TjHv5CP)W^K}XubZA zBp%vve4Ojq16Q}78`2)vyDnMJSK>ZQ6U}oEAI3Q)Fhh^rX4B)t8ZY!+K~>x1&}$+r zTg5#4wFq~NwTBM`!H&Tu=W&N>SxAn&;GX70HZJtB?gzu2Xhq0W)rxPDT-+kID))94 z5>S|Uis=&E1^U?5^-S@4eKrOCwlLDGVxCu{L~>J{v)<3-#|J()Cq55GO(ju+Kjmxb zGsdZ7HRN9Nc^1u_94FS5oqB%19vAnHw^Q(&?N}1)U8}G5@wy84kGVf*qnR2mRN)7D zbrjjzFl6Ubdb*ronVpu*rf{W){(rUK@14EtEW^~j|Ljb0pe31jS7exyGy*(i{@ppMUHwDTN13cI}K@5?^PLoy+=_V8W zozx7onBe1TGCVVyih4szXZGdhxG5gQgw)22k&Q*-mZRw;FLEOG zHLK=d_&R%-^*SeR+S}D**Vw&2*zjAjWZ8jZ6VeY%Sb1wg#3JFo3&I64ip#HvOp+MB zK5wY5d{iwAOcfH*Kmk z;fhkeRX)fHvGzXd>mym|xs59?JKqGpj2+4ET+|aT4Gmj9Myrp@>_fR^o^3JD3InLn z{*NShXK-*cjRb|@hL%;i{Y*#aeMB2UZFNHN;u3N!nqUv%F8&ATg3i0Y^e?XcZuSq~ zDtH|D(=KI$P-qw|^kaVr{*&OoQu9rb6+(j7Y|^4O&p3ieH@p-uEuu9mpo$UXMT!yr z)7s1msu$YnZTTFYR@G-L*~k>u(EKT!T%?En_@3wI$8)(R<&c)qWa#*F!ptz*qieZ~ zaQ;!TDHq>82%bM5r}WiJz3#(hF>B((mGp^_whBkGGw{#ok1qSKzI||I@cDxtDD-Of%rB=Ps{-rJm!lUoQp?qp?*?uqfbdi&;Z)M@I_LoYylWRAT&@8g$e7Y%CqrUndaCc6+CgwHW6Khdqncr--GW zKVj~|78KuMg>by<>_gE3(-8BqEap>dw$H`@tk?gaU!5mjYq!aiRrIwS6!^z3H_DD0 z3#`tK7`b_8v$}?eW!&-tV=_!p4D)k1O_?%Qc3#J259B({>};m=K3UaPJXP58Vlm;D z`6XknEKeAQ`HCB4ccTrH&Uu}tX>Ayeh(Mf9`lQCbvOD5YTBC|b$JeDU!h@_`?~O)G z#;F`s6B5j%LPl>hS!=K8q+2JkSa@>$5d^7jA9%a+k)izsY`rT!aR z`d}n^LQ}hKYiYW&9ohcG*DXI|_0Fs&ri?rd{)Al-4^FF-^56GmMvf?__RnurAYY`# zH);ZS#C!U(lm6hvd}KnFMpBh{RKT-mHvwDgf9#hDjpX#gKW7>cEb?ZgGveb%4X(*i zqjolVeEf;J*ljJ3e3zG#(U<22e8D@U_CdF2-ynb8%??B}jNo&};fB*?r*}u_OU>Mi zC~QKE~XLD6*`Gx~R=NK6prSrc!K8WUke7soSd8 zru5>j1b;^WrIB%|z8yECb?qEGzd@YkJZ{XAW; zNK+T#?`vYVEd)SKEq)igf;`(9#=Fe23<|Idc3tIV%@NUwbC-DUY1Uek^z*JuefQ}q zY-NU47ZqQW=6OD;)DI8;#7>dRS+G!N*LEe&lx^TthBqscX` zLiMqIj;NL{f17d42W%M&86QhRCM?icvh~tXLEmbY$-j{FZ-%6Xr%&I-Q|cXYCAbzQ zRGRYR%ZeViaN58!U6Z0dQNr^9o1DWt)VZ)|o)t*49W`c@wa7-6-ux?4%wx0F>dqQ( zRrVln@_=w;6$S4Vn{WQq3LA%dlb;oMNflu{T}~!~uX1BqSm6J5ygcM4#J!&~T5;8` zJl$`zrcJKy>ONN1*{DxFp)jilZ?`R!*PB*BQZ@D8m=gCv0E$85X-aaQDy&=v=3V0i zWuG#gMnQf7gSx>;zbIL7>}qV zwY*FS6z&r}JL0s1k_M_(* zJe~jfp|&gk`{;SzSQ0OLh4OP-k-zr-DNIzZl<8vGnEy6eCU%jkKXwVtJg!}VU1QqF zG9iKP2GfHJ-BWr#n(5k+w>t5$D?3_5;!(_Nlvg!GFpwVW)2PkqEn}dPKUqGL4+}~2 zDUJqqIUE8-QBGz}jh!!xFZVmQ5_ar7d-~b49YG^bSBYC^^L#=j8QqO#eAY~1K=E1Y zuhdHqJvusH>lVp5MZW6k))n<5V;O4+^2A`{b@ijQF2X8yu`wab4<+VeuGeRmK0FEt zX}4$36VE5mXYs5!1rVKz@bJYwo=BDw&+qwcZu>M3(t6BcdtoV!L zKRjdVF#WLz=Y82@uYKJ91MY3j9b#P%*}5Li`#q@(b&S(H=4gnVz5QlY8-e7IAXWi)C=%pB8od!=5f@F==w&?fCrjdGlc_xN*U|+ODcTbaMP%_t2;Ed4~C6 z+s)0UWM?942qVImt`08TgLT^9!v~sW%<_U=^`pL2eYXY%pH(G~lSk{7Hu$rDBNEhZ@Zd z&n+3FFP}3Oz!d#d*Q+y!AJ$rksjf%6P^gwu`uyVLtEKpS{gw0s;U&ORSS3$jaDan%r>iSt%a^4l|r27#DSz zDqS+b(j`5~d+NJwSn+8RT5YnwgFM?8=V5q#s5zHRa_qy6+7|s>ZT%&Fd^h;FcP{`}-qYS7 z!sRnLsYG^8G@*eos3LP)=*#-}pZn{-zgw3AzNvi|RRU#MscEd%i!!!QV_XWhU4~8u zyy$JFvJ6qkDslwl=XwY3+=_s7(znaeLqV$kUY>YA$qOm=y8`$IOl#BnOVU|=?pz+j zMZ%ppt|!w?jtjd^P!EKRXFcgu&4pbo-P_}LKiSK}HjTxQZ#+%u{8Cnu#1P&3?;x5! z*1fz&a`QgR&4?w({!!XFpovSZXb~$Co&DF+^L}~NkKCO1O0pPMTly;&FCdC8^hQwt zA4Sd#DYDK{KZH}dk~r7LIP4q2SFUb>yk0$O zpT&92Cb1f#oOw}U7!K8DB*r-?w zT)GaiJMDWH176eFl=A7-8y!35LR5#y*_-i2lH>}8G{hQMi2VZ$7Wl1Sy?+q{6{{+b zwTV4*)+F|9yx52$f!8o{c$q_r+zWcXF{ea=zv(86`r@A}$}Vr-F_>J+ z_j^Z=N8tM@E9RBb53}h!q3kB-FS~86%#~8@t>lU!RK;_h08BR90$-Z~~;Oove5)x$aFa#_hy7H2by{V{f=eNyO zKkwd(0L~_>#Ss%#ue!GYh7I<4rkAu4nMs5Ya>>lAuGE3x#1K!;_ zH25$)xWzM0?XK_sMAjFHjPU5OO#C)hLrD4)lo=S^!Gf*2(E6N0ET7^d_!KAfzkL|B z?=H(FcPnX(i|~KV7d2vT;!^n}{A~t}88HrGkfH?zO%0ga|itGEVz2NC3vP^r7+)e)_P5c#|>g&X|p$#bggP z$0@x^-^-PoW3qGmodCh!+o#5!^c~F?^yujD50SutB_=UmN;EPki(=-6u8lUVS>wa`LzUsU{ z#;CG_;p& zZ>;`@@_4n3JNKGYCgne+`3yuSr9W@O5Mp|2G+x54cF>Gd~CywBrm zsziIx{7E zJf%aLf?@f%e9JXw5a@Z!(c24_fbS{4<7PY7X8-M6o<{R}jtb1cwk5{L_lgN~(ya8y zn}Uhlxkr3brL9!$_nh<_nD;KYtIDSgcJLysvQd4&lw3u!HWv9c+u!bQinFFqn(D(l zE2evrMT9sbrsUs0WyzP0Dv!)vV3+FMiTE~?lxKlmZWDhiLF~i&N6=A+6faJO{3f*P z%ywFEIX$*TAs>N6n;9aawhE_VYmjiwZePQ5lE#5#IC5l;SU4}u@=B7rcODk|ibgM} z3(j;8;=|%RsrMY){t>3mEU7S2joMXkj)LSWzCxl{e6C2oAHiq&%gbrZ9j2!1fvNo0Fg5R>TfGw04F{SAjOSccSOrRxBWm@9SWHE}|^e?*jxBTwzM z3~C*ZkSmolr*Ylhp~#Wcnph9Y`hnDFdF7G0!wnSn6L&7(keNLWVR%U5dy05{K^D_I z*`bIA+V9XoUOPr!bHhvvGKd$a7$!=lBC%^M1X$P*hC|$cjA_T-6Y9^M4WPb9xgB6E z$)iI{QGN3XU?(*3F4gzJn>NaL=6jWEbBWv*z#C*Bqnb$X&*{8sW!H) zy9}l2k`aM$!a(x~r;jAlFo=*KTnHVf9aqv9YIr2cg$Q|G`fY$3Dm35pzyU+#^c_s> zl{s>e<4FgNn{{(fEexd2Gqm1(*bk*ZYTy!*Q~k-M_tA9ce2Gzk=h6^qU!Y?Eas4W4 zGMCCX%y#j?G#TEV;;M#)cB!wH7?)mY`&qWY&NB{_GLas58g>zNn$lZ=K0KT88ba+0 z@Mz?6I;l+R>ajf{VY`@cHJv~Oce^so1e1m6UA9{TynfW(fCF4`+?arn_1~xs@RSz< za9<-;EzYlKw3^xE%zKO$`oBY<_u8EkfUIycj(;6s2qOI2uT1AycDn+<`YCing#?)w z+Vrr$4Bie;UT0}d5M~qpWxPLY=gvCix<_AG0Ssbw z^&w$WbBL=>a*-~&YJx_wQAkTHC;^DzN#cg93QTgc-XDAiD=&w-bDV(RD>H5786^J8 z^1ik$UOfvbyY)PE&8;W!SD{Gt0ehkqTU4!psaAQ?Bca-d6By2j>Blv(O^i_y)Hmny zm!Kg@?fLd)x}IIjTpr8Uf+AOX_k`wWy=MsQc?s#+G)Sw@o}QZEsy^jIH$K-sv<924 z!(ertfc-I&ts+q7MemI+n(N-&qR8;z)Wv{tgOsV00DDB%?Fe97OQ)Jbopm=Iu?nN3U zCr!bG7dEx5$dyukv$Bkkt>0z1^C&9(d1{8M@z#}m6|X8#q>cYbfBwpysLv{r&>^Hz zsaif8I>7{LCR}yXRd`ywoM4mdbqN)yr1ttdJ*;UYA;;q76MoTUM6QO%nj1fDU`w(V zMtmTjUh$Nkc3vlCkj;aG?c(a&DW2zu#G?G+vbQ$pP5NNs_78c4dGxzgJq@)ysdV5?uLErVF{=D=X{jkmiPi6Xj%0R{tFd~q0~;fV)^R7?_v~pX6o!>2%!no!Uipbc9U4a`J@G0n*(Fi#$JUST znf^w71c5I2T^`9*)i2i(A|#ASm{2w5Q2D+~o^c|@2z1r3X{3D-pru2rFW27Z^l2NB zV31DMrPhDy=XVJnv9zP@Vl-H>EV|zT>sbaZJ&8M5*GP>>3>w4nJ5aHhg?-r{&9-k% z6D=&@-a)mH?u^BIzN%9{o(mg{HGn@tY$xeeiJvk|u6{UlBgK$x_OWT;i_nh}Qqj)t zgNt-W;~`Ae&Kq#x(2Vu_RUMffMK~i|elBbVAH_CEX$C*@(T1V~%w|2L_M*_Pmgm2U zMgaa7646VFT-npbpVmi#AAvWp7lM-kzf4fjJc@8Rqlho%zDlM+Up_SgTI^(V?oyWw zY-^Durj0pikdwNpSJCMh;|A-#eVviC_@M{)TPsOJkPC3*>pAP@+xv9hn=AQ3n41A! z5KZ>*h)l2eH4*CxS@;qj8J$@D*#s9AJhzf*xIzuyrj+K~lW8ttmjEKXH?KreEwzHh z!wk%C#RMpyRL@x2=RIR~-fy(qsV&Hj(5{NbDHMUvkRZGjlTY?73nW=#)8An=l8gHbw8ej#N8!H?3fY-dq1*k(n+aD)3!0*t z8YVE>qoies!f8*J&t76tQH1CNSE3Hwp8a#P)e_S7P-1T|`kwYXP7*>CBmn*5>Gdep zAxWFRD3YSPxAly2Ne)4T6J~(4e^xYhPs8#bAebc2$z24DP#VPvtmaVl#6^u^6MNh| z5gY%jBooXNiXpBR|NIbYLdEA_w>2ITi!7)Yd&e$-5CJ}5lSj%4zvIP~+;GHjMjhU< z+(_-7ms$$_V4<*V*wMKk?}aH)-yk&tIQ`>8?Tt&S^Wb*BuIy=C`3rb0??Fb{qAY>n zD5J})1yJ}4ths%4U+(PSHZPo$#1Aza5{h?zM81m^F@_&O$|ii!g?9g8dA@g)g;G65 zrq_Bs{G&zmxywf)QwZUZN);pXb1e<73~1;MUf@n*fqSxedtYv9*86%`RZV{rCts;~ zhl~C_NQX$rw$RJHWdH0nEFq)Kok#gO{s3$vNfwXYYRp^a!cMWYUBqZq&Y<2(w0(2l zhErbcm3dV@W|(@Cyj=rHhampV+WQ|bx+z2>9OA}Ag@FlULlVKT{DU(PVt&lC(chUj z9B(VI$rZ!itLlxnSMbh6u+yrn6w6)5L@% zpTZj?0jwvcA9Vhq5m7iWrY^w_p|V%*Gc1zLJ9mx=S=zrh*!m2_<@2-NB(A%Rn<9m$ zAlvru4Eq<^_Sh60RU9oswk3$87A!6}g%wTdjnJM|ecG)Fp{5HOaT9s`pra~!p8~EL z)=bhuw-AHyN@KK7lI*ynL=f%2hcNp{_bh@f+RgJ{uxPL0g8bDt0Nsh&M+eqb0VrPe zl-1Z6yRtSI)M)Pzf(2$8<&l9a#!zCZie?~3CKII~q+7pxrVytj>_|2|SYAoTF z!yxYoP5S8(`!o65-k$3Zl#+*f%S;SkV0L1^*KVJwJ`*kO4qa~j{^k*n4|+$IEecy+ zeP#NSLodE(Josn&)#8oQFWv7acK%rQ`s`i8@}`=@GDaUanzw7*gR?^y#7GyZCD=Ns zo-!Le?Xj7qeSv!ArE{lBjySyw{Jv6G)-R>peZ6vQ;}5Tt8z$}_^s$XR?8x_Fe){8{ z<4umeiQfw>ET&wGR&~g}FhMz^+s3?m9f?WTizg~(%+aYn;1;$1*zJ)IRrm0A{iMlePj@YNl_#qZ(QPrM;@+MwE%{QjqHWgA z(DySjDAhhe0kHhKsJF|^=7#Qksnj*ao;@kpX5Nr@VS+q)SwQV_#dB5iYDmom>J|i% zrLj(I`8D8G$y!72+LaoGEIo}&jMCPTSz~huM@Hh0XRNk?@q*0c4K?EB(?{4TFR-;$ zbQRq$w@1=wl%oh&DExfIrb||eWP|;$ffv*Y3F?>AQNN1F7KrWqmXx=2?Y)y4;gg5o zl9@5bYo=(XystiCx3eM~>$ZO%)-}0be2mpfU&l&goKf5`zo1kqNlc!+WTAFB{oOY* zc4_uTOVHZQUeob%N6yAQM-3#5P0w5n*tuH!+nySqpn_SgOMGNQk3aMn>q?9Fofh9G zHWeFwouh+FUPzf#uY_<8b1c23;ZSLexT0XZrR8NX)@ zcpGlDk{lDDcCmCnIlxG=uI5JMUcXYo^_zAZzPG!4E)j_)>`^eeIkIfR4-B)-RB>AR z%Qj=IUPD^BpRu$PR&;#8Ycv0m*U5_76Pkpc_V_^MSLBVxAk(*nswd;!u3%BkV*Nk8 zUS%GwS~Kr|r>CFzaTDp&`!v=s5v^Igs%()<-4yaJjJ@Tt+m&?chtX*nG`Kgy-*`0P z#liA@0bi6eUbZDQ#SK2-WFUS#)aTLHX9^qL$r-6!*xxO(L5J}uh>Fn|xBJQ=HH*eQ zw`nqvrxULBb`jQ+`MwLDG*_G>6Mniz+GFyq*6e(#rqr#;ZW>tw(6Y$){oe@@&BMy= zku<)X&8b&m(lE%ji)}dzHg0La>z7UeuRB*;EDxmN*gNz$hVK@~;gbbPV&11Tgk-ZK z?s`kT$#SUid2rILEwkwJ$?v^;0jj=MS4nH0r)g?GNM?Tes4==Yili_MSM~p*jG0ka zXp}vC;iEHogG2iL$GQ&k_K8F$^>;j$GbQ;Nn{N6||p2SG0Wb)&;)Nbn51MS@Yn>U`25BnF~gqDE2H z_hF(gIg@DqD*R`fUU+|iZiwR6DZO0h<6t`rf1PvrLB@q63CSn13Z}nTAp}1j_|QcO zdCZ{3^)iLW&4`*s@wcRBm-RG~`((Y>498lKH-HY_^{hyvlf%@x=fch18}OP_*b(=! z0ee;;B5TgQm6)Xyrfsm+rz`Wyr4tn_;-yzSZ6>hh?&giNW_1V}1O!B2mPDntBINfV z?mzSXI7sRjI`EDcII3wdq7GwIM|ys(CO z)0RUeAQz2^Rn_-PY4}F2*>1J})9JO_4{5m67e_}e&mz-+JitoRJnGL^3VTbom{XLd zET;G>!r5=wvNtWaVD>2u5!v?`uIF8^ML<%rf*b2Y_{l&Gl0rZs+8fMKy{2Ygz!{8A zri~YmC~VZEQU0*`v2Rq&RA4c(mfb{|#45pnVgLnRGdr}LO*aJ{v$w##PpONy>1UeG zpx}F=5XA>=){qA@46XOS|Dk^=vVApK#w*)BzBrM$(+*Od&UHWTP1on|H7Lr8@3u#j zt@SVL10bmD`D#%&cjx$CbX*kXSQmGu=%Vqob?0`(N#qX})5S4xa<2ehPUPb*cvoel z;kS{!Xn4PL{?O>5*5-Ijzw~zlqE)5mnS8xZqoGZY;<@!`S6`Xqk9H{Eou4%GfkX=l zw~bpuNIC{R{#bW_t^iV#F`9O1V=)oVj0`n$x0#wfY!`7At3*ES(kDgZ?S7-a`SpR1C)kx$r)@0xLgl=`kW%NR`F)Xui35j zBROQ4R_OdW9Yd~HC60phS;X76KjKXSfq|v5mkxM+$}V1rOJMJywM#RAAYOw&sI{cxJiaYEYZ;fSX5 zq(QQI)M*^JBg>+}5qOA8O&}rui}+lzL^$8IJO4ECT>p2+3wu7{V{)JWdqrxo{UG-? zJr=*R^M1#i-ni$zxqW3P9~4*E`-}yAu8bv@a{i3`FI0P*9Wt}@Uf(@OU!?<8pC*c# zMdI3=n)}o7Tgw;^5aApa{wili z&eiqn^y6;@X@r0HT++3pJi^@A4AMuSVY@_BI_+a!U$H{Hkg8Ei`jiSGhJOv0?q}c% zkbn)7R&ZgPNT|9BI|XHBnXTUAsN|}jMyfvegTzuW9zB^ElSO>d{^q%kdf17Ih7=T; z+gNA<(j?_rm!N!l@f{dr3vov<861UgWFC|F)V_E5of zKefJi$Kyqnw%Xv8!RvQ%g8Dn2?isU&a;fUH?ajIIGh6-&B<`}Yz{nN+?51b>;ZMu? zl2WIuKK#H`dO|(tiBI_D%u$MnxYu!E@RlestmF4#B$T)uml1Za$lJyCN-QX*2P%8q zAI?R|5Dc1Ofw%C3Yfw$Y&8!SVa&t6VhkuHGmPf&fh|2c6p8~xYRj}Wa zu`%rC)OF{+ZkU*VKG9S{FiVxPBgfW`i1nRHxqs5gaT4V?w>! ziWzyq=UdJU3!e*@(xf@gEXqGn>yQLaNuYY8keiJU_E_apmS>%}Y)VCY`wFl&wSajE z6I#c(6D2eHDv(VujZkRO%&t-)-%UGO=P`QJz+@9SzAbFCyXa_ctPDl@I3d8>(%2YM z6WhyNe6@$tQf-|VKua~wAhFj#BqEn*Q=}$1q$ZA_J4-k_DNEwkBCT@4M==V5V|fq2 zv>`3@@>rgX-Z)N2&9JvS9Y zXJAgKBnqds+4|UOO*b6ox?S?XR6oiiv0*FLYUM1v8+n2q>|Yp$^n1R-Ckvq{?^x?| zAu=g!p3T*msp*+1JA>Q|Wquz0n*X^PP{|@CX!b9*k?@ohL-!9jl%`x3*t4LtgdAEP z7CrarP;RU?UDWMzhO@sA(drpX68F6(1a+W-@@l_pc2^KF@g!n;b}P|k*v4dLC{(NuGTDf z4UL0GDYS-d8g@&Ii0w?Bx6gB)UhjTiH7oxC&DD*R&a7B_Z!jgrz%Qn-?@5AQDkvAz zFH^+-qQ7yrpzbAYU!_f-?t0QKXOa$UVCmCl^5G5cgat$|s>FoG29pqBr;DH}6sG|T zx?Nk7ScE3B2<2GKX$U^_1bI~e&VYgw?dR<}6Oo-SwSee#_5|mX2cqUUgT#LXJA)__ zT@f3TK@t|~FUMR<8VN#q&RQ^T$N<9rumc<8DNhD!k;6OUuK$kFCGlqJO6K$V>aJ|E z_a)UGsR_^iR0-2|wY;cZ8taePbOU%kfeAu#l*-6^2Zgz)!R!t?NK}J}JAgjUdJWP{ zTr(#JGB)I|)i%Awf>aSgc~)JA_#U&n8cf@sSPmOHnwO9$;>CPZ1i!>h5!)Jy?>j5G zlAc3;a7@w^*^(sk@u1YVN*mpaJJvZa4MOn}UUda9|8ewSf*9uQW{WS*l9lWBy3^vf z0E&WmH>dG?>pWN_8KrXF^MMqj@n3gT*DIzt|Lt1@T65DGJr5N9!L%Ub=qu&;%b07t zn{L5Ke))AM$^`|bz^#OQi0sVf;gKS5gD{zMnqCLznrj77)}{WYynQ2q6Wk-X*t5}v zDNQ%j$syY5m1IGFKizOPaY7I)JNip;rr+rErv@hesI8{q_Gld_Ji!H^^_?B$SdE&s z5Xn6^qpP5Vab&!a;Wy{C|NST37U~^}4!Q(b z+#%9)GU~eNNMt$@y$EZ3V?$l(dl(6l;O)2-pWp$vw^wOozJkp_a1ySlwGv-d?L>xAHI1^u1wztZVYXqFfK zcrtTWr16L8FoodOg*UBm_w=fDD2~1!LC%yVUsDLZ`h=2;^?c3*H~=<$l@0|WL^ zv>>q#Sqfrpm$e35AD65GbEy*% zuD26K0{kc!zFW<(vHQTX?{$<`LjbRV)s*Y8ot%Ttd9O#R!nh<);wi!8*&sT9GqArh z{}3u0IOrC@#iw4n(SP(G_(rrVD4Mw5lB%3#Bd-3R)IuzXH#2Yg0BFeI_| zmegq!ajur`3g#rEU2KS7n0gSjo+h10!hxmde&pS}F$#PsuaX#gMOSU2MiK0FRNdMC zWz83lNS=`$!|2*qNpPn7kv2&73xFztD~qs0cX%^mGyz~DPDK(R1NYDhD%y4tG>zC0 zJb}z6kMM|;d({DP z6$eJ|hjz0Bg0+oJYWF` zBosHgKbieFRn*89(OUQZr%YK*nWrP+B5__Y5 zG!xJ2&wfp$u7oOajEk<`x;0F$r{?m~a~fxuOMy$u6M^E4+e9seo*{%5-WC?i*D`tL zP(}ybh$TTA3wR_*xt(kJa;EakB%2%^GnG0YFm-;uMn=iZUmJDtKC#MZ8L%EoBoqPe zw}l@l4(6YQU?oZ z>9()DGt3HRti|btFz^VrkL_GZiX9g5 zo-)u*v&V57kO22wMYzj{8_m02+LH4Xo&JqU`eyRhrtC1Pu*xaO+noPkJCD-tHWA&p zuZNhcUmJT`Z;(eF)m`oQ^{GuFVeZ>U!u{K@qZJs9go&EuiYMjNPEoP9nDDZORgI6J z*~D;oR6-&o3rn=g#8D7KqMZ=~tOPAVp$0d4Nwggv4%B7>wRlAC9fSA9`!@n&5OAjJ z_?1J;hbrHF%&wTYgHSfoquFodk3=Knlvpf$qQ#|yu0KKc(ULBle-=nXU1(7pXG?@I zZAzqSTNo~8q!sbd^fE-#%{byXo=Bj7Yx&wE^2tA)2w(mg81IIakq#|;a7v?ZLP?~v z+>`4`Wk(7^>Z8pI7Z8^W+2Q%gM1E-*U(KwC4!@;!(EEp#i?57vh;V+?=$^)&;m=o$ z=`Rw06>0F}TQw2?!19I<@9)osCe|jkr|lv?qd_R((4+*&v`%17WZy?9LWbV^-l|M( z!p9?c=(2DTA5PuqxPJ(=FuwxcebiErPAua)Qf|nUfmP68olkkQFDgua@E_DuE$9#G zxz|zgPR0bk#^WZH_?o0}5dj}k#8HM53D#z>KaI!xO+YVw9g8{BNPwjQu=d{GH8AEH ze&XjnONs`(z!lSQoY_~Va)I3eqO;>X;Q zpKMX*1sXiRYxU{2iH>22@IRfEi!Kj+HF^~=1KcjRKz9iA+0Er^{~@0MOa(`Fuj~4t ze5prF_gLUO$C@!=837}=K8d(XWRY8L&;HPkdKoV4`0Af>kspugmQrRR!Z2ag=VKMC z7?I%0_h~=ySO+FJBTg?*2?ZF7P7N5Rn=whM7up+cgS{+7l_%9f9;rHLFoD@_PYvP? zL~^cSWLbMUHWI~Qj%5`eZu9CSTh=fvmRCpD~jZ<~*ye-K(1R2p&j3Ui!`6kt2#e!7ua^>s5&SOtB%4EjdQFMd`p{{et7F?h>s zyBuj>)6$8n{zb*A*fQJQjbpb=@Lj#5ynt~pnY^dwGi`8dWUrxk$Wukny1 z|82Jd?m&Q)(9OQ5?whYU2C(S40l&0kj{k>ZX3s5aC+8O?0U znw&=OZ~C6C&kRs&T*1R42$41S_tLo9nYj6kq zTDc>yI~R(~$B^5iF^q#fMUFRXZxCa)#sd9>)n1kH_srKo*~i#a9XP{x-|%nEtd3IW z2ou$hC6PK-DC2sP?BTv#O&Oth%^zFZKl4g8g%gt7QlExJ*C!6$L{y+460!#`c+a(RONhio z7uTP2`y`pNSKd`L?l=IBcw&?V zWeR+-W}dCV_3$LR)hHILtM9ENBlEL(f{5%|vbK9Xl*k>CV@)PKx$76B&t3OBhpbQY z6Ke(!f$3dNV3Y^U2e^jZ0|HQVA*eL{xiJ@>n8vjclLG?6r)M2da5zO<@5TXMG>Fc4gWOC-Cz=$4l#w9!p zsj>QOBK`yc1%iSvf}n^^D|0}d&m8yoR_|Vb4h7f#`Ttc+QPsC?SC(~mawANKN0JGv z0hnoywWE)G=xMd8`{AhUFA{9mux{-U>OQ1uAZTQ6_2En_wO_ZTl!G%Z%%hSug$^+2yuLyg_DRpQ9!b$oCVz9kyTPf5Dr4?P7Ykf&I28)w@ z{Iad|C>Do;)nvNonk%ZpV%}=K&iDPCg8omnfI{7Rxf!PtrtZ>sYvZ7F%6fF z%m;~Su~SgS<$jaH+sgkN`D~xj3haOZz1i?$jDrP?vhS67xj{c=#&BU5mO=csytw12 zjGdzVA{WgbPpy;wn2DCEhP~iasz*QP`K&}q1 z-JSk-|I|PvqYYNH)uUg2+|7|?hYe(lLP!)H`d(e!<7498M!spuC9tMX;ih$+UzdN^ z+MBQF`+KU5r~a!f6|g=WMHh{OF&#riz9P6OR^x7#D*zGzgCQoKZHIrZNe%441Ro}V zN-_w=urMA?Zz47xf$bEB;~Ya%>M4%hXCUkGdNa8Z(g30IW`1|maDCf*a(-F~nh3oK zj*Ul~Ke|eiGl6GC@X>?aRZJpC)DelplETsepsCf7R7oH%t{<{x2VjtJ2dBZ*|2d%t z#3Vb+2n0SbaBDn9IwXdRqs_xMjSBeS1)~Q9OZuxXd^cZ;>R@eZ_m5xPKpK;Y-9ON_ z4|^2wrSfC5=8gYFK%<>lG&?llrCGmXQ5Z_V^=)#bRRdF_&}B>4gqw3b9&y5+>Bw0S zVMD;ZTdtYCQONudd!(Oy_m_9}9m}t+$B4%s0nW9`dCa=dSyWl_RybjG#2v_Jhu7Q2 znua6AZRAUvM$bkn0=PfEzi&<%`8+nGC;yNDS5PrY{^|Vu9G~vg}}E|z=vc= z(jKj;&XGUl$U@T322UoyiyHcgT6{u5WWJnxns?GU61a-@e4kY|08jWd-gz5EYZ5f< z_`O9NW9i~UsK7NR{1|9Z`N6w=fWGZ+Q>bGS04{rfZU=>vtwUK!AnK7&QdsV55`~Mt zw57PGEt;Dj2kYy}r?fKidCt_yAfBJtWOLTX=x$NL0z5xwuL1e;7c@ z&Z7?GzMu^j2M<930N$nX%}q{y4cwspxeJ)aBUtkH>#FSc4SXdEk@vuP1$AGAXJtl-OV(aXn+cB#`kxw~HAv%6&lml ze3r6ThfNswS$=k#}gp9VN{F7LtB)Q)qx#8{7j|QnT-Est^mTvVxv*kHr4zcBy z3F&>=cwf*)i-RvWT0$`23kIh4NP3OHhD0PhzR0Y0^WALk!E_SDt!2=p`FNyJCzw{g z447#mt5$%BzP#$+t=3BSP|OpVITdN@d*eg}h)K#}OpuuJ ze_%V)WC{I{B}WY0B(G!(wUfOmIzMzxsCp8PAzOB|zLwZgd|%kzROAMXp@f&; zwc+r@$~`@8F7zMa+Tn?ZNKCJTmNHURO53GEpXVYn9zYgf*z#8FQbbOWhFJM4bT7%y zfUYmxtQhyy52ZIsS<=yX*3hi#d2O99j z0+yf-i+;O~)~m1!XTfH#YN`Jl*vpd*=wda~ztIn5jeL>>#l*e`U{>{|l!HOcy1_fl19e*aj_BEZI|HDx+u-eg@RHF42I$3`D*gKj@;WZk?-G|HV= zW2`tdY``vN-lY{x+lB4Z1eS5zu`G*CZQvxZtwCJe79JlmM|wXD(CF)MVX2gv;>P>* z7T6Uyc#=b{@w(Cz@~Q>f_+2S<_+S4YVWzAQ&VAz z4F+)ZL)yQ)$t|Fb4tl2_HE~P8yx~?f{W}T?PbB>5G7lY4wMnui+GjPytmk9b)7ib> zMv_)GynU3i47!~h|9Zb;&nN#hbjO3(+x=00`)RdaHlHjcxbhlO{-K8n-Bl^<`Y1{~ z8l;d=VdBbICBJbX;GzC(#i5P6x>|(Zdg_(%tnfJR6=4557{{Bo0oX!FHfR$;PoIoc z!dX!uG`(KxmH=oO&j~dD`quorRLbG+#TQ#%rOm`}HUfKTX9rZF=J+@%N@>4-yov;4JE;r?O3ksxpvy3>BdOqfuny%o-fEFL6D5Akh?^H4slP_#@ts1^00DIIjV--ko>uR6Z&Ui7qZ=VSAwLwI%cqMW4^wMWc6v;ojSY99hlDrQv zeNU^*t{!YXm+AQ6mMoKAeS}1)1;;AP#X!Txj4zLp2+di(X8O9r2d_xX(4C9s+tq_h zrIeN?M@)M!w#xvwSg^o<)Ah;Ig*m}@@n>B8=Zvb_uW60=WGfu^qSi4-jY5m4{#<63 zJ>t{HlDs~C{`e)gzfua?C(zvLcNc)5hcPuLf!Q$h~8b?w#nTs~s& zLHYC`&7U@zi|G~>OWTntT@r2Y&^5`N5UjrC$I2SmUpP8*Z+GntgXt+`kD%U=h;z}S zH20{uiduxMP9Bsv&}YHL@F}kSi{Frowq}GbOi(#P_FV)n!|cC&wRf(%WJ8OyY}gh5 zO;av>K;iq8;Zcw%1hJGu1geiKBthx%lh8sWORI&qp-KQA0xRS}`^aQHSJHWYY-}fI0ZxSt)t*KCyB(h4SWt3SF4GD!vHlOM(Ezyvc z5tTh6vS}D4Dj6wJW+5|s{m=XP(Chm<{vF5nsJE}4&-1zO>pHLVJg@7y9}M~q*bQuI z9Uk$VhutS$Ty#S1pUqVZ-09v4FY^d1`s1GUDm78dDeVe!VG;NCBjYL2H5$H4?b8J; zR#@>Ft+EfLvt!)PJsn8R9N*IcPfTX>#$XAIO%!J;4P7vDWRZ<}urlww%?yE&3!JwL z=O3y#a~=QwF{GL38KjX>B3@m)lNnFESeADn9L|i`Ejm22an*_u2?8=7fDPCstLye;Tq_AX!DI`(`Ne6zw9tH{o1m zlGs75RyXA@D(M>*317NzW8#AaDBX)8r!$E2`^tTfe+~txTqgPX0o|Bsrg~SiyJWG+ zKL9BC<@0+9GZeiZy?(y`SS=oE!Z&0J|KJ_B5_6APT@wX~;-o`nPBDg$+F60#Y_SD(dX3{bjW0~3KDx`(BT;T{YuhCxX*qe_aXKhk0Z99Lh z?X!dF>mRI^#RRTa58jKLgN8VPDr_tgOG-T}b)^Hh>K+U@cQtgK$4@We6f!1birc~w zmz9f$LLc}M%-4QVzD`zlM%Xp-fo&Q<-aJ_ zOk@&jf@8J{f^3-M>9PvUUES8e%B^#6+xhWJpAmiv$l25a%%NOx~i`sPDW2t4Vs@jLRPY7Be&i4tdzE zMK&k(shh1k?wPl6hovUq^jYtWyeB3jd)BT!aq)h4Md*W-Pq5jd*OAJfdmpPk9eJKL zujTn4{_?38FCto*Im^)ci1Z>sN|PlC^D8GiO*~({S_{#s>vR0evtQ;+^d9OSh|3lx zTZB5r`|6YZL1%<4S@=2D5zfHzJ3=ZAIK^Bwpa4}0aJo*Vuc1Kh`Qu`i@a2hhVRa>! z79*&&o}PIB@Uvn+J=vtou(A>Ku`<7Ub_&(;Yx2@-0U!;tAJgw{&pp=G>_b+0=i~Dz z7G;x)MaaVlwCbYoxb56ezI|g;Z{9t1fkhIfL^SalwKZ(?8 zqby&AX~7{F3vT{^n@r2<$LJ@h#>MGzqbKQIh2_f0)aBx*>_=UW3s({09(*YJjqK~e zmF}J8ukTymJLB~SC|);bf5^$Y+okgHA?5Y_A*qQfsEghRnH0SPmw`4~03ZGch4R{*9pinldMwASU>%KrAX${ZsD6y$L^&M0-kT900$phyEbVv z)&?N1flO~7*?re-rz_G~rzt0r1S$!YXP&1W>xHE@1BVEW03&w2Wn*zAMr!!LpOIpf zOJb*$+s&6#j(E~0QVrpHI^$1%UMGBvx|h!{jBw%USy!fG9cc()A`jeL-UGY_3O>Gz zj%)C;cL2Gz&P~xZCHEEB^fi!ZB*qj|aax+E^HbsmR&Xd<$Gq?{w*pefg6NY3j zdx*M52nL}Zm@VfeT#*Snc%_B&kxx%FRCWr%`l1x?y!RY&ki%P@-L5_j^%5NQGAqT7 zx%AG!)3Ha8|M*vz1w!L!Zo+|g4mIrm65?*^_zWe)&3=EgAS06gg19?lj;$ddK0+-~ z3{Jb#N3w(Trckyflj2R76y(40RvABX7tt*A{4Ft8mw{3eNNpIqonyD_$jeth|E60}23;mA;X4cz~W#CJ=>d`w=flyu09u z#(j_5^r()LH28$m>7Pz?qOVnv=bwK7QyVO@k0&qK5`*G-hevCVk?&snXMoH=%Sr$j z30WG-v94G|@f6(Q()AINnxSshZc1`m{N3k|%)(7RGBG(K!eTu==-Xk6MI@C0cunvv z*8=f_zZ&izJOb~I59vlK(bI!NG6b!f(CD+9u^WLv%H?R~w^^OrB>k$rNr=s$f|~)X zPKTPh>HJP^(#D<(;OD_6B5&ZqD`BM}?)*6$f(Ux6o?oG<^_nNU`!}6>3z;mRb>EO; z04Mdmj)2kJFfn8?1jIc;yMb2ZF6bJ~A-)Maii&%UEPs5&24tS%!kN@}7E;N|=X*#c zkG+b^#Yf47^K|%v8}@u2e2M5Bhn!&0rK&!-ORWDBY!47@Jp~OFM0SU7L(&xsSBf-5 zqxojJ1D{9M)4mA}d+qBa@=OH15qW(6EL6%Bf;|X1EXsA8b)u~CM&u(Wm2T}bgdnxu zY3F63oTRPp-@A7~hc0M5VGk|T=o|vPEQ%rUBIbIS2%@>+D-e6DH+YD5t^AD@*9>6g zH6O3$zwAve6Txv&{ZPaoYp{?~3yOO4e-p!pSEv^z3z?4P{8L=o@|*H#KuUGy_l_Kl~VX?=YrSuWDyH>qsh)ldHP3RM;Dd17Yla_-?YD#B% z$+1vvM8Vd6Yxl|BW3dp8#|I_v%9d+tfuq@TZ_H5=hsHs0`4qkX?F}7@@a&M~q{62W zEhy4|==fMnTrP-X{@}_?k~O%6XaDJ3;S7~!Hw)v?aQ{Dx@K!0o;?t9$$29S{o!^u_ znkIlhD{rwlDqbukaM?~D13I!1fPxJN_-S~X8@>aZ*(e3M`mtis{*KohsbCjWWK!fv zW)QsN2#zIH2KP~0`6~b0S2PBU12&vqfB6&LWzue#1cA5JCqdvAt7Ddz{;F)t60F40 ze69Qozuyzx)+`vVkm@!W0*T~!T#~-Kc%Due3pIQL(L#7PK|y>;t!@)>pG2rY-exffKIWTZVG3 zFv0mrZQf6-)t)EUzTLY5a~9h9z+8}l>d~?du%m*lq3TmQn&ZXF)%=`1cRiV?1Lo^7 z!P-E-iH5R#2h|E)FGIwz-g?K@c-HyxFOz@E&%Mu)jR^tEx5M?vko2;v$Q#1HvaWg0 z=RGx?A}E2qL3}{`D$z-n`S!B?$=(7SC;^SZJOCQ*-4OfX{B~{_XhZXH`@!b3*$wu04zyZwoAE=NM*LF?9Dw}oh&{t)HU3ghr^n@{0iq=`xN|{v6WNA8P4jO%l3|IL`h?%Sw<-}f+dh*GeFd;XIo@|iSvMf~0KZN|jmQovqkD|r5Fi&eY*$EqZmeXbmsiSAgjLq_Lh z+h<=Zh44jrN5F(ckXxt#>h@YzP|`aDmh|d=V{CHp+?OK9SRmJAB%bvkj~lnoR_C!^Wp@mxtVCojJRUccAbv zE(ZDJ|4|3(9Nfy_CsrRICUey^y(gfDe_92|V4^Cooek)x1oKFQ()C&|rch3{ht+wu zSTtHTbA?O99t=((V*aG%qDHZ70faHqCtQGe>#OdipB~RBeO0JL`}_+T!qfEf$WHKx zPuhuB%$@Ki_Ee%;KG_}J$M6cxeOi5JOm{G&m#zs^?Yj=LAY{F}GC+On$w4>rdGdrt z!>uBU(Z+gb0Ba5>L6{$92LTUe1Rgr1C&R-*=9+Q0DoD@9Br)^zn45o`B`OBR{B*`z zL89V-Sj)|?I9@Qrr|}Gxe!Mj`uEnXLGQ4BZZR)>(T(&rhi&D2%f(diw|)ZFWQwmT<=IoD~L4uVvNhT z3g>$$3wIl>b^LvmF!d#rekB0bXs~-0`BqiuP^Vs{O=L4DLLQSMD0H^|sI%V6K{@?n zpqVvWZmS8DvV)kv)aLgFKodu42hI-0I6VV7zLUMaId}=>fNwokeDUg!&5XgLulXZy z5*{wB4@RG;PWwB-+)TNr0CREVY0q>IiVdq;EVMx|_g8m>Oz^7ICuhD8A7kK6JHp{* zg&t*j;9=-c@x@(8%lfVs2zhN5#Y@7GI)H+}(O1lJ)?hRT{r!E+03f2c69O6)j0%oK zNy54=-yvgw?w;|uKD7I1zU4N=67GDtrp9Zm+2Z|ocEX9|ff0GyRkg8c@tT3gWSB&{ zzK9@yO~FgZDGb04>0;+lQ#01#UC(N72#{aoh`WujJ@5B_qdP@LlsK`d zF5tL30;a0+O7EFbR!EYlaOtEZQXgMK=RqEmljk--)x+d*F_Is^@jl^JL|PY}Ga=7* zguscN8}?ouo0P*vMZIKjQ9M>CR;xw^E<%HBqQ8OWy0Z-&}xG(X-ZZoer=;i*fnDiJlgXE-HbKKuNf97tg_~dKfElN`0aH)1C zN$2>gRc==6k&~oZ5L`SSna`M!8F&gmAczP@wMVH@}ZR3k$9{X4WUhc0}kF3vmQA_5}*@WkMmsco=@VJQ#pNcL5kGglWn~z?@C_>JmjOq4UoxLtqd8_x(+ES!pV4M_F;JZCYHX%6fi_7J%0gp|rK#PuJH=BDgIAwkOv^{9CdL5sM? zVvo2JtlFzHg#RfkB0(d8r(`?tfQ=Oa5j}Km0XN zjr%-RNQ^C9b#B-)>+Bv`5z|UYA?3Bgp9piE4cZM@z~$%)HupOV9iU579pT@Uj%N@E zK_~6WVGCAp-&Ac_0X2gZH6EiRLe|PSm>WHTciaa$@guZu_4#Yo94;*3PRuf}obYP^ zEFGmcn3|SDc?#uAUT}iz*%Ml~KRcd2+oAk1a_@H{fCsw#l&bk;X3?$TD0Wny>1@+G?Rq$WmRNL)Nb12sG= z`komVCy~;kwBEeVl+$^A`?W2=kqI?FV;)?4B$tQl-7XC{bPjc^wPz`>=kFesVs)oV z(7tzlK)f-aUniCQin9|Mc*2_k*6tM_v-$5fq820$N>P zIef@A@1efwQc(6qHC4jE^TqgW;P$#W5ms$~!Kwd#p&4BnvBbR4@zP#RhY8+|PP&X6 zJRIq>m^1`7xX`7_oDvCT5{OpqY1$5eA>xdIsF23)%6D4`WswZtvEUls0|T3ikgge< z)n?GT$a3`#Ai-nicc>zDoTf9_)I?kdICDgoU0fIOf=$@4c}D5K(N3-;7JG`n68R0{ zZlO8_#fngO6j{(qh+4PztW=OFQ_y5y;E=_^AZS4UYAqNg&;ek}5n}GhhRN_A1A2`t zk$*RX_jt?np~Ee7m&b55ENuKUq?pGsTlR2k5Itmy=P|{`-u$Z2n9~as+=wh6&+6#FQUs*t}y#)zO0{BjZvW;^URPC1{b>YMQFsY+vY@nkVlz)F#sum;0;o)2fP`B0C2I`r40niG8j6 zojtH?*J!;#E&lM%a>*v5DM*eGzC;q36@=CYrHWc%XWv32g)~X1ef$EM%qE)Z zbFGMM=RhW@Pg()&vSHY3JD<38CH|XIXa^60tzT%POqarPB6_<0#KWyP+lcWs&@w|< zK*Y7F_v;16ke;S&8>69WwGnquCwu$hpzd>>0R!10a-LsOb|q+e()9I9{?0*>Dv4WY}+^7@b$FM0u*_rPDJYV1QqwkCf7ASip*{aE_roHMePRnoB z1Ng7XP!Oc66unB}a=00>eFD`39Zv7wt0b3LkN)ZGceIy!J+^x2>_NWfeW-+Be(A9| z0c{Ztx|GNE_gSw#DtF*C0g91k`&Zz&T?Us$&aUq&I)T-UEEzf{gUo`;cwUqBe8Jw6 z>tKVuc8GUl#^WOJv>^j~SCVfarKE)}Om(p21wKqB4q>3X8W@u4-b@wl#8r+mWZk^2 zYWA0XyyLn$`_Ek`v*y0T*iul!*%j66g)54kE~0Pl$5bdCJBh>fMs;;$ky^H?dp2z0z2 z&r#11=8%PQRBdpH>~#6F&iILuty))?2^RY%5$Lg^cMJrEs zhxw>)e*M+6`>xZF%|4ah?#MC@j)UY&wAjfluAM)2yjsL3V%eB=+xhjQuf|Xkqa!B^ z9lV-_dCXwP)6+r=F5!_hLKmAiG1XI>;2n1H{X;ozpJ5#nN*zX(2KWdBUb+Y_Ts0tA zCsP&0bL}-~&0^$<$*{7C^jua-HXOM;KdVP&D?OiJxC~h$u2uKtey3H8CvSG8M`Q7^ zw>we>KIPYO$UU!e9y_;q?JN=Dkb!WKswzPQrIU$gHHW&XvZZlbW5A0CeeS2`Hr8D@ zWm#0Om7K3KHu`XA)O>nb^A~~qx-WO@$PJJ8Unyn%NkaY`loXOK_?4vgQZl4a=Q#Fx zp*@4s;K6AB!@I{1wyD)P@;!|35E1mcbn#f*V6I{B5bNsf{)}WjyqK>f{V$s}%`TH3jecdYz>&;ru7GC%8lvE=#a_9BSoEA zkH2r-$k=y-S4DB%O<=VCgOLwS;19}wJme)n6q+Odl!fqcXSw}6j8yOT$Vye}TAA$> zv&LU)!D&{iZ99`T3e5VK9|Ao%L9xt9iTm`Zg<_@CVh2uMQvyLj2L|(T7R(l6r~(pc z;5B9@mhyFhcu+JGxR`m@gkf$}d67<0XNfKLSa}5BRRZKfOK|$|GuEPpzIhUMj~YYO zq(p!{V~16&r?OLqCjbEiZ&=L3SGSo_;gVXDfl zEp1Bm8==fkRpjRqIjh4kgAL2v$X=BJn?2^*KTuZGZ9btR8y`)V8!(x+$}dlzJo!N; zx^SF3x$iu0t*xlMq*3(E0v_{3H;^#|1E!8R>=)G+K?u@E45${7M)H7E7N5@8sLAV? zu073yrguqfOL|+`HV$4{n$!p1&IAZ1_!=j8Vf1r21^wc9M%8L*Mp<&=i}sM<+`;jrwH4G}uu zv%Y1`8!^FL5jk~(F_gTFJ_bNr3C)eg5-xAiVc7v9MX&~XVd1w0?kfR7a91|dr&oqM z0awV(8i=!&%IS|0EU6tCE|Js9{RXDt`nbImR#C z6+Ro(J)~@o+S_@JWE?KVuFH=;xIz0V>0H1Lv19O9;`~WQb4TTE`-nN=hqPqz1Q*pu z=Bm8gv&ac4lrZzro9|hqxf}iSC|-P03V(jBIIAq(;yC?B`)^L?V)ic=PV`CK7?6b% z9?}Vh};B_ZeM#Jmka$ zFY0jX&lec>>!k18w+j8a{LdfHF*Uv6MxYD6r7ymNxGC4aUS(fT$vaG`@&^o56!%xI zq^PnY`siKT8o(?)egTtp^YO3t(jebxecXPNB$JPic!)b^X0DzAGM9ArznoL(7zKAP z!+Y!s57r%S$I&*fjcMr5?2G@eU9D}5!H+{lp)=rz;@Nk&yrk6?y}s#Ek@jL_YG-Ks zh#A^}Y4~c4G%QRHNg+e=m3Z1!|8ugES4hhh6ELP6_x_?@nWrnr@hIYQ$?+16{>m5% zRn|k18lTw2UVOP%icH0Jr*(eAGQwwNAuWp!A2IP|xV55K&oR=OB4 zyFknG?`Mo%>)w;2;W}7ShZRECXomvhg=wSvs5qIxpcF|X2WHJR^joeGQ<^n89;6aO z8sPWghhi?M8nVY+xD-AGrt1ToutVKuzOwTB8MEp3a6fQv9LQY<=3z+7d5UBr6dTDb zB43yv5RYbg{^uaUuGfy?bJ~60Fr9nqQqMYL%>c?#7S|wNC{b()Oo^y?YaI9~<(J`H zrO5>K=`n`8=!4>(`=Y`8@FjAw;20o8lSF?bZN92Y3BZ?YBQT7`o|7tqgBJEN8#qqO zVVS{%AOxjj*7<9zuo$oYZ`^l8klO}Q{CVUUzL+^$wuDLlowj9a`0I#itL+oudaHex z{2b^%*}C{>CgSq^{=&zB5n%D_yZOT1Nsz!Cn1Ln`rG1_0UHArxZ3;m793HUxL`W{h z`^09%1Q26F+7zhh#9OBdnJ)nB_y8;uIxrPOhXl#MjPdjZ8x|d{Ds2z~GJ{AaK&Y80 zUIb_ez|n1-9C2bQL&1MWT6H{Kw=M(6mUxg1B!h1BIy3Ei$?00WxV{m5xvhvdt6PUdxjGi^Jz3l%L1t0>=_4(|XMk%E z%Y2i%0pM1-HX^U(V@hu($pN?v_>)Q6#wKv`*F{0f!n0=0_Z*fW3L~G#+Inw~zG(yD zGqYyFVccTPj=>it!va0@XwrYsD!liAWf`9ogvzb>%yYP*AhNC9)HOBy{k+*_~&A-2%4yE zp}OwBC^bxt2B(iX794*Z|Ab5jFnuP~Z_a5AMvC9uRw;kATg6|I_58SJ4aC<+V5>*y zZ>Y9muS1?!SGHb$dfqtCEON+;J}GMz!Az3tIIgQiJlgWaQ(JYF-{ z&_pF`6ce;+3)B*QAiX7-`^ZNJnn~bh!_dhst{F3ja%;}X%dfs9?F>P>~ zaODo?g1W0N0un4(->(t3nuT;dqW^7w=Xy-Y!WcT7Bq70o5GDo`#y(iN)hbA&e~RSu zyd=50?5aQQ*=mQ`wmw(is*daxUrE|3V0=$U z-&8Xv+fjb}@E0_X2KKgo5Gs92Mrb=rK;!7~xhPDx7oIR~J39RJE=FrM3Z?hvom`CG zPbu@vy_^IIcDtVZ!hzJ^eJ>NI+jqQbh@O}Q0BvG3+=2}xGnr%)F`|b)8*Ez8GHD3Q zl>Xl~uYI!m{JFnw4EPHCJmuaQg)rI{4A9O0v=UP=+J~2UCMbE_N(w6u=GGV)^5o04 z7}J8JQf5|Y{mq~S>qeUW74L#Yu8qy)8`G&7;}42?DUgVaou6GkXwX@(c-Bpo0Y6Y( zB|Hanm!6uVr_iGlkdEk6`Zz7&!uyCIGoJ^2R_=F(WfZohd(v-;uI6n z>tXwgx6o3o^v>PrZzD^h({KSi!QoAZm?bmBBx*4eT(0=#yY!+QR$|`_Ckl=Qo<90DOVsgF`B0$Ezar?4Rbhg?0v-YQ^(2V8l97~?_$_- zj_06y;AigFWACd+^0vBv9>^Lq_4)lq`kc~t6&Y^N6SohEO(~T9np<|`w8gQgBLcko zUjyr_9Pr`DbFw}9&ZdZ`N%@~2ALcEe`n6`D+hhCgz$}rD`lf*qWfPx@xW*=v zvbP#5%U4j8nzsZy->nxRKQ)v0i3R68aP2nRszdTr`*Jj@0e-2D@{i>nOx4T-m9{HZ zsJSzGu$!k|BI{^Cf++u#bA$OsD%yILw;VVbDlhAdW1lpAY2c~YA4W;n?h-rI*fZau zEoR+;C#V+`W6I5IK0n{J46XezaM?)g1e)%5`>DTrAt(5N^{|TGp89=ZlHpx9_ZUyh;Jq`>E$(HA89 zVSuwPgj%wF)E0KMr(U(GH$Fmi`r6n9x@x8$6j+^~JSr%Q+oO@8c6O;fzEJfUvNA7L zByi(*{2DkhJm0H9fv<&&Dwx*dc6cyov|eSexg@_yr|fUfR+VGwX0H|4azuV~uF7_H zPOes9V=S!LZuQ5>%cC;OAi}VeK3EiQ7hC4)^UFW3walsB7b_|`%d+}0S<%i_&xt5^ zwhpj)r@+E?D5398N#{X)8j$-9FUOQK>srXuM|hqgyYZVmuiq`ZkHXaGMTe}_Bd;1o z3Jlze6wE#*O?_`!xh7)l)qBG*D#dG&Rz&1;x7`NCeYTIMFSm`o`SD2m?#E(YNrz6H zQ#_DO4k3XpxA5_y-2tvq8gs&Yj#URF7PI&+)qmbr{=|GV6AO%eV@Q^y9MYa~W@8Ii z{X;7`x907#d(B1Pr)qBfD3bX)Q3@B~oK0IsxYv)5>lW`DdMy_%Ly1}qTsH4f8GFN` zciZ@>=H4~9gr2`fMEfg^w9(y{9Jm?jz{;H8N2Po$@O2w)-mT^*teTR4yM5`}(w=)4 z(zrYNgsJSq-GT1Iu{>1DVs^85!`s^xJO-xWUQ(}o<8Wwj^C2XfG7Tx9`4dIet6<%6 zJ^b~Nn5GtwM@d;HTdpqcYw`>G<8$9r)TA>S3-8+Dq;GRh|Hj*^#uQ!`XzcX`S-|g`+%z%FMNl8xU|fr z3iz{X_l`&fIqNOm66*OFNl(^=5{DhdF$u*iw(}pK3D&p%@?_`7oA(@>qV1_8 zmnsVn^ZN+izP)|4V=J%oju=uVC`t~2GvidUm7?9Y*qcvrTW0tKS(UL!+#Nnh^|qDK z>hKFxsEDDUL7cw?5|;#!u4Lr(zUp{1ZYgRPQ?h`R>~gU)jN0$G-<(n{@9!Oa{_LYj zVsQZ{Zt=8k;2t|`zg6#I@e#K3ufD~h0l3D_Z21*Fayh=s$#oXk@0m8kA@j+@SXR3| z(^>?ny~|58c6z_}FcEIIBe%kh8L}%8&wi#|Gx2zdWdB*U|HQqWTKG8pdcv$%6ty$Y zVNDminw!x-)C~aNm-ODSa=yJ`OP%z~%ydZ9BdKt#1y$wUa^zO4&vM=?Z$+xZOH@CQ zvrdr&ms^gUdO8%$UTt0R&M=#VOOi*^ffH`cY-G`}QSxUu@<~1ckY(Qg9MK};XBZ~x zv#VG5)9YW0nV}<37kLsoJG8j199paa0MdPK-+XlS)|mxd_e0bqc?{esKHz?J{so3) zZa5di_2iyu8V{5giTP=7`KGT-&NQMJOQnH=?i^!0ONSN>XR_xQpQujMirSxT4UJT= zdgY76FF$>d!#*?2o7evMH(JJ5%t$Qsr_X`fN<5lO&74T)OUUtR(`~>^faM*Ff4D5) zhZ?;|_1|Z+NuxpJXQ|%_Y{%ZqyZI>3T?)$4mp&Qa%U&IZsEn~E3nD7@AG8!WtSOhL z5^m+U7<6~XSvEYGNAJtvzJ6owM$_GZ^w^(e`Q%Pkofawe>sF!{L{)$J1OT5m|MOZa z8gG>-HBcK1a@eG4*dkED^E~SjAP|>w{J~QZfqZt(@W6lVed|!duT=feB)MHLLFEuL zpPd&AiF~|U(f8|Ji6NP6AS7eBIb8_O9PLMu7&6^^0*}8;tdn<*lPwJKG1!sz@d6O>iF@p zx*E5=k3B}wdg}i}(ts3U_AW!`H&;EB%OWcUlw0Q3r`X$My-Os+M79Fs!Na)pr#>DK zo?MJTc;57JF}>OKw{9#lz|P;x{P9WqLd#v%b%y}HBghwK^!R2c9}teKVTQ8Z+5a2L zg7Ik%&)2m(@7@x)hwR)nI0HL>yll1Pp7G-$>bZB|Pi9UhaE?b$&c7{3>StW2r@2DT zjX`8JO)9S6Oup?I+?=qY=g~a^fh?hGDB;ZrpEuQq5k6v`PK!m>5fIB!x*GUXyT-FJ zPNK=RKYco5D8c!?a*TVe0Ahoo2+QmC%LKEM2#K!;H>?=>ba*V?EjJO^-yHn61bVg zp;Yf}B3Dr@zuT(x1i@7Hvy(ej^9weJyTfV3av;58uBAr_F|xj&Sv!?IW{&Txyv1G( zyTnlbkcQDZlVSLv-6R+`oNW@>*2s2;P5Z!_&?j9yYi~t-Dr;##j(BKUh=%*!PyV&% z&yx9xELHx_j+$>qFD~&gb zaO`UmNYrGpl1v30%fz*R!nISPxxb&$U6&k5 zGErhdGZrQ_YN=D60*m~y@D>K4g!KMAu2mKNa8!u=8Mx^`IZ#6CEGu4$udqu>Oxyn^ zRWD7tFiAu7>n--P`%*3LuiBzzH;pTY^)KcjY6DM-ldCvO@m{eEPBab`Q>cHm;08;F zX?c59l*W5Et?`Hr{L=k%R?F167aMXD#~Fn0DYwn%+J`~^aDf8mz#;x*|Le`c80&B3 zUav$hlnxj23yYTN+OxAG`nGHA3R1`BQrTWWJsu!Rxl?`ovt3(I)o4 zEbw3Qs7aQ!r~C<*k|b9Ym0BP1?}fAjpH$7$!Lty~1slPAligZ^p4sVK0j!I@2PwBO zcX;l=&RKB2vMfzH9YR3f6buTXJRZkP69#L_0^%p?b^e}XL_<4wL7lNR`KJ4i}f)GpfNwbmJ0V=tjxSSNp!DeZ~JbuL@9;wa5OYLa6Dj zxv5{Wn%#2)e3w1&MM?NN*LjCtX2X>=HB_SOQ_xTd+Y<1t4l(X?Yh<>1;)-fZf^vg%c)~q4ibb#&?VlLrbkgAvfHH#duR4%2kMa zpbEsf-r%o<<4lU)XGi+o-$$4x+s{jp7eS56`Ibw!xm|MEqfyRRu&1foZLg;IK{~bT z|DD(w##e9QcHfpG2qDvrvR!t5ciXM|p9a=+EC#8`&Y!F#3+6<8-W%XNvL(RTT|~;4 z?2Pny8cCT{M&X`-i(L#Aqd1kX6$?F6iV5PHzHwVekYz6cLh-!aM2CF$m@JhTTJO^B z#l=_Xwtwf=#G z9Ef36>#n~-g&eSIY3Lke(%!k0nBNEO3M=Hefr9dJM2yzIh?vgG_CjXFEH{PXF3XuE zB>5B-cYa*V$A9M}Xl|T$GTrg-4XfrNvZDWnGhKttKJLm6*zJ<$RX&{+>OD12NNo)@uP`s{fc7{4>>`^+ zAeXDKQV6bq&=7UZ+N>0@3{iU>I?1o~iXlyQ<W<>q#)OOSF9+z>f&bW z6Mw(}ZFNAT0?NKYj+C6Ql*P_li+9};2cg##f1z$j&0`z?`cuf)zlorD(*CSaB$nvs z@3-U11r0!ufF9bo%$17rV=t%e4HKQ-AxMgq8*{Lml;*YV#S!6;ba@4mq2zMZ1{|Y! zI_>o4SxyTqOEB9r<`wi5(2XalL+DTws60quEBo?D64<9MUtg8Obe0AN-p6N`2){!$&4bP~AppM2wxs;CmST^nnJ;8>7W6K;ST&}C?_p3FD zZ&o9Ho!Jqa6lW=6E^}EKy)Ufi*#v)d~2NmPIa{3jX0jmB%rj=*pjYSm- ztbGx%w|f1QCM8vuR|}sXOME>uo4bOTjDK*n&|~S)VuJmrfc^Ia2Lj8LJ7)kPsJ6mh zz4j(<;iJ`O~@@>{4-wIab+>-FgW*;W(Lj&k1*7&yN(T9Z8+W30{d-=O3chx zszcSOou#rik=MLIBCuh^d&+8I`6cEGcR*N& zcn60(7gSSS|~x+NKXDs-fLwnE>r@XMfO~ zDjigi(-p!`i*u%|fE+++y1nw|pvv%7$PT>9N7U7AjpWT%pYQ^mWu7V3v~FE~jP~Jd zhz@{cB64tQFr|0=b7bTE1Aa_pMDdut56$RfwC;xR3erllB(;_8%92WvkGX9iHn<ra0zS6? zEY|{%HC0<4UO?XaYiP>#7Qd1DQMXHMu4OfT5bpv8QbQ7y^BGP}s{Z%hTa^#mPc&h+ z?Qf#k;?~a8#cPtBo*62-XbWiPuWzq4Teu|8PG%I&UK5PM9`QB$#bqF}9QJdL z&`5fjm+pZzhXc-R*@Lx)H)2pV%>w0^++2aqTWEu5nfBrp)x!lnYN|BI|6lo8AL4OrW)>oQ@C^VF=5g9 z(!{;p#O@@)!a!sVv%muGjUAbfZ;}9t_SK;GSZdR~5Cd3_ES%M1p`>QSUS};3M?V7p zOjcN4Qq0d+ZowX6*pNC>{|GSkagGq-fKWE1JWQoHW!v}QOlX6MpeA=p)x!crVyovn z1=fDn$6G4OZdxWFa0tx*Wy(t@3Y_L-W1wG1U}QBN2xpcid=&YV{GAVQr@>^YoL}-o z*O>U;+_9Y2P7D>_{{TX`v(O+ZkIsVv7fAibKRqj;`e3Kk-xX0UhhOpK&%3^7z(o>r z_Sjfklj+$}G+X@6LpImEGtSW5^bBz%Nq2(S>b}n>%u#s>CHZJTV~h;--BEEj?{2zn zvNIed7*0mHcA6&Cdbzyn3piJ1y6md@@MMYcRUhZ`wr+;>EyA0s&JbU%|31;jkb|X0 zE?;oJ)}8;0^pf|Ke1xyZ_20v@K^#yjOPln6y_9@4l3Gmg{^!!aM6#Os3_A8hvf|CQ z!bik-1m54qKmW1AfQ6U{$dA9Mq1{2~i}Pz@Cy}}ki_&|nwSm>@CwZZyI5t2`} zf1%^qIJxy{qmFnJ>EkekOLi)I8G0#7pT;{N+8^|zEl6Y$lX42x2U3c3Xg<`p5eg8N z{SMZREt0sb=^IG7`RP0a*ye<#uXS~PhlLwu`8)J4RAnePC-K;Ij;4i z-Fw+Bamq&lyle3(5^^U#pbW4!@JEf&!SF|BU3xJMcjsgIoL~G`p@ik5Pq}@@ z>b0#H?-4M2fz-~%32mFBq%>IYs9K}-SusxDaS2g_Ws9%}_MV+EXbPOMbd4pSB99+W z(qz&8o~1uT$Rkl(-lKPt+1~#x>5Q{_{Qs;$y%GbQqZM}y_RP_nhr24u3+sCJn-h}3 zL-fI)&0d4tZ5437fd**JRiP&|vAJuyIH~39Cv1DF4?gM>lgIa*<}c1?GSntVR}v+c zChBO{Citlz%pE%q3Q;r$+x-9=K4Y)NnI^{-WgvUL_5d|dYmuB5FAtQ@7$ zBL5_&wL~q;n&)AFh$`pH5z*Us%9z%piVT$g=*oK1phLPzPi(pKE4)q=%UMim2SfV) z@&C~WU-vPwDproE5=|}h?Gb63h_SFm6zD`YuQb7!sf-(>d2D)2h&cTVMD zT)O?84~^D+=Awn5{J(aVeY?D2p~m`!ErP+KKvun>Fs;JvOH;0Li-uvxk$A{kG&Es_ z>+=7zk8F6bDzY2qn(T+lSa5$ud)5N^39j8D86n?S&Rq_U~W%0$3-E)JXe=Fc^II!EPl757Q>oaD*Xcs9Uno~IUm>re~ zT|;$krTaDNXk!rqWh4DM-v-w-oP9Ghe=Q=OfS<0Uc*Ps6b`EQ|S4-KSfYTsSB$FAb zXu153CjEuB{T3e`m~7nhb%E>H-iQTWX}AvK@n6SK0+j4>T1*$a6)<=19VMU9U5G*i z<3b=*mhQKhho=H=*hfX%GtNRYh}%d%I^u~=IyLY0IFHhFv{){_#C zMU8C#?qNJww7uwnaH8fEhSmwFCPd%5K)h%)C8GTP2XJ6So_kc!vOm^`#PZz|3{kAV zZ&0C?@qayQ(e_7&%$eMPF{M@_u&g?1>`=fNLESy|may4Y_8EQBc43uT&%%!I1`h|9 zBPB?liWh|l2Av~<4*OiPhWNF6afby5nb|R}f{z`I4r)3^$rsi2M0HmBKTGRrAx_8< zKe5O~mj7_=GpBg!VYJLJYlizVAC&JCNk3vTF5fo6`ZDThM_BnbYy6R9eYarjEa`fH z{8<{8g@WJTda&ONcqsTE9x7j8^>qX#0~sa3<{x-F&Si!{1p?5vX1vC+2KBW;n z)t^W8EE|3qH5|wV?)D6m1feQ0RTX_r-{=Wt5C7}H-ewAnf&-8H#V@ui6V%Ed)+eW| z=U%4vU^9jKP5p4*s}Udt=Umr#-Qz4CxnG0oOUt*bZd@_m5|baxBJ`YYk@Cofw z+#+hn(v@{M#~Vyxw}P^r)#jsUyAz3nAUwMl_9twR>owQO*->%lYxL-TScmG{ok#z4 zN9Yq(7xsM}!tKoSuky`ADJ}R*5SCWF3IFT%ET>?Ty_~LRDBa>#_dnHcIUsOTkx65% z0wbg8(QNp#Yf&#yp4`f;>frz;;-aQ7HOP<&NM;KPHK5O}gH1Hf>71;kOB)A_Z#E*9 z6TTQ&U5m;%0x#Xk^l!OKDN2Q+J$~c*K3piLB;m`KFFxa4dTQP|0DjQ#7T^5zDg`#x zKv%1;j%X-_dH8*atk{~B%e&H)^kZ2A;+9v=hh4!~TE|mCdc+W=U4rn9X)oc3$!5q{ zzsmthI4@$0PUYvwBG=x|jOloF!1oc3`bb#5?(MA}EnHPd{hw9$PcRkF3kmM2Q%;nV zhgR_G%0cW&hO4oJToU+B!QlyMaz;_FMdK$BFU`gMJhLOuxs~ag!W-14?NDmxp;byT z;8ldD+!wsrG%K6T1CYd(Jd3|xtX}g0v=jjd5Ja7m1S@ra z+z1%!h#05#w868d1DuWYnkF2Xxr!4c`oMj@7K5LO>13O9sQL(?H0nkUn*Gs_3iQys zA$Bh&V~!EMH*NU>Kkpb6G*WMgHGG8FS#_@)kb@y~ADgrK`-V8nqtP{V9y2D299z!zrMj67 z)3NGZr4^PndNKafvkcu(d%o=E``9xun}7hI@aZ9qbK|tDl$t9D=!cB+R_7)p+_^6e zKt!)-6{<6RcOBWI+E{S7OWnt(2a85qe~Zb_uFPAH))>#la|#6xypLsE~ zzh7cEA0ymwyt&49#dY*BYeY|j5?z3J+m(5HPqjuMJ6aQQ3i#9)y_*gGGcPQ&u z<6XPu|7eOYMAwhl5p1f$P=LD;cd()j)l9X&o;Cm5<5jJem@##{4Mh5gAM*W|={yZ5 z5I9kfACZ0lKNhhX{6+(cjD!^#q6=$| z+O*q7Wu1juhURl0OculaN`;nIRw@bCm^*IB3F152n#+y#cCH_op;8iL5J($FUdsaY zagN$!2X<{~_)IW$N)g2gpbk->8M>7pEWrXOUjtHGvM!=a&YY&8?4cEe$By35;FTry zt^WM;WcLaM7Q>k}zeciUU0<1_^W-vGH+?vUfOEm7$qh8q6;L?)mVsj6;-Xru&VS5} znl_amgVVCV<0`a5noDMt0!M)>^0s6RL=s$kwB<7u$(#~~lgf|V?Lx1si+v!42L}D8 zZTae_e#wc+_7_JHhjL;@a2|DMKk5j7cIOQ;0S8g){16@-97>GFNM1~tL$@6G*bYxF zbt$Y9AhQt>tvi$&M$B$a7T-Ed5FkTLqGcF0#7-jPAAfT=!vJ%P$YCHIAfjN$cpUZ$ zmk~*PH|tSJ&JI8Jv*i|8i@hr`ngaWYe>hI16u)0BpmI(CHVK)AC25BBRz0w{NuYvB zY0jpceicR>>a81XG5M77EtYtu#F@wuavGoNq;B6}&a}Z6B;j}s(^$s>s460?iG1ZZ z7}k%&an^})Zpi8b)BjIaGomrYfa3QEEl}+Yz~0yM$eiVXaBx>#Br}oNnubl3 zsy^J05_F=N#S2~V#A3Ol0hHt&_Ud1I*Ra|=@7_Slx^M3P^d9!y`yb4XCl-+!W4^X; zCbU#iZ1e6h^tpRnQXRN`DKU$kR^1WmUr)NEe_LzurmzP_L_G2o z7TZ7ozPG8xTG2ChWYMU(5ZH!Eo`cZz48hWw3rd+m%Ss%v1hd-nD@>l2^DJ>?d9R!aUoey~w# z$SDgal&YGocqLGvGMs)uP*4{?N!d*wclU#XQ#JAKB{ic|)(%yS55~-4cqKUg)^fmz z8ewpnEM0*rMq#(JRf6qpl2ebh4)mVydBY&0gBi%}_3u?y^=1yL9GJWWJulc=)6Fi{LbWz7^$N_8Ge z+CDM-RA~N&`tdOyY!ca?4>oW&V@>R4`RbPpapAv71SrVhM_IgcP;ZSUc>vZ0u z-`(UQKuJDCs=?S>vK_TtTtGSQa@xwsODadf?s2T6s@7uASmS-?K`Dl-71wY%D!4SEg1en}v@bi2 zy34k-boddfQ-EutHxDn1=_UD3(+JjL5Lh^FtTFywKC4XiW&RdLSfx`z*mYZ5V`lNi zTCwJoO#7cQK~)`H>E%FGoo_j^{x;zR_0kjTV7()^*b;p%=A?DO9t_|dc(kc{!1Cw% zb88r9AXZB;hs~~F0-=Z@42tcIL&=WbEw@^)Y`Xz=OnN?O1rx`@h(zz6@#tMZNh(2a z6nXZ9hzsl^?Twsv@W}*Cl%}mdj?#H}1a|UW$hg#sA?zB&g&7<6<=^4z(l+~b3u#18 zcyemn4*y8oAG#*!DxkFWQQa$o<>&!!m-8}9CW952E6u~h)!#Qhs#}aNRLu|V`Z)rl zok-mN5nhbe>+QwSSX~B~aK=dM&fJja9x^0O4PT^tr-`kB3SCI@q5ff=o$}>gv4sZksL$ zJDifUO`U^D191m*&D6+)7I;#C%+O9X{b)2Q$8IK^r`jY@J`EpD2~UjmQ`zQt`~gg4 zsm&IQ7cwM2&0O$IlbW}s?s82t-4hB9#+EN~NOIS#_IahQGZ)#$a$fs1whWfyj0|LLpXki~Z?U2gK`gSw35<+HnMpjnV?|IKTU)1mV;||Vw&-*^(^L(D? zU2$ghVvA^}0Rt}1%uq`+Ya>vVnE~39oJhUY{V=~L`~0v@rzK$|7O`cm?Al)28J#cLq&V3a+~R=ELG0FA}4 z;Kli}4?Up@hnuyG0@U)~ePx1?Zu6v9l+8DN8-EG<+ykzU4B%sGO9}`a3T9w{9TNM2 zeUt-`!eSxXF)b}PCQk|?ftg1>IK`?0ST6i4Kg8@;po|K_M%PqzWE|6VWiiQr%_`uF zlpuP|wV56Ee?+}X^zqxa$Z)G_8XOXad;m-gLTex+-=b}b^+sBG9P!!LboJdVwqw}J zA}hgcA@;{m{D5Wf;yXyZn}8kfTU2pBQv^SI`#A3N=0ip((Zm;DuOtp1grcRLdEgrW z0Yku7SdPOF952`HEQSgBtd29j6@RI-@n;i4Y)kl3vDGIICtZ%iyx&@jf?*9&OsHv5 z{xKo8WWgJ-0%#8`js^G!ncKjTUQ@4N-Vqj~7FnWnLkr*Fqh`K!)9e?q?Ym&zG+sAB zPOo*FzJg*v)*bK^j)dBW`y0lUA$`~0+Tv!C)neF$1?!cInld9GUN>yn0&w+uR)Bw4 zz$ta+bE^FD2Id(VSme(d|9FC7L{_gM0kW4E` zXc|@I5^DUooVI?C!j^P#_Ti`N0)~ojzzM3p6xms=H)DP7_lmn=Lo99ab>_b=UXQ3` zKV|^iB*S<-7!Pj(G)y9nFr=N^t+2)8-$5_K#!qmu(+RDstqIDzXrZ^-TOul#(+p7G(U~>mB)9i$_aAC!!-N{5nW$GQzgQ}^eL0v@qmmw~9CRA?-w6)Pu+218KPPsPm6 z9B1~F$yl^>IWJozczUmDp59R4B@oTp)d5w@c^92{2S*tOb2rs)e43qHv~4F=nObL> z_E2n*X$>?t7$&4?-SJiI><7!iAy0HB7I29If}v>Ki4K-xij!EfM(OC*+FL!i{8=5R zXF&d|4GUg8Xv`nY8y{e@m?^MChw*lN5UMx>_agqw%eBs8;`13UAuI`B1^HEC}zu&hvP<3bQKN+}~;*<8oD?nbw&EgVO zFJ3ul@}qh2WE0kGENs^2<}oJOZ6$?Ot~1ykC3E-G)fR)!$yTK+cHSMYqb+03TePUj zd*BBLV|f6=tQYWfDlvpoaT$wg(|mIoI1N zqt7asRKRsQPx-^b+>V64`yG27 z;M_Lq&g8exy~P&A%NaSI+Y#@P1iu5p13t{Nmfq#^PJX&(fJ*Osj zf-M9@_!cBxOC5^a{#xwJPRg}t?@sN}`e^XEOu^B1D@kl+c&3@bjUSA+rq^Ir#<<|e z&pNwZ5)iA{>bxS4anq-^ciZ3REbJG!xx9xsQ=bDPoM*!9wd8DN47e7HV;`LPNaHjD zgZb_}mQ%Q7Qrj4gquqiTYgn`x=Hxg_eSTPogw!x{cJG=LlD7 zPYhrzfO~AXn~V*YMXBG@!A``5Iq{3#T!qetrSh)u`uppe{|B9hq#Y`qT#s-1&(Fk|L>@ z*xhCFk)?}5O2P5N#D%WGhizYXDrj!5{`t>r{}O7rcYp8w63x1e0rsch#0sqdwaid& zvD;$jpJ;A$Hh9~bdsK5vcIqB0g-fEgS_z%Gpc>-TT&$itEF0wZIcSmD^}krXFP*u+ zUq4dWfcBS|im~U{>hI`4u|%+#E(a4$@FQ37UvTGUE=ePk=is>-b@#wjT<)EVy>gFLxVim~QVv`Eo{xvL(^sqG9rojUlPL#^;7b52AZ2DXvkJATbdQ;o z(|x;367KiUG&s44G+u@_PZ$o}eeN$V>J53nsTx?0uoh)@EWnsCK3RaV#KA_{FtB%oL93?6$7Y1B7F)H9=bpBRdMP1e@HPyAv5GZM zd)jz=(7Pya1Hc>%TQ2+$I{d&mM7}) z1MVV^+sgV!NDR39#&v^HI(kzaZoZN-|`A{ zN{7hfm}zbAo*VXUHh<4ixt1X^YG;|kSUv*QRayZB)~0TaQgv+mr84WD>Mn{p8GP{I zVbCwZ|bv_SiAM`G9(Hm zXVUDq@R)e2o=S-<-OdSWlw6VE4krW5-O&0H<7ux26Erh%G>4;Av~II_KR^Erob+Qr z0y)E?eEsX!*f?8;o?3JL+XZX$n%LcJoi=n&e`UdIj}s>Y3T3HY`_nEMwfc`27Ic^p}2%ysV`YL2oBp?*#gGY z;*nAf^7Kvn9Aw7{eC6`YpyqK~eK7!WzN}pEbPQIy3hzrN(}2Oc4SSl9~Jm7GDI>rL}Y_ z-;?Kk{vnOt`{0=@f!`i{vWGYptZm#Dpt$8U1Dt+I%sx|>3Ih($qq`~Xv$~TH;t;PZ zrcIYDiN6?s&Ebb+^8P&MG5h9{L5wys-rQ9jx%JCz^ME|`EM=L&^>yn2*OLp9UYa*@ zxTa0D7CDXbnw4DI>@ENYWv&Ep*~r%KAbkB=4>GoM#ko5)Jm!slrueKY3`b|E`J;go zCm7=VVIBZs3ELG7~P;%w{uW?y7UtzcSrMtr&)w5&CFK=(Qh zB1xS=z+fq>ZCd*}Dwp%AAT|L^VhhiY&0f(*Ti@HoPi3;t>3QjS0LSrxAd<#Ze@#~3 zCq4XOWwcQ9TV%bA3XZ%m1(f?`lHCPS8HuE#E5|T38A;K4izF^~jxEoP)RM%&_-4L! z2Gr+I*-tO_ex(7t#G$Jby6Vt1(!k(+#nDa}&Z4~hW%8zs>}<%&y|7Z-atd)}J}&5< z_Kk?cYk)T4oWeX1=ehP!lyN4KeYHRCs>+0lTJQn#NKQV!t*3`a{M7A=3gT_P{LS~; zzkJU3g_ZvC;C8L9e)RK_kC+x79a zSa)TKkOyq;9L!nRY9=#8U>16S5AaJo-i;&em1GzqdsV@!? z)>$j(qXB?C58iX$Z<}De`2@ zEmG{yiV$&tAzm0DQ^ zW5XuDN(fB+t0d391Yqu_-COo3EJZ(8ZFOfN>`>!&9O?T+2;+ZP%w1P&{n0JT|39|D+n zngcKwSl>LfYDG~!0d(;vftXJ<_jADUe3j4bJClE-*AQyX6GaFYMDk7N6-wDi2I_#`udlG7wwjDn%)Q24)?CWcwhGFJ-Mho zT+QwY)AVLKdO9jVv#GpqC17o3ajJO+wK z_gG?4i+Neu*sO^wPNO!3is{;LsW!uW~SVxGqVgLx(g)! zS5ro(u6VC72>X#&iyu0N70C>L&Pr{4njU6*N)yQo;GZU&G2Gax-Cok4;|sZf%D4w; zBX_2{-)+|}_WQ@~X4xd&NFQDVr`WfxJT}OfKa-6U2_M4~Xt^V%qz~_2JQo!y{C~g!+fYtRUX6QQ6FR*gqZ$0HotPOnE_zoGZ^LUP*MOKf zn5IQ)%~m|0SW~2g@=@eEP)0gS+yR1}G9eNfY4`?s-IC;kGduu*KHveZKw}|ymONwE zw8RVY1VBjvh}fWh2tzxoAi+XGfg7X7C_gF78@M`_-@IwI3FqQQrxBk!c2|;1&^|`a zb{}X0r*knxy^gx`*YL$LfR{qXsUdU^5&CR7!8b1P zErKhBP$zjyxJniIUV+UKX=Gh1N`ow;*3*p#XBWE8LUOV$gB>e&oalj{lF!f?@j*g2O0{&qVj-qh}!p{XEwX#hHJqo zL|zb(i}GGb(eY!IF^$)wS%Kx{QY=t?LdW|vmmn`4Pq@_N(R)8dIr8k?FLjl7-O%IDWD#1zF8nxDbgOVRsbQCc;palc>-#>=F(Ecril3M-)sRxkR zqWZ?!H~{q)uMsD<65jp)D5UX_^+hPMpLqgEbes1+Q@(a@^d-D1%t{j9&B(#<8WHu$ zm}YvG`T>TgQH&@BtCP)!>kzeS4>`3>pil(mV`S}cr!cB8PF`Y!kHe++U|QSLLpL~8 z-r~O*Kiw3qBKa9u@07{gCYM!3=b_Kp^+H{T;J~YwyGg>Y0>WGK`d`iu@w;- zKYbBe3q1`=TZHl5h9du3&1;8PtVKMf!PicWTCD`iR8~61=Jh-VW%9ciMblwoj*p#m za<#12yl*)g0a}ee-I?u;OtkEq1sG7Bnn>(FT(ivFhm1|QwL)xy3gm}h7%yh^?R$q; z*ELqR!^5(h+Q;^5Tx0@OqUze^y$xBrHJ30K^`=z_{CqD7 z#j5sowZr)^bEDH47mYnt8>bpTPmkAajzPK&ld|7|<{7{)sI1iw#h@{`YIUPn6S!mua5(|U_SBHF%u zmhIQE3xoPOP+#z>6OacY-68u1I^$6Yzbv{;Vg<^U0k|qj%6kr`*Q|~$r>sU13nU8# z#Hf?-QrK9c1I)cV#1~_#Q6MJXLt=~(0!2a}aoS9;|DS7{^6Esz4Xb5mGt=Y_S}o22 z0t_=aQATOWBjuAdjnEr;^i`}rWXxF?3V}rr0gjc)+& z`Vf0%in0XQ!b8bs%<4Ia64k*V%O;TE>W_EZQNHLUrIFE*qsJvds_)LZZu~`2Q}OxnjS%7@nGgi84XF=dWe<<<$rv}P$~bs@WF%M z6$tB1;)R*p&p}e|Pub2#DBO;DGxC|qqHZtTG_B-3)6td^it6l(lv2P9P@ukdF3o1R z*CsH`Y(|R=5v;lK$?YQSNc(`t6 z+x4p=IZ{C^R#AZAnD2>*=$+rkJDh}ffZJ3)190~QmVrtdWbxN{tfRISDGTluiBW-^ z?6bOJ4lsoifky+!zfC0M;njOgQf4%{PN%QGX;c6CMpuzCc11kLIKT@t1#JD(=b^^# zhkJepR5$m7z2e9!{WrniW2}vCa<@dFsv7+FFh+j|!8noj_lG*$nNs0Ae9?!s3aw3rb5M zMu*z=TuOYE2$%1j^?~Frk*@GarB=sVGqD!E)z)D0%{)Luwfp?OyQO{`>M?;fntyJ7 z1p4egk|w$}e`a())I7owoMWm)Su$#!S&)R|ghvAHz#+#$>sAOaU56 zv&fkpBm+ua1_8k^({dXEC?sicH|*@jV{r5WMPjpEw?)~8MHp3&*pW-|NEJTJWKq#B zlC@YhK{(jMvi)<-%w0G|yD};`Xx4bk4N*LBKma(dM|{~VTxD$h5puobMv}lY zsca83`$p9!C&zNreXLtbUgzV<6YZ4_-nJ~1+~_kH^gF329=5~1gKCrEkZ@C~4joEq zj9RtwJMd6EeOp9ZJVybmt#bjyEkN^lLbAgEZ&S6s<;IrDx@;F~UO&XWFnd12i4Jsp z>Xpwqy$~ z)|rtFSLWv$6;IhTQQ*m-U?vhZk3hmzo-q=yR$JpOwlt;`%|6s5%pRW_uKzYZ)!8T? zcx)pApfwwRf(5@kfzq!R)6$FfCrxOtnw;-X%gW{t-TR2?l;cr}?AZo^y;k5xSPxo;Q zER8Q4bjxOAnEm?Q@Mrzg#C=fLcILUBTC)*;#S0dt#tHL8hW&M$EmR=TR^VCw4(PGk zS`@vonL_kSm_d|a1WSwgcR@ysxkcg;PaOzu%w-@uGf^X2_DStpx~duQ5xwtrtxc|Y zSrOZIo5*L_P$GpiO|wGTG%UU$Ccofw%n++t?2J{5gP?~{_-u|L{mlaER45=qAXmBd zG_M65E>*}$*lI=5N0^FifseqV3yL-<6YTRcgB)ljiU%d6KzE60X>c+*%z8C-N!2pi{4f)`?uoI=Nnr1)QUG)9kq2VDAm5XS^wSr$vsxD1jMp?Essf zz16KF*RMihbbXOlHiGM(Fd63%ZIuh zfl!6N2rywzfW{YLY8e)DtV1bP#t_^KzkWAx)h5ZG)8Nj0LJ*1pO{+cbg7ElwNf1Ek z1F9=90P(%>M8aAuFX+ogD1)Oi4fQIq^gKiX))GjMy;meEbWtl2cS{zY;YmIi1YWRDn(O!$ibA}keZT4HdBgm#;+>6p<9**)28FvH+ihqi)f+cCWuCr zVj*&{dI(JsL^#gKqnrz|g{UiF>RmO1`Q=2l9~i^j92__)&58`Q2H;y@agrj6{|cg| z30AeyHL%nOC(T6)$I&#|>r^=>$KJ`oyt=D^4N9m+hB9ub72wqoD(ho_1z6pHz#glP z0-^`1!Dnom?~!(W4016Ys0=|Aia$z93sz&~`Q{Zh86XdGYZ_9%Vu!B^S%#f^0d1ht z7gPX|(6JZ|pd>JJ%0-lKF-jGzmuc~{TZT{W0C6-YFW0P${_Y%X{*Wc65+(h6J+jew zc#jGFGm<&jp93z02C!sBzFYa*XY{oIcu2)dF3=Fg#|S79%vcn(spHOjHV(Lh^>V-i zB_<*pt&9dzgg7QlvOvjUr4|$>)chpa)Xg%*g)5Lb|MM2n(ecJ{YxFst2$>*Pm?Slo zghrZGkcB~>2=n@QAZuP02*e>m2ES(uf6+#mGcn_sc7(@`HGmmBW^ir+29Jy5 zp&Ae?v71vCkTgJ5AFtoM%mM0-;B2B4U8o5a6gkLDQ2Q|ZjCX-=HxW3c=S8%< z;bzg@nor}gHD7EvznieSp;G6ry>^Hz_JuWhqFMD7m{kOr6%?9!lz2hWTM_qNpy96slkO?5Hn%A4_3lZ#cq-g zj!(mYk5iXcl4XypWmH5GsmF!s)8NPjLkR{)k=BrWRzGQAw^e?bBxic)4O@)}BR-T! z6QfwU!1*YsZ~*$i7d;|XuSTn+P)VmF&H=AV0z8I9Y~1xA7nVjsmtiEV#7Hzy@P-9o z;DWwspo^{&&)mS%I#!4G4bOX?27)`7aq|B;A#_g<-j0z6&->DUYul5QANZYIQ7ZwR zr>>WUbQ*&YPZKCYb4I0hRNUE2@jW$dkzn_dqn7xmTwH~ z0+vzifLYij-%Z(FLDF~x|FGH)CN1uj+Qa-TrXhq2!3gzsZcz;M@hVg{^66_@K`JkI z5RfzA`j*4--$e#m@9M$OQ-!3MQSt1F>g5xGmL;f20+lxoA|XmO#r4LlNv<6Na^O{% zg5S#$We2KrpBeB%3*At-QzNH(#-$;~+D{BVC#GpD{ZUyE=87x~D%5a((-|{WQYecr z3s{En5gJ^%AJ)d8)edA$8|06&EL8JGl>8L2j2P~kw!Nx}R)C;sc^?QcAP}2@;OTxG z3rcsPNf3^y=@g{Wzl6|PzUHl{5(E>MP`JRm*_?uMt2v0gPAXC!R%+E>dqNAXgLMd5 zpTyVCzM9ws#l$vL+OHV5#?#!zm|Gq?fRL61wNH)Bk6;y~0?MUm`M{7SpqK>ZHV6c8 zVGr~fCemQ-+^ty?Gg$rv^S)vap}$hb5>UgNfjmZkq#xMRzutu@zU6T}R_(X50(_T!i6qV3nYD-1R373_UHLP{P zXO5u)ffkD&0?}xMFVyZy(m$V|IEYpZ!2$ptsOh3Qsq9{cR=3*drL)tghGWqTB`QZR zoy}Yn@?L2l5@8VH3wSfA`kk3t$LNEI-_O0E^e%$NM8 za;*y_CRnABGvH9!@x++|D_&|qg;I`*5l}cR3mO$g2{s_lj@^GyzbHQwWkxWM)()eX zdJa|YNwJY6n96qcH)^U!?$56d2Sb4shdm(&(0xLFW)9ClyL7tNtaAX=3Uuwx1jy)s zes6UCB}e$L0d-RF4JZIl4+J4MrU?=l_#90JBeD&(ppa66x^4qEb#DXjsSJV0sbPcK zAg_lvKQ_PC9?H~caWmp4(^H#~yU-q8h0u${N37O^31d_t)UmI*YY2^SkjHk3yqHTE z0IDL;L@~GK_!%_Q8Hsw%H>X|5KVtC&{-q1jK{*F8W1>|{T*OIR4M~hN%$IB_1*n!h zN6hS_L=)9ID60|UgJcHQX~LDF(7pALe=a1KFHHz**E#}Lo72HyO-z_lexpX`ct}7T z{w#TC40>nYh+SWcQ@>_cwOaDl=)3#NAr!= zTMefgF1=i7b@J`j$m?8*b$a6)uRQ$QEApkxo>fABEjn$HBayoy`)Km#!Vb?_!ykb{ z!{X*2-CJnsl46}BOEK!ovGQOkPtTbjw%uTr6xWUKTWtoXt;@~K^5IygG8p~7BRgaB zrn+z0r_NkB+fx+X2_5C%K6F{+W(w1cQOXJ20>&=KmGcgfxy=bqqP=#*~KU7{JKboDVEn>T*9zz`|l{``PC z#D8!&wr6lqljENt7>9g-wv9cz?;eE*_^=w4{$p5QgQRRX?7!%#QUbh3PxgOrZuRje0vfr}%^vWSgVcsVid-52a zW;u~4sme^=(0@#;_f^{6RI7e3hCGl;BHPAD0a3$Tx6iEwcFssEH2J~~W^IeUn|$#N z4_+icyoD--JI}82Y;~TU{qyj7sL%0@MtC=iA7)g0Y>LZ=)@-}r_LjVV z9-S-W60G`po#;zVl&D8T+`VI9t=K*)v5(NNqdRaiF?G}kSq;s7^TtSIG0>OYgm|f+ zVs)$hH7`k}$Ae(tO`5mO&%Qe>Tiz%m(fY3-B%|>J6b)7rs06~ziaD(?`c(6>VDbCV%V^{%@~a8e>^qkP<*$BInVsp6Zb1|L@Bh#0_!BQ5*X>ZP zZm44b5%T@)3+xgWAv(cBcH(VrQk}(!0v9f97ncIN2AlF*TkhOB?|xr=1aSF8{6$Sy zcm`?c!nN(_S6GDFLJdW8?MBj}tXJ)?_smUF#O+5^ghxTg{NsHv-FvcjxQ4>l!}eXQ z`4xEC_*bXw4O7r3mLnA%#H+y^?)~L3giDlE`^!JRn)tKfN{GGAWV>! z6a87WrK90AV?I2ha&6$n#_7?U05{Q*(CT)c3nP4-uivr{SAWJ)kUBQhnw^D160T?= z5+GdPEC2}pn}E^CE5#=-Yb?XyB5TW)0#1F8!M{Dfd>X+4xcTS;;NyQy;JClqLVrjV zPG}KCo$(x1hQOjFA2Z^krjfoC^m-<4KaLXECZ1n4qfeM4eS5|yk7`%+QE%`CrvsQ7fnf&uBx&k zNdANjj3&e$P&I0AgU#kg7Zj13VR@#wcHcK4Q{^TBkI4*xRU@mmdL0g*7!Qps!K=2x;#2P#oAUe6TRA%h#olsn<6;er5!9J3=w@ZW|D+I$vUS-j;<4zPem%E6Si!_|_m z_?VJ$+r{Mn0Y~7Nf#o3xHKrC5cqi8xCgsG=G5u{AhXFYwn>S9(ezv0y8+PR*(a5_` zRE>aQT;N{?#w*dUFq(9cT?3%rpNKli?`khFcVM#gdt2J*Zi{@wZKod5dG*}>t|KQ!1ZuWfcX)yomJ_R0yd)#q?R;N z<<2{7_B`ZPaP}2%G&OO^kBo>0mHK8VHm|Ib45X= zucPp8T@5Ba&qPE*0r4+x?~d>iD&~I=woW&-Yu{w(qoJMcPO%QHsK>pm}7liGU#QJ5AX139q`#{l4BAm>67nh!t-Z*9$%B)aG~ zba_mCAMk}!py1uNRTl+(`Q(9Y#n|D{$PQ@3|AIF1-u9h@=bxSEDhtk+b^0leuo828 z2chL&!!!$f-*Li7{ISL8dHw+CAcU6sGBEz#dkCdOl$NAFJ){0M2!$3b`#B4HaK0Xx z*qLWrB7^?GH2yL%{i-Lbf1HB;%(UtnJuyU3w@Zu!!edMnZkAIc^zn;Xo&>--l{3x{~v zQ7_=-0ul6bjoWAI!KuRcxND|jq%M=BX4;F!&8M?*e$)Kd@?wnEUev)HdQlU6gKu8^ z&3`S32DGK?2>#mL03%2WC)feM%wi$Td=PqER2>F>g9z(-_%#2r_hHlESvWPNEnQE( z3br^^J)IEt=+a3PvZ<#YjlDIv9pL8`aK;O1foU(0Bf#UPb(_6Rp&9?>wh3(M>Z`}f z!51S);1YViz(FaT@PuvufViLvp~#a}zNXJ|bpVBdY#^S845|5%D+(d<`w0Q+=PeW< zQwZ3?QvJ7+ozFL8{^zTHeweUgesnSOwUa9;Uz(_5C8Wv|w5N`&XpyKW>Gwrb_#~xW(*#Aq&X)pdLm4&_R0Ie|Est&=4{MpDJcBY6S^&?#wpMQfbq|!(Am7yxB)a5+Xep4I8*=>c zTVVV_xq6qgi81NRKtmU<2^beubH(mO|AYGtXcC*^!BM}KAT`+jy^!t2TC?JKL1SZM z?~}E-z9si1$NTS&ZJEdRGhi9c@Ex$L*TMh7-f>7#f6W}I(gemdAK;jt7#{CZ494}2 z^@IJ&G#+-(ux80E9GozCzM1M23lIxvw+mSsA!h0rH-dZ0--Mo~oIPhdj}&XF0^$^ zA$$TL6F@5ekEUBmTjqPDVOxspv)n7^Lx-Gs&%`B2pI3JWf%V4h{U-=Q3dTRUu)?7A z7ZKfPW{3yVeT4dHGkah5RGRhZ>&c3KTJN|mf5(&^64Rj(NDZJK(1W7&4MTK{x)c%N zuo=vS%IAm%Vml(XHx zGTYl(Xd&%VO4Ex0-Mff-=4PzOe>R=cvnLXXrC1mlBj)Hvs6XR2k;B{wMbT9LVX&+L zCnp427nk8bR}U0jzN-y(SkS>dGi?3+4tihy1;nw2I2n{_qxooVZ?d81gm1XZ-Q0=) zp?U8r&r(hy=oZ-HIt%9Kwws>;_+8Ej`2mOq>Uv$~lzM&U5Lf|&PRz*0F|_IplT(lf zqoRszC#=cr3S?1|Gd*|kutpUqv}+jNBygD#*WGlJk+k%N_cONc%ktX!W|Tc#kg1EW z7=COnj|;z61~s|Q4WR1wnV`zVAn=g@OdN>9zQM@lOHXBoB zLf=C_w0`m_hljWDUwBtV6|{a6VZ8E2WGBEMen)`zS&i?7_4uFBD?fUKMR5DG*+tqP zJ0#OLp0MFCMm(nS52UxiU2)1LN0)a+bI8WP4t+cd;m^@FEX(LL zI5)xjjAh(auoOa7av}Kt=4XVZ$Ug)w^{WM#QE*f|N#uDLQ^c@m$*-et+J;6?`-foP z)a61vLtwHwQKF|2`6hnIh$v688GlMB!hHOPKY$q04RT1&!MDYyJBWG#{sirmHhS8d zV$ieB@~V9jU7ugi;mGEs3w_`{GEfN5baL0VsGp0Oy=nv;QID zD;|U>yrm=4)oSc!co~6sB%CmZH>5f2f!r0$!38fkqbF$nYmpCYJt0yB+J?;= zZ6S>{6W~!$L4`K_hw6&E6zvTui#?Eb<$phaY+hK7GPtlV5Qzh(vb%q4UWFO^0t;t4 zMm#~ra}{HoH=BM51`nU%!g?)Ds3JlwJ994&9lL4rxJ_w&CZWc=CnW`nY$xZzeP1B) z`U&E7eM)57_64sKsya`O(jB~1+vZL1ys@~GuakzLUjV(A{zZ~HT)1PsAZ4&w@Ha{F z`{15KA-`YxU$CRWg6m6^DEMMRQKE+)!2Ha-Lc?L=0nm!Tm1c4l%m0jRztWtnsmf3x zM}~~zD4dvsj=*;QFZ2sZ6Fm(id}`q7P~+DkXG>{4yqoY3mh7+PS-7!fFc4nZZ0$PdQ0AL| z_HD(5Wur(4i^c|Q_gCoP4_`2uMg2sW6xLWXV7Onbq+}lC`~~q5n_G}FP5ESI<~IMS zE4alqYoAkwnH>1}5D5U|;sPn9s~`Rg_=l2yU`NNq>)r65wA}G+Blzz_o`DJ2l>gi} zZz^(f_^|Vc`4G&;04D8VhGGFE%i%tb<;fBjZnOUv1)Iqe@5eqOn52>;XjodBFW%va z&2oBY9vlD4arvvDDv1Ble+@kT3x$#2hD$M3oRs3-Lv~%V^Ikx34{8xffi(QjcrB33 zzJ9A*ln^yPB0L`3Bsc%+pLoM3juYcZHBSf?gz2JrYP8;F<70q9JmcrLBt@SH86paV zP&~$&4BqQh={p8Q1UW$(Ez8pk>+zwl+s@G06!pSKv;GY6zfa)eQ6?=fZ@xQP2Nx9F zFd;>vS1gC;Tlh8#(UTd$G<7)%pBgI`{JGM|G|zK(N(@&f9WE zMex{9u{}!~e%F7rR}Y0^vDq4YqK^8AZuwM~*HOh#A)OE715Z#^;5dvkD@=|IKmIKL zXaAfn$9vKta1fZ)sJxT#C2Do%tv`({`YLix(Z`~%x}bJJJ3RO8U`U-{N?<-}X>sF# zL`kE2@c7RE_ZpfB3jz2U*8X^gGQdvvn&xWAQGQ{P{3AFfXx*ZVW_<5|w0HxmV=$@T z6pH4j5iO+GiD@}ZK6DMUc>bc18FZp6+n7U>_0;J>li+BbqmkE-U*tFtaUP`2x(*oNc^PU^iYb3U(S$#*(23HRpW!>GSKi%$eo^)_i))Psa13950AqVg&pt{0cYA3LhFmw zyJ|5J{#7k7+aKQqTBChfnQ)-`xS_TaXL&90O(1xS-52+&y9Eqx*FkLhBPYGNYx?#D z^DRzBo$qL=G7EM$Zoi1H`q!eL4bFOTZ!GJau51{qK4=uXGakR`aERE@>OpU?Y`(QJ z^(T@X367Jxm9LD+)`(SH*pt8JMd9>HO^mI*kk~~=;Urd;QX}StX=v46SneOl3foPg z>pO@o@R%W7H9?Asdj4$D=a!+O_qe!FQvJ}Mh?F7USmyeC@gy#Q=W1!n`1w9qC3(at zMFp-*B%6Bkm$BZy%hRKUQRn0FwbocSv5yc7F`6EZD(;>lHs|4wKaIZ_03v-}5WgQ^ zB#U*QP)}5=KY?!n#a>qZT&8$@j7AKMctP9fr@i+sVuK=Pwc8KhdW~rvo*G`l6Gl&p zC}HY0dYsJre%G1~cY)IX>sT&*rn z{LuPS6>!!HIcTij*Tkw8rx@mS*NOeinDTDL2e3UF`kylgn*9mHfTQ%P@GXjzJH+xz z>DhC(WyFLfEQ zrw!ZYKF3AARlQ2`-^_8JZl@u)Ny{4FGv5;)Fj9~7B-^WZoDUSZ)oh87H+<%nUKQ0r-MCDHBDU1P)`rQz0Pp` zwOnlrF8tUk*lF3#F3Rda9Au1Mz^L2@4v!q=^DQOR`9yx775;ll=EWzHo7TAc=bHMD013pxH~qg$>ADH@M| zKqx;r*dAy&8qCY4)MuBh(leeva zT5N#QgQ{Hl9=MfS<&sk4o*!J?6+jIHwtHjo9dg+Cp@jzby0Cp^xs!?L+glup_y&H| zu*s|1=uO&*H@MNx2w3?-tipvb@;fHcxIxrsg0{H&@RG>Z&r8BmNC2pllXK4LD0gDd zcZ33VRDDRo-5=v9AbZO!G>4!b*Q{ogH5`pZjV~O)Lj%p(xWBh|3$ZaC``CLv-k0+W z=i6J1#9&Zwztgn8nP^OWA09OA)5k?at^0JIk6A=n7X2A}bN~D8uyPUnuv`2WSn6ts z32ILyVGAMrqWM7^8TZ~ia&pwwpVLLR>TkM8B@c_mk^|=KJW7)cW_0;Z%4L!v@DB|I{fmZAOKhD)Q|{Qde$=> zgtRJp$q!JZ_r~Vo-eoywz9BoICos1_M1M+!5iheFTQnE`f$?4c^?^6~k(XU^KLy|$ zNicQN=O|md*~B+CEFmKpyiZYy{*8oa715P3Q1sz3*Sd9c=%w`-Y~5m_-Asl~8)&|y zVJbz!qkva~AHVLOvkU6IWMKzMK*UhrbHu?yCTYpqiK$-r;Xtsww_3tn_~$aT6^Zzv z9D5RYYdiBP1L8L(Pc;Zw!6a)w{YZ29b1pQPGZ@!iL`HhPll)xG`DFy1V5PyM5a7CC zplMU3#NkoUyi)aVvEpUlm;1RbGTmsV1j9@fpBp&+f&-2DpMFe05G7#$1bIm<<<1^3 zrx~61Uj1L-{3`ri*pDH%FrJ?v_w7)Yd}v>3(UoJk;npYtI`BBP?NwEK>eMImIrQ9- z!bLR2N?BSRQaSw0A^Za` z!wK6pe^IJQ5HTgcg_1ldJ4P$-Vk3_I!{1Oxw5eFZV-5hL)YC{VV-f@`0qmw+#D?ek zL?FKH7QLfYQLK=;)-7C!Ai&+17Z5XA2kFss5$WRqm;*xCr0axFQjXPl5!{SgqDn-h zg7bca4MOX*OX`8y8=MYYug6IfZL8q|f2v{q4}UV31*_D7_<`dw@O)yL@naiA4-`h@ z8}sK|caHfDl2;cJI7@LuR=LpqXp=F(*oTUm-06{zde7$+eo7-o8$}cjz~GoOnI9m@ zI2M9g0-JWDbDobM7fL@_@ptL3TX*KS&}X<}PW%b~Eq=o*1oaBNm<9(I!dF9#7P1GE z?4xBfqBpcF=a|^Z7aDtyPy8^XXA0B^k~fCjZRe=jI;}ZzexVbD-2FwpPacunj@hqh zQ_Dx!Gjon^l4fFcXkWJQmbsX9x`>b^<^_+!f}jWAZ*$Gzq@gme>Zvo560Auto)G#kE>cL zzZV`HLw1tPrrF^?MY*W*RT9Dza7W?*ppUP}TyEg@e^@7+bJMvW4IzvU%5Q}45UESO zZ`M1NatU+Tcu8~}voc)R6RL~h0wzG2R!_Tl-_WhATY|`5(OAX6)>=Nf%?ix%tmept z=E?dyI8d~I(-`o3Qo+{$#roF!FN_ca?Br0*RaZz%hM5hVU1()mPjzF9P=Y;|L^V5* zfxbDuedz8&3#P18gV79qE2rm1m_2!NAZD)%8V$8;#!nW&1(RIRTe&h(O$^`oh*9gv ztLvzzQTWT{QVf*GQ(btL#tr`c3+Jkjpa-=l@3p_lgTsgqRaSTv^~(p6Ni0qF&2M4T zoME91pUD0eszQ#SYziO&Wn5_56iX#{>S8rF{)-K#E&=z}aGZGRbx9MQDU1-24CFN5 z0-&Q~?aynhn$71}t|@fY1Q49aSDU%!9=_EU9ohQ)Jy;%*n_{#{r;<;w@q<_k@OyMI zSxlOmN!o9WkgECeH!q={CLx4+t-Q*P|EK3_*?rkUbChdc;=l)KJ`Mj9_?Kzi#4c)# zmW<*v*zuXJTKs4QB#sb0z26K$Yb9w2TIyg3T~Y_PT*~w$iU9Du;UP$`5!%x^Fs4qw z1@PpL)j_~!x#kKp7mor(g-LDHRtM7811BNFdmj28e+o$-)fh$VhdrMOnlap20&@}_FJ~h zDg(KzpXQ9dZZ#Qi>9riJTbZz1Rz%Ct@;LNkb?p~~5hG3^!l-Y3oV2WeIJK+)qQ~#x z&HERq;q(x7+v%Cl)GvqM?q3D0Q3$zM&GEE7efm|4!jeTysCw&ehi_cSvqR zIo9)=ZVsOQ-9%QC2_UvQ;r}yiswb+Se2A2tixeM2=zZP*eFnD7I%k2b|K1tP+?f8| zfmd=kady(u$w|jS@kNzbOZQ>Vose8oKSSjM0U@ud9Dz}W<|Jb`5l9>q9_Pw;A_Q`|C z9RH0!QMrjDLojN@{~5NH`qnOciJ9rf{=CKqxqA;eO@E#q)R?G61ND6dQ*|Y#zxW8f zWJHbd{hb0O=MH8zx$Uyntnp2sxiOQ{!ocgC(!LKmwCw*nM7LGnj|;OflK|(#jqZ;# z_oKdC%dAEWuRaJ24EV@8I)R~b8xYe@R9|pU4rc{-89Fr6B5rA`?s?EZefh?lrW4m^ zIZ7qBCR~>CZiA)TQP#EH*S!@z^SXh5;Ua$XCkcOVyfw1cqIRAezyZ17kJQr^uf}r6 zJuXSfe|hQHGh-7!Ws;GYwF?dmz$%~q-XCUqsPL^^h+U~yxAyf6dSpqtWNvkfR^e<3 zmeF$@YZ*>-jO9XZxkL4Ln>^AN7#R4T%&fK2wE2)Q-MQS?0bsyJWlP(oDCb%t~OXB+teIg&~8l>2%xO?V02_$rs;p} zeQ7|A=^OWh!GFdY5~Z4nv7|*RC8dLsA&N>-l%lk#5T*4PV`DI-%h33q_B+onhx8Amj+R6ieEt%LzC2Dp2&s3Ih(oJs-=OD7 zf{(^}o=l;F=(YpX@L{4aQogk<90hKm7TuUGKh-wtl7LcLz_lJH0g>%t_T-^GSnnUe zzU_sC&X(nIMqvY2i`3=;wxbQ>OR!bU&jz%}O->Oc+ zS=FXwggM9F$h%gtTAkdFC;Jffj(Gr4$^paIQ)!+Lch*(tX708(E4_-BTr}@1y;&*E z^0I?U=R^9HqSTU$UU+7&bV{(@$Yk=%2cfEMC!#!3zX!(U)ykV>y--36_1B$hP41^> zT9ZE`hu0#%YK-~2OpV2(L)8~HOF8}N$of`gFmY4~MHCwq05Qmk0CW?$Hq~^hvxj&^ zv+4to>fAlMMwIJnM`*+e;8*41pRC9p`oLcxx5EV1s|X=~_|wFJzMK^KV6Fw35hLS9Qv=d}K4IAA;e>D^FELy3w403hO~vjRIE#In8@X73A(?{XRWtd;$q zBf)eG`4eoGK^5J%`c?xVx8X;kRPFVe%80*xH5FTi?6OLXD-iee$h%rnr53SnAgL;g z&mtPe3C_Z3t}}t&Q)4nDG1en$e=3(1X|TjPV|A&l%u1BWFYUk3k<#tim^o|@n%@V7 z0&T%eYU62J&5j*AZmQu*!f2t-k7e=!cn)pR98xH_=6DR9z&U(IrBg6ZbI7xxpl9SJ z3~cxq#G7Q3xP@O{YdgYf^pyz^7lA%y(+yZM*|ozytV3p`BH|zq=^5E5I{h06w^u09 zISsw7PBl^zzO?@YvMPO}?!7vrW4Z`2(H21M@ncjn^P56IV*Uq#*h1i+f_1drC}9+Z z8h~vMz(Tv07zKN1#_^-GcxlTKt#*CEa;hzZ>AfKVIbHbQH(U={A1ti~cH$O{?Q5d7 zq8m9ab&kpgmM@@v~8j`+yuWk+WN8royM^rU_Eq79uGW(Ta(RRs(!QkTMH} z%zi7bo8Qy!a>bDdiGSdKn#*45jQ4hx7RZ~!sJi*kt%ACgN0T|$AD{J&;Nr0lN2hXz z63=5BuAznlg=Xl&6=hj`8-_Ah1$U+U%7p!K!l5-D8~S9Z56|X|l{SzZZN8Mnf4J169i_%y!#d8~tvo*5h>ltg*(IE@l>-7A z!R^wHOmjX?=*F!EyCFLZxQ@4#c|RA;1*=9LSFvePkJn54=(_Q{fQ!)V9gO=G3KQes zERlN=PERc2d=mDMK#I>;k-`joPr@o(V#kqKQ8Sp zAL|?4(--`@G=DV40|~Rj3!*<@TXwS>yF|&o;_A$|TGDG-5P;y)huV11-}syPMDVY)wbGlfJAjltaxObdL`x9{$i}brLoOHj1Y3zY7p?*6AR*$bDLS)c_<8tasN$bipV`DGUtba z_k8-c16L#MfZY@f8Ms?vylvbfZ^_Zcs8rJnfF=ZDv8mQEc6K8PhC&X_sO9NC)^V6LD6yOLeZ zzkNyuR@$7Ifz|&gy0?HVDB-CJ3rOY&mp+(e>b1!sy`@}EXDLlS&@$lMI zhQR*27ra)*TX<}Gr$bAYxOr%~#GHrRj~<%wmL(Vs-j7-Swwn7)i^HS8xmyLCZ=63Z z68Y>y&FlqAw{4Ue>!X)xe<-fmG5A_QC`YH$$JlzrE0`G#dunuhGbZc@Ml!N6T5)yt zHtsn*9($8N!ho09+t7%k*LaokI=7WQ*m!RxQ9IKPk_}c&BRs)E#cE*5UIR4o2O>zM zZO74)g3$V#4^ysez4TjENNS=WAHV$4yfFMA(%2U-M}}HD`>q}K%Y@UDqgpua)5jVAaJ(gR!dH4+N9ev28t4dD1t<0X2FKG=6ics-tu22?5d8nP zp9^v;*v9Wt$=M&Jre&DzH+D)^egJWv^mGjo;r#DkRX<(KA%mj`vHuC{e@=ID?u%45w~&UkoZ#aNx!)&+wFq!ALK%WsfFa!r1abpem=;y4y~2e zF;_P8b?sbHed9osS|q-C!bc@)3(xXw=9v}Y`DCQ8Cka1HGS+y4w@kcOK_aG+V1CT~ z&Gv}#)!J`#eB0b7==2(DSVpnbM7yf8Cpl^Wb7V&^XY_ER#%`P{u*~T)<8i;bNNH`Y zC7cA#NqqFMuMFq%o58N{u2&OfD}$eccb6W8*=dhIXZ0#e7#_v>LwZND`Dc6C8i$Rz zC8^GB5HfRZZFE}aasdCdMx45@dqTmIe703I9iTFVO!*9HrW#JQ?%kZ*C#~C(H(Zz3 zWxR0hVG&_sb`X}uj;LyZ4t5X0K)A)x@v2axaXtt0maTNyxa|z6H}VLIJFy2(wX5Hk zbKJ7BB%7p4hv!iS>LD4Ez1DVK(5>)+0OA%>+#f6@2Cdc{2;8*@H4rU7gL_Hajq4S; zxswZsy-kMn-_v1Ect;kTWeE@O4FgHEd^{*hn6_jt(udTWR*zh;sXGT25H>}BLh_H$ zo4@7LeXgv(iTNrq5s!#4G~(5^72xIZAF8+c%UtKa6#HSdlf$5BON&Y+swN{hf~TGz z9DboC%{ob-`zCHus#w?bL80)JNxZ8cRNQ{fohQYk~dG2T}|8Bmw z62%`e@oi`X6I`i_NwLu7%SY?8zOJ?rpd_nm^lVX{jZOKR^C}%3AlbD@vI5`I){4o= ztoZzHH@O%Y(qXVHb;E<&-xtbq2-g#JaLA#$q%ScxiDRm#d0^npgT&xQ z`MRmbKiHi)#p*LSYoD1~SPgdJqnBulv{EkYM4p^k>>Z`nbYQ};`)1#G1w$*;N?agh|!8OoYcP2By$zh1F)N*pAb@F6aBK@(f}I(DuIcs5e8$uh5m zb4Lr;@-a7nZyHe`C3|z}ZyP)n(h_eIC9T$aO8cgjGqwM5-nKRFa=Zmcj9@UvbTf+m zQ0J$E+-pCVArZ5%Hxwgpc>?lJUk@`!yo^c!f~r%Ea&}c1WJTs^eYTr8R|*o)@bR#h zX3FEE_a@WgI+FFmQvU!mP|#%QQ9&qV_Gum$v8NxkI?Z?>b4gV!P?LAA`hw1-$Hx6G z7>k7~-4qDsA$mX9I+6;+QcrC)t$AqF=RnaYE)N-~+}mhj{(~j_M($6bi?2dTRog2+j8@1vpNa(Ki{DKroP%X@Y{a{!5X;2`JAOh?ByZpspc5wAIkmyM09W4OC09F-Ck06^HDSi z--H&IU}25hyE(n#OW|;}HPaLi3RhkFURJ0yC#9iTAT;+)AT5Nol-;=LiRlvN9+yZZDTWE7M*1#mQ`M^%amDS3wmLDsDM4~EbUi!u0QcIZhKxreY z@YSyp5#){ZP|X1_L43Xfgo^uA4vH#$eXqs3CyN7eL)o({+7gET_(+6a3lspD_gEiU zZnfsZ5GWr%3o{n{A`<7m#DxdBD(7Q7M+bYN6-R9$2C-t198?@|Q57tLKB`MJ)M}}YeAay-;Mx`N~=2Q;;hTs%KV%o zPH?$&?u!SMlnPo&(ivlF9T-YR0qgYf8r&HwG7&74i-F1G^vBz*ubh(|87x1n^BP43 zK?W?t*Kr_k5~5Z_-uUw6ixIi?4jYdLk3hB;lM-{w;v5`b32kQ86y!qVimkA@S#<#1 zNWb_TYnjCgJmc((+;T~_+8 zQ^wo(CqG!_l+YE0L-E;?E!EBr=f5;pg3U6vZss(^bE>rr2{j^@FDfoBE+u!#VZ(y| zq5lx`anBPhxnzlhFk#03S$h1cI97KfsserfN=sbT!;)ud-gj)Ht&nGKAy{WsC2ln^ z9CX=xUoigYDsMeVF3}r)XHwHuy`&|m5{6CEibo(AMMJ{mcY217Hn;RXm@TsczK{G+ z=Q!fJ;Hr2qO_Vy$|0T)f%-eZcM&&EBl>wTwhk$Wc29{?uBH4=V7F5aY#}J12M@MRBz3O zQ+DtEmVm&L?~XvCxrp`7B~vf$z2Lo-S*edQ&r~&>J)XTCjbEW@Xs29g_%K)K8U!Wj zi|5fyizb;n2V4=v8L1<83DD)KC=zLVJ*v;9#e&~8mRU}N^A3o;0C1jix&^K#+WQ&= z<7TzLY|qzk{{?QQJX7+$VPq+dNuu)Y%b`|0Euj@Hh84X}OOd>XZh+M3zMv6A52q{p z#V;3|Xm>rF4jzl+wkA_5!kWB>)Y9yp?I1+u07PbKw8TnUD55eJn660aA21|+GnHW@ z|0)mN1AuUu+#D5!14G0{HNl1x!WLnNAWpeFgIWt=VRn6j=}@&6tyu7cuX_s9+zAxo z91DY-i{Sk1UqCPMw%L+?GC*ev?vC0rmS>57wT&i8(K8hpfJDBO+;L~1=s}sFKvkT-f>A_4z6?J^Pd}`4oRfcr z6mW#}bSdlgTt=S!_{1r2KdM}()e9=S7x1voSRft&6>Dgng{$sd{oMcMdJl|2qo*H* zA`NsjiHl1=#@G(9m8}_8JevFed@o7%`D5BVB$ig-9-H}_+!K77`eua4i(CmEmCM!UOxymo#ws=q*S{*vOC1;TY5#3r$>`Y&Wi z!8Aj*Nb<3U>+}fa1yS}78C+^XZS&n~CS(XzZ+w;+gl%##8Vt}n1)gu1zZYL`O)Z*1 z3|^{t@m^Jae~&yupeBc)f>mDQSb`?NK$WXWu_Kmuk@a^u*{q++bAZ(~`8%j~Dnl%c zBH(o8>jjl;dp{hYjyKc?&w>)!?OC%S{T=2xQ zit|bo>>GDhkZkG47jjJZb+iS^G^qy!j)*XZ-jZb^tnZN$?j5Td8vi#^GY{> zpe~vauvy06=VAc)><3#ep$g@ZtCj&arH!e6M{v(uym`^Ji=uQKo{6Ws1%r+bQWUKAq z>3DCj{jH0(ygNV@fVS}`{kl9uTdh%ya-raNdA7__gt~^K?Qwhqc;du#Xgi)(4f&8{}e^17EXIJzt_z$6b1;6JKiRe=L>WXnNM%+;Kg*j~;VE z^h=s}8bW$S{)4ZgjSnCQP;nL6g)-a|~5s?V2uJ0`FU`I#9ee6RYycn0c zg0iE&V*3%SM$qcDxY^_BA0O7*8 z%SVU^Ad}C(&^u_UGZoEi)8)O@m^ro=(D8kInkGiyY&oq&O5Fx^>Ia~j5-qY(agC{l zGTZI4Zyl_*Z(q1izx!K_&9|sE3_`FWcFLxtJc9N5X|Qw@X$~qKsL-Jat@7?V$oFhpj-pEJJE{xqEX}@h|-U+ zV zL_qUoI z*8!6VgPt(+R8+h}B7oG2GagXVU8OJwPJ>f`6UuQ+LkLnpFqGDW)$lay#EUzM#fuV@ zx@FEPG_(=0feKlmW9^7#wW=R_dJ6DNQ#$k`xb0*-por`ktk|JaKw!0|8;F#EXi6qr zc40=BsY+ye;PyqS@(5Dpi-V$gmRym#K8JrodNFJ8Dr8*C`XA6QM(vM9n>i3ELg_^E zd5R<#5VRh67$K;~LTg9V5RP>vbb>L-Zv%sWL!~i|3DDRGa^O$$%n$M~074Z`(km(N z+jZUm%9m7=&+qi1K?604J{pz@AWoj#ON-G(-)A`}fr7cw?3mcX^t=6Hscm5d88^It z>bq$CPz$L4evL>Kn0?G(*A5(vku9&t-9YU2^@iUgC@*I2WU^v#Lcvc~Ze*J7O`-r* zuRH3#T^Z{B+F5I$1&d8xKa~7M-?IFT*SS;}ttlYT9AzEkfyfw`Xe2e7bGwMmT4QXk z3ybx|wQD<->f9a3!~9xsemDj8F#&y~0jLfFT~S~O`TeVq*Hlr^^jOl+pACf}^U^qv zPo=9P_aLANJ_DR7L(`wIl*#IXc9-%xhUA-QW7pU*zHMmol8!g{4Wc^n8tuBwx6a%w zvSuC@2fkE)^gUN7z&RAP;L@O62dffJ2p2{{HzpI`U_}>91il;5%ebvnq-q+D8GNnV z14TyD0?;)or*=x2k-BD=TbPtGG8*9Ue7cEly2kpcy+IRo#B=U+{GITfrC~2SvlkcM zpF(nCCT^r7V&>9#BuSbs7_rGbO$#K|3^1J}ZdG$AHCvV^S~PN;n7HO;4A~CF6u;+{ z8Jx)c@qiFH3+fhkXuLr4BE$oz4aJ>OYuF4v7CDFV=G3-@cswzIHZL@QxQWJ~zP#L} z-|-4`A=dsD(O|22oF~Ic`z`7%d=3(1h;u&7VZs$%xNQaMM-?DW?a+hY7CC)yk%npp zG_9%xUCSqB>H!pQuga=dJjasJ~NXgcydBKN=5^v2b}-IEt16kBnU1PXAJBE z5iBj)*;H|QPixC$2BdRK{7~sJ?oqBlQxwB}z!k)Sx<^C@bQS_(4=yf2YQ6y#u2<*o>;l8*C+RV5fZ%wJ(SUoj_igM&I!8RifLYI_HP1DeAsh!`le*#QbMGPcyIQVtYpdjq zx926gjUD7*!vbTnGerOGsEF`haAtTToQwkz!euTq!bV z&^oei;s8k36u7!oU!}Q+5@EO5b&KA~CeFHKQeE9vB9pw^ZK`*HwI4!0349^4A5<&> zMpu0>&3^9rKF`<&`(|!~_W$`ob>ND`S5z@ty$J!zpu6OWXUxs@wf?TKTjU`gaG0+W zmX9?9g8FqUB)-?~j3>uam*ZX_OOiq7H z*(R2y&|y9H5Ox`{kvDCsNk#3LvYEyFA#nK6-o*(5*n4mhStb_JPQe0bIe}5g^fd5N zdH6Rgz|oM0UGb#Vp0A?9d&^V)7#e{dpa%>;VyHliq|>RB^U1*7=aRM%>M%Bex@q*^ zyl41dlrc2b5rG7m2UvMLG!`J{V$!tysXjIg{e>nWS{T~fL7Bv^X(BT@GA+5+VQBvn zSS$D;-p+#;{lb1xQ?xAtiNC+Q9?#H#Ues?-jocM?K6*N?x#=`U1ArO32Z~0>&Jeu| zCg~AyLDI~yswiX^owdskc1RK4^eQtZ4bp(nam}KLLs7N}PkCF&^Ah{K@NHg*&PXdy zn@k0?Vyc|Nr1l{o19nC=?gbTQcxQx3=9U3(n{y?{7KGHG-XZYilY3dPb{7f(q`sh3 zG4R|0->`*(IyNn+YqUa95oF)m3T}|P8xdvDDxYp{MefluwgkWsy=l? z1t?TsR3&?RW;lxwnD}0Cb$Hl230`to>W83#>5}I}z=qG;$I4fp7?N9q+6M?Y0T80? zo+dHQ&rr`IHUO-{5$$S%QVLlcyIqbonX;|z9}@DP1!@5`48iZ{|4c(qh0_ARD&A9= zmW?n?>f36VI)v;UC=O8@6>h&hVcOt=3bYOv*wj&&fg1+un$hZyOS2q7X45^M&L#jb zLFMw%9eDU4*@W9~r|vcpdU7$Z#O7OIFw3d4uGFGs#G$cID*c~{`ZDWTzrC58r&WJv znI27M-;J6B(w2)C34Ea0^{(nI&RRe4K4L{%)XwLuzsv6H|894lzEW`To&HzXwd%VC zR|niu2t2n*K&RJr^hveFLXo#~H1DtO>Fn{{WXPoDQtDJ)vE2 zz0rhCHtW!xP*;2k6NpcfJUtb|hSb--G3ftjt)DQok-PdhnqNhmz2v0bGI^UZqrT!w z(IYBp;_}S*p3UP@x^P~<-kCe0yD}P`Ho#4NHO`>Fhl{a@JOUG=|M~R1<*I;8B`@n_ ze16h0n~o>^xxrRW+&e5X?%BSM8I5a^cgY?e7=qL7A~TO&;=EU&)aS?wj0|s~md8Vm zEr2<;kE-iE-LK_*kRHa5b*BGvxxq%1Y1I)?Hu`#gR772Bp+%#c{8+8L=LUYceR6X{ zlGnFR_}(9A`8K?fJZG_KC3!L<%$77ceZ_3&_#v}3YxY)};nSq*;w_RI-FqCnTKk%k zmZ+(uiLu&xSSQ9WjH1b7CJ}=9=dakWfHfXIh)*0suWL;szQYF0LtXxhQ|N99&+nlV z^DJ-jWel#%%Iq_j9nH)>B=`C5#KCxe`H|=Ra8j=7=6eM!VG&QyhpU1fp+SIf)rnD& zCek@0ffoc+I1F4HX4X2bW<5*0|AIc_W%b(b{)aMX0>n9%koLvh zg>dQF4+p;T??U3G?|9aqZ`W5JHzA88IdDhxQ$hCV;rpyFXyS@c%zx^`j{UwS(V&mw zn~lzuheVNhn>@x(J>hShsL@sG*bE~Szoj*dWV6;Y+ganaAU>aWpECGCc4FH*@X3eh zg~h4WJ)Qk*;4;WA->~Pf1bkNotxkKeDQQDrm%DX~c9(U_3sVqV|86I>&H@!?H#!nq z=KC_vG1wBZSi7*mZmNK^_l4KO!9`;Qk+&cnoS4{+-V&j zEi@c2(4E*AWdHs5ffaN+i=!um1-5?vDu9pdgI(gk5-0zHRlB^dtnCwJ4lH??RO`Ao z*#`|-vvhT2y^zlxYGfYk%;-DWzNzn1XjMxiD~`34HLPM09yHrB?qJCnG8*p$NBm?@ zxYNJnX`C3Hv+C8ivt5J8-@cU`MgGGOuDQ~ZIb0qSyccA+ZiKagKa&r4IVbs%*1wqi zO<9|jt|1RrE*TYj@|yCJKgk10VaWKWz^)ISaxRtTqNEQqghz6Y%4e|#OPfK64b@IC z<19lqrF=&%fdjarKKk$f? z``5k+1+^46gudFLjXzkbGbYY)snfd&W6(^4`*_2r6pMV_@lm_JH*HeppN23WjqTjI zpZgB$@bTwG3*zJ0;7&&;C2;-o4}x5TE2q24kr|e=)w?lkHOt}K{61X~7BZlw8&$J} zZa!XB@F|+VXQR#$8GrBMtEzw3fm^@oWYci?^puP=t&&hYO_g0%!BY< ze8MICz0VDDw*Y1t!F`O6Xa3E2$R1aWHsdc#Z&lBehCG9u{eqPjK8Om(Hny~ecAKVf!4t}A+QRVQ8%^)HRrG26JcyXG+qzZk z_dRJ0sRDu%$oErV4DNGuZVoh66RxA=k@OG_EWl(Gb;7-3aEHN!q6)7#@%_8KqhQj& zQZ?ANa0IzNgs0;>@bCob@^=9uhD8ffkJgKlcqV)Nc_#AcjD)-RYg>BWdWAn3qkUBeFc?gQgS13VQx+)NjVr(aQ_B7;*9aX3N z$f%Ar-Joui!M1>qWYADd{YkSyKE;OW{U|?S!+%pZ%93U;|4*=#vt6=oC{09H6Am%F zLV%=#q zNozRpL@*!HQ~`vCNuY!s{hR&gMrTit$O^5+Zz>wLW)ZUd^+pb)q0xUKPH%=Mp8Piz zEj~j_Ox7K|Zhfm~s6R1I7=Ei|OxXgayF0nTqp}3BjkFu3=ejgxK3>zzr^$f*k+^*@ zj1r@3YiUqrk|p;?KGGV$JJ?1h>;;|qNgxn@x_ zz@_O6ZCb#VeCizhes!9FC0#!Lg|ZN_Kz#ZbfB;UmQP#!p^-(fSk&*}dhJ21`?E;@K zJm$nfv{|r_o$RMCTZWY06bpC~;<2Kbtn;LgV<`%hl{5v}Y!fN(>{db%?pJvEKes)J z1pt)`Ar}&C(c`W72sU_DH#?3qr^dj?wYHL$3t))SoOQHdzAIf>OxhiV&tLGzFyzRw6Y#`b z06U47`5>M=KvJi1s>uagWevV zH$}iEY#6AQNBa1e%9DwgZE@R*z5}Mq;T6S(@mS;^Y?tJO9O#*|wV$DkU^hrdb@S)5 zOChU4c%OnBp3Y|LMeEg$S_Fkbi&XseDTz)$o@)Rms9ugH(cxD#J7i-gBfUPu*f?`h z|N4our%mxmadijsdaXFTHcdLf(`NQFY&I2lGX+E-i{K*O7rtf@SeB3g8LBLU*>>G~ z{Y*z!>uhm%Ght+IbZs@xmW}fw#I`%F$!n4@)bPW{62gM)*X~mH3d%VIA*&#}(JgK|Y5)MDP{D1L&d$ zDc7KD-?dIgaUFsPg@NGg1hM-C2C-|&cTnZ}oBRo@^8??!Xo`|@Xj&52v4lXhhNl9M z$iemJk^wSOj=i3L3oC0~QM>I(jS0M2R#)Q+X%2y3Fbl>a!5a~-Hp<)6m)<7wHT@nr#s(Zu z7{D^Da;_n)?=Lx?+nXj~E8`tI=7spg&Jl-fC-K}4x9(D34b9R#gV$w>l;NrJu$K}2 z4?Nso+phl!16;c<>hUUhE>8~X*|lsi%-Fxo7fySqRr8n!wy<)RAHs_u0(2c^Lm@0z zb_)VFUO2D;6$xF3s_gSueZJeP@9!0p{fqq(e=0d)8MKCK4?}BRJxC9ZN*ZpVtL}>s zMGaUhx|&wASKGVnTgl)>FF%x<;o;p1V)opgo4}Z%wD2bWnqJB#2#7Rg2-C;>3IS)s zTwBKGCoX-meBw6TBpSke$k9&l;X3+WpVF4C+ID3SvQbeC>$it7l-%I!AUsH|sLrPz zqsdkamI9T&u7V+iqtnHgk&L6CUXalGXVQ{s6Kro}@PcKktQD`HKxPk1QdLW6H{o!H zmjF-E2h%hn^gg9hhfPi}Tre#uKR-6%BNhR}dYd$ zoIhsml86-yzf6Tv@^zLFJ6m?+}#n}wxrf}{?L zS&`!nicwSm`$1Qe2pcyuT)5OG&J7u&y}eglg&%bYl`XDAO!M>MvpSsu0PDhLs?Oa0+*N% zIpGs-(;Ehn_!Z0|0GH25kn=2&s732QHa4mw5x(X`E&|>Jxq{i8Ve5zCZhCnLCU6d1 zCj1|T*rV#(xfL{Y_HIwVrEkftaeg(5%V=OCT#Oj1WCyM1Ueft8db-#hnHxMzvd>H! zo&;6263X2Rh)*)hW?zC`hh)X=V9#;tAh4Q@`^T# zy|-F}=nP8eVUtEDl*x775P%4BF=N*#DmKG{=3+_e3soTagP%PFS$F005)NA&m|Hu9~T?Jw^65n#aLh!)@Amr|%gfidio0{m+f+7f0 zs}Rr^Yq~foi;PE*GBca%LP7HKaW-=2~uuTDnbZWDz3#vai&qNv!4F_ zSbtn+BeEZOr;#MEA?oHnlQ*xsin2^xoE(=wL?BdlMa+8o$nbr+bWuXQ@8eWA6wrwY zIT$rE6-jkjz|Aw;zEl}?xAyrZRrF)~K9&D1Rt0Ar;#V)5u!HPaCoAM#8W$HgamYSA z(3TI=J%T*K(ca}vUZx&o7yOFbO<@2`udo<8)MYjfU1`J03JMT%ZFm_b=^lBqJU1a8 z!V#``MBLO$#thZTY69EbBqITy4PtgJ!7^sY3VQlvIA}VG+!D;?q^=$J17mX`0f}aD1{haFX*;=WjKubJ%j`@@b#my+i!(AU+%`LqYIN+l}4Vs?=El7RRsabGv2gW;G zjZy*v5Bo-s!wT~301@`s$2zo*>D|@HLk8M6Znh@ef2V^5ZuH&uWa!5#*rhXuZEmUl zP}Fnd?6WU>Pj{%ai0ey4KLeujn8wb2`@Xu1V$vt-7UbY-hG$N&ivkQ2ec<)EbnaOJ z0mvMsaxC*Z>v#L!c&@QfMYp%6@M0-=@_VaszhF{#n9n_S#>eX7x$=w!1W@cesXLb2 zA^O{U*`dHr@5YagwugKF6o51csy{g%_+mqL|FK|HuA}Q{z~n6|v=W}P(t<$-!Q9a! z62U9;j?II}VW=^3JqzMHq{ZN|G<}LxvuxE=hL?muiqPUK<#4t}r{pVSWf z_a>8u%l=)+`k#MD3gf4ED69Bs6qF>epGHB6;-^tilK8(fiY8XV#rZf5aBV!Mr;l?V z`!?hc%XgEt_2mCS|MdZdHrT&p{KJMF?BD6_rgm|#e}!%NgX+>u{?2#l>&btRKPmqY z_Lj}K*8hVqblPu!F7K!Ge%c=P(=~numjCa=^1-9!(HOQU<}Za^>KOJjgZ#|D*iU%y z6CV782S4G#&$mKuY3wI!c`AhMTLhNq6zyA+WekH2_ 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 zcmeFZc{rBo`#=6P&1j}HB~2bBib_UVDs}suf0pBW9KV0Qzu(bw9OYT=`?}BTTwdqvJg@7%J^oZtSi5@j zY6^w2_PC;)8im5*hyMM{1mD#4(<9-}FIN@MT2UwhJm}vF`$!pE3S}GRxZL4W_Q8Gi zD}KolTAz~>C(@W*{yr~!Up=4u--kp;1>nSQUvbH*tG@jPJ`Jrq%Ko2U(tiH_-FF>O zDBpGPT?YsozWc#<9eno#JWGC$h3~QOJr=&l0zQ@A%SRgg<9S^?a!T+!D zfaa4qd&_Git5tkqyxl=~qT#nSsS4cnE6h_~1_*QtoZF^*^ryNDtJOa5z8S67YxHjY zBl{COcW>oBru6t9|CiR!GEe!-+DIHc;rsiVV=N0R>>z3X$?HFHkxz{DE z@S|19XxA0mjvp^9MG^7t-}Qbg{_{)4BY&dS*f+0jza0LrO|0(&VmkV+Lre|d-3C*` z_ejCi@EvV1HBi2T!*_7NJpujy9UL%SdYHZ zVm5omE068q9u>C}n`r&wH9Ncz+h1EhFww5eIKzJP-y-;j$NQ?D6(mGd1%^p&WMO&l zu=C;*6TR7xr14C_A6WS9I14&##j2|(XR>~qGV4$MaHdaL&e8T&?akN_vkX@g$v9_g z8R>>v!PhGpV=u6gjR>_%L7%>AXqQ7EPHi^cb-LSC+>=kOR^VbMMAS&$+%|^qi+vlq+@3sNn}{{8$?u z#HiP~wq9&8gH=qEbF7WIxo(prtHpqIqVeR!r!QZ+3(Om*_eiHCopV6ji}06|7pbpR zWuMi@VNQ7o8cFe;jSaOv>&&hcR&ALyv`Vk5Nj!f>Iq8|je3C}6xGuj`yW`o+#4Xtl zqsV-E%f^%YhcTXFG-2-^g$Hj%hUj}seey@z-(8|>Dyd%M)|PBY8S2PPNgW9v+ek`7 zAsamULrI9@1D%mkIoFeGgWNqsawClo`J0FxXiXk;(r{R6{~T2z zdO@`7Rp6v>dd5B(4Toa8wDbfnwzzuFs=9PE$}6X9B4xqP144(D=PcR}>!er;9!Q2? z7-9%LCjFSIh%BA8kG)W<>Z5vAGP-K4oebN*G+tl6U86;XVSV|60`s|cOO2^xKNsTz z30*A^ePlHxo_xjCK>h)K5&h1%ahJj|j(u!~N9wq=?IBU7j9kyYBT) zP7YMl3 zM~>djC$zrNvGo#%$>|g2T`@u3TJ_*u5LF-58Dda>@c&4J4b{vu#N(B4kb;2iuZ5r0 zQ{!2(eDceXF8HM)+-u6YJc7EvX26y=;x%)EG`hj2j=~mfqRzt;k;hN6-~3W)edW@? zRb@Zu6*25|wzrse&*_8`Q3LOos79LM%;0r-n@=Zu^_L9|}j4F<@tT~O?1udo6bTi6ypw%1stIzmb; z_~c&3RUK4G#br6n&)vC1cT^tlDS0pM_)OPT2^7!PeA+zI|HRKtz#^DZ5{DZS7 zKxNQf?%#bfTox2dm1~sBX`yEVxjX|p3YE&KXZk;X0HnJY%7_ZtBP7bs-O5b&@S4pU zJ+x?(s`h)r!s(3<)XzQ1>nV8AA#|R7$LlD&^cbgKHf_t#HApa$oskpt-L%4F6ZP@2 zk$;(X`K@bM8F<}(^A^SD6n zad@R2S;;Hcl`#YN0;!Kf*Ck{-P*5vbou%s-4tlTdG3p`HHH9KiBeUhq6GF>| zyJH0}Bqa2ed(YK12H&1eRqLVOd6A^ysnuaf%mXf|QgD19e&v~qhN<@@&Obf(Ia(q8 z!qXdbJ$vr8zP-HHrl9Dl#_Hkv{O-!7l>z314CTGAzW>nUu0(y9$Hj<1iIpx2d-?O; zs7S$SXZ4B?bujcT&cZk)AJ=z;$}^ZqRf*Tr3k!SR1>6X6DBUdG?b&bqM2(l!21O_@ zSOV8gxZvyRUFy7aI!xt|orQ!A(_FEV9e0@>ubMIa`FP}>FR^ONRC3dgREa}e;u-Jh zgN8q6zRL2;2mi5Ql0!m*KNS?Eo60d#I!(cdjO$QG2c(T1{Zf)!L-H+qm<#G*QqO+l z+CQmn{IGBMS5m+n9*bf;pU^@tAGn>6fSOdO||x zL4OH`I%wOMuJz?i+!Gb*UsX!m!RzGqj-KSYQDwSuZ7}m0hzx$FC2NWAic-c$=!%X_ z((3u=j+l(*?M?xes51RhozJlfNi94bhtvBKmc$c2*uM&sRkj?0fr*#N$eB;HF*nE5 zT;-VY!(a)+J?}y4gy6_Baz+$=ck%Z)a=|&Ve}HKGj^!$aplcf@p16kJ^CRDjMvE4F zndmsx^CRP|o9+yKKSKIO{+3|%Lfgl-BusK1-l&EMO?(?396^jzTDno3C!C7i*rK1z znN+IaI@UPR;rH-qaIIF%wO|G>yt+f>0j`0o2JhvCs{>wEvKyMX#BE$9IbO+54=gh{ zC~zh8c*vVVh&0n!l_$%l_47t`Ton|dxdZoNqFDE`IN8$vU=R#fbS_JL zE1QN%wCL2vrs+3z!|&N#?n|s>GK-XC;a5HVDDhA4&+_IjE*~WarRV|XSzKazi^npY zMx71+AeB#NCSxnO2x3dqwg(2QMaRbv%#64hQGyKBgns$wMpx)6} zV&x37C!pDLYhET1ok=mVCZEB~5%2SivfySYX(N@k0?$qZmpDI(8+xM=VuOX6b}k#t zvuA~DRv&$G@3)pDllRw|#fG{Te6|c0)P%Y616u!KoWN1{Kpk|(J3T+x=(xTqZ6eWr zx?@)N1gR#f5)-jpX>SCPrsL|e%Z4ee#oR25ORp|HK;p)BE1P(h&UDsjQSrZJ8Zubz z2BZ{2R!I3fD0U=gjyC2_RVRhzCuc9(_Atn>_$C<*JOpS86JwV9ZGH&*>SV|LS}aP< zSV-=`JyAoEqiw|y|38c=0}@na%A?DQ8H%v5fO#fDL%g#ytLbG1E@`BOn>`Qd#%fHe zF-Q-jJDtzMmwb&u=K9RZZMc$yO%B{1ZYc2*KkWbxrBG?Qv9(G}+C6=e^Rdcx*0bFz zC|{N)D@ChItHXs0Ecb_+hTL8pv&(j0H|Xjo@e6r9Y0d36#QWU85HMfou^(u$er4r! zJj^NOrMA!I{KCTAi&zQ_E&?di{bIuWezY7MI>R=W9YGdD8&f*D7Tmb!)9OEK#YCz! z{|wQg0~ob;^ux9=Gn3d&AwRYLoclEK&jY4!{(N@9EoHbR^^9JzS`>$NlA3#Oc{$aZ z%(UBTf;Z!U++2I$UgA>?r^!E&5bMMB-=i=R$aCcw8257?J#JB%X#|LzOdacJ&U^%k7SG|E(+T0 zYI_Uw>rwZKDr=^K+LbT*6s}twV2f5Sx|6*S!)x#w0qrl9FOM+n@Ke^G>8Ee!&IjGO zw(f(Oe~atfAa9Gz!cgYa(1JoV+yH{Vnl({`K4-;=9X-T0W@AIXrS&Eig;5WK(sF$nQugE^C%oNEiUiUI&^;$&p*O59Kw>zp;upVYl3X2W#w zMdrcZ0uDfq4_`F^9`E|{8m|qLQ=Dpn5h%UKFLV|`M~tM*upOyVie)arT?`HJpf`w= z{4nbIT*hJ4#c9yruBCl(c0R>w@UZG)gRj`C^l@Yx<01D(kj}}blNW(cCEt!pn5sWp zS!7xGtqI+^zno@g&7;a_s{4x#MZ$!BCoxnul#1^K(0>fjmh$2NyQ-k^%JiHW6w?q# zqbNQF{6s84*K?gF?QTNqVheqfE&Q%d zzyqXgpN*ZV&*mEL*}?^JruX?wS&PhEJL^hk!B92lp5dt*yjdFso1A297^&W{jLTCQeX z!K=F9sWvybSR>hJ?z?77$yuGx(NSg86mX)!j7gz|5*epJg1Nw6CgF=Xv!LApa_Y&p z&@$PYJsS|E7BzQM_2z|zfOsQ|$;KN~t^;$)mB)t^A+TzJ6o4T?UktZ)Tk-UQ+f;A1 z)7#yZKT86{6h|JJ8ya-EBjF}276(S2D&|tni!pavZ#M`SNxD}~phI8Z=R}u%IDb1D z3p4g_aIoG~9Ifu%h5>DpD}STQ+wdQSsWO4IyTMNLhZaQFT%h0}~ z>8Ce8D`YRU!f3O?tq9Xd$@9t=>83KG{u0-PFDQ3}l>!&Z>`wTZa30ysDsI>;gkHU_ zzC8RNlYjSeMfq+8I@dUV$X>1k! z-Q2x3G5dM`B^`6euziwN!-=i9{Dn3^RTlfZf!V0^@w*;kaUIGFz%sPT_3~3CtA_`( zhi9WjyAo$ATSI;1D_y5bcpp{G0pnzPJ`G1AbO8e5aq^mZ<2rB+b&LK*CkTA$i(Qf<*B==Eb zSGLr=*yX|Zj#pgb+LC@FMy6f264{F0(;kXEW7Ft_S^@Oiufb=tdhXYN@WK-RkhIQ6 zEFVlaVg(b^FVlmn*l%IL~>?HfhCBuVQlrycg#SP|> zJJ8cEe~N!W(mS#2$UzBDsKV&G4Mh~h>UMALBH=|pJeU(3SAREiH>}m3p|q$lKiHipWxFXV^A;IVmkb z0{3TW)x?u@g!?V0x)Un-RwjOo%`=*?C*zVBjLe?N5ysFbjbSk{eA z;63R0y`y`bnPTq!X5h?s0&;I3DproQuE=c}$?r~ZX=wX|ar?f5Aqtc40_mLrcY%RC zRv|~$R~C21Sdy$6N(8U2?Xr=#9JPwH@7EIjvX{-CJvg@NUF9!rMRt>$!F|+?hkgQ^ zr@YXDYpF=tS$vV*>XR3!Ja~%#r=^;g`JYb)*ah0E0b`(af2!ef?M}^4rwSvfEyW@AJe|7`ko8yF46~8X&;0*R5b}NxL&Y9CeLRGeFNb=yg%y3E=|C+Sk@%-d z$Q^#*XFzqRGvR}Pk%3o0Dqm3~GmxVmX6aljwkRpH=_A@E`*Z)~2oerj5L!=is+?`& zd{{Wj3S2Q)iBu_yg&ZVa8w>jX=L+VO>ZoH7TLqa_TO*A%A8rG=8KtES;!FpCkdb!dzt$(}l03cFf&q)|*G59K1Cv zD&Y1>UE74y)hS_cVE>`XHYm3nx>gpxo-c)Z%(6JJRNs1SB4ZF^6$+epm;!I{4@`NUR_&nju+^OPeYGWfI%5|E|KzEI)R)7CY))_TcH%e+#!^^1y%2 z?Hku)?^9on_<0T&o`wLw>bmQ|y|ve`ieS=Gxk8JTQiuG877lR|&!SrqHCgvE3RvFMv+a zIPix$+Ikc7^{bXl4{L!p%#0QosfiCJVU7ngq)A(HW9f)i-?b-=qiGHJ2+|!iy;fee z-cnLk`3rNxjRt5NF}-1W2rkl?_$gYH$`a!Ca#GLG6fl%u^5)O#B1L2-kj)l9PDGtG zn`)E_l^yhyGW_=7%5@yafwh+XeRu`uIe^C-4QbnBR*2W`UywD&9Cq6ZLy_qhBd2yQ zK_gGG2d+b4kjKm++7b4L9CM9x%8E^b*rn6J)Erdat{A*SwNc*OW>Ya%?(5U&@7iu< z;~oYRtq>&Yup&2b2t@A+ilQ}ETg*?hv*k}KCp}-^dm>A^%L~XrZDX)fi15D%3=n8A z@f5e(I#KVw3xKn}>^*uOa}4-zjI13k-yADmNPDU7Q^FPn0ecj85Mf}+d8n&Upcs9@ zO%tgR5f4#owDkQIoB_S}at7qS^g1 zr!V)KZ|eAU_Ah3$|%MQ>IE7WDG0gmA=B zOA5**`{&mD(d3vEs;~+d79+ko=}5eLs1*DhU5JsXUrb}!ne~MPo>$W~rT+FGkP9AE zd0eK?d?mIDV^Yok(BCl!3_Q9cL#olCgdo!IY&IcpHbJc-)LLU&39LyVNc7M0bHN8V zZJFQ8e;5^c2k2kxa`AT?CZDIk(qKIrKM zX$k5o+-*cb1)#h`g&ZFs!vGz5u4wGc=ZU(HHw>Y66^B+N&=Z^0J~>6Z$U!sPHgaR{ z-=&V-Ul-zL3F(tM#l8}Fd(V#r6{ZnX%}HpDSegUu**N~7qfVjV&jq-ngUS0t;n2Il z(z)PCi`>hRl6M$=JQ|AuLF^yX1KLXFblyEJb+0>9h8LxQJ$w9GAs1J&r-H2v96!0Q zntqYxcg*-h$_F8#U80+zA2#@A1tDY?JJH42(f-kB0W@Xb?q-EzRpsG2tY1a70w)aB zlgq-QC?0Ku>NC)s{w#3uAdh(e#pbb%3y%xcy%5=z)+hYUIfw9p3Wc4T*m(e>t!S zeLFCdMXC*AKFKa*PoiP6T?<5%ueE^|6q0ly8_3E z7G;7S1m1RQt_5(?om_>I88i?uTPFGY$n`=#wtuustp+0ciq7$ zOYog=4hZoxKx1JE1*??6G9Sdi6HmrY1>XCbge`EGq%_gry%_PEyh^&?A&ICA?7iaK zsiI3LucgR{e&ku7+tuA(diAHH-Nh=-JM?&Aq8NXpvBDwvng`GOKxP(DgnuPTBq_|o zyzZC>Dn3f@Ix_zh4R;}mQ`;n|lFzB{Ev(G^Xm12c#Ms@KGSn-OeJj~3SbI-=R+N$+ z%?o4>>w>_$cd(kQnsOp6MlkoIU56GT%w^N~(=U#;62ID>`$y#pSRzqiRx}Smb!I&^ zsANpjwpVwS%2!uTRqRQEtm^e{XbXyzC74%J*@Iz%&{b54{NVf=kkpH<+^ckbo>1D0 z13?liR7|074(O~BVqt(cX`x#bs-;()5!L$;D2>jU*vPVN#%Z2W>b#^Lg&qgDFrg3n z&0k{Gp`t@xU zDCv`cMLAkbWRR6LXZKkP3&7&waFf%E*I_|QArz^k$-c1)X&Y#%qJT6xJcPgG3hfWLNIVZ}a34f)7!jLQALWwz;ZL1QZM@PO$P+P~_-vaFlAe<1|*x zuq2s^XJttewb2UanzOsNh&cXOU?l!*0I0>%OfQM6XyZl^L5o#CZb*Bf<$0rFrh@qh zbHp0N@x#p#Tc}^ptj@rN94cKh4Q8-KFjRC`?w$AL{@4sii|7}L$F4;ma%7Vb+W6Ma zsBHr0lfN*dkNFLUXbY$vdI^zbQ#xR0PEWc&5H+CckDczoxKM_}+U1L);pd0;FH3CO zTOZ?#04uA^Qe{u%K@3A9XKcK}kPR)v<;Y^yCiI!ld^`_7?{;t!*2qe)DQeh#nneE{K;sIk?+Zt& z8Yd;D&$~C@eQ2x^`fOQ3aVM9Xgru=hDM|pag(Nri zr7_cfw&7N^)y?8hC1+3ZyLeMtyP;18^&-b?4e)d4rz=pqtDkGmsZXz@E>;aPk-hU1 zWBMx6m9BF5gh)9~L{#E=hJFj2!xQA|VYVDlFmLpUcOKQPPo3OmQ(Q4sXYxMq%GALh zC~diRE48c$6yZR9d$SCCfiHXY1QilK zh6wFiDH{iNABXC5d+ zX`gL~*=D7&@=cfC8A-B+rYN*+#IbAThjZc~64$p|1ESVFXtmou{}jlt^(LJ?Oxs++ zkuM#%4ao__zaa#9J*JQ`f=}6TJOeX5YCsU6cn(`{fMvndSM&6Qt(1}8;=l|S^F77| zM%2KvN5Y3m$+H4K;hWl+|2fRA0?iapq)x4l2rpGwRlw>*N{%sbIR%$F1FWL6F%)4g zRZ9MMi3e_Pmc!+D)+||#7a&Ctsx1X;I_a#=eUN3^Z77!JXu;-4lat7TDXkfWJ9x==vizb%D%$u~dJO05z{6n)%^_I390D z>{v#I6%kuMBS;CDEEF2gL2AO+-wZ__zy79xJZMbN*R0+>_tbfX{>He4-626MJ=|Uj z2kLfvm#z;MW*-SpY#!7kC3cb~$m&?bT?A$Fl>j>^4VM&j^7veg=-MDmIckii`_^M? zU$xge-!(v~Ud?YM_r%c3g{7lnh&1|nw|r7d-G?!>eg)1>fBzJ=nB=-{=b{aZ0tysZ z-jis(1H%|P|7AbRF7*wku{1rTqJia6f&>bKV;~M1f!v@wh74ee{SHWWoW(1|4Mo~M z6stF~lF>Bu`R3r`P1wwiMm9*GX7ow2pP?}Mb`pzH>BbfM;(+(HS|Q3I7JCYeieez( z_Mt8ysgpK8)8J@QlA_jUp zSqpR87+M_kc@Fjh7-t(?(R>nRS#EB$?t_}C_0xr`EJnTb@)}%W6#`ajKaSyb(+g%F zNWrjdJ-2UabQVwu8z%RGq&hn z#+LR1)(cipEKOdYq(efNShG0RI2tm^Sr@`y5tsE3_Z%A(;ZX=!ErE+qR3nR4n9vA-;`uR*; z0k+sq_B$w+Z3&6Sj0-@eOs79^igD4d7 z`-1WF*{PgL825!4>%W*&Jb}%8=23&l&BF^sPfO2rAr`GmuAFs#05Af>I342rbawZ; zzDXvE{uo+maE#A*AVKnJTChD@^Wuv1#s<@W1wcNRJGm%i!^BY%Hke%%a3TfF5A#l2 zlg;7NEpQL!H#P^oSAD1X_wOfLO&lkeS(-J!5=UHmD2l5U-u=ZYQb3Gbd&{$J9=WQf zpPsQ7?+92aGHS&M6lF})4ysWSqiGyuF;i~CrV^z>LtWKlv1;z|gW11N=G}N^m-|jwxhOUxm={mRGNO2s*!`o>dTUz2e z@zn^wI8fwe_-cRyNBBCpcBpRIqr|HcWnn|yn$X{8VSxs-oE$O%@^_2TaXn;zAJ}tX z-=f>)k2Q7BgkfO#V< zM&qCdhto4*gG*kRY2)z_(&uHlyUnm&x;Z~1r-tny)`}gPd)Kt5EX>i*vqK{T*an=s zp5?!Fk=>867CG%a^bjpz`%g_+*t`6|()d}UGaWMv%KNOU3obdL?kWtCQ>YPof0=T* zapg7#Ldrya*Wkg9SpyAF?c%hSsDNIuw^oPt^)WrdwJ2P@d!Pa)CQXs$HoJq~=LqY- zt}Ma+{g1TRL(OB)ory(jKiC#NHw{{y@yyivAJwgi`j*s3`0j1u7OOaf_t_k!mmwt?An^ zAWsve_rPh|_;R!X(6-A#=4>NfUt$--GG{%#W-=xZExZ?EXDSb?6{*%Adb%q89G~sJ zxR0Jy_K^+Gv{+w#@!Yw=IeFxBAr>F7ai?LYsWeC-)}B(!%OHgm^JbZ+d3a!2{^kvgTXW?J8;I&jpgt4Q4vX#blVqSsjBMby6z zB*UXTP{w)gKuhJ)B9OobhIZ$gTX*Vz@K8_)*VytdSAxxuEG_4@j%{D{aZf9AD zc~s~TgDg8ZBdr|1oT~|F^hp+Y{v?!~QVvs5B)$tiM1LandL?U`67mGQ_W*(I)-c9w;3MR8&f7k#QEb!uES>z;%BlMH8z z{RY{mmK;Zfruv=h18r}%&bY^n-TXN`#e@p@ob5or|CBCsrEx9k zagGBOd1tocr+-k6k?BxnR!Hho#Jm7WFJ?7^0)ZB4d$ox*n%77JG+5KwW6@U;?sNW!+&cglM5jcF+7Aj+8(3vlRZ+;{%=|5OKjhOapk2?>SJa%hgw# zR!#V+tCCl4ULp0`s=&^9977AX8AtR|jZfwYsCI@({UZvSJp;}tv13r+v# z3OnnuXgSwVDT+=(uN2kPN;GlGw%>$~VC#>q^!sIY11E;f#ZtJ}VuIqdTvL-OsuOqdJsLT{?V<>_mpI5pO^v z$f`aYEQ$)vVLPRiDt^s=H=6YQyZMz?Q>#fwQ`C2Qwcc3wOS|)SBptkDSFz#_&g)hA z;{}PTsY}~CeAX4td3=33>)4}MWmz@}u^_u{OLTi{6>IL&O;B5v2-}SLtx???{|&~7 zNP4Wf0fV1XI>=a3p#9pu36oeVT5aUgb8U`#O)s&|9^typic)T>%KKkOhGOEZXEYkR zl+E5F5h+{a)r#}MF&hJ83L60otsHc%UIt=FyYm16ujAf%o9Ix5(D~b>OzJtCP6OR`%Ntd&3-5RHJJ~PV!xQ1(illrw6`42?VhI zHEIEAxa(lm%R+;*39e}z#=4EaCC09Bjen4rFAF~n99t4ZH03;Ik8+D>KJEWV7QCbP z@7Adyya>Osbb-gbIBFXm`HvjIo2c_M_*8R@J42YqJBee9m)WXjB)Zc`C2>3I?&oO6 z_*o7Yf6zKr;?0W$K`+w-yjX9QrVO_X4A1CDd2iipeeFuf)H~#-hui^#b)|&+oDX70 z!yvn?v@qu{dUa#=8G@{f+AE`@tfeqQ&shoAhU0@7_w&iBvS5Bh5&2JJrB&zER%0;N zb)+@IP29$4_c&R}(L%T5lNqCf04R({d2dD_K#uBFo^}O)$vOH(=A8HEYS|t}&qE4F z1+WI7CURC3d-3h+<=vZMeEdoVkZdq4A%jG=;}=|mQYMSm2T^Fid`|9l)S0<}Vg#XZ z5e6-XxmkOZm3_XWPs4!&Ws6v08m zOz)E9@+g!jBlS?^KK$W`(_n@E(wu?_qYkmd6ar!WGt6}n& zr>EY0&?*h14n&ujkw{eZ0@;+Yz7<3qK1BNuoK5_SO|n+;y?jE{+gQd#{=msq^L9KN z+vS^@YL#Uc@Z6U{;$$!4m{MUCYI)dW?Vo(@Ep1;PA}mMfUw&;zm8C$a>+8+trB#Mc zs=^$w5`wti5M?A9OaLx%G3MUu$Dy(}y?izqEKFSu*1lBO#NjF?AQ&4NYTeY6>|n4H3MHeO+D^aHT+g>vxlq)g^!!LoB6G5pDD zs%cRjM#A9MXb}*l!f*vI0ute*=Ud2gDr!c{g%YEI51zpZAcE}m4 z6ih749&SZ4T6NYvm`Liyrwh67!Nh(LviUH*uc)v+GHEICriJ3mb2+nMBsHU zQrGbiMw!q#H07%g&|gaUN+S1$x=wgZ)d3rYNl(rj6O+bm%z}9Zc07fqy9)=vio*p8 zYtUSQVYixlXKx7zs2-ShzkQM=?1nyn)r zZ($b%d>1hrHO1U!?0G z&wea%(Ny+T)!ti*?;4!_EFX?i^z%~Fiu{2_CvSF#Z~hmVtpB7AA z15sq!fjb=;z>fWTfbl2;*jFeVx|#p+l3J^lv`6zk z$#?K0!_#&LXOUww{*o6#UxY^hhe9ZYy$6v@RUhwRv;_Mp?H;jXfgA_^elryL{qk#8oFu8u|C=#FcS}|? z6!sz*-_wEylt2V@ndu1%LrQ}FF83nF_k_F2ss>RBE9E#p1p>=U9$~!`4?qW{q`o_7 zlb);?_4AUC4hhvWid*uO|AU}a$%IY|Y07Z^W%k+{^$nQE?Z6?KF*dyU-zI_`XZ=oaAI8S&vJhz zzyB7_)ROw_87bw4Wea&7N|z!@*RT`Qyct&_2Vyv3g?i}jYde;sovdvt)}}{xuDj0= z9y`;Nx&*P|JS2_8eyXrpidM!|mSY|H++~p^!mbO zkmM}&+K^s+)SX}$-O5zjDvDrtyuBwrC9<~==eIwmqwmI(UZ?_Y6>aD*| zy1fI=tcG%S6|5HCf0lmHP#e;N!Q+XmK2$_JV%Nl249u;vuzk2yQ`0PVR1HlcT+`{* z7jGYpx@0}4vzIzl2Mem2vi9yH>7o%1O;`0`OlaWk1ePdeE!*JQs_w8&62;19oVtkN>EHQu6}xOZ~L1r{&n)x@tcRIUMuAl zCbvXXt!wG<&rcaq&32y{;dSJ&CZZ zd(qMGs_5mcOSS4Oj8;IOx}j$&e$8k%SfYn-@(-Pxw-ArTn!@Pk`l#%MrY3K_d6(Ve z^i?mD4y{k!tKE{jMHAGMpJI!tXZsh;dupnrsU2vT5!OQu-0FCXDMGC{GIXRO??Q?m z&MF!rx7-l1ON{)J{`R$!ll7?lb6rP4Gu>%J9mh_w1gCB%MpVcaU`{RTBMI$*3Dp4g z@-vz(B2GcmthuOqDM#`(OV2laHZSm59NrRbZ0cTr&*@NB?1tgt*p%_Ft3J3aPFl1t z&WmVl63o+b+&O*-odU1$6Bx3~`T%W59YreF-p8RYALd<{;>I*j8yHy!%dcDu2HfNH zKgPjkZONNjt9w>iwE8S^)Cep}z=(3Q#cE5kvSHkh##zZP93;3QU^`B{!t2Hi{_HS& zz47+RS`7~$vqtx-n8AYa{52N+c8eSNJ=vD@CPByaHWsMs_0H)|it-?i-(3QKi%cFE z^D@t`E|%S+r{zB3A9?0%#(Fpx9)~nt`3av9h4v3oNj^|e?odZPl$p2s9M0#38naS1 zN4dISp=EyV%pX!~(HYIx#|F*h`n=r(vqlsA^F8f;5akvnhN{rU&=sCxWBF>=E-N+| z3>1`ynrmmZJUzsOLhr#IC9awl(@32IibZDcFzQV9f*OmY>1$J&Kg972dxiF zU^IG6%?Rqy_pqN+)xj>?wGYBAuevxV&9%|-*u{H-A&WYfNP)VPG5_qYm|wfK3B=7vLNp(6AgVO=B-wIc5eLbDbnKFXV;C>B_7aIMKZJ>tuyG z{4iOOoz60!zQs;AJ}JJ*qXa>FyXTsOYTe^0qE!y86DQOWTQ+ScQvKP*k*!M#`-npN=5bbn8z^Z>HR@`|)K6qhzT->?3 z9ZN~!b{l6+PWHv;Pw++!IGJ7;Hh_5N0tXqGBZKleS2F5sZHs_L1f*5MVIf^0xB`=}$THxWSn3Us?M&G{+f7ciODFfD@HJ#wRNDgN2!H z`UYqe<`?Xqs4*z~^od2-xzmuehyaS+23bNcUHZ5kg`t&1ugqv4Jb+K(s<~cM7Dx8X zJUGOTYSJjlxG?=WrM!$}DT~oAkU+6&tyBy1X==>qP5%XJWGP577qtYJMzTE)hkKG8~#E6t;Ihgi5h(k z@0eshIdo`q`_~Vcd~(k*pWCCSR8LBx)5s- zNK^n37%n}f7fkOfyzs1DCs^XpM&Jj;g4%eE_aYxyXgei`>G;=D9SK)n zylD#phsfbB^hkH~K$u`adx|o2oT7eRm+wA^MV$VPcF)MJrUv(g1~2!)`UT91t_S(& zW4Iu>0iQE1wBsgbraY|a+r@qMb_q1u3{aC0bHs|=ENbA{vl;f1LZioxmgUf&gAUL~l- zSYD><^GPO~1sX|es)w_kuzd0T>E2F{j^!nFGGqT$X|kj`QQ+ecY**Q^VSdp z7j3{YCS~9mY`%krV}u}cK^>cBt3v#_6Py9jkYAy>3|JFvifJY%;&(j46I#2wmx%{# zcWEt-vjLWt(1u#$ij`{l7Z7IzspvkN4)mL#IaaW{B#CcyozKn+agbNod~K%bNoWoe zW3J67aaV#SitpQt`4>?0RY8wh|DqhZf~+yCUuyD~yNe>6O&Z6gZmiuUM4)8O4Ltim z8)tixmwO2Ww78A^?$Dqf?SOLBkCV0A0H;1QcjAAo!K7BMe;1&xB{R9a5!^!uBlO2{ z%Tlb@V_HFFmBc%TJhXf;^%WLdlu~VUv5}Hl{8GgZvvYF;k!bWT`I*ds>v2e6|69cR z*)0#k9kSn)pmDo4)o610xK8FS`ogp~_h(+2AcYc8hD>xs|NV*I{MX;;P<$NcjblCrpIZMxU-m;n2(5)~;D2_0B@yR=~0A@9w&R*au}c5nK zEqNdzV^5L53-%K?w)Mz`I)|_L51!Ah1_attx95^Voy#K$v*COHc#P7eh|N9p$Ene}bMrE=>u7~x{8iiStBu*Xxea{#kC~f{>+A1YHfLGb zj}Agn`FdmL?UQGeh{v1hGsB#P_v?8@PhXakGs-M`lcH>HwRhRfbL{b3(s_(B$F~tT zpa?w~aWn9XIYCO>udn}PnWRF}*iN;apU;~2lQ(9=ZGLY*yX_7>2X79)wi6QgRHx75ei6gFLiAvD-bvU1u3q=l%=?il*sPm(UYk+{>2pr_gG=h1yKQO1$IYF#1q6>fX4k4=WGCrC(&L-> zrd{e`$&kZ-o@TR{K(r4Ba3?6;tLyQ+#=|U;Gc&fT>p*YxCtR(Re=S}j&C6Ffm^5LE zr%;46mb-yngo}{S-G=@haRfHoUkly?Z)q&A8o&71#l$}*ETnkxa@J<`(JFWMu0b<- z%u|V(>H868dDZ9ufr(vN|B|vwc|qHlDfpXk(ZsJeFFvq5lH-0q zIMgdOpPz;0&bY*fE7cLnNt+ukO5v3Ncx;U!e&Ndgq&)%xZRCDOjxEzigo~>pcWVYf zByMFc0so;I%`>i8o`+xY>3DS(o`)&4cs_>H@dzl4*@;CoobhO^zC3299X(1&nuR}w zka|)we@l&%-1z z!UgbPh3);*my6wkuliH3t&@Q#o~$U)6_8%W5ayIdlXaY3Ghy%XCFQSJVoRr@&q2! zI^4sAxEH#{b$foKbuNR)>R!3zvp%345TTOj#^wBM^$7FM+Wx9#w#azlad#cWbcV0f zLn;I#?n0r|0QMz&6ezgVOU5Qj70?7Lp z93xuH;xAZOY!AaT2!+d`{WI%NI^Y4gB-O|f`Ej}h zKg1A2>qP{ZB{w>(Z;JVYe9mFBFQkT(&nh3Cie|@qdlZT^fw?J?vGe2|c^r4jy-Ofh zajh%e*DG8!Lpm(&N5UVpNrZL(7@c{kDGVODC-lm4+9U9Od_kqt{uWC)y9hJ9DuoT) zzQhbBXM7BzMBObv;J%zQtRXS33f>udgA72bs>;7$kQ{350iv-lRGoE6=Lv-A=LQvW zdd#t0c?(^mTS|6Zl})dXI4d7Cai5P3<_J z5J2nhG_no$+d5rASti+jXUC>^Dxn=3%XthC3UU}oM4=olOCb^#oe8%l6H<#}vGW#k zF934>p+j^cW^;ydJS&j1vXUt)4^mfICNJada4s;L4c?ApC!IQ!F z#qSW0GNTC(&$f#67raL}VwM;xu?D}I{C(U5cv9bu!br807+>A4_6{&<0#;>gRz&==MSEwaUd+XIHPyqq^q`=?Mm; zf_3p{;8J;jERkv(a$8Dv5OfgW{RK*pSIGp~dU4ppL0WKdDx$4Hai^*CNBK?Q`Id1E?a0BuK6)SQG zxnU8sV8(Al++V+g@cB{|cm^l|kKS?cMz$HF@e{f`M?qXpm(Sf&R-(j6F-x~Cp1gzeyUJH?kUP4ds*&%!cia=zgJ@}8} z`-n@$Cc~pQvG1CF56u|CbeqoDEhKURyI~Dxm)InX=)}^eCvXE&Pg=`m~e7 zL#?yJv2aH-JEc$y6b0~)fCQ1X16;0af!Ppqv^OK?sQaFLFj3B*_i@x@^2DGVrwKiT zsP-4}GnDs#>r+C00Jp%R0nNEFXD?KlmtXF?{5bCtPHNyJZV95$jC~nA0lYUsJ(~q| z2!#P@c_(k&6V<=1C@A6@;J~f*?J@to^6}Ee@LAS`#hlP;^5KrnwZH#2;w9o1AX$lW z|EpCb^U>eYwk97a7SjI0z{S2wbHoY`SF48Lpp`?#>j}fl>7wqljy^4wd2oL?KAw=s z|0W?3!1M&jaVaUj@T_e7ES_k*)WeCE?MTm$44&2?oUllA2)En@h4XB0fo~Vz^Yd zQiLV|$`HID=F#vJkPpd5hDRa6Gp#TVr^}7co_1acFAxAXE{SOv0JgqN52-wh|H{Fm z;t6WX3Dh?4CzvCJw*4-UD7*?9dtLnq!=+`(iMGA-71{(qPX$txq3ygAPd7R0^B;OM z0d;-lzV;p2SSFCCZy^YD1zFfN*`LS~wCzNetas=oaE)dxVli?aSdZfGK&FAY0bM0v z7bds_p|tHpOx7<;(?h|_P}jEgB4T27T7QK6gv~E!J>j)xOT{TAgN9#&2zR>KrU7$b=Gx7^m}^s7CN3{cRT|KyluTv1Rjw0(nB*QGYQHkTmw`~;wq(e zCBVZC34TJv&>Sb0&~vVT+s~lf)M1ofP#B_YiDC{20a7q%08xJeunYdi6xN)RhW&->Hm`4AlOLG+iUa&Iq9KJH+-w=~ zpzkB54lFU@6T4h$*G{4YYQE}^^?y)-6f)xl4T)u!k@x;|9?hNgHJQjM3YRP$4wRkC z+I%Dbzkw;9R=FOe1pSnHMoJyxSZGEMJAr!sUmUO4;&gpv;KnHL=+UZ5qC3pb^JW|_A(j0cMf8D4}Hi-|Qz5^_ZY-xMcRj&~h5RhmD zMF}E;#Bmid5CsGU0YN}TlH@!Xk*F(CGDuPZ5lNDjEP_aqj0`#F4D;9N9@M?}`*|Mk zu6LQ9({-xedh4y~)0o9a(=Jo#;ntAVZ%O&XbviD=4$Y-g#`B8RN1b;>my52Riv9Q^ z4<6DEBOhk4M>Ns8#8jxT4nctO)xX2@01oSO!IrD+HlP0=UOJ2q!wo>1l33`j!lXPL z&z|ABW{3I6G<(rWbsZ)0`8-Ur&Y3A3vLP-Am ze&typugLZP2x&3AM$PwcGN$>3O9!Y*^QvWy-@c++$>tE}WlWp>{04~OFT1`1e?&Nj z)+J!VE6pPrNIlrTgS(sh0mDCxq|F7a5W!6M0HVj_D6N&Fe`L!rT(O(G4B|rPN?Vk$ zpxM6FC!X^aN`cGc6c5TS#O41;0o2FDwff}@CI8U+2wOw&rHPi9tWPpRne@8p2!%WOQ?P)lT>r zKVZfm?nTv0I=GFjL1V){2HevZ3O0S7Vat5#O{@93I+KD9uOsb8q!hnjkRA{upkfvW zUem4k9)$i+&5+O7JS>mk+vczYt6#rSLXmV{22#kWQIgN!Nr$-Xn9QG0=iv*tZZo*d zUi5EdBWlTo@H56rYs`pN(h{eL5ds3UhDtwnx% z+4>OKBq5V$EXDxDe0yRRYK~Rn_IpS2;9|9{mtr{ z{c-7iEZU=tx)>Juu_RYSZ&kZM^+uMRv7g}k zNSur>aPt!wuLEC0WP%~_y7%SoklIt&it=}w9^VT8COkIKwMmx>p`b6Od_Mj=o+!p3lhP5B@d`Tev^?SpqxX=->SR5!|;1nbRDN9?!F*d+`wtdlR>FTsE z7emcmr#2X)K04W-Ujen&lK$v8;cL!H)B)LS2O&x``ySj{kK5WuL%k?d?G#6N57ori zvWCv7yp&U}34CZjJn6EF0_N|?)fF*eBev1wKd9Z$|zk10=r)EMRK!nT$r zc{ERe$$9cY0~wPoE`V&h6SV7{JHcG;?S0Pn$LyCf7Uq(3XU1t8SaQ*COKR|}HrNOq zjK`_b^Ld!rpfY&g2%TL-Qk)v-dO9~=SED5o`6)A?`=1t2mU}2QPg3V#r z0`oV$0}cz$*%{9$pGKWUgi~;bZw`5q+{#oaXTPI4q~MDTs~vpgJND!i`mQIhvHUAQ zUHlmgCiC~&PkO!>pQMSwjiF1v{aIWzq~!B)Y}v4SJcu3)nvM6H|4M=PSFvb2mXx+X zhx3q?d3qu>92~YRfzwqvQh)(uOlI8(1SWU(ENGrioeLS0EmTg|p~p{M#%)J(MW8^Q zEGxhQ-{!4W9%;>n-cR%-SOcz_h`Y8AiMxS-(K0A`F>ppXNiNhi%$bk1*FZi-4ucV! zy^_n*^K)fMZ8HD%CWq)hh#$@sb@yVhrA-H?tvbSf%ZoW(fi8ek4W-uww9?N%Mz-p# zhJ{4IFSEr{Yoz9D?%qyjt?#gXBd{d@5er@^$VDl+E39zdWpqd-`9>h{e&`B3C_H&Ww#(PmN39LQ{#n1_fU1B+3dsD>f;r$UtCm zYiIYH&y%DmB7GCjj@kzte(#_u3kE$S(lu~WFcGsZYFE180Hb|id8(n8%$a8=U|Fni zYFXNOSG(BsH3$XLnQEwryq)F+sIfYB{~lI!c|{@9dSkg!URcte`nrky=L?dQFbX@|yDr~+PL7}9kIo*9V9=g2S+kB%{Df+Eo0Dy_ zu!(!w6|v0v~=v9rySd~H~2u;yT@w|9}G`CHo$v>H1cEV+`9~^H=^&0 zcviAw?yt`-p1GVIpK~=!f%l_TjMUxK6oHE9A3OCJ6%`|ziz>8>GWR%TtxIq@y#287 zJ(soIS2uA!4BBf!>APk5_MU&2xiOo!GwFqSZaXK#lfll1la8P`kgTWJ)n0&h5?;lG z6Cx5#123v5iF-4RFIuiLK?0$(;q!6-mT9$M=V^wh?C$AaeEiVeKA=z71qY+cy+x|`S+XkNbQl??~k05%`KpY6jp1JUL246 zsHIWu^ZSUR{3olI!N0QHxA-RZT^yPI?TqMXN7CByksE+naf9hSw*3k!!t`mCY`Aae#Jl2XQ+iV zNvR&+{9I<`;bOEYnb1A2YRc;TCrNC#CBw;R>hH}`&vT<*D7J4_deqCr$)bg0wu(T7MCH?Tuo%}{Nh3(62oa85zNy*}m(eC6i zb$eQT3S41;x@#_#8%tQ9mqhy`b2g0X)5Nmo7hjMMhM#Ta zyC=0&-6t)-uwp{hxNAGTY6bZ4!g)u1gBDn_H=DVPtUCIz+CoUR zS&>ou2^@=wN!U)C?s7_Q#fMwqW5jsO_NYI*kyENGilee!-HE(YJLjle}Kv!&Y#5Iahu#|cs(Op8o}qSR+<62pu$=SfxAXG%ePO@ z+Y0DC>2zl}>fJkHb5OsMa%U^EP;?c{KEMwb8x^5n`aM|nOu#xR+QE8Gapylb!E?5S zwXxdsf<68@nO~nvEM*n@RSyEzD1Q}EXgA~qi~MxtNl<6^*2p=r+sVXECSzp=Y$%6Ui^MfJ$Zlr4 z*0_4oHOB(YTg2%VEQ)JCM9a+)YaUa${Hktwt#8je49>p_lM+~ijifZ@J}j-<(5AZz z&~WQF&TRM)D>tVQ`2Gz4WQzegQ&jUQucM(c8110tF6`!QF;O7@u-#ms7j^#??xhWX zL0Kfcv`(dKHoT&F;7AkM(DMs<+$5M@sLkB^rnC?hMmef5F-2HP{)9C(@sR$hvHjqT zLfY-evo(+S4^0~D-f8M=i|J0DYjf{(YtCizeeh)U^YYZsZxrz^nLKv=kHzn`q<Oe#60nG?l)9r=2-w1c{T4MPr-N|zLz(a1u*O=75Fc6ouTHw zU3jM4Q~$xDC^4sG^(f7QueGk)qD4%D`OGa!)h&q^egt8tM7n<>CgzwEU-wfrhfyj1 zp9{@@_|%lk1lEapbf>pXH`%G?=iJRRdYBU3apHsVu-kH`F7%v?KfBC23>W29g;Xa? zZ%cAjWM>+yOd5PbC` zpy~iQSf4GQNv%2$b`Lg#3+%-;7ZJyMtfKG)By|l=hO}i&lCaRIT)+{~}Zv8YW2XX@xW3 z8^6x*+DDy&0?b!aWY_BGB*2%D8*Qydb^Y&FbB){oEiWRV-C zDfUB@*m`QOYl;I4T7wQeXz3~aHFfby;6&j!-!_|aXtp)c53Z~bk*!&?ykE%;2=W?< z?cg4e-# zZ5Mu9ha+XglPgx(?UOV=z!s{d|0HJ5&1Tx%CcD(0vi%tn5m>j_mYMMLennD=o&-Nv zvD4cldnElXFrXvuhy@uRj2%6~MEL48j*GA86y?s9FkC4~{v1N>-w3b6e6I2vZyLMj zb$f4MZJIi;KBBzMu;+f{Gd3fS(sf!NudA+Z3=xUvxO->NP1fjo@|4y&{X1{IM2_pK zm^E_V(fR)HH2>{S;f0R~rVCU^AVvBe7NtX>b;zlI$Y$bo=J?otW}T|4m(*J{eW@`F ztNgZ(%_(O8Nap+yHi)F~?pnH4kvsTOR8ySfDo4;Aqd%@T;`{a;@?yWK zy@7PU7Qx5EY^9UosP-W$_7AWHi0tL9RryfDz^lgQ??}FHV?kz3wwvFQObNTT`o(UQ zdn6p@lCTp-={}yWTkiDroyPDaJRJFu%?PJS(F5<%r)WDX4_|1|I4OL{QAa`J_N-@bt= zrX4R1)fU#4Q4SrFWu$9-`MwZqalNv--MYXisjIy-H&InQ>&NbKUDEN87wd=&-geL5 zgn3WjUhbFpgl)$<>piXx^CXs5is@iH2<{H1KrxO zHTdNA`GY{GB!njBGlslUbjy6r$w^LgIz!Fh|G``6BpS2&v7euveUXS4uC`QFBC%)x z0UC?+3*miTm`#xHritH(J= zWPg$evZ1Tsu;Oa?&Xd37~^~B z8GqZIUB-4F+uTU3&d`TbikeqL@IC~|85^G^(p;+!#=DzkD9W!+rj^XA=CIK4G}E$% zw8p>ay8%08%(-{zBg~4aF~nQVZ{ow$tr@8AB!D9oMk{ zp9%*iS+oFj$N&Xw*fj`nnb75n9(l&*N3ZAbT5fmjDA2RlS(^Ha!Vw=NI^YA5B~ayf zGIVJm$c!v*Ep26?OYN`ji+mds(-VrlRH3M+Jmxi)s=o~)j7;gt+0(w@-k^(ZM4v9K zJ=vlz_ea>ESG!@{)ABVERezD%Qeuphhsx&L7MshtH=L_${EU`H7qVqzYSFVbKyWr+ zHlnOEo0q2S<;k2PkLOjb&?MV0&Inu0Y2!mEq<^|u9pP@o?@E~)bZuwD2bpquQiZ$< zyBd=UyUjJ9`k!PwA96+qS@4B{$y2m7N(3&Q5a|W%xed8bb9W7ohAxojDnC^f7pY!n z@Aj>uCd!N0ARW%wGpu!c6_T`4_A-jhjojnB4@CdVgm7i*=T+I_UmS7beD zUuuGQkjUPot#M4)dDaprZ95ouO(qXr%JV=Zr}p{}>T9dRyYO&XPm?f&ciY8LV8WH? z$+0G0(Qjs>EBAUIQ(Ln02+BRBX-__nw|sZKd2`D>Ye{sHp*o%>gymLh} zQOVtPQ5ev4c!*C%V=p@qx0v(OxEeu2%ZlEnBR?;}FKl)jO|Q7jik z``Zp~*qiA{n(tRRja;*#3Cq*66s@O*qqp2K?25>@u&^reXBct4ot|E_61;aG7>uwF zN|lqL+3^Q+o!VP|7yG10ti%ZS7prbThq4n0a$oKjWuZl)bRMt7Ws1_zS_yIQ%v04T!9W<`F|neOTQB zdnK5wDkSOt&49UVuxxAB=+;w-Mdg^f_`AFEGNCyKPo`SJ8`2)4%^I3dDt>F*tP5@j zNrUc{Q+GEiQNl|+D~8TlIuC^y!$^w%uBq8T&;< zw%I)^{$97JGck0m`eR-AP6E5Jd?LKVqpe@^9nvERQ!f#9o?!Z#+_5NBLfX#-uo8i=9tT!-ppZDQj_1S9tbF{MtULR>_VAb&r2GJJN?m}U ziTzNOC64+W-X4G0Ai1XJtgW6Uj(ep0+GzeUoCeZ?M@s=23oQ?}S6bVT;6s78ZGAnlrJ zVwOe*OtzTLtvzh|HwO3+7%lQ`7h&gZ4T;koXj7k^T`daqS0XZgj)+Ds1W!_nf-Zb= z+OaFbmlZ2Jx%dZ9vbZ7#iaEJImJZ!?#*9~*+B+Lnz?F%@G)GX4z^T}n0vqX_?=_~3 zk43h-|NF+XvC)EX?K_vW)fkKNag|ohVM1XfB9f&Pn}7T2ySeEAu|?k>VUsKy zxGXcH=+Od5VirIjY$js-)W(*BbU>=0Zu|r`yiJn$K=mlnF4Nvmto-&XZ$P{D$oVi% z^a#s61xzg(>osuD=#_6TsxS}p{1x~h98h;Ao4PaNR8%4D6M`+VF>MTm*NN#`4N2@m z835KB2$(nVH$_Gk6BJZb^lQ@;R&(=8^8@D{3|f|spyR~iqZ{X6|IT#Lou$=UMWr{E z$&r&su!(Mkn=`#1qxv6fot<3psy+OT?8UOI@^+z2Jz27WRdp$^5q5eiIj_a$h6UTS z<8->pb`jAHwOdh}S;0r&bKy>6=DC8j46o?2U{oF&JV!6U;@;VY( zs8^N1US$*Rop8I~4fuF;B{*sLdElfwb&vb_K+;6cB0YGe$`%EY#E*cNX=)(@|Ldw#ytuwW^u}u0XQhD7tJ)4}t&YqFi61d(i5RD> zPmqvhA$6X_H7^3!=KTU$o$@^PClPb(?0l2xJyAYD1vqi+Ndw~Ft7=C$5$=z6uy4q# zEG50rT+)^spZ{6kSsr0X4pdhsUuu8JjN|}wSTCH&j7$Cxubk+`z*K<%{!HF7na2FW z<;6G1kqvFtyK$mGVjG)aK7;+%K@Nc2-1-c$(2pi+8pyYPaoG+JETNt6V`%m+_EYu7 zp0)z8uCgov@YYdM2gu>#LJ33vYco=|?3DA~Payb_8vJxx9o~1o5!I}n@seIr&I|L5 z)hCYOMNR%~+;Bh6P=$+Uu^wglfmfltkjct92Fq58B@n&)sJVtNXSLifOUh*uYH^fS z4l~!J`o9v!Ksb^GkMV2<$L&ZD5;V!;XA(YxhzG=$4t6FoN$L3U6z-L-_XcE_QKut3 zxdRKo1v1m`7_mNJh4YLMEEALpAbtq-6ewB7|cHy3x4}c;g-`%@hC3+f^$U6 zamZgkWnm;{ZfNMxuWtT35b`fzI_k5Hp|>hA>6gP>46-*y(l^lg0^I^~tL{-wQ2189 zK^f5xu4~hQA@Q`Ox*)2JrxtgQ*E+&m&4WhWetQY~Yg}F5IjBxk0veM}p#pCW?Eb}8 zU=$uuxKu}#+`W6(50-+Tjpb;QbZBI^*bsoq^~J(0Jb6c{e+tfMiX8k z#4J*>F81bhkXV_k()1LUG^A>WW9pfgVFrRj9wkXp6XEe9MAPY#Af21_TFaZr=qo!q zp73I2_Vn~LYkYO#`B(j@h=`vbyi#>bynZC>N@uq;?OB{1^|l&sYcE11|SE z_Vfb(bG$suuA;^M;*hLcyi#oOs|zn8)6&xXw>XQRn4HO|aHmY{r+c62EPQl?a?ueu ziA3S6yg7Mj2b71KS9+fA4My+K3FkV7731N=ghqQ?2b1}NxhL$r)OXN~ZO53RZP#=A z$HnxrfmqT!zz56_h~jj_EE9aaQmmXO45yqsbLI@Yuj9M)jryVLsYgQXM>1J)N>KW7p7(#pvh%8R|mD6Bbh zoUq>QoTuQijhF=fEFajOq=k%rj|bEEr>4|oqvkZV54RLGJ<8B*KtqWKlR#&GY^a+q zWvME8qCZx5pf)}}rNak#9yL_uF0O{^VWpX$XR2VNoql*cjM7?;ngLW1p6UcX!uYM< z{G&J{vZH)}sLgC^=vRGmTBf<~VwzF%&9Z+2gw1sBe^fI6D%en1BQH)ryf{;||B~NX zU0tX+g%4j+9d$>17=%AyEN$H#)hoyixpqzmUXQ2c+C@%D81IrjW>)oyNzwX-60WR< zKg96ARNxmldZV(saZ<~%eR#6B%6qsutte8;Ju){};&fwRd_saJlYo9<@iyj817@M( z3MPTeoCTYVfO`X$g7cN`$4h$AO*x-Ifvh$6gW~!t$Ns`WuG1I`pQu<(XbbtU zctEOQ@pRb`5y3Q*=G~LMU)`IUn$GHpzln(0>v#0(E_B;V^OO67EN=+Ue~H^lm`|kS zzr@fnULr-na;?-Y)P4z>u{gR=Q&ob5;9j)fgoAh<2%Z14`s>p}a0HM6Zv%^4Sg^S< z++?#5AB*nS?CNM7-0%EkPtlA~bLYz9sJEij!s4s39PuNQz(MiH)f`{F&IVmsYRG(l zm^aF0x2+w;f44b({)6vv-zw4UlY&%1i)%^7L83w@jawLLIpt(D>V)nX8tRTDq!*_d zHS4S_%|py9aTY963mc#s3@_Ha`oO7*XfSCP|EoxG_*&ze`l?IX<+S4u7Lz>qU&O}X zqRd5H^cMRNQ&e;j?!D->rJ<^W?N6^#{S?e|t0Scp)zyPUxSyi?v-*6zD<1KDEK8Ff z&U77z7`{kt>A78LB|JDj<@rWp;_}jTNKy%E#nkl%`{NvkO9$V^Z_r#E?`pTe(pmTu zl+GByw#CK8KQeBd(Ez`VfAP;7szt$rbRf*)%{#;asGrHNwq35N=xHw2Sfl0zG#|!Q zX_h3-##I`qT9X^zHW8aXYtDl)J@LChcE+>(9e6a=vQ7>R4l4C_`7SIhsJ*{=A^V+` z(TS5MV=7&&F<2yZ3PG;2Tzl{qkeATRRKXseY|-~oj`G-4K;KpTE@~I*KPMZGCSy1> z^{m%v;!3dP%oMs{ZGC-AWtlqcmWL(p|jijWh{gQKovxMcjg7~B)?^Nf-bTi8GEYL=@dgLK-uUiHkCe9#5=BF-YKSo=u z$o???AcE5}g?ucXcO@g8Z1PxJ2G9b9QY$~lgA#(>A8VP`8zJYdI%(`td@iiwk{zsf zL9X{XjzVj&1l%Exh|8IV9L6oV&Ptfr1B4SmN&z>1%PJDcDXDh*j#qoj%Z4b@?b)-Z z^HHiH!a}83k|GmH;{B?Q#D;XFaIS6fw}9!6zMzV6SDdqs##Dwd|EkMLYq8RdqYAP4 z)tXdTrSZ)^^ET%T*AuBCLXEhqCfY^IY%V^q54hYYiIava2pA7g2~`@RMp-BYIbIA3}?|b{_XHCb`_d z(Si=*49u~<<0rK&6~3xhXXa4IE=_TJS$j|_0Wc6bdW>AiOOZ#r43sy7TW4q6p9kf= zMv{zw9|5Ju+Q_r>*M{K^!R`J74b*jzJ_}S2i`R2BEYK--H#=)$)Zu-{Ia=H zp~!a9jd<>he;XMYi7nSE@jI_9)gJZ{jyg`5dgP>s=^k#CP~+5YGQD_C&+445o(hcG zeC4;Qbo^a3ap2Vs*N#=qF8^t;xOimG_geac1}oXN4o zKl~4z1n&E=w(g)czRCi+>_Plh(-YW12Bfvg(73y|(HlG5rQ^YrB&Sa4TezTMF=Xi! zPv-qWUt>V9u>`^9Lvu4;;}Hv*U%a^(@=Ue-2sSH27rl2~6C90lI3C17nMRWho?p1% zbS}Ly@k%xyaW5}8LyHd;%;PD-1@#2#Kg>Ne1Ruzrer|HsM-M&7+Ym1}r+5Ai z=(hZ}GrPe)2DjvSUVy0Sqq-H(%JeN1oTqW)w!N!E zLb|;z4A7aKF{4Kpe`bP(DBZK^geSk+$~Nim2V8HME2)n0oYO1a2-m8V>3t3(H@~q; zS@nj(<4;Oqjq?#&RbIxfo6?VZ!JyJY9Fk@TrP5e*Zm#%TWr659p`*1jbY)PHY2u=h zQGz1VKh3rE^wv(Zseqa5`*D#ldKIVyKsJR<_IqJF;zu29T3z$$f2}z{P;wZ516%V# z811-8Ol)GuI!52U?0>K;IL$zHKtL3B^0sK>=~qL2j{{7(^98Hmg_kS;d73d+GOUkRkj-h`6}rn%MBPw3AmPmMw0k zG$Ka;RwjsHwJn-!82C%r2&i4`bjC#aYhlVj5Sr48gaHS(M3hYk6Hl>Pd@bZkOnM-F zSGmHPSB};k8wd0TaHKwN>xI_&R@Ul|sc3#kx@%XP(MtNq!eML*p6HW*@jUAkhBkw7BXZd5@y&=$yr&Pk;%|A^SJf0et!uz#xsGP zfkaw6^Q{~oqKJ+iu#UoDQ2301eueAeXr83yhJ$yZ9!J73GEg`^Z$OHKPyraPRFjwR zUPTgjNoRR9+VsN=t)GO?$;!{m>r+Ea9(w^f+VW z!Z)@$Q5ryUh&$g@4FOn*$AI_`qJ3wlWHu+y%91!} z6s_bEEF~{I#@%O@O0|e(bL}M(OzQ+5TsHXj0P-anQZf~LQ5Qz+BN`4Tbj7b`3(zgy zE|^G!4Hlm$I;drPbb#mXKq{`NG0KzOot?_d~Nc;Rlu`C=OtqoK>#}?`a8X zZ`}!v2?*A%M^x)j2lo`2j)wyQo(kBq(P+4i><`#d9C@pyQSe5Fbt>mJ?SqaKq}%F%1&WkRD-LK|M>&YzNsVm z$&``|->}Xb3ZlllFp7`4X~oNQamx+F%l7vI|MVl>Qqm0cLN11^5r-qq>LtY6U1O?> zAF5NtKyyoyufi-9yaxj~DIF@n5v%P?8%Hwk0{3K^a)A;|ZGH;IrMb*dory`r>o@|j zEdkvk9`2)d(>L;fK-R)iFwCL??U3|V^l$&x+l$SG$>C$6`%o_bwc~xzPAFLi&Hbme z;It9mGQq<9s3d~BF$Nto3Q87`6efs`B&??;MaIN0qIcH2L#>l`wOv?djRH~sA3?Mi z1nN-VzV|UjQs?0B;voEH=?=}o>uyi8t3x4=RRiILvJlNk3o{^0#ph(ops@u_exw`| zs~Im>Lx#0$f$_Oly-5*F?|8H@WlM^5v3h52U!i<2@v5b@6WW$)NZ`zspjU zjRl8aUC?1B^SkkPtbM%uANs<8icHhLJ&gEh?kumg+U2~~Fh2W+V#2mL^%aFuL*VPi zq9*Qm`+UK9BxRFck9XB@?%x;rHN3)SdQR7Sev&iudR4GTTW=MVlL2&`(l3=bZ(dAv zx`y)8>*H_Q!nfANr}lf+Zu0Ni6Qn!;SeiT|*}OBe*a2s{0q-#;q#QWP&glL;-8lrq zn9gZdcqsKr$-aG&si}fy+YUP}8)`v-C-)~e=ydp^qEFBI-mAl?9wQcARTPE^z;*fw zeIDK>?F>al8?sPPa;(Q9MWv^uFNwUkQ9}c6z%(V)Vc{km^k->#kY{fysB%RN>`ua# z8ZDELCi_?pb(o?Gn`XOCtrMwANn zC}Da<-wM;w2DP)T&31>hWVxxyf9M)G*cz!L&D_Sn& zb#Z5K-;a-?yA#?2@PnOV{=!W(px(}ky$x3(8SK*)-CU%@e`C|tiTrgmXH``px?N?x zb9PI6BXY03>qM6UMlX+?HLPKdr%f!26g_#aq+YyQSXT~Tble>*{$AI%uV|mZHc6@} zi=w>MTQhr#Jd>98Q1Iqge>P{B_9z6iXN*d+K^IA3_YL`^>UiMvL|!{Q+#E?d>pjjub*xNN*^K-|j4 z<~|~h1`!qX1v61Q2AuLl8XO`H400W7h_2F1Br!2CDJWZYr7SPq^!JUjYdo63l7Y7EIdD$ff*^AeHQ7XpR3}yP;IvZA{Oc~gU}QG4zYy4r6VzM5u|cU&OHv4%0^)e##m4Sk@cEo@rWuXo5)83t+_ag}L0zRBbu4SLNcWL0^g_Qr`-WGVXy{gP5~XoDm}6)OcEd^J^)^RAK!;fr^3S1l?@ zA9cUVElPElL)2?udufOXEjOrW*S%J$uB(F%84nZqN~^sWw4XEdO|;@J zg*-bYn3{=wPkD-o(~*@LaDkAwV8~-X;*s zCfaUl!ki3Yc}dtwOT-v}^qJR^T1R{j#Xr0=hHWsxzj3;vtWVU2$8tlypzX8_zvbUC z=y~Y#y0`A$Sn94~cQ?shX%t3K%96pu>)G;;$lk#J+2)(YqhG_|T{lY7U^fLP=M-JJ zB}D@zEX&ATT@G~iDEO?ano}i8nc#h)d`2)p&^~SNzTN8?r=NWd$Is-xHghn4 zzO=}_kJypE5*^Xc)pfw_+^Z;4%;VGhX^Ljsud(N`s6P|gBGFNlG8|8Pd@n%cmk}pE zV@e9^bHfT;sV2R0G64yn@)AvAk|u7KH59gwt}LtZabhdtPzrVExhXbL_)WTPG{kw{ z|3X~EwXNH>6@AzHa@inXFQ(U!Tm(iq`m+6*$odYqm1cKtavoduG0SuMT@-Ig*UtOu z02BmMdhX3nqJRde)lQwOxFbvpe^uh-f8@AaddBQqT!miSDT>w7P(Q1pog)Vxeg}Tu z`5TsWWOm*@mKi;_J+~uAQp8?%mU+(}xBB|};|&VOuBod>y?Mh8I{M|<{DXkNy3JbK zip<;jWC%AK`fMCAw4JteB#_?P-|q6WfkU&aQVoQHpI)6dh=kSLs`QkC6ke^o$gL~j zQ}?6jcJWP#vx&*4n$pE~N;`kx^;$92qVhD0N_2>CoX(csE}iCLfG}8G#bHkkj}?Zh z>otzs%1v`57V}{`x-=LeTkeD2$Tv|3EjQ;sv77ct(#!LQGL=+ramPVFcu&~8OGIG% z!tlF_az9m`#pD7e<6`&Xn~8U-S($a~-6-5nGZp@wOO5GxM=1Z5Sq~V1NWZu+(}+WM z{Or85TnLdqt2fks-$@gIS@tmoXF)p2Yr~JChnH;b_-lUL{`L+#Dpbo<{)i0eOzY_(~>> zct~`V_Uj-rd444QjKIdKH0O?9CsCf7CUAyCN!pY>m4y%~WpW2}HTPs5l=B2AQrvmZ z^Q+n7vS??QJVnCG!NfY--01`^@cZC`>yg@jlAiKPcHM*qHHo|5apms7U$0xl!&W|= zW;z~kKbt%^6l)-ztN;t@I3C3?a2bgCb)~(Cjxh8#(xBlIfuIqjL>3^|&Q*YK z`h96@Ynm!HSpkA7Yl19t_L0vQ_vkd5rA%|9iAle+CH1XlUH0~!KU{io&mdkEo`ekv zFFzXm!rh1&;X8qFeDKkO-o(L`=*#gk2bEJ0IToL7q0Pz3K?7|cXS11T9PF&{kcD%? zFh;dk2_0;~w-anq{O!l5FKzlaV?GWob~~RBQ5g4YUDl@af9vK?F*d+22jx)6>2H($-b6l12)TD+byekFNA^Ujl+PwwFo{6{02X z-qe@+X^LEuTV3;MeT**un-!Tzk;D@763PMG1L#pfslyz3o7F)lMXsJKx=KtO&~6>6pT27IK@owHw?zCzp3cs9|F}Q* zB}~#!BX!+63*t@zXQ-_^(X}j(0!Fo`9FQ;qr2+CA7E;qd5ZZGDeE)@};?t`LAd)OL6X+(B5djcGYn z6e`VltD{%aSiI;TeNKKq=H?PuSj;2mE}rDfV|lLbnng)57mu+!GEmzo$(Oi+j4#R# zpba`yj63I#ZJ-Qiy!>7|?G5t*+20z33J%K9&$gJ$Lpq5B_>yWbFo%25vB;J{!C|^y zmm#RLqjBcP?3b!St(B$ZIqwIQgSqny2IP|1hT{G$nsc|_4=;8b=s=$mLpqhf22o4Gf4QYehi|$`-|}NGwvZn?VmzD;6drplPK*cvttfnB;NZBJb7znl zag2QWkUe4O_H|K8c%fm;_s*5;dZ4P1Y? zg+S~U7L*W>^R&(twV1F&rCoBQsW5^Cl)K_*RO4yieDao0^2Cf^?32>x<*BIAvYS_O z@occ<;uSnkWbg=L(;6A444e1p4j)On0xGnE9OQ+JP1mo(fJ4XY-0a1#aAZ53b(aM{ zvNzvp6m=+JYuQgzo?H7Q%e&9@d7b#J3CddYVaCJyO`#m zcnLp4NT`?DyAmr&_zq6Z&Xxc+gL3Qg@#DwH^mI_GG!*}J<1&g=P&{+v90KP6zB9U8 zfIIz+W>4Z`@$x;EgfK`A#D#Znk0b8sFkJUUXGlkBz~}HWAQS2BdGI2TIb)Xjwh5K4Xa{$GyIH{Z6$Oz{99$w9bFbf>#^ zLW0swe&5}CBhdGeYA4N0uY-wS{Mf@Zne8J!k-F#mup~)8@O-2G*@#RiU*PL~sv8>op^~kytLq-!5jfl33a^pNHB#8X5&iO7 ztoa%OpJk4-Hp)I~iZ%zY?A|UP=n@EZRcBC=^T_wnHH|)ocfFcN+jo|1cMwAP5C5(? zg-RmCb{rAhi!471G!Fh2aQR~N<*nPd7r$0{b2(PQrK!1TYxy(VW!_MCjE8)V+sbFV zoi!{(WUta4<~=CJh=_(T#TJW3K`C_hEC$4zpXlm+;YMEn@{Y4_hN(^t+Vsiksv@X| zK+A2?pdI)ud^y1%n{op`W53r4yFlCwynxB)=i?8lsnK#I!wFE# z;LO7VK5xRXC+&|L0%~maJmxW)mnyN(NrV6w9`57kAS?FX>>QSL4!=oCf(9}D9Vmw&z+YO9O6+!?^o@r4`>;;e{Wa6G zuY;~&&xX(s0#WaTKOpR3z@9C7H0Ax+#ii3V$%U5wi{t&N<3r=}xV;02|EIn4>2R(6 zKKg#TmxhR66J&0qAv<~Jg_?MQI3mbm{Cs@RMAk`M9-TCZg7V(&QB&>Vd^%IAW~lI} znCE0sLCP?4`?E{N+A&c!TvtA@cmXj!l+!=;D6rbu&SRL`g#dd;D9}!j3>!}r&z}3WiRhY$4pT#YvNz1o8BbK+ql5!ReVYYL;t;h1YnjA)h3|t7h!i-_z}AdI_V)6qnF9JxJ@~uXoos;xdFyl8Y}G(h z!AHELWKB?37~d#cV%%rud@79t;WqC%=m9hEnpGcDob5QZnf< z*00aEw4?k-ruX%2f8!*h#a51r;Y)*2-!uf`jo?dtwBnWB3JMC;Qtzk?&>fLUR)fY@ z!`;3@8zg6r%%=;$9GA+_x2I46{D!WpRLIsdF*LH<4`ECHH8c5^ksaA>baqBm)@PMxLF&W&0v#N^ukHKEJu!`Q#V2z^zi$OaBdR}H2-97_=)MeP27M;iZ@M_j; z{tJj`)u72 zGm4K>=U83`aPxpzd0}#8XTT%#HgQFVpDpd%KFL%y6qX;HF53g%)^TmPH4E-bVcO2w zq8|QvlG;PSZAgf@uc2#>WF+a_bWSf;P&+X8tx|p!ur_}is@RP#wX-?}wrS9&A2Z6B zGKb8-+nVcm+Y!LwP!4)qN_m1cWo52u%v2^!LfmpQ{VQtGY>+R=EW*#0Qx)cEzcTDm zdzeTsazml#I8_HF1e`^Jf!n1kMPHoL1%A8p5o~(F)s@C8|HFxfb7x@%m z##a&x6&Yd2PQ@d>hW*yXm`%B2P$V+{*M{;TzOc#F5((;2$A9;AV zA5xHV4ywDMd%?B)4ro^@J*9yS6P=w`X6S444E0ny5zSy%0 zOtY{-l`1tnt(wg?$+9vmEWkBEr4Wn?59^7kXP+NM2@@%uYA zOi56x#vA29vR|Y8Je{`|8}TMpQ1$e4W@hS$8QJtMQ6JSfg1w2Q)D`Z_toi!_^7FSw zh91~$dc_XGS3R@LvKn?va+-!`X5w}vxbKGi$xO|kv;%blAms&K7D~WgUO?I!khM@* zTBW|{-&`n8J5g>uysp(){Th2%=9?YPEsZzDP;c`{ZChxl4QTedppimRUamhjrp6*J z!RFp0^h$0`+(p6ETzT;2{4BiG6d#{RkuU7mVxd~<(Q8bbQj)!tdq~1r>(Y|*;_hM^ zD~i+I%=9Acd@tp9m*eSBnJGXhPeh_OE$PU;zPTZ^>IP*j0TLerYbVbvS{Fu6Gvf-Q zLIeuR`~z1(@N54eA-Xb~`zV=Q5P0q;EIQoH!rB_1@8rtL$w3DnD<{0X)-DA5+3{d^ zYchXR`jCf)t^}?a(PJR%a~0Od)+oGxrQLB0Vo1^vjWwolsq?q(a;-kGPxgIN7;4XR z{>09}acKjMcM-^$kf@KCAvXSZOl>`!J^{bY5w$9XE@el9wcsa?Bk?9!_V@z1GG?wF zZa&RZ&J8R}dV4D?HS?(VVprmIJzEXZQCx}$zlimpo*m9yxQ&T6rSGVbY_8f4Pnp6M zNt>~woXW$&w~_GZU|$lrJk+FdRY=w@X@%ne3d@^cYjI z3Cg5yW;V}m<}~R<{xJN49_D)7zy{tm`_-2qA#X0KG6pM-@((QDv$h`ii}b#U@~IJ* z#g$!t!5EA_!FST@fY^b$1-F9vIdP*hVXcTm-wZ~|OHxF8AuD8J*CVcVbabq4Z-*EC zgujL64^VsMjP!FJ6M3q#38UN)ou-adKjJskGr-$c$FcK3WS&*E_;h@WSMrJ9qGb(>QIECx+Fn z&i%6BR*cOGm0=z|=A<<;0y^6{-;2>8qt`r^^C-#Z)&+mrY=x*4_=_Oc?jk7s1b>kN zFq3v_`;M2RH?hIlj7`}2S|pm9h6aY^Vyyb!)5m?UMb?3yX{Bp6z~iK!bs}-fr>l>!^N(RxxOXP#RnRZj>Gh zMAJbT+mFq^>~p%MkKgd2qc00jgE+%!J&YLF1@S|T%13;G%skv5(=Ih-l&`C&zpMsQO@HyAL)TZ%vSnW8}I) z8<^Tsf5<#mcM;cf`(8}GJ=s&sbl`-x!s8>g)??6P4*!&G7XzniDD9ch{04VKI)N0# zqwVj+?|D{wqMeF6hvd_aJ$Exr?69rVUhCMY`{+Q)@bIwb^5QJ#sgt*-0sG`g?}eu( z)X*h8yc+Q1vz&DQ2c&2*x7K(-G4)sNfXjpQ74tPn*Tx108(s{3%ZI)RmwQwRbgK#U zzi0vOhWT{DD!6t;NW%5b1^er%_NxvB&p6dK4N3f>NBe>U_Z`tsqEMs2}EbFcVK z`xMYsp*e&sH!*`WJox7PCy@W87yUQ^O0)1MFZfPt(8K5w`%lo&QRxj^d{^-ka=^B> zwja~AN*9;|GyqNGX~XYs(1ck~jUd%3e0QBNVTO=Vfg1D^+tBnK{P4lkL-0*1h$DN8 z1Yrdrf%dIhX|$Z{$jAsI3p~_VaDWl|Bq)vr!tX-g5(XiZU9mt8ysQ+oa_TT#C%e6+ z4#xfK#Zfy5dn&i%5SkPxz3J1a!`~Ij^&m~XhNjf-V)WdkY>*DJ=w`k8EN3`^iR+Dv zY8$9c%C7qj+)z!D7>~bINkSQ_#ksi|$KSnFA?yV*8bwWMcq$|vImySjqdCp!QpoAR z>FH_UIqyZ_8GG31zk^wBBhaUI4+*AHfMRHBqbD(!`T!)Fb%(Gi0@jJ!Q}4a8EObU) zU4Jy!(29$T!}wPe*tT|)!WC(6Er1(kSK1AFJW5pRM3DIYFMFDX;P!lboqE>+YtzIh z&V!&5QyIQ>W)za#nsQ@MFOmOrcAZy3m|fP5k=T5}KnUww?e7K9>;$qZL1RDiAJg@~ zRZzhS0HDL{%0W#v%A9E+#Z)#o20W zbDT733l@2>={xG{tU2@*-gwusKltBixdU-JG})wEz2J%GnGC#b1Yaj}q3CZO%TG_5 zU}v&I?T%P(pzSETo&~le>a(1>E(1HAy3G?q$c~t!A9ZzGRt!Y3ZZL+BPcV9}`t6@t z3zQ6AUu{m1g^8P6;5yYcH7`US1n~(AGp<{=E?z+UJ1Bg>l1elFi`BGzxXLZ^Z&ou( z-h35IaUW+tnSqDgO&@H{v3>8@AU@yq0@lT$L{*OdA6r)fPi6N0Z#C6a zswsqQ4TU0W_I1YAf(WS)*~^~nYg1FnjUx5tA-LAnW)-&srm4>)!qQ zD}iMXMf@h{86hjVv{ZV2{u|UNc`1pBUbx(tSCf_?gwKUzYso5NCISn` zLa`8w#g#OQehnY{s(roxN&+K!=U2hxmz)Nb-$q#@1`J3_wL8+-%fZ9auv@&6yYh z*9joSIf_N9OJ0iqAXiB)n8`iiK|ZelwwDmsk<)vnfJWr;+2@~LIcKYd-`%luXM9nS zIQqDb>cF9Y_?m=A*SQ+Jp$f{dwWa|FIn8<;gKyuS#gx~Je+*?)Y(9d0WQY`oj5OaD zByzFV9l>Bh`6qDpmz-venonT#(w9v?@UMmy+)QdcSE(sYZOI46KtHiuXK2s>ZC zeBOf}!%ZbR(0yOXt0A4bzL~-Ju2fY-C;+3a6}G?!2&snPz|KG`f%6*dOrNWS{RZYX z)1*EEGnE(STwGiXtk0hZa#vOSL$~mKuL>?mog(_Dx&FW;RZaXu(E+cPL$x%-Dqhcj zY2o9bJYajvjAQarsgw&bI=H^CPv4@WSQPeC<8Bjq8SUq0wId-s2$7C|9nWFxcvx36 zwpo9NBve1}_AU@#M6o^<12XN0s}36F4xGZu{w* zoWQ0DE&d8R&A72PLB_}}0S zCULMUrr70ADYW2)3Q$a;ja-%Q9DB0{F8vKuP+|p$P{63m*y3(Gg}P4H*rn-Ycc`Uz zT2~tF0KT0lV*>oKWzCwy#&A>Bzr(dE9}owDe+CLRE8yDt*tfy->^s9asWNWPg|qsz z9oR@kgunj?DA$EV4I+xjW}EoI#O?eo{}Hw%j*5y@6jJ)(Ez4A3&STq%cH`d7Lrhw0 zO=#W~^i<=7yrZMzi?1UOz%OkU4Hp8l1vY|N@%}v!e;IkPTW$`BR#=+h`a#XUnf~}; z{WYjC2=y!R)aUZxcalO`CW0!PF5}5$O174=G8uH17CptH_wMigTB3LU{P`DGv)Ldq zG)k59D+Xl*elwKf{WD>3G!YM0`Jg0|e_9bx>P@n$L4iGmv7~Sc$|>Lpam3cN@GJ3P z(p+6#48CR=H?4(a(1vfc}46BZ21|HZ2 zFK?m$A~ZjCTdYnZDnrbe6i4#|z^dIrr3Jgfr+co2>q6^0=u~o0jmgIP8bX^96ie7j zZSGQnZPlg=XX$Q;ZFJHJIBFzL5Yvmmc*T(*3~BZ*jtw_doZr!i8yL1rnX2@uR@cu) z2hxG`7yuk*-fGWs8Vpo4Ff-$@|8Vy!#IEOJSB<*zuE{c_J=LRmhP^&)odSwdRt0ZX zDiT!fiAZS)I=GkohhB#}HrxqHVc|r8QP&+0IZKAH7J9&^>8TBOh@45=f7rV+@vshz ztRjS{Mj4WJSNv&-4rVjMI2wUf(GucLAjmKrn*@H5j#~}9xXMedEl+xnsrP<*Pb97%tk`ch z#^fMOCRKVi0IQNzs2Cj`^+``pUxZ5X7f_o0!CxH8QBd>@UKsudHpA?L|6cX>{iQi? zX=$dnIBv9g(jcrcH6le(Z?GX+h5WvW*>dRh-bFj?MJTN`q+72m#ov>xTVfB(KAkOznWl$Sk` zom%(Q*Z<4s_87V0i6;!QuZl~zUcU~2bou2nV!F$@zC1LY;^v_}e2+BUcydG~*IC^oWD7^k5 zj4-vYM%4l>`jatdq#8fT^mIeYnoP~T&*fv9fr9kb!`-{i^u1P zeR#c)E223bRndLuz-@>Rkt}H~Xy^5E3wWE!h zK4t{J(VOB<>vI&vngNU7cjwmJWAHvW2iONO%0W2L^m2M8f4VS7Ewmb!N?Kb)X7t0l z^O7a^G0{OWHpoO&Je@LilSC0ev}W|oFzs>+W4zA>XvF!P8oI#XACwi14xjYjE}Q|y z)9_a+No0w5>%@xg_ot4b&I|Z;?~O5y7MRv5?H<T>1XIIjqTm(9l#v9RzD1OgrWa2s_o*svCkf)d=_L29@kve>c8=oS>JC1*rsEyH)k~t?+JRZrA!<`0>_d~}BQ!NrP!Kax{ zhDoeT6RnAuQHMvpq)1(21ATbt$X%2# z2G%(7syHSt#8_;g8e#XF?XgA2hOW9RVh{GZhUM`TK1Ix-&=~w0jYk&7S{K&!*T=)pJv5f4c}2lgG&e&)9!uY zD*$PktK^bj-NN4KLS@9vS^0*QWAII?N6XH>hHvV}h?x1=j*ZSE{# zst64i#y%7#RfmFssM>>T!eH{|YA!F`#Rcy&KQ3`%*aw`A>1d^FoEpW34iKWAZ_55# zTxgs}@eScG;m7?yk)4|wZFVVN0!x~d(P87Q;zu&3BoFJd-^Vq}^of0YIXthr+s4?v zdFU+cvGa|o%nsB6`9cC+9s;)nDB=ozBOZ-cka>gA$@r17z}t(h1DhM0jW+g1mI9Sm z=0xroD&}#1k(LUx8L!ew!<{_nV`$mzGOjYxxLIl9=ll^rpmz7jj)A^FN6Zx1Vp)x~-=mqe4l*8+Tp8T~Lpu3`yvTs8dABM#3eFvG|ra8##P z8+sOdK6c|$BhbvB$e$p55fqiy{v$o-Q;^3|iX#DxlPM4?(mIjVBLFWLcc~?&*8&^t zRpp8l;Ygt1Sin!&p}W$41sp#8=CsR#$6#5*^Gq4a@Z)DWVO$J6+Ur2DWfa{WtE)8a zq|3O&Ed`M+$PqrvcI9Y&A&rme7m*cy5f%)eqtRk?F5doYIVfgKr)ynYo%IjQp}nSn zylF2tY>N-N+&bqdQJMftB7URSJtj9_!`VlIkR(NYVxf5@NffJ$tcEuU=7E+E~! z5{SB`;d<&vSxTBZP7@tJMbQ-jj*P)he~R)pA^T1ID+5t}$_6ZRn|^tkMKo28=#*ob zqWA~RaIxQ~fFa>tCRLJ0uN3DKTF=!xDas$}4vg z>kVho#zpb;;9rg12Y-kVHtwg2CND!Aw@xD46b^YAIKB&+g{z;FiPw3eYUv(@0DPiB+;bl@CLPH>5@cA@VSz*9V?d0y9Sf1Ztu149Ic!6BO%?3}`-PVKvX!~GPOP7LO< z&|*XAWnz|FDcSpfK+W|_**9(gOkDfCMj@hpzbFN52RQ_54}$r(k*;$%;FUeVfYUhz zw9rb=5%&&l4kIn{sRcevhzg=le-QvY0>EnPYzGFO#;bUvR#_!{4$g!SzR4WGKWls` z`Zf8TrR3(m+H~6Qh3#v&0=B#LMFgk-hm30geejZ{@g@zCxt-rv-M$SsQh5AeM%$lV`}_sRhXYuqe@4f2 zK@}+wjt_@R3U9zopI!nNgHHN^0s-5iihHLU0;bXjyW^|R;uOFpcd?aaFa#mhkqEM2 zrPaI3@|dQGkM&?A06KCv;xXS8Ll;9B7T_gt6JAj*k`v4qRov@J=-L9&o54jdPNt<% zn*o3iCi1|K(PCW;aWY*VQ%$%fL??w-^ik+_CX3bUT6OTITSI?%A^Yj{}o`%B60MZH)*vHYBc0 zA}5XABR>r?DzM~fJ`fqm>h{U%x~SQ6UDf~);?UoeXCDhZ3msPl>Yd=R-jA6%m@LWx zYhfP%R(2`nm%k4X{2Va11|*vv;YKfz}!;+jK1nG^GvD8a{e$?EW!(;EqwN5P5kG! zVq(4fCxc`jNX3rJsS%W7^W^(f<^uvI*U8BOGiR^M;}Hhl9Xitm+6I#?XfOc$+AVXP z`!URU1FuFzd}jcK9O&nvhEyo+gKxS$0F@_`BHc6XAWBQ6PmJujbReKt8e-L|zg+bg zqa-({!jO_dzegXklEP=Iae_k&f<+x(cuyqPRU6yCBLN}LQ1M(tJ-~JR*dW|C1%K;f zeQ{Y3I+bN4oEGZ9?GOCq`a7lQ{& z8_)eZNi38r1?tGCykA%Dty#?_hv4?6sSR8!j!0GN42fLs9WF(98Kbj_j=K6saddI0 zm)*jrLeipz9aePyZoFV3x~WN!R$Nb#j#2l{lz%xzdAg)zb;OOV=S}>HCvMn1hmtzu z=D`|+H&HbUB|FP@j*T29UiO^N7HM)fG1McxyEY}w!@KWSipWk6o6F-Or;ghz^dv=# ziDwP3{ekV5K5^8_Rno!Yk|m}ni;S4ko>q#~Idoo{_t;JFWC5r|_K53Ecr#|i5ZCT6 z8;qSVGT3-h@~OA=vDnxPBxhV<+H@ZyPoQO7%1VOCZ8AMhX0uKC*XzeX!TC(;PpTWh zZ!oIzM%B`8P?~i${9Co}Qx+{e!b1Cwh@nU+<~vXYX6WeA=#@ZV4fK?Gz)#jabG9YM z$;#Dq-03;H$LgP^qt6bsNfgbr4e--wRsJQa3EROBPvzf}vE)470ST3Dw5)&*<$D4= z#9*6})xB6Ufem+k`vkYr&74)QHC96|5b{iE2R5IL-wkM39w22-q)UtP=UieXJRcpnwb{gFJIW@{mRe#F7 zmV-rRXa_JvIZ(Lae}-I9n~jix6dau~UoIONKx&P3Ab|)b+wHzOJ6?0CUF_BpNEH%a2mQB0l@)E`CilMp|r7vwBTK=<09J zsWoj!wYuTAZt|>xuKhq=k`+XO(4q9$^>N(^U-*p-fPr**R> z==Jm_w$%{`@9G~YT`8q|Trk&h52Kem`=U-3U3Xkv{JR037I-~ImySnj`73(zsF)~~ zopkBOt96UZZ1mu!s5!o2Rxlpeu69L@8jgh#eha=0i8r4qmi9rE5PP=NdpMXy=H9FH zjHRFO=oqarrk@WabJO{xI$??jOe>#uDCwZ=U37>%5H9f%A~J~gUU{rS?9jPB%HWkf zLwZt@-`6_<{D&%>hH2^6tGZ-FO`Z%p&XH&?=0(r-HVQTPbOpDoNv^#`+c70qo~&nI z@xHHYTHZe^dC$gUj7|`k$3mtueBbd{;rCAxu|^ry@&sjWU(ars2EBtI^M?x(*Ch*A zx7p77hMw9GP?jR?)oQJ*X+4t(MroAFOTeLR+s=XXop50SAf#ZP68eqkP4eFJw@6xT z%*?s5TA|en6LmvX%F*;8L8fwF@9KQB&#Bb+oO=11bUNjq!Ygu80daYK3ydVlPja@k zRXolB{}48YH^wbjlF4Pp%%Z0wM(powUW^5$|Be_9^4&xUiI$%Qr2}+bU4Y$ew*tmB zzB083{wzP9(Z=Yfn0G5+#N3G9RK9z=FN?UnL$GpHg^Z_O)UsyVgOZ{4lvO!HrKt5} z%s>$m>v2iJ5LBOHF;DrU!RTNX`lj<3Kp1&?D4zU2sL}x2m3x1HNxbs7wIV`O9n%p8 zuYeo?x%`oH(!c_#@|i8eXwDr%&Zf_&8BJ@_xs_$bIY9yYp!oUK&?M2)V1Zt&fdY-MNE*zlAMORx;U9`7fV^U3IDGE-z>t$OFS3Yl8%KJzrGs>N@43>o++PRyfHT}9ekMr*sl7}W3k*c4m9-}i-> z{jA+qY%rAmTkRB_3DB^EoLWkdsGNRs=<2Lp8+#v7+4{u;8m6^d2;36kJifH849sZH z*sZ3c*IyZwR}*zs2v(_**VA&Pm|yT;923gXIRwEv)^#yh0)h4OwD0*!&fZeodFREt zBg$yiqEC9qT0|TwNO^=H_Y4?z62dj%Qg@O0yg9hs<@a~|GE$vOH_mevef=WRBnh)+ zIF3kn@bYZh^aAi!8S{`MPr6e|kO42g%Kc zDtH|wU0H_C*Z5Qy-a3+?py2BuMB`{B1JcZijY6iN9S|P?q+c9z?C7WOm9mh#7x14` zj7hbEJFN0h4aJNO*a;_-fz_n?5C|ueykMmHA#nPYqxrb_L71&t-zLR)Tnlbf^0~cU zA~KhnF0f43;wKPaXgl;PHIgP``a2#e#tgTO9-IjBVier4F3}{UGIUNITMw-)f7D5I za9?r$Bhbd*$NGA@Dly(z#dwrLpBR?4N|Q{fP}fdT=VoAXPAO?_N)bLg&S`T-?}dok zFRcmw@Fj)DrujFtGz0zFC&Q75yi18U=bc=tlqC;)776ECh#zoY%Y7v(z+Sl1a5j4) zb1qSr&+x2#omtB1=wFD};BXd|?1Y$l+(5k^Cbo9*A1k?4!49i5EPungE%UQC7zb99 zxCNtI%pLTVj@G_;9`m;x-)DRqWppIT)vU{a|4- zfB73j8R86 zWcWQ}uV<4kefnK8PYm`{;OD^!{ zd%uusRy-YmU!q2QU|5t_!5NU|dmH*i5ql{U<*5I1(_9_xYkX?PMzqt{x%GS#XKrBA zWuneN2N%2aN)Br0Mn5R==xnFR04_zYpJMPwvJ^k0V|3yyT$5t>9uX_p)XdT}xaIF5 zJ8)fc9cf7|QsB^DVobgEdD3i=i@NDif%mTbg*{!AXba=TJ*e1w-Hy3*_WC?|v(C|| z46(DBW^Y&t;a$Sb53j;RR{EXH+gfQ4kKltzqMq9-D=Vv9M))+>*HKMQ^yNhO6qU&y zYZ<`Po89}_ML`*lDjd$!?Sy$4w0B@j4O?T^xKwVz?Lu2-%1A{fBos!5v#q0@9ctn2LnKdW<+D+850w-QvY5pMvM(A$Xoid#^9;y=iy_fMv$PZ8nRvOY4- zbT(Rz7qJ`gTJ-|zqI#pSPV&ANo5O%Lf5`fWkugv9WtG>Ik558Cb}L~z5J?Ts#z&rh z&y~{N2Z-(3(Z2I%kfz>mEp-Td5VB%@L6rur=`RYE5F$yc4yg#WWQTygw-VsB=T+&p zwVk}9kR*Drv2bAFi0RYPB1tiiOwN6!`Nw*kOw@0!8fblUVDQdT;;yl?Po6~wq@wMd z&dt_PBGS{5BY1DeGVT?b>i_hL;8NW-+E~j5 zWsWC2)@8m%ylht+e95l1xtR_`EJvBqwwLXW1M4w$VbL3n*3Q=Y30&$S*e!E?+cpmd ziv__fr492I_Wq$pE#!hcK-w`QX}D z1q?~wM*Gg{itEvs4wE{oSHNLj3v;Wxde~uo$6meHFr&2lJ_?h=)QFVqW8`nk$NB7U zUIv1#PvMHRbw+Z9@nNDq3XULW8soUzIc_#uP7$RGoE-sH??NQqu-9|r-6pudY}D~Q zvX22$gnTAzjke*TfLKacXO=$xJh>l6@N@}}a{#y*s{cDt0PU@6uohGW)$tr#g0Pf&zh6*~NdeolAt$f1S9O9lw`^Co-| zbns2A>l3sd8iA6DYqSKc2pGA1Pp*lL0=hx2NA3H(iJ=7jTUR<;55+*obdq>G`FtufY_HTW&IBD392YGs( zOgr-=58U_6{SN6#QQ%)N!O~Qpovo=EC?0xFo;UA|FYV;zgD(ySAvB+QhV1uDrBO_9xQVGyTon>4M*542-@W&;B1Cry z%p>aAHHNjz$H9m5Vwv#+16S0_Or-3-12?55uS|w9Vdi8T$Dh^=qrSfBO9kd9sa{Tb zx;wl=dE+fGArF#_29v|kE2jU8(I}hZTJgE34kmWZjoLVna~#B6o+G8fLpKX@u(;8x z-79xqu#}lLSHasR#Fe2Ji`|&$SuC1j@-OjNX?5U>;t6z`hU?RJXDzHQ6vX1Q; zONp6g+cq5z8FQ=fgP!DyYFK#BEGDy|lg)HQFOY>MXMIPcEyd)zgb}G?p5>y>cnM)X zl>Kvtcb2(*;y7o(Y{X*{(mi0GSa(0~ke~UD8#C80fIGkKhl#8CU5n5+j$Fhpr{NMD zwq_c>k$jw1*W0l$&TdsB;NV&+MtQFhkdUsb`HG;<})ySE_8jcmxRD zK&jOHMJ4MLn0Ao@BYEVXJ@Ih+u|wB52=&>V14RpN{QT4k(YIH#K6_JUE|h`$N6h-i z9x-&y%iUt?VwByM_|)Q0p&I`6QqUU0qSG7=x^SjZg_b>XeCql}k;$Qy#2 zDnneDULy@+ItMFtdv-mjw*TwS1iADY^+#L00_NX?)A~0SbXAq^xK9^!}K^M(7 zJ<^EIEWObEyoK{R_+}hV;N+Eix{s%Y^=KT}Gvrh#&MQHzky$U8^8ZSv(TjqHx;|NV z>+cEX?azisZX?n7Uk8OcNKe;c;e}^dh+f1JnJM$oVdVWd4_2FY4y1H0l=E~ht0&5+ zwXa&Z7L!f%UU5ev?-k(Uh-Vj+DXb26u0wr!nPSIux%Vg6K8;a|-5Q>buD6&zG2Zu> zIgcLQWH&BTp8sH`tDNIF+~c+ro_rKH-U_jQTwtqy_q`SXh(q5iMhX7kAT@bOB!dNr z>%*NTZd2=vHLq9V!MHacR}FTU{OX-1J>k=8Ov$RQ1~+SRY~3@YAzvGnviQ05R=$TU z3nzD~?^Mq$3i30LNqfxZuWY|p@m|IgnaMl{OFzB!#Xy8O_jahrx8Joui8I6+z0`~_ zMxoG#3}fs%d`U$cTp*9E>sJO-7|gbNWg>Y&=YhrJj`OmeYD_0qPQ@|`L{NxteP=kF zDv6ldfZT%qC>5>xzl0iQ@c}bgA6bS*6xwZwN?-dtgG4vWPXMQ<@kU_E$3MLkE9{)d zrX+3NohoA3#??q_(>jl`>_=asfX?W=B<{-)kHc0WSJ(_y7-8q4e~T_?%kFBZWh(bG zhpVRbiPg=@{>#pf<=(yhLc#)D6!=9ctQn^s*QO~mg0Dvv`gne>@+VyAQ%SyBGFx5h zo%Cb1j)f?Sp^DyyIJru_5C%Mx@&V8Uo4ZXH4k`i%^fC0L0#)gMj1UCQ-(Dv}&-20NY0t!rxtX zedR5qC3#il4*%f&EG3~Erb3tj1nl6brM(QF?`6RMAJ~v-I{w4+{5uh#kLYVlZVy#L zRQ4BS3H8rPnJiwS)+|o4`fDA z0osW&Pv4<}@087n8a5+vCX_K9iHZk@QzXOrdhuGv{K%9Q?q$jwgA)^U-V3(PbfOmm z*vUb)NMl*!KsH0Wzum+++af_&=Q1-SG2uD{RXM9-J3WcjJBHBe#{mic-7OF{AS<>p zLvSzg7{sli{44;_lohJlu;CFl`*ygTC;9J9MQ{eM40X$ge~`5}o7dl5Jaw|6m1-M- z)&!+6fO0smW~E8PUi|()^fRrN4MsrNbrv0bEc1C~(sRiO&y4mF*QS?ZoNh#3yS!T# zKm)y~NdAO3+7*huQ9h5dd^j3go~t1K*r56^;LO7qw%1cE9x1{O`M`4KQ^bJt^(Rmr z9YqjwxlzOjEHLm%W)wD-nB!q2Kj$2UHiUNu)lWweh&Hah5&-gjCVJcU>jSz11Lw40 zaQ}|7%S$~Ub;7kf!v_ z@I0O?;R%+2mjXFSiZNnLS~?f#0eN7B@=u{)4jp4;EO_TzouMUPl?&zKbFDU@u#J{l zcH3H0cPHCC>%xw`x&#yCh}Un~LS&K9XNYmNwdfUMHC%83 zi9tk-Y8e)W%cfB<5+&tOBvj@0Gab|02H(QR_Ltvg1-o*@8f=-Gv?n>kvwoA8s>jnF8#ZrJ{~C<3lm_*BYe<$iy!$0R|lA_IIniUZo1pQ-0QXL6i2L#Ha*^~z<+DS@d zYsw0$ej0hmhlb-8^vaL;)7*Qs(nVg3XkH_SjE(izws}rpyGb~bmQ5FQ{rYw9+E6a< z(9qCJFZKU8B`mz({NwuCis5iWbSC`AX|iA#7Gx+;R)qqGn>on9(SJ$fHG8N8BlmS@ zgFPZ9IcrS>G)zLRuIecW${UXMr=s9tRkMl;7QK!_NNtr(iFWga!#y2l zx|uBKp2W1YG&OZ~f!+3{=FlfYhAFZYLD2pIqf<@wcPKoNC6}FcooS!XhsXpfzi?T;2$ zHr9$ka%74z z%p+v|$i`ge!s73Du(x;oqGQg_7`D^annFEWP;j4vgG=hpN4KJs#5MO$Zhw?Eddu+f zY4$hy?gv^9s0f=pOfZx+ptwP~Cnr<%;GYNN`rP}DemUx2FaKdm=W2$c*oWgz(tmnB z+nAaSxO4yBz_@5*>5=H6uj>8bX|n_A$Lt|p`Dk%WQZ_Tg6hsfMc2jJSTQ@t;Fy+Fc zQqoLKqu_Z+vu)K%DJ4lM8r9fsKPm3+U^GfUFnA81fpY|_zbrG>_As}9HH=DRWZGSa z!<3-0X|+0b^~~y$=ZdwOmX^nTgT1w7Uqs!;C$>F1N7KT_y``r5gM1D-JV3DAJ81an zisHLuI}X`etF(YADKTM_hOkF)5m)%h^AzQ^8S5}meOjtvZ?uoFXR-9U^ORo2dv8|T zYu`)*qou^{55dZ)UW|WhSF^Pj>RBbPV+pK>b((Mi*L0OMu#GH<+|1u6YcxS3ys}!! zrW`^>fm z((t;u^|;HV#wb-yIgRkC%C%SvD0+Cs;%}PU5D-egS8?n0Ta(Ph`%G(R#@(+0^d7tM zd9Nn{L)SL4N88~xHTd6`8#k4e z>q*TAP)73dT4`%}nlD4~7B7t+QMTFVJnqNW2TNBUR~hjqXYani7UW$tS;)CQy?=vu zAEOil8YdPMryni3a8aUHfGXB3_E7loAi!2*vmxSf+o>6IgRf6JMCOC+nmQeY><=Cr z%#O7nR+jLJTo<4t#J$Ch(PR;5`&bR$UbH%hz0Nd z@Y#@+FDHRzUH_U8gyM#JM7e^PC7ACA(A^#Qz*PhjP}{|%hpu{}vYWqVpEE8wtu@1} zpE8ld9&0U&k7I`Wo!TgEtwuC7;Mph4N3qnd3?5$Q5Z0;>;~PlB4%&9fk@JS(K*xTd zWnNi9((Y1Ldb0+rv!xm&f%DRgduX_oqBXubOmt8$q+O<5a)TLeRL^MEM;D%cAt6>- zdTTr-y~X}FSi9xmM~6FeV#Fu?uVMJ`#iym6v{yM(ZNzUoHQI7OOBX7VJ?h)-tSe?EWs$^#MO^i% z3qF&1OU<%tBbj6t!*^`v&hXya-}YE1`An@Zb>%y@3kHVsAGhBp^T)~$@O}7cdMtxf z{kRYvBA?q#3n;CEv0v{tSgC3Uo2CZo{-K^jNCczk#UKO(>nGJCgmdED^kS|oyjlF# zjUuCQFS-_D1Ip*EV~LfyR(>t1kb69S9cJ&?t}r^vVd%E~%bT1w(j9X&pr|;}FeBPe zvBa4r*S?quP)L24_jZB_d*z+62)H;y2o!|l@PfsNuY30nW3S#|z}&;yoli4!PM+Xz z!B4b{=pW^MN4vFBwKot$UtVrp7Y28Vr~4+{gSI(>s!+*HJtes;p$94Cr$T%4(Woq5 z=r-lNm`-eHfY}wQsxdwMJfYnD+`Fjm`-XQv-{lcS8EJMh2U z2{V{T^s7>2tS_lGH%y2JPi>JZ07pJ;))0}oaXCCoQ5ruDX5E=_;6KNA*>YlC2+HhYJBdJ}P2WZ+nOzRkLa@r>#xx zGA$z=n&1E>9#}7^O%u~Uj*aWqrX|qQY%brkvY=>ZXE*vR>y`V;e7<#k1n_wXGhJa0 z*l6Dir_5c;LoycIu;mqtk)%ea#%@bzRZt)#RXW*msj#XsiNM_!%D!V@HlPGG%UrN; z%ab!S*G3!WqhybzSZFCR7Rn)y`6j;ecNp8`)n%bkfxzKhc~zWV*mKphyzKpwliP5^ zrCf{4u$b15A1U*CZ50P+VWN`iZzcIpL?<_l452igv(?=R43%HWr}c>-m`MF={l~;| z*d*iAJM<}Dw0~kQD!PZ*dOAyx-}PC$reN&@?O(GoCSFF3wUx>wV+jsptZZW_XXUN62t z_#vhx6Fs*t^y~)uBXFOaNBvv+6p&^?((BrEbY}eP`YoyC8yjX&A9oix&C}y~EJ&xTbYwKxl-Ckw=voWrnrmD%+&JP zk2W`-S?xme9y}@;AyD5V)#cfzT?+7{UuW4l=YrPr6tFE>y`~4r&~TeB{W+Q?XR5MG zVm;D_H*U1RbWyK^t)Ds$eHP?E56B?>kuG-CkN2KcVXL%Ey&Nc}s^2 z=+Lgmb~e*@mb&w^u&kF_dkBDZAEY6xge$SF=bd+B@tD5DEQbavGn`J@@QalP!Ok~7 zql-4o{-a+DB4fpp1~V7s@~(yN%3JPL@#R^&KeV`7m=GpvcowQpAI%PyeE!t^Wrd#C z}D3?rm=tJ!XEx;bvFSYQ!R-gI^VanC++M$-3T8egkE?;o&$FWY$}slpm9C+<+%F5CYOwn+xHO)IU{}KqAL3`R9c`>#d=8Eh8X)ob= zLM+Bv0D!?*XXqfZ&b8JV0gWD6d>HZC8TTUUhlPfEk%an2c?=mIqhd1xVe0GqhI-rn zvKTlZC_t@7P%cAf-lz##&D3~FEo$Yx)PVbOTxp8~%oXXwDr+-yn>%z@UgretQ08o{ zaQRFpP!Ph=YdS+~h_?cj2Fe`4bW-@@^M)-P+UFhiR=U%#z&}~_QF50(m79weVKjOe zhlC=`JC)(X>I01jfm503U7+@*xytDHg`sDQSM#Re)bQx_7P;74bQcO}KglM3ZdQI6gwBWJO? zOi!i>K%8hX?Y}!xibrEBu#WZM{COQsE~^5u4m-`JX4OD|3#a=l>$4DDod_y*b07Tj zIKH4j^x6R}gD;W`a*B%3xp&&+=K;m(Gq_GzAoAV-D{5FvjL(C4F3)ToYhgnB>ilxP zy_)5#F0(q?xc6PzPk83-A(cdM(hFa{XJ%oEj9pc;AU2r*?yy6ew=?fWz>^a258$x+ zo?>lC2kmra<+-e??1Y+;o!YQm~?z#kuh zQa!#pvo~`!cNq?U``74g0z3EbS8NQpr_QdMscn_+r|ntF27BiB-1i2A5vHZmEm6B@ z-y`M!7-F0fYhbdo%W2rF4$(V1f|u{NhCjktc}f}T>+8LJ^DihEb2zGJ2f?U#se|@{ zL69<_Dh5Swa$i(JTVyCLsy)prfHX?DKFueTKD9rAJ~bW@(K7^{zuYBRZ5x*%SwMJv z5|}19UdXOchmT^>(O|xKlrEs!gBXD7-5c3KPbIbHY7c}*jGloyF|XQb-#@~ipL^R#1&rHFqGG(=hW_CBSa7F%n8KGT ze*5fH_+qvFD6fyb++uevTV!p6dNHR%Z~ zSFBzf{-{9M-6%=FY@oMr8eWYr1PDt)mk(OocVbXehw~K|eE+ZkajJ3n{!tg;ARyQR zowHjAhU4>sJo1f!XKZKY+F*3zKIAt5r32Uaj1s6lYwt=GPbJ{5*DG99^t&{SvXDOz zag&op+#SE0rRD+FuClv*5CUv|SQSj($uM0yPbY1A%)fh*!%Uplfz~8lI`tFK^}K^X zydxOvc}$8-4yc^@Qj4B!RZJ!&rDg=M$GSo8Q7J!rxW|W*y|H4ZQyMBhVbpq0Nlf5k zE7Q&^ULWkQebb?sWAtuY|7^|b`HGz--3KBF$=PrW9NR(!=hWt&jP>ie!0cp%h^3!X zAmCFRf@-I0G{M>q9x{3Ae#V~bYM3`-w`T}tzog`U-HCh_gU@gOPFAN?G;e+3ZP^Lg zGOwdJtp%KcV>h;}rMyF4QtX8cAny|h7?@qT>4?BD0lM_}!LyD{mfue7f8U@C91j#) z3sdM_x%v*qgID-%wI(MH&=KO?>Mwh+T=iJ`0y*An@R0J?{KcQ50der!D8N&-fO7?7 z%X-rKr*1*0PdF03$-3;@=;tAtqrZy9-{RhDK;zxZ2LiXh*8&i{ZoZF6^{F_irRbV| zBI2;ly=??*?GiV*)}5`aIaz&x94$UDB=5@-3saA7?`i?xo@srV>||r9j>MQlTUQcY zUinCD$v`X0Et8XfZZA?w6ch@QB?9`lE5LP{Ub$wEq&yEy~Y?Z3R zPa;m(eO8{09iI!6vvN*6+9x+f04bX7;H(3{Qq`Rkp6y`=%8bJwZZA%epI~iW-6{386>Xv43DKi^I*D(tzuB>kBXh_&tm?66U!=h57SJNx+!lzA|QS zoWl^}Eb6qI=)+vEt*l%CEOQB$S1SYHd_k8JRw7ID+mts?u?{mq0T+)%E{G;UZ#G(Ql3?lQHEo z{#>EeP!U?W6PN&1G#D~+hxDrDV8FawMN4~9;ZPxH7hw34Gt?pECx7Qvm~Tu+=kvPY zcP$u?{8Y@{)qk~zG{vO)&mYs|^So$`q(7)is-?(iL%rrKsShnUjO?QxSIM{p4H( znKO}jPlb2f=LY7$aRxlUa`b@ixjm@Dlc0K3g~92>zKJe0{Sk)dBf{yGr5YJcRq%H> zuq;Mj8k48VPgvkaKh=ixwett;;fC1VyUQ6jhI-{pO-)BB!c>uS7dX|8eZtvR-;5|< z@wRmcBFj|6E|%AIYE`~kDoCo*z>?2#WmuL{p9ST6;*2(5sVl_QRc;3%PQPyTid@d` z+k0C7EulNxuF~&Mx5Rv&W7mJ8rmE2YK+Wyrp9IAFKYheUC8_Q}x}bXD|0%bZV_x5s zI*qD6)6%HcfL-hgMEAl}|II%LGS!!}Go|skB*!_#DZn4IZ{xwjMl!d>{u$6c22+SZ zOeX%tPqF+`acOTjlkGdb#Pm2qcd?lox-6l=YgZ2Pt56ISVoQYJC}&O=Lz|I50ZFnh zI=a_3RvJ&@kJ*Cp-!dmuVj$CW{lDa!z^(jU*0!5X^}I%*ooDdz3p0+^KH&lhsJhG}JJwhzrE>V5Ei`9rG0@ZS*ft921LCoPGZ@%C%ZpaWWi`S~2|? z<*cb@Lcp zTK;OLrb+^sqZM@(5)+5pdCTsKLDU4WhV|hC^*EDb{Co|JXtgxq?XR5>fxTc?gv5%9;Co*>;K_**cqJ|1gXBa^OgW zIj@YGfeFU^VsF6O@;4{Put0|@)>_AV?SFDTC5b!yI(H{J^D5oVTamQ2fG5nb6ywPZ zBdX6ryqs5RV}u?+E?)`W1zAPlc;6!`Ub`In%hYYnh7*un5Q2Y2q@0+#>E#CGwUpx? z0^vjC+J=JB(6jWoU+cmJ*wOnSy}{lm%N**Q7*jb19|r9@tA5L9K<{#ANj7e)(|xVv`c znJE7k-F#YeEa4sp0xxMZ>N%ROhh0(cwd8G@jGyK;^B$tg>FBYZ6c3D*B`J7j$05XP$9&5pg-OHkzug3xuPg7}llO4^2{A*3h8RCAF31qF(#m?yk_QnS_U>+P}rtNg7H+BF1N{XtHD zVS5gI>PVV&r;V~j_ov?kTe$ZE7kJnWv76Cf!fYU@W!RbChA>Fxk51&UZ1LLqSh|C8 zX{E#L0%-q&ccI#ncy9}#Mgwit+?@Gs)$6f4w3%2^&t={SC{b z3Tz4#u$_6HE<)~ID2K#El}apS(1|X{NfT7L{#R1RYpdXKd-yJ;`J3^YPX*3}d~h?h zMgxPAuFQd=FQM87CXf=K2(#%FQ-Sw@r`;;{Jau(~8C*-5z{8>~%$0CmSEKb6>dKh* z1M#dd3kpumCJ+)ee2!o4p#Uvr;Np<72M3NFK4CCC^i@rLf*L zKw4@>Aqu<1Ng)>C5Nk;0{%c5tToleAEE+m0r_?T@B{NZ-I3<1fO=h@DCP41l4Y6 z^9X-2J^?{re1iSQ%>)&d>hSYAQXX1`kRAHItk*Ebd->&U4o|{a~2?-#<1G%)fo`Xf~%+u4;0pl8> zJOgqb3c)TNh}Y6rzy}U@&cHipIJ!RsS zkpaFPI;cBeaW)TL2oX7Dz#aI&iiJ_6r*dd-RWf-cVNTF~?H2^G>e)c%fal~%$baUd z83S+)M2ESm2Qy#%Y~jB3wHLvrLc)gk2{fF8{i(b)?kwa4tcs8_fd?0*1h@mtMwZL^ zxN9TB;!#~ioj*KG&CH-RMX8)PII| zxoexTGHPmBd_-jC{j}OqEJcRcGo6p6hNz)~)Ni-5TSo0M6@;;lQio!dt$YB6VysNL z2TzqjjKRI4ujZ=t)f>JTF15uh=Nok7f2;u6y@7T~)G~=c`HaH0EriG9>qxuDfkX76 zb^Gyr&6n|D+AF}F7R`q}Cc^M=$4jNCztG(&kzTHe6Xg?PDBeBa1_yQ^M{j#&{sO!r z1&55&1V;`64P8}W;V=**0(pFwBhCx9QU z;PDgbNk(4fA9n~dcHn1;!{1gv>Ac}kIIs`6w-t#O5j9c2HW=$PZXoP=<>TYiuqD79 zs@GuV>Q|KT!|TeaH{mL!^?e2z66!}dOipmH@g*A2B4^{n`iuq2#r>yK};QXOs*AgRnb4k#Le zt39#O2$`^Zs5#?=-7iNBzSYGf&>hIJ%+tlQnhebyf@)<5+53tZ16!6e?EbNs@nBax zk$u7UmyG+Ef*pGa`Hn+}P;oaTxQH}TW~hP#OFBPyp$*pxJ4SkOy20Tv;!!te-Nb5X zy#)e1MqABWxc^S^4V-`kLkxM8$x2@+58oZA!g&jF)c^>Zfcy3-u*d$2?6E2kMpbI zr(Y-urK?#`K$!je`Wv+NzyOZKw*L%js~TL8~6 z$GVe1j;7l@RXPy{<6^sJ?~;8OiJ)7K-1sINR>Fer-AKFPBL0FhlBpmz2Uy&jzf^fp zRl^pQol}K8jB8ev9;_<#S|n7wA5Si)&MLm@0rqWsCH(NhUP!*pd_T(O3Ct#rW*AU? zuat+i8ZShxPG3*xf$31;gQP45uqWQm&+kazSS-WD%H(MXBaqRiAy1Fzm#aJm%Tw0w z)QFLJH>MAd?Lx2xnX(7K3{W}?oM7PbSEzJQJqk~15LuQw^n+&uA=nxz60i-9u4!1_ z9QdISpwIa>^el#Ta{Y<5e7uC+1b-OQt;NMqy*mY#ZJ9Dh0#HQ<+OtT+89Z=^tIc1h zWEJlPB>AZ=q7MdISQ@AkoQgemY`wc3L&Kt^JO&1*0xl9rs&6dkr?84`Q2BXxzM7KS zL{0&pfcCmT!YV zpCBWCH8*}rNFvLnE%VnSgqXMqHkb%cP;verq#!w<7_WKxvmFsk9HI;nH!h`0AF|RK z_BS-*Pijj`N3Zb_*se8%+nH8f5P3xn*o{i!nNnOWD^|SN2`Zre&pkg@hmaz8qSg+f zJAxy_CWMyVS;$~Alqbl?$L3~H!?t8Ca0|jytu5$CtS^X3f!8mA+$qEo$0NG(IiX}3 z7see3p)`s{L%IY*pCXjAm)gR#FL$RhGkE-CocwN;Q3^m~4|n7_{AoeBsJwQ7wf}>y zgdqK|>TV155a+!+#c~J|gm}xl0hb}Z9{c=mR;+}fICkuA$0Hha z!D9AKFx+-e(n9ShN+GPpJhre$j`sGaERg+#mVWR%S41L9d9-v0?pwj+1nVtG2X!R7 zT3VQ#rbolOnMrj&+9hEa5L}>Fy`d!gzmqnB4O2MtGN;w#kP>gT{k z3cK_HV+~4FSH<+SXwo7)JUrE5taHoj4ada8=QksTDxPn}z{|C0BO%7pLd$i)(V1NDlO+QJOiq}NV_QFH_XgIQmOTIa+Nt@y(CU!M{Ja08b; z_mAdPqoMSakKjyZ;;e3xA+RWgbTuXiC^~ARqieQSr4e6He37Fw4@fh5gw%aVAt}MJ zCdI+t9+{4#@ed7}oQ7eDkG1kRUIlDxqLm!B_`L;_Hp9STy4 ztIlD8QY{$9zF%^j7{cK3ccBig$-*&oz(=G0!ir$`%bsW82Kzu9nh^_|KllgqvOA41 z7NVSe#M9WJ>K@w=Mqt9hbjF>QUG<1jCsDrS*ma1cdOmVJ>VrU9M@O-2zA!E=jUUqN zf*S-uU!iyeDiUz7;UW!9Na_Dd(ErohXL}h`6qTa&uUS(bRDnDGMH#g+1_ckqhm$SY zxo{U%1ECWidk%FKUU$3tC8l$TT+f>)r+`W({rTCY**^jnqTSVM3!@7Nk3X#Vakcdn z6FCP88z|3wfy_pqZol&B0J+A?!@z%U0KpS(Y-6Ey`vM^BnEs|4-C3e&WFd^?n9{#2#-l`5!8tyw`ZkFBB4Or>MT>VeZ@6b(3WZC zTi`V9V@Ok$INh9z_K-^}`f?jV>dP5WyWvqj*WU^GdH)X|lxM`h@ zlZXb-@!J5rLRq>Wjb5P?9T8Mz&%N>GKI3)9Vjfa~?gu6thwDQp%Y_RQ-Wqao1f9_R zbq*pn!UdT>)We@CY8!HCd{^dp$K&9paE5uIX>UZ!6{@FB#$nM7POr{ZRKMGI?N?v% zz*jFEjekF4R&2k|HKFQPBkJ?tPPKpV&XzQO!m8ZEc<-h1vZL6Nz+lbv6&npr>FMdu zLSOj}=Vq;LwF>-kL_N(P#zfLZ8PL#k%YAtdRSjs!tGEjcb|y&CAA2_7)yW!0O=5^3 ztHM(Uq=(CH?1_URGC21)(|pLRy#wi`b^{-PYs#BV2cm(rmCwq)GMH_mK~1@=xVZS&1{H4@clJW%7zB-;_CFYcP#{<= zH#`PIfj1E1`Fd4(l?wAu=(^eHJab9I{-jv&+U+jN2fLFi8kvT7aU5OP(;G{I+bi~L zec*U|ac)cebLN#nte!$haA2fubc7eNaKMF`(A^qPO+)$8pZmh0Jn&eA1b)D;U&YsA zMw~Cv@%z|^`H6#~f&CJXgz9SkKg>JRC=?e(GotTJg)Kh#KvBYUZsD zW|*W+)t1hHZ-6tVPqF@DgHZR()5xDN$eBP8{Hk8#H9nVVN9_As9#?-*Ub?({s|81a zU4bBehe!O{eBOED<23rFhJri^|DxD(`mGOU?#r>U_A@7X!;+0$BqwM-!HBO*@*Hp6)`nIdZT)_yI2a@GzSnaGaG%f-0X8TIc>zT2rOr#$q60AiNo-nZ zhG|I6bER}tEos4TV-AJ{ZSUtjy$D!A51%|*Z8UeG;Z4ss!`eJU+x*G*{+GCzu7J%< zEO{}1S~l76I<`%rIMJa8gMtZ<8{>k~b(^SJa3U!MQ?0_ERTP)H#GL@Aeiokfl1Con zz|``L0US=$j%Of$x?P!EN`-H65}^2Kg;8AD2(1J70$e7cf?KnH-z5v!sP0OkQLA0# zcz0_CuYiES4ncK)n5PM*LJ;S@A2+j<%k_l6vw`&-m#C{I5jl2|bpt`FclLtY zaaWVZfEl4}VTf;Sc&fQPdUpG>!rM9~+qZ9*5fFgNf_`C*OCRmWI$52jMuE}RFkBiA z9MEn-(Jx74YFP}PcZoWnHLJvN6;3NXJM&YOth?W1>bFXanHOY7wgt+KSEXjpbc$lZ zO1x}12y5nS63Q%KkaSEyj6zg$RiwDYeODOw;%#v(frLmIUjG7AQ{xo zQ78Lzk9jslYBM~bTg1r1H9E#3OU8Z<=e-Dtfe=0rTybkue~YD0r>ORg(J*w)t%&x| zrnk?ZKW|PmSxE~f*DXb$p{@kinA^8_?mFD`DXDQdm%tN@FM=H_sw0=uws-W9J!CD?2)LswP6 zol%3xYj!6lbvUDL_Qk)3N#-57^+C{kC*Z`~3Bk9x5m?*o1tc}0^>=QGpYu~i}C>g96+qCV4{sc735^lSg zaah6{P4p_(5S^D*-q&6ThId7)q-12A=a%pC@wH%|OG?D*%8S&MXI!En-R^94N4lBX zaC?sJ*=l1O8XB6i>4l=dEcm{!=xe+*!$Udh2i~zHn5-6~%?S^79Lk22lfB-1;8+Q1oxgTJnK*kzE6zs2_)X6)7E)W4J&l|`e4xz5RkS)6Vv2;g; z4CB)TSy6{kGJ+H!=mf_5i<0*D1qmv7N|X7FDOT4QOuB~Q2?+W)?ebHp1BKoz%fdQM zDdU!db7f?!3{naU7=jPmFD)&d`YpA5l{*6oFhp_-CqZ1dUu!ej{@{lf?w+8uO1z_3a41o2v5ooVt{4pZzq2w5GG$*zu;aE zMB>>2<@jiqzhAi;>>H`()AwGW(&5$0MO%qGHPT59ms$cp9(Yh7IYdjHu1+4XPsd(Jb3?%evl?%uqR#D1W`X?sn2sQ zA5|AVbuk=W%ms7rZEETcOyq^4(?z$2=#)!GJMz>_q!jNsmpnrjqTyaaM+wL~DOXd> z<)rI)*8GbzZiZh)_M#dfg1OdERn9J&UC36e!74csxkKQn@-t+w-AOav(uE!6Na((=({iy zKK4k#5;9a!u}b;-Zw$nu$t}0+%Ew2*9wYmz zzuVcc%?W{kPkDEz@4S%hbdT4Hb-rv3~q^Lo~&-6m! z^|LSc5B@@kk{Dhdw+7Tg^1KX}fm~Yje1x~nM+=Y;edi2rt-W5Oc-{eF<$bsUGm2EPf33P`J0YokZUs*mkIRrlKn4OYhTaK;C&DM07klFCMS3VX^)r+EpGoLI+t`m+Z z@qI!B19j4JsAo6~*9()F`At4E7*Z@>VWm8b(o&=glgJDL^g(~7?p^)VGJ#H_$4S(I z((y`D4z74GAhe;Yi!E%w6Few~gKv5l41TH^0i*?O&2>0NisnkLy}VIe7cOr+u;o51 zUm-UmOc|w&vM)Y84dbM1o0=FLV3M1%B`{qvu|KD$%hA_7SHVf>V!LLSZZxHulz4l} zJ3=_y#qb|vD{F)gA5}}jrQ$(V!MDMRqf4c9SJOWQjXSo-pgvYHbuPjQk=J%gpZPh! z{Hd@g(F7v*;|8wq6?kO1X>qy&(M*|DI1#@g$LE?stqTiwnF@Gh-eG*NgB_D0&MJ%>=6H5o)cVJmE=cfC*G z06M0Y{I2%Dgso+Mz4(TKY`fY4|!i*h&*2hyo`85V|<;?t!TDFuNdaRvZejQtJG{u|R2R1Rqf|`% zSx{icHFv%8&YrUZ`^A}fbJl>_7zj7CvQ00&)+>?#tE1MhkcggXyVQuMn$^c1+p`t= zgp7U?LQwYCHATo(4 z@8!V|rtDi?Z8`j-VCF7&*(&ExwOSDRI6L=T_^5A%#oJVN@;r&juXFEm_ige~i zZm2BV;>NYXzlcp>d3=-g*tRj8@|;952Uxb37N^0bAk=ftYi}8fM35o+b>YN>i4fv= zqc$z}Gk62OQ1?u;`dRvJgoUbJLyHSvG?2kyZ%W1GKE>I%-uN7PkNdsrlwKY4gveD@ z69spO1phgI5GSyD`{A)vLRxYK$eGwwvtjD@$VWy>moEr4M9&H}_1PXH5X@G=EXnOT z;G|lY8ksSlQw+jMamyEg-d~ZXSFZ4CE$LCtd%x1#h(`jD@ zobocQ)LHiJBeN|I$c-b$C8EH|(At_ORylINP{(bx!E`+)W@b-d^vh$Rqlcoe_+GaO zAvZdhT5h-Z`M2^DwcwI-Wz~$NL?q%lA2&o4NibLx&Dsx=54X0pMFTMl z1;EtgYV=;Z<)v;>QIVwX$fytexQv6t^h;e9MmEXCr6TWM`K!oCt4J5le#K)l6Qux6 zr2do-V{g+vL!AWE*?VTeS@W4oH0~S%H3OhcQW_{!X~=v3ppuvBHmNAQYgvFN&3X36 z!0<46cA~FrVRnLAEjqR@C~bazp8EBpe{EY6dLxaz)q}IwwW~MP9iLvRL69*1Ac|$& zu0kBm?SsruH>c6toZzCnI>_FmP(tbfXV61z7m|3*_1=gD zx|-PArdV_1|NhCD*z6TrDq5sPvCHKm4aPx11LB6q zlhC8RsNVpY#NtK9VoZ+|-GEH3#gP&+k z0E2%>Lke41lro8Fk_Sfa9@BJE~mAOj5Qb9^OhC!Y?NhZx;$P zTL?v-MpqLvn|93r;4>({MvN`!D_~#q57V@J5Pas@fhn~qZ(?HqBZO2*Thnfj1b#3= zg&eScJnB6$aAn714$6@BR?!IL4hh62`|WD5Pxk+Rc>bLs!|B_`tejvoRCK;opY7 zt^%%BhLbo;!DEcQ;}#USnl|ZcGeUE&UkE&AjqT!4Xd|VeRe1aA%CW7Q@Zw)j00Z2V zmE@IXHH8yXoW*O*7+4Y>4*Ypc>s-1W)o-&g1ikTF9g@H?f+%~>00Gs>bP~lS`n%Ub zzKWPHh`N$G3kRq*7xM|<1f>)mpdjjzqoN$hZRUN4^%VRCXDdxktQPDZAvZyT>3iGo zj*U#ZVC?$3VcCPqoGL^cSo1W@i8!S$c z6366+^Id5OTTo>xVK)Y~Z*#DSM1H_K96IU}Gt=ikhWLtLmCB%W%G5KE%n=(&l3jw(E60w@C0s!xBtR7T^MKz)}Uh>YIME@MM@;kh8#W^9t`4k z9Vh})*GpK_4vB(;Yy1w+M?`tOtp|I8+-E8x)KwVB@V^;1ffmWOJqIZyl4n}qC=1d@ z6?S{JY&4&owAKDU=#=NeDl>=uQ@7zi{)valXCI3Pi;W_&Jn0nDVTzt~DGDlce~EhG zJnzdOlUbH20ekHS3o{;g3I%q5DVf)OZ2r=7m+OIj)e?# zq#jwNVT%t3yG`eU7K9rcQ=*{;QOc%rZx&^tC4v@$ z3)fZBUFzw71)1Y-@-#^8uEOd)P zNgWciZDN0!@XSvN&b$C}4kjQo0>LBRr90Xs0c%5Dgq3HC>vi;G)M!+@cJG8q7Giy( zHm!1%1i^U6IW4rPxVx_Sc}Xx~y>=lhO!^(2yCqCUM(6%f>@6i8D-{SD@oVrjSgc^i z|7(1Cs*%r1=hni5v*T@%vFwgqXV2}vX}9dfFpqFJwa-=vk%dv47Wx?;VSl(8`KO!oQ-@~_;4>vvV&YK8(ikn^u1 zU{KHr0*c1SmTH8u5!H$4Whtx_+mi*mX{7jOEHipo80QCN~c%+B_ zvi0$~&O(p|{r8Ja%ir_-EK{Rt3B{94cV_P&D_Xq1k5dPY9+C<_LBEO0;FiaIRZ>vs zs!268v;Y(FsBhut2Zc{%l1mGipNjIqLZoaB5l-Bu%5kSxz_h>%y0ocGh2eH5C}J!A zYi^LixU^`wU>Vl~2zD>%X}d3vUzBc-4BH)78O{6s%Tx4dr6F}SkO5Ccr1qf34#2|x zeR{ePa<#q)EWvJ7jw170tRZZ36TN6vdD>TV%VRa`6M*)#(8Bh~3-A&?tKN$RK?XA! zv{16w)x2|Bsj}4brrJXK?K7Uy9#pWP7X4j!_#r^0v$VDHf}J6!T`U+%Y%B88TNo*1 zJO!^`z~&F)>c8xZ2n+xz4Iz^SisX1h+-&97Hi_JTvjz1Tx&<%{^W2n2V*!#sx)A#~6qa16?_=0;DbDA3~WBOo-nQ&8F?)B_89uIFXW}?9p$m>^*NA za^W?+!tv!?<;>T`#!dQ2dK+1%v6Wld7y2q+tNb%|zZ7SOHP!C+730B!odvrMYBopC z@Uw?^z!7-+aUMo6k)(F(D`Oo4KiHet6T+ zX~7w=rL5uu{}d?Y1@KM2v@XDlCnB?>zrEE)_v}W(=gRNDsK`TZsEnPq3WEWf?4MJQ zh_j28hOX^nS&AOwwy~7OoT^{dp>E{!8Ax9u_lBSvgeAp{V9fS{#m1E;4LlAd@NJ+3 zj$iMEtoV`Q?3t2BCYVN))gGSh4rL3oxanSQl3qHI7Dw^CSucDUeeQozB}IR!W9ZOo zB&-sRq!+2_9cO6NNJmI+1MoL9C(Vi-`r2g0hRl!D-K^FD(`@xx4g^ZAE!X&;q8JS{`Jbol? z5Y9nn)>?7t`~A>71XBxm24EuFdc?zyTDqW1RhBMw8)auSj^3 zVycOf1sn#*+(VG2U~Q8ftnNUPMLW(pA_BfU>jPxp>bNcOBZCI~L;%dtBS@o`g`a4J zlO=zovRL8VrZ?ka_r-v_+=_#OMU4_iBErN4M{90yOyaKcFJ>s*EdCZRt=eYU=$7!E%T+pf<4g;DGGCTqI@*tGvq)=`^%^I3e7j~fRtq% zfTC;MH@&iS9r;$Hj^c7u_3%Nd2&ixP;*HLHph;lF)cY$kXTTH#M9Ajsa_I?#Lm@Og zo^Yz~lyZ{GJdT!IC>VVNl<&edxEk*No`euSiHwzcZ^2^Zqmk7v7pjJu&+i@xfgtV{ zIVpCog2n6FuFE!qWawkV9fWmHvlZsL0~6;1;2hW9R6P&EaIol)`9E*)ntZ;Gw1C9f z6j0Njrz!_(k}>n6U^duE4ZWKJ!lY)o-Xd)DKEv)Hf^&XQk?l}QL<-!{U7eZAFCZ)M zWqM|}OxX&Ax?dY zU_8Fkl|?-PlVtGSSM}WD*;gXc8SlcSzh4JSw%LL}Pz`_HKS4~53lO0S4Sog??fR-X#P2z0)|JhhYrhu%|HMQYIi!S4h>VXgk zjQ2!xd))MLaR#ql{@sGDGzNN%^1Hs8Rb!@}uv9zr;-ISpA2YkADgzY$4as`;_jD|2 zMz70WEnNY=Nk&@ci;NqR;;5~&zn(_dwG-(Z+!T~Qbh9rF`}Uo4D#UUk_;N;*?WIz- zxt>Mj?wU^#%+vkbJ5UC@BE|qJQ!*Wg9vqFmIDwz?lD&SXZ;h#kiOCH^Vw&$P{PcMBK9QXTqNnA? ze~x2YESU$S!zzJ6;;jGQscB_6n>*XzL8bAm-4n zNio}e;eJFdai~b(Pre`*%e%iy*`p#UXX=ICH zsipCLG-U$Fj#5oHU-(t@W9H$R6*r*LzrV}cwkmQY=XZEGVhi47MHX^9f+|ZR&Zto> zMn0i?k0$~g?Yw*a8Y=6-g5VCpyH{#uhSc~6lnL|N}gg%bW{M(-iweyW-Gc6xmb6R%9?_VBilQ(Q6qhVx(c;5b{ zp0nDs-4}tJerk2o9k7I!pQUGHAeS6OVL(-8m;-jH-go!-dQ@n4!vE7b+a(IhXRb(_ zKr9$1_{CY{5UB;BrgP;Po*Z;=C-s2ZK~~Ln{Af(}!cuPo6I~y8ezdW$TQ_~24W$uq zH&_3+uOsE29`dOvC$~YHH~82@49FPo18srA2?b9G2Aroopb~&2^7batLagw*5)Kaj zE{jkd{OiK_yd`o@2}Cz8l#3uS+k?+r=lxZ(*0MS1=)Pcx$%q+DkZA!F*Y3}@K*Pe7 z78uws!xxe&$Mk&UC0|9R&cMxxNkkush!hvRY!rrGMyo`xMmKAToQ4=wVX#=&hdt8+ z#CH-5Z(nal09F#Qj@bwMHwow)EPjLT63!PaR+c_nv`;Dgap~A;A*RO2Q0n;+a7@BGQ^)3^Tqi>;fW}ci~`}J_j68{}R?o_bq-GWA%K@be(y zS-Q=8*Z6|Lin@$zheQ`~Ckv?uX8*)-q3ONM)XM2TOwQ$A;g5w!{L}BYdB~H@vu+Y! z`Ef#Gs^RZxuS*h!Dt%d3+S?f|CPGZX4@w^?Iss>1GwaYv29mQhF3^RGng0_$&ptup zP{_|51JjG!n8hDhi8fjnUgAo;10m%?RURNNpl^xIG&vlGC|w zucRcS z5Z96d0g-7zW(^HUJ zk$P|~Gg}Z&^@yLP8KM}Zyq%WYq=~)vPbBc0pS6{iehs@_%sDPTpyBjEE~odi`7Zvqg_SJ2w9`A##NfuG^kJ@)vg{1;^C&*5Eg@F-)+1z zOOSOSZMB1<5#8BpOI|n!O%53?nMJm_0uXUx9Y?2%JLsO6Jv zB|Bm-fvm()0bFNjL$DcCCA|=aQnGd5>wWoDB(tqm1>&~ObGFqnF1Hm~_>PU-;pXz5 ziM8o&njFp*59A8Qjzxs$=b}MC4HO$*N?VP+{lJlx1j&|2g5|On2iqL&*r(N=J;Prc zY=$eq1doE+LHd}wAZmpptKDV_tl(xo4+4{oyfgiEAbK5pMYBK~IRAFY?A#5kHa03d z5Xc3{A5$H_t*Gntk*TT>W<9eo)J&el=>Sz4$WD94`QkPXp#tl~_3`Snp7QUOl z&MQKck<>YaYpTLJb~`Q4K|Na3S?9&)SH|5WMR0Q`yPo&U4!p8HZowkaw~!2(#I3v2 z!Y9aratf-7F#wIgjOc_U{(h8zB!$-f=WI7%aq>pRuS<&sv!<1TXzTI6N9yvOI&>(P zR`%juo?{J9XL($9x1xa27vpB~e}L*STeP465= z)W(a0FI?XJz=EvyA3@fCg1N3C9*4$&Y-%Ist>~j6t+h^+4|KJUUlBvCyWPcjW7>D{oI-+nt$uiYK$SHGsy5M>P2HJO7r0&kd}8(eSvS zpMu+yyZc;t4~eTf}0opeU_$)__rvP43EWT3wfmc7{K zTlU>oPd@xzSe4i%kI!f_D#wF~}tR%~sYKfzkf2A+?>Iqo+b4k~^v0;OCvBcLoK; zQlF1+_ojS~bcPXh@Tl+?a#~AHtK+c6D}zrDXz+@RH!VpY^eIqR3V$csTyKAAq2ysr zslMxy7-q@2)c=Xjmgk%JbW)pP_XC{5 zUFiluFdSfKu_aap_Gx7wKxjBB5xywlTWy{u<)J(?GVVMy-us!n3u{;}gulxZ2)6K; zPo7iwnQj*`X-n@kozKgsHax=12f^Wk8-upBv#D*-X5R+!_a$ij`$#E%i05H6^v(&L z`T5AWJWfbZ*7dR?$N`i|t8a=8OmQJzabCEOJ%4cQ!~OewqG@-;Rd=5pG0Qu)biKsH zfJf3zAu>ySU|DY?zmvUeYF7C>h`f#)mG!l~up1FXm!z-C()^jeW^r)@{flt}lm&NyW-a*~UYUV5p z#t?Yud-ZD5Ei!$W6>hKU-)h^HE51p00OM=&O3Yim_tb|{hFcLVZn@CG1By>IU<{{! z7^)LeWx12P=f0j~sK|ZZ%N3_f*@teq$B9Vt5Svc^EaK&Y)AM-W$x3H$33Cv2latuF zmKwtS64;8ap9S@(;Eeg=*ynSKk_CmCp7^0S-Pa$2ljz2>o$Xw}F_ONLD~i7wp=_o3 z-_tqlymw2-{MA0|Cm$Kj8|1I8Zw&c5SJo+RMXr3t915Jo-OobY?$y4o2W>1X5#dt& zvLS24&v9JT0l8EHZk#{})@N%8_PUlSQ2ze65OiJ_MjXu!FJ;wCmsUfM3N=zas-PyB z^ZdAdd{dDts&7hc8rHTHo668g?R)dp6&DqLA!)EOoqXr?Q>vf&Et;qz41CG@9p&C^wLiVIEe7R(m|BK3&U@xO}tl z6yM}${__xb`@egayt$`|)&+M3<#~vrN-27+^aFHR?alkx3OP`9+JO*d=fb0fv)xei z-kpVtwN)%P6%hqwb1x?FpEu=c+TE460E1VbOutl`y$?HS+H5>E>}+TRSK;AjF$CAz zv42u=+sX;Q{nrU&hJn*&HgqoghP*@o4!N7sTJ zCGf}Zc53DF9Mf6)C@kP$HWGSnX(d;9TEtwduDN z(~~B2#^ zNUz}WIbqW@t;nsDSH^vrR@ z=%?$H_2BkI^?yl5+GWn4a_GQ@J9a%N5Pc!g90{OI|Dsm5s~XY=L|~9EM9(LOMK`x0 zwu&GukMbB%6@q={J@ss%tZVvUi^|OlrOOTKi&|Ml9UvEV&+JQuoi}kgD2N4$iJ}I# ziwp0QV*{GtkEbkE8Q9K@_G-o{G4WgN4?aFMC0q>I!@Ye;SJ_US?%EqNhdb{^4T2Gu znbwhI0t55*)u;Z6tT-RIxX#YhA0Hj>nj9~j{afW^u)l|w2<~geA)_Wi*Hq-D`;#WX z@X;kEMol4Ceckru-rh@kCZ)UKnUhUVhClC27L1yqCdkLz_Xo)JyjZM}l^e6=4US|( z#=*b(YODl9PNq+Ambpv1p1tMCd;>KBE?uUU_GiMEj?3i;2aD_?=+`lxaXz3stKm>} z#nsGc&=!Xyi8m%bT{=!vUb`_5Njbsfx~!Aa{6|zcSN!c7s1x$n3*Y9j!qW9gy1nAQ zm4kZl1wUCDjJ2hv+`5Y25_Q&nsE=V>n1l9>3TKP7mO7hrryd^XJvm_y78l=^xTv9i zlXCRRjYSRqN6b||!@nCS9O9T}g@fCTSsed{UW-9$;Hgi<*}*xa3v@mI7MzNiR)(Om za>-^~^3i#@tKX+|U{}%RAEN^ zFgU!Xv@uyztsfl4-8<4^-+Q+it|Gl|R@PZJT|mF#3gPeeSS#H*%+d-X)xrJ!&>q`2)1wS(H))17C~DgI8xLi;py@ptH~i*Hb>xnDB* zY1<34Z=t3DTleaw9%p7qA>X{E6yTL269ezXXkpG)fY<%;AKa1du`vvCr@QbQvP326 z$`_zPX2Hy0vO<_fMn;aR$$^kk4Qu~YXty!6#n={r3vtqDx^||tf4X$3(-3OM2dk<- zQ`{DW`d3s0PEx57on&`4l({L;#oUy}em-$xV}e@ZMF<)mtm^0+6BG4|6z2`31a6Q| z`sE*u!C5v48xi6~V$QnVUjFoHlpf-y!>VC5{Z`7Lu>`Vjbji`4FUfw$#_i3hCZov5 zAK*{f6YxYcc2Gqe=^nXQ=e4&xV4`)f$Ibc1xrJ}^iU^lQTOxNzB6vi|peArh%LcVC%M+;IDas!x!E#IDItN#M9~ z<4uhtu=voB7RO%A4)isodV%PIf<+@l+v^)>7sg5;0IQX85>*uGw~Mv72uO_2!m#*h zt-(F#2UmT8oLV97it{YJS+F%Gs0R;2~rZ<(D zNH#L>+NEqydnEaufW$;?WA=1PjqjX@czxL7VP-wE1=oHb$=1n|U^VA6 zQ|S(}F^%V!$6kFjVjmzzsIS!mG&Sy;xSZr$#**xN=T)-%&B8OtFf(jcgBlpJhk=#y zp82~5)j#B;IjAOpl!ar6wTZ$%ho`YUjC+UjkqXi^(*e}c%|4XBzk6t9y_Fbz%a7Vs zl5=Q+1SsBYH$6tUeX#7=>YqijSK#LIhx1f)624?I3VYsJF>Z?p>&r?0S@`cZ1 zf>4A;C_*}4~IHwDaF8}ypT491QXC5C|lTAgu@T#e^?o6*=w&`+O zZSB1F_P+jdeOK4PmlxRK;LcQXdrVa(dpFv_s{nh~QnEXom1^7u!m~`er#AWd6CS3) zMaRHyqh?^FujYI$A2Pz(pnDJw%sWwMH^PBKx@>`WvMmlT%{LlURW~t13#-_d=w9`l z9sdZZIND=UbTYR3UfO9$xZr5IY=ZSasV32I`Yu(PB({6X} zoHT=46Y!NLz3YNetzd1})P;gnyAc|IPLXi8C*W@H=}3Ab+Y-7SRYJk?tBelFwXH7=5U zd-QyyZG%>P+JzJU(}`%i4obY$E$`5fD`U5|6VzisUjYGNHwXPuZQ=ex;6&ffp5cA- z3U?#;z84ogj14rrse(A+wtd67Rp1gVB6u?G%M*&b|8=d0iT1kJbP(DcAE(Vk^#dec z;(EN<-NAp7Ka+1V2S_izzLY{v?6BSM}>2wk5nK?#>>L`WnBZWGZFO7TO9_poX1aWriRf%BfxuI4zD>(hpM~#;*=->wO|j4S_(smVf^}ml zdkB5J4C8^7!_aOOI)UaLW6<-s_>qU6@K`oRU>^b>eJ6_qR_VmYGN35D?BGXCkhASp z#CK>itTk&SR*+@{?Hz;blxQj!${sPuxO9-4r^9UWukKR}FB7os&7};?%~xm7tc2D) zeV(+wGT&yQp#-DQgx;bexYO~00T-@Ahjx6@3C%V&|JWC12Z##)URb z>eumxBRosUd_ZtRIg-cbWm{2#0vy1~32CtbYE-_FARQJ2kX>vo047mxz=JwLkaoWQ zuP=zTXm~f_u?&FcfwoLs?f?B6g2kjX``a@zV(o&vmt8Ds-)txF#OOGGd5sXszb?7g z9~Nnltlh-8Og(X;XHioCRa%mW6IZ8G9Y^{pA|blf$9-kw#wPyJRCo6h#cgz&isB)2 zO|(RopbZ371T{eD%kwpcP#(4TuUA&OI{`5v#{s`5?BWFk(qXn40EmJP01!mb1f?XS zVtSs@rBSCx)UiU*osvt44OOs7v&|US6yhe=xTXstH$%9*xUv0ZFl&8*qmbSGn9PGXQ)&3C}OHX!}Qy|#dJL1rLgq=)s+=D@?t>O z)lRJFtN8fU#&^_lBW)1T-yssso>6l^z1#F#&ooa;EzCVnK`OZRm7|?Sg$E0*0er2H zr21!v5gUf~pXW=%9P}kpI3Xvv8S_X@UwR z4y3(=qP*QeRq_AB!6|T3ZgBVz!Agliyo^{G0YDzb>8isWbR7;Wqf}xA5#wL{*Zj{| z3Sxo~+yKc5RuR=@_BfjBOX+f%wjOR5ekVlC6n;;Cb` zPKX~WPDkApAlg}@-+so*mhizJt1~itk4Y?D6q}uh9ygj+!2zp>gBheD&=Iq6S(0CI znF$;KpJ!$G^_%K;*F_k!8O*oI_Ec(I0e^((m^k`>;BYU73*0~yM^|PN@0+1GboOa) z4JIsWbEiTP*5x_47RWb%SK5URM@sb}XrgY`*x#u$qtyFFtiflPQ}#l8g7MQx;TTd( z-31v=mjXX-c)>{rJ+BPjvvMdiI-4o-W9>(Pb!8H_4Buo-sZAh;AT$PaWAz3C$MYj& zZM)@@J23&kQM#V}uYZDYO%&(JtB*adLGcT%opPhq^>KMeWHw3iXKu2$ zD=XfX;>HN7Uo8XbmTEAOgemj0V(T&kJR^uJBqVx*yCY>?hMQpJrYZODkTab3Vo!y1 z1Xj1k03x{JXHB{cnmVDrf(+H3fK4feu#oLKZevC6(GL{b9lMVbd;J(7icik~-2)%! z3vi)u95WoRk3Y9+V@G$ggF+z0S#~B)!g?(I5~e2@feE+<@m+_TZQ3PcI9_Px2HKdI zZu$quQoZG=HGV~7sNqA^RFJH}@^ix?)=(zQ&($Y3J{@9?J`PqMUsSbcth43K?3cV~ zjJtvv?U?Rvy}Ju_LQBq_AEeqHzb9y&xqa+9y>*3ta<8{vh+3pxWwj?PGZvb2fM_r} zK0mR;z5^$y&Jd)YR%Y0hmPO{)L%9w?t+TLv)WgNM6Ek;vEE9b{A;70NtDv3#0%`{`tjLsSb0|{D9~xU^s1^FDOe?2*=WAK^ zc(Gb7&s8Or-=6uX86y7f3^1?(gSyKOxp7JSH*`Or01CsZtr0m71ma=rA?MTo5Wv}k zg+s>dRUqQ`uwVB76J2O3N+=!MuF=W(3|htX^h;YUEc&_;1vFZ(g@!x1?BqD~V)f|U z-6vXy3_!wVWYFaXOw{AFPf*vtD7wk-c0Wuw~lY zDJEiD0oUB~`FTaK*z&M52Up>nFOkk2Qt0ShOL{}bJ*@QTT%^;Mx~C(A_;w2Icjmmj zgEf@IcOXVJ^I(MDj|fEXW$lX!*#Sy$4%ve5XHo9~sJuf~Uj(H;WZ{poHeAJzY7{Q6 z(!jVoE>H=oT{C5gGk2%Ynfwes;hWDWwBM1FS+ zW=JBVhzmv1@LSm(Z0>aQ(N_sP;WPk9S0fpOA9ywWKgt4IhVsUk&F@8Q7=a^0t%Ogy z?OKkE%D?V)LsOD+QiWOM>gL z2&=u%)G6-?qop~Qv#<7!Ya^%_{E|9^MBo9N&i>xo|BtV?4y!Wz-iMDe>L>y;gNU>a ziqa@0C}AR9(jp~Y(j68mASm74DI%RJAsvTKX=&+(cRkMmbw1zg_nvG1z+uL-pS{;! z>t6SLKYRCN+eawqa#eUwWO#nDdXr`Fo%{0A==U=uhuaehY_B%b6IF|PbRn<+BZdlWIloG>F!QA2vP0<(!P2l0bND;H%j6S8Yu?>UhugIm>WW4-F3i79W3dh!?aAyZ$X|EBr@(qA z`MJg3XZvsjgT0ZJcM&NJKvTSd&_IBTXK+;|Bd8@rWXPc>W~fsu=mP+Ga8(Wkg=fC; z;4MyqvJH`YeLn)M`Xct)g9#Q!3K&Not7@f@Z;IDn=ixQ!JWTh{V&O4@{6ws>WfJe_ z0DFu`H3%>s4|{;n!t`*C#$imAbivg#zlY*CIR*2R5D~v~q$cj##^DuZ|wY5+sA}Gi*n)M(}@$0(fe5nZoHwkC9HH5Ff6Y z|70Hlef%GD;j+}Pg*xDZXe|*vJ*87i3Th<(ca_V5If3Ykg~k{0e_SLFJIKxe7?9bIKJCFB)?s}Fr&Q)QD1*<9|j=qj8iyk87I;ge(kjqB%#;^C*bW>$@i_`y;NE{CF ziXGj%>SBj%B&#aF!2dkwZ9Bc@8&sH}3QrJ~+2lrh8oh5dISU|WL$TvX+Q1CBLFtE& ztIcZsMHY|JXr*NKftH03v6-NftA(Kfl?ByDW8<&G70$NDAZWe#X2X-&6a3u*?z9wI zYkkbAC96-HaNa+=2misLUL z%+pkqXu$M=UPgt%)EL8Knp&#CI};yEc(czrU$@crd^9!ubf^>wVyl0CoTe_`_PyNJ z9LB4q3~`LmyqXJc(@2Mp$ooJ7!{*VA>golR9^}(ZvxQpNF}Ln|!~13EiN=_#;${4g zug^Ju)1#cpysaw~j(dJ}<*hoJhks`l!)3-Jo;&vPhTBQ;>>+S~id`lFu&x7-kcI|b z(7*nMwotEZrY9(G${kT9Ax}cP^*98bh5td zB;Ztj9O#5E=!h zx~M~-?XSr7)v8MYx(O1&Y|LZP`TB$2nq$q;aQRaF$#Lc3c`j!bgev2GVTQE($BO2> zq3=Xk9aEp=rb=j8*bfXpWOn0?%+ll>ny`wxF+U9&>I@F0iIg@1k2yd=j>(Q?A-CW} zud)JTQrCXHHx7d~hQReFN@!4mPJh|7 zeN)l4G9f8A_>Ja##8v5qK`G3=Stv#S!y`WKF*lx|V1>QRj$7}iVdMd1EU4KJe`q7Cp-eILx;OzB2qWmX>sr+3BD1r3XXiF!%U(q?7yRqh*>-of zzOYSL%oU~@l<3!pkOYfzX>|n^FYz%6oUzdBi`_*;&aO^kq&gx9SXk@VQeee_gXZ6f z?p~M_fAcO<4n4^msu7jb>ZerAt!Qu#$_*92dy>EKO*YH?JKV7zbn31__QJCb!{KYQ zdzO9%<2)N*A5ClN+~U`X@bwPq5o_aVyFP|sOz0Ykp%wtm2K4j)ERa6Xz_DAVCSFM2 z1R#{|2m*gF$I)wnpfm4Z9SB&TS<4U!m$urS8KcVDXwF8L;jr8^;G2<9kKL<;x=Xst z`5`^^6nO0H6pNOsN&*qES;_EVVu7GMBF! z%cVjSJj3(%LJ=){qMP2pdTHU_iqri>(4CNG`k2e6t4-ih*V~9iO1bkP+CT3`{a2NW znnHVzqt zD(HNvr^7WG%hE|{wJ|>W@bFzIj6qBY^tEttaX|XABzv@2 zro6>Ht^{H~^9poxnJ@&>P3-;`%PY`;!7k)%;f9xHYU7Y;&nj4(fJ-+&ziHDMfiJHh zI-U0VT~A>fk8Zq%iOT2%t5@*EsBEH`A>{1gZ;~`1Gf+XrP*+AyI4XYx4vyZwYw&)y z0Qv@{AnpPw3>zP!?jmH*77>*P=xw((k`Ue}tV1gk7%1_Lo_yOow~(&9d6~l{93(A7K{s9Pmex-ucn)Fc*uMPKzdVk7+U2m)R!Y4`+%T}!B~{GL#GQjsvpBPbCzYGhvP&*SK$iWkVX&qLdUhN zPwNB3&xq}35m!caW+*Amo_gK$hk-S-g92vy+u!0ZB(u|%xV31RjXa60@E3XrWmHRt z_hij>>G+a-Wm*B)Mqk05M&OWs^Gv|$EU10}RD)WPvM)6YkPfk9&Jn}g-MzR{aU5}K z?MzerOxcj&!IAR~A*V0$4-x}&=xZI;DNpKRwN&7yz5dMEDUP0h!}#UV>eiI#!3yy$ z>Nj>{kjH;yc1!l!nuWD<2FUDNoHmDEjjo^JT z=nK@?i)rpVAjN=fAVvn>T>S$hD*})XkQU-XLs}18(SqD1gjB;>Ab!d7-)!Zg+NxZZ6 z+hPtRu7+Y?CAo4l(z?gJq-x z1y+Oapi`>1b$SGpvTgVcq`#wY#N~|j&@`>Eg*abEF5a!rKXn2>AA7M&QK#)CavAKo`9nzFld1 z4X)Tlsq`Q&9GvuwTv3UL{D^*Sa4C`pN|!6*q0=ha=Wl-D*oQwFyVjwXU4J-TxZY!@ zP>Ho*d39^0A=#I%gpA!lv!ibnR_h#WayDx;HN8P>65|tyq<|22QY62{Z^hQn_(dsI z*ne$c9g0{Ve})HdVb@QM5iE3d%HKh)L&T%mL=Q=NiE`o^zpWYZFB=)n?Z8{gbkX~P zOS(95*9O~qCOKU+oZ|xXgRP6n#8*ZC2UjTno}&g^oH`p&_aCIFnqQU#wgqUHs{4s3 z-yBd4h-3`-%5!JJbfuC?4epA)oUrLl=}o#;dKyfpCR3;A$|YBGssWV4=$Mh|LI}sF zL%Vt@o^g%QwFHlrP!7r=-EbsTboV@xDR$lLASIOSEwr-bF&h}@Ccp~?AdeyFR8KYU zGDEz^OHTm)h^HAe2O!DN=G+uG45Ql+HppF8fYEHg@w8^ljQC7|wz!3;Wet1Pb8TcJ z!|_h>h+mN=y)hnuF9B3NwcQ`?mjberWo;=k8;xE;7e@PA(~wdF8S8qny7+dU1*`X7 z?vF);y8}ck5zc8G#_r(!XbKKd_;d!jEKV0?1KSfI+7ZJO^x&v)-1gI;fJveb%uM!1 ztomcAsKf?Qn3F@Zp^b`oT78JIiFIyEHw)1VPmGLGlqr&^&6i$+xJ{0~eu{f`OFE5` z*N^wb?bS$jR_n2*H;@+=ZtnKA@n9d;9hr_{F)i0+V8GR5TT(-K_}>@zmF*67(?NO= z{`de3JlzqQvi(0??~ipIKh zZ|EE580OGlXaD@?`EB=qlP`*#rc!c#Wl7wPy)RK@@f4Xa=vOb?bts3mb9O6Uo8zby~_D`vA z=cwmADoV|mdi`z>d%v95tztdtdUf#Kqh|i`jc}Si+PqQD@TXSF3%vGpSwf7tu~ZwW zi*h0fzVBMWA)#*`P{3sewx7k{`icb;_S1R zZTz;1j25yCk95SrdLTt=&Q1(JxAWV6wM3e{v)BHT&w8t1ZP17QQQh-mxP*e35c~#S zfbZOTd3e)9vT|j2=MWZlH}c^eYF$p5+k7^W<7en~`Q!~f{P*IE5b-+z3ucaTc;<%=Rv>U_3x8_hv3r@Uxe*YbL7pC z+_vsQM-C}g+GW{A?E8A>+eghFlgQ;=vNEieV4kde=_Z1UuNG6ct@(ICkclSHC$S7K^7c~1u& z6dTH)!R8d#z%ghTj%-nED*1Zg$;gd+t?h$X!ZmfQMv4qY8R z^P!AGsyp59Cn0t1Uz-6x^!W@@222Wfn{QKNm5|Y6Friagdv&79R*VVkDJIq$;zERX z+e59z62Ebcszy@VExzyb>4d=fR2kscm^*uaeud{k|2Rcy$_2)UWxH6fL9ETSmmi16 zEV5e^AF3=w@GU9^OrAP(`jriXQ`4;z6|%Le;Sf69$RHb#eD6x zbRjRCAqo?WE`Zxj+bdEzU|MAB==MJU;_9}XPQx^XhSC||ntyJvP4j@qK&X=l_GyOT-fSPBzBz)vDW>NvdI>2f~frjC3;@Nsl41>4a! z1B2<3I+&BYD`n0La?E2Q3GALd0oIm-yRguGPlY0`rx|wQC9q4~5iNaN8rXMHn7m$` zB150hrPt>E$UA0;zSo+3MPna7hBOMk@vxqk-W+)OXk)v!q4+a`iON2sGa}}(LZH9d zN8i{9!->*A`1(_(C+i$HC`1ddsI-+j3$?J>%%EEUvoZ;ZXm^S+Gq{m$ZfZLO7wC_D ze+BwUGH;_6y);Q19W}Y63Zfd*fQ5U?d-JDRJdGbk-wsqR+7#D~l2FW5%bL9^wRMAF_Wl3H%T`_xX9M*2V0DsVPJ@9JPb_CrdZ8 zY<$DbWg#E;J4RS~#l+%iEUhGUfXZTs#FHB^KM^K_%q|6SPqx!0!`znr2L@Zih&jWb zi2oyEnQYwq5LWXRd}nXtGm(C)`;|8zBI_Xx+AUg-F=8id2)?gOG$kp14F%Q+gW;lj zhW1;oDH|&q!4ZCvHCIWq;NL_3*!1Oyz5}O(y)Qfwt)|A`80U(IMo|I zSe51o1>Tw^uPtRzi+{wMtWm#3%R2zso~-M$B2@he!Q0-Pq_b;ZSDw0 z&jc4Rr|MSg2yb%;@N1%T(`C7znp2Ru5h}}0%hoz#mFPG%H|o`ajwmYEX-CC^cdylb z(lbO#G5tF`jd)07Iq!9D-?l-oK7AOG4)0%`vNcXU@5)l*>ZCgE+Ct@b~^1aJNehMQ(V?`Rm64Gsi=2XcWMiqVI6K52Bho$ z<8WU1S%Ll+&XxJ(QGQjzoY2PdqvoH@a^`I#55MEW((6e*u}zdY=Ed-rB)yH*&B~ht zmjA%Xm}m;e$lk1>{+SQeW%o}5W1D-U5qwcd{?ucp+1ZJI!LbV;DV5UQ_3O{Bl#PG= zlw^{}@}FLYW;^z-&#j^nv#z>7z5zNbAg zccA1$F+Ysv@KA=cG2im#?oaPO$g-w97|lL|X?DZ~^KlsI6<;j`2>?9{L5vDdpkp@# zsW440e|48QInhSGuurdJ430j#VhUN=a`f!T@f-tdo;cR+Z`1`BGAB>9J-MKEPcm=5 zBj9sP>kH4xu)<|AP3~t`2+EIF`}f%eKFuP8vD|E#<3qL;Q>0pO&1EbP07NRZp0b_m z;ErRYy@)GpzlKpWUS~>J_jRPji#n3BtlAQcGw{*VJXu>T7eCyh%OAoK{>0|T*B=@s zA+LiP1*w#>jlzw4qe*&Oq*mpyJXSAn1aIot8RwXgNfqmQGi5ee5{3k6Z;|x!$F->n z$uW)khX%7DMFBl+9D2-bJPnd7*<=3s8a*F&F38rItLhKBM(!tL z^*>-j{FPXySoba&^Wt&;h!xe}!CDlF>$Y8Rxx`Snn)<*}Jn@W0)`j?mucxW%i~mKo zRHDfP!klSM(OS!+CO6)5Cg0wBd$`3KOJ5#}!)K?aDP%Ov6Uw(y|$fGf`u!^iHd8;o*S|(`ye~QaC#9f9_}-3`G*($KCAAa z`>pv8rru2R8aW;FdK8qsAKI_7ZEuh&8(u$EYb3q3N#%xYR+w|SFcn!C5>!y??xiTq?%eu7Rr)fT|G7_pkNtNoA0enUtse(NF_;EzXGc_m z#@rW&3Y;n6;&wv~hQdL)Ke1MmpPz4yzZa9NO?(tq#DZ$Ft_@2wOFVphg+{!w2{QxK z&F`K`W69S2J@79(4b}H2uusWvd9BqfZVqfaWHw1(Esz_Nkv4IzQ4i*UB+{Z7d0wB5 z+c-ZVcZ1;qpmZdSNPH^#^Fm=;?hfG*Evf`rSCxMtNS8~9Y8sdsxY;$pw&W^~%lRpl zA9gA4Grrr~3QgMrQwSUy@oI^w{GnEkwlNxhVrvpiNq3(#RkGU`U zKlzscxiA_&QRf~6Gf!m=g*AGSj4s%I%o{X?w*Zh2$6LjU4mB669!SKKV(Tlx-gM%+ zf~}p-yh-zt%RvwV0iE(OF88SG?5jJwB&3*Me`N6g(6=o^GHp;jCRJ2Lt76Hv@r}SCRx0Pe3dJAQ<=2Sm~M@>?y-|Bwy9!b)Wcc9TA!M8(dsu4L_Go+ZK zA9-i&8g*GBIPty4e#CP4LB;q5vG*gk-0jb+0j0a?cFbdEWNg6}oQMk=ho&cLIXQjFNytiMd-JP)*LzpaAad;seMwoK$zera7);?6yyVvcw z(V#(QcfF@&#CK#Z2qbglV>PqQy0~9>uqu z_flo(>l#>%Wt3)>7hjx#vGry&O&5jVQqBe>BVGeTjI(ctfaPIMhkTIYZcHNsKyGyW zG@4bRaY^_p=9O$b(Z47JuoJ$xx%{&#UI_mh6&8t&ayR6PPvl=W|34D!W5esze5<2R zC*4ELN7e6_=vQ0b#q#dRWFn9bOJ>EGYn8Yi9ZEB@pcrtt<$2iS2dM>jf@~Yfw{ioilYWJ;@a|Ugh3RYW!=se{(TBhzQBdGYO5+zh=4`hx zuI|-TG59h5ebT%&VQ{0~iIX)L?r|Knd15P4kV?FtX1mx>$Q4EYdD2;xj0cbsj4
^EumwjotuP1fc@}m06%M|A2X@i8_P|o8DtE-vax_UwV5-x9Nyx`zt`qe znEw}~-MpUMS^wETOt+k_2gFOMh?J_|PVGJ-$1p~alu88PoxHvzA+E>>E`oY4k^y^- zW8$`WA_r5ZKvYKtf2G6PmGgMT9*-5uWR6k$jV&=ENEuQW!TyYfQvE4PZ6<+~CM$1* z28HB=Eo2*-YME!6o>9EfPaj!$%^2B?@QLcMyKX;Ia|BDmIntc4{e|$cZ0aIR-mLF4>QJ`%I2b<+L6k-7Qkq5Q$`ZVeZsKpRJENmo-cOhL|?) zU1jJf5#5!luq2cU1dx!7CqyE##(0f0L-QPxHC=nO@pv)awsbv($fsFe;(Trbr=;Jf%NjEB`1gHIpiVt4c5K*yzJUB|6O5azR74p2 zB%kGFk9cAmGY}_T9SFGjmx>R`x#y$K{a$=OQ{^7QST!{;PxaKrqQR^#`@HDr^5&B6 znF$CF!Tx>pGaxOe_-^@o(lOp&nR;Lj$`!u}$5Sa1E-x*-Vy=8_aevr34QANP2B zbH7;L?O)i$gQh~Z*e3Jlf*%4I|7m^;kPjJP5<_mKe37ROC>_Vc1mtCfi_Vb>6# zt9mfiYe;GX43Xul1aY3iIU{Ft5l%%zKF#%R);ZsQ&UR&#*vd9Q-IZ8`-)m!5V~eqE zqNc50-K2cst8zgY@-Qty`}-M7)4%V?mKgLpzl0e! zx~Ru{Tu%gAkzDG{pwV~GkR(_@GSfCOk%oVOutaAz>JvnV<%;f?1a=sWYv}%l#BdKm zb6M?X!Fx?!%BFVTm5LE(_@;Q`YJi(i07M3i)#|~6SCbZ!{kpud85eQ~H5}7EzpfZN zD8l5%-Atq^GCwmLo$Yz(A~XQn0h#nES0ZO+PVQr>BqR(9)3=D7Zab+m5u(?-F6UiN}aq`)f(0M}vsvQTqf%!n4A zi~|wMe;ag^TwYA%o6M=MnzW4i)9t>C^ki!x`-m;{`@qiMP9w`t1I#aA>E+f0_B2G+ zW{E-@^%NtWhVcrm`P3oV+3g&HP)(n_j9)h`EBY_2MCw~rw(#ue#=O<<%{hT23d#c5c1?6X;~F`ZeX#Lfm_;n#e)(D*ZU-jLN%SK4A3j}w9I9>&FS>>H9)2mCUw6!Q z)b@J0uw(?&zv3mQt0Npzow#nAM_IhwCq)?({$4iHsZ6C@C?qSNUXVhD982}%AfLwi zus-6|{$dMHdA`-%ay2F023f7Uk6h{gnjMIRUP)iuSp=jFB!4J!qgej^yWbI#BI*8K z?0WS7+KXI@rajU+1#!BOO~De$?mO3RZO-p$2OcIIITSi%rxQ_d-~|}IDHiE41&y&) z!|`g8X{a_M3ARpNH;w7h4LWcp@&fcu%oh~!Hk7dRcY!}A_cpp5Zl>}yT^Ld%$#i#m zM~3r}nByCVnG>=x_k*>!+OzhY5kfuqdggv%x2pLsfR>to^1ryfICvRi$5k11>$nFN2&kvA_F$TE1cR!T1l{@6w|G8uO0it{-MTk!xjO#e@Az;H@(-FhmV%Y~dPWAi5w@flhaV z>_7_z-j#Qa4=Wv3FSWMBLRZjxJVXLdDi$btS z*vUIUhuUc%n!ge*-3dGeV5RHwJ49Di9!7qyNyiZ~A-&n7_`*I%EwFB)^#xt6$&-S! z4?a#kA2AS)BbobdSLf=<+T8lr-Zsduzi(ipSLZk|f+H};Kwv{$9a732v_uR+Q;Mj} zAYle4^T89Kcag#m2IbH)coR%OV{_KKI<{nI`x|Z(uQ0UF$)@xtR=Y?I;{8=NIWI7n z*yMO#pEp)xOf?b-A3qBoR?BP3c_+W>zrWUS?Oi@V))(nLepRbb%ZZHB#-i5<*crYA)sJr#>8?%p z-G_Eib?FE#5MYM9TCxa}P?K)HB+5{51O`5%DJt^vv|0cnhRIkN1}K2GW3*{DaK`(1 z6)#j**))ehK(;5Uct=h3{W~X&?D1U3wU2$C>T)SVQjUYZ-!)4-jKOJ?;OzPnsL#U9 zB@KjWpojS`Ke!1(kf0WVp50y*2X7JTE~uyi?bD?s=la@X>dl2wKFnO@jBmGNQ^WZc zZh_)Dz^S4}Ww&yPIl{B_p-~ICt?ctLB(*6k1^7Ckn4)J=6J%3ZCRwHuNe9tKPSwafp|eYH#7>HnphD1bDFe++1@KSBw8tNML*A8j_z?WfWt zjZ~96y45Fmt*@j4L9!C(C{k4d2@Uy_^YYnVr4vRphsQPP8e%$25Hq{mqoy^?W{nfp z#g~&>PKMokAaOP0b88roIFn(a*05DS^%;s3u51+8Uc>d5rqg^7Q(5UneqGKl;W2pL z^<6pyhplPBdfJF~|a58pI?C@7FA; zi@((@!>-gPCXoo&3ZM6cpgiUS*8{Y9;|+{1CIyVj7i?Vc?|Ta4=c4XTp~W9Tba~kngGQPb+edZGPU*y6sUYUu z!8$D-QRd1&iQ$sOWk@-e4Ek%FCP<5%SJNPNkD5N$38%wP-9H=AI<2KEhKjwW`?MLx zY8(jDL0;cZoY1=xXT;;|J;{U){s#*ZFy8#l38rBWn^ROShc9KU&YzszauGnf(*#-a z0dJQ0mG;Ov&T#29WXHa)0A(B~DcFFbKJpeT?1~qJ0p0uzpsB*y??$~|p}U1prQO$N z-p&3fI2pySl&fZZjR+yn`s$ze@otrZ2;>Nq{*ht&7oedb4QCYL3!na4s9s2Cjf8VC z&N~c%I2q6xvQeE4lrk8t5{>SBX|N#GL7!ks98!(I&h13!>j7Bc(y%<-%aenKodP>6 zgz-I2$P>(%-vh|=%v|&#mfos?)H5kPuRzG|zfojW)aBEvKF0~J13*h7tbxn@jnP(R z1B*QdG`5j9jg!UG4!cer2mZQr;#|#A9sU}eF?<1JA`}mQ3xMbNg@>8<`M;<}C_Uj+ zjNgCD@>s6@&e`NFB>8$FUxPPY_SQ^^a(SA(!ai-}t5cD?Si+q#ns}1OXVTK$gxrXx z+r=t=+r1iLvwx5nO}9;%-$htG-Tt4T9>@zhpjb+KdEz*yOPU6iAF9-Z+tduUD!;gb zNy@-gD0z6z-Nd8?N$sW~G&Is%-!wL8&CnE+#Qs0>j_EFv*6&lU@D-j=&$f|l6FSkMoC(yu?zD=#59^aKI2?YAQ}|Dy=4b7s9tZ*t^F zracyGzj?6^`luB9ZK45^c7biVNb21$EJr9)Q>smL0P5S1)<6$A6)~36&i`1D+C!kI z4CzriX~4vAKVF6_d?|t@H;yC7L0s6T8*A=j+Dl;8H}4Vi~k2uqQxHLOCSNLGt;X?feOINW0Paml6e~ z560`48&4JPVy2CKsXcJUXG37mG1VU1m_Uth>TlzOJOPUMb_z|pzdAQMB5WKX{6*%je^va)H35-Jk!K5 zIe|h5P{6H$tfteg!Yk(BS%Gn#eZ5a=gnR~41II0{K-=xbrP3dsdysq#k>vAok1ZY+ zLFNw7%e8JLHjrE;`GcBa8OnASy<1P#oLRJKn(OptF}@5b2+8VmstN1{5uFoB7v2Sg zs>#$a2Q}I$Fb=3W(-Dj(5Cj^Pa@M7S>N>En0XohTy%_g97^!Or9|Ja~?qo9n2OuQ6 z(oKw?CWAMq!GnX7@W6?JQ%6RH6R{*%IF=DUQJX+6?o=j@Y*v`Q#A%)wSBkGpfne@< zfW->bOii|Et41i5BxeqmvBz!Adrg7(%D*Y-=*y7xP@vGS1EhO^oPY7I^)afkX#$(@ zpvdtCBR+cq`v8{o;ksL{DzwB|$@0a{I}711i(XBMFXsZsv`5&l97 zoHMh#>t&os3+T!#^R<4eT5a18b;+Q|J;PW-R5^?$C7sLUTpn|zrH4OALZ3h|y_PA^ za+lPOZ_R^*nVHmdJ;YdFs6=1rR*~{458KoSuWWp5oHn1iY%A0jfc|U6+Sd{bxf>#0 z##uwzBM?CFLPlD6kxnPEr1`Fln*5UN0%8Ym~D0eo1Zm#n*s5K5Ecu zhPOm4QD5TEYm6E|=KgS6AF5Uh2or@`QZYq4*-0+Dr zsCODa9H>hNlC5j+LQW*%)H18RxGu-@Tw{kZqC1enID3yXG>5zmZAt;fcA~Et#C0#G z;v}hLt}U+5dPYSFcndJF$r&KV2~HgnH1+_g15zzu>#_0E;;?~2+F<*Tyh=PZ2X+J) zgDbE4vCkl^qe=+!XSxHz$~N?ujPzl{eU)H+Y@Kf(FJY!?D_;chueCWrcW@K5VZFj8 z%e%LJ0IAj^!|;gI!215Ud6Fv41KSp8--8J&IH|iAbE|vp*T|C&xo&>*u0VG1FV2J` zd1K*k=wku|7FVC#gL(PJ{Q|r5b6zXSaKZ^F>4*FX6YoYkX~92j$)8YY3lT@b(L)WJl+aZr`hcQ4ECTM8slH z2+}n&j}{iV9z&KH^DW$QH+gIM^Yn2b6{4Y;K$LVc0wkfIPai)`5Lny~B(Ia+oZ4ur zpID+%{(dr9y$VnMo*#8;Q2RmRM`#T~OjHfXKN%(4#PNjJFV*SXM*IfzEdSL4nxF6E z0Sx!pFDN#;peN)l={2aui;oU0N+--YSz&Lz&jg0^2|+0~nUkJw7x^*IP%W5`Wnxz&_F$Gzp^Y`5RBjdl4ddOb~ zs~CWI0Le-+MiK)!FXtpU?Rdp#AbchAah2YuxoVj0K(Bc|3i9-{g_thKb(c#rzOh$; zxIb!@*(Gpfw3zI_&a>Bw_c`Btdfk-d*K?8){T^^^VUTUJ-y<_-4HV$F9 zLF+*JE}V_6;3_^G4g|>oI(LYDlpAy<*Xefl7tKJ(WmyeV`5oD zoW3}W^e-^(e2KEQ)J!h`@GWdTTW&y81~ux5%piM{KDl^2<-$8IsXs7ps%c&$ zoHIZQpqdd_6jJ^lV2T*=el!358j@VMqY|{hn~U55mIs=v%{)q6fh$o|pjdPwK?YL# z-89|2umE|)DWXi_Z+Gs)9h#6WQt*Ia>GNj8k7|pU+yDafVWd{n2@JV65Q6-+_rPA0 zQNmG6RelH)3f0LU$mHtXW8k?!{8N~u8~fXi2k~y-Kj^W_gZAs+fldjBaNsKA%E+>4 z$>!8|FcuPg*(WaX%Ay1AYIiWH;QGG1*A8kP1;0YFABiU@9S}e&KoEFEZ~24&0P!YK zVg&IlEIbNV7ca$Lf$z>ghxnb${eLF!{i_%;Y$p4HqxBWWPX9vL++Rtl{2FM^*olZj zNs}5w-uQDMx6ED}xQJ*6Pbr}4De;$`5Vc5HALXa5sT1PA#^iXHM3fCS5#NAj#<`BQq? z?fv?p`}xV%1ET56V5wSX#xb!(4o|R2ow(gAuKP_D5)>$jFJ_)jo@)4Gc=Ow5j|6s; zI%xH;5!T*k0DqM8hKh3PgCm%RbGc6UK*IvUDiZN%f{?+H1WmL1i{c(@Y7Vr_k!vBj ziRD=y%;Hbeq-%(!nSnUM$Z1}^_`e2@=84EcN!H>BHpl{ip~3GAmRds4r}isx&*h(y z-s07}VDjvW&qw*MjIAxQmH%S&Sa$C{NX8?o{b?*}jsgQ0n50kGK!FEDD){ELF->cG zBoVtcRI{I!NlcPH66bd_r`MCU#l_O~)yiUj^O?1AM-E04H_dZ|w@CGDy&PTPfk8<~ z#EHW+)!ymUtHyuFyxH7W_EmwNY&KtRp7W9{Jqc)VAHvm1QZZ2lV7rBhI`t|MssFnr z1eoR^kW~>k+}QzrT*9pSjesIJ5VWO8|5IY@fcyjZ`~DJ&kAf?DA5e*ah8Jy%*zTT! z2}e&{Pg~H}Tk|D87V-wZj{lH)HaM7C*RIEbHxSxTWIH~i z|HbxlaYLkG%J-UnpE39SqAZp7{}c2C--k>GGgAvrLxQ+aFye&hhh+J8cBO9P8hlS{ zq$7TY&q{8bPclvB_YQWRkHD$qn}1^jeITaqr8N;VKmWq%67q!)l*&PWkvO-I7^zEX} zQ|?QK%JX6OLgi0qf{lvYFvucis)Rq&@|;%MkbIhrRKn=#QxYcWFsg3^0P~jYbu#CFs7=9e)Q7}d z*a-lA;RvmWQP~%Glfo6TcUAaDx-`J@Jr*fj+O&%$;5ICIb_FC5sTi=XDZF67lv4nN z1F;Qkh+XyYG;*d=n(g~Y812%Zo zPD775DqwtCPJg5MAa)!U*31U6I(Lkz(OB?Xe!KfUv8;J+WfJ*=!01ovZcnQwE} z$yaqE@QSv&OsVO-b*6(XLsst; za0sCsry$tE!8lgKgY;2GXZE>#L0J_mMnr)F14s$m5W}}Tj^M-a*j}!kzgq>aB*stn z39H;oNYpsmGs49jrb8Iic8okJJ?@k6t9ybM?*8$}Q?C16)_Ktn9jU5zEA4YryDe`; z51;Ao>0g_h+6Wq=soL7OUX8cOxmgO$O!?O#Vv1$|Xv1{&DGAv%x;y`PF1Hm4=1ben z#{{tG)RWVFKHksa&^NQ|U)Xme_;YKFCRK?x(_u{7{i`j>qi^PA?YHyI>5I;BVg#Lj zT(MZZVqwsm+lf{fxhTaSe&iInD5Vg-Z%@R0yC~tsI@uDdL`C`aQn;$TWMFDSlGZ-+ z%P`t?yNKWBQKZ$~{^J}KZG{U|9b`#wrPA);TlgxojEc`i5_0ORjLYRXEYYoHw&C%W zC0(diU5Hd;8EyZ|G*I9#1}zeddORg}Qif`t2>-%cQjB{072;SQNq5se4U{|AYME=` z-Krm980VWt-c@Qlw7*~&X9-@FHtZK3s;hn-qOlRY$`4j7Gq#5ak&wekIV6nJZZ2!( zePvuvmz|Q>3Yb3gN%5AWSL|b^=$ZOd9Rk{{)Md3MK2z&Gk?lUPa|@+7lfdc+{>LAM>6eIZNXOv8ucEno=VC?d5Mrj><9Fp6mi(8Fl_3XXZcm z9TBy+JsbNBkE~__8!+H{^F}0c;w5}8MW>NA>qg`Y`v%{g_A4YVOHm_OAe=sVzsnHs zrbjT3?-uFyIhZD87qhC-UH^6EMYlRQc&9N? zj4m*p7MVMcXr(0+Qze~~lxRvgQxs#hU|RzL!W_n1lq3`xhRur&sjp&y=Fa;I9$RwS6MZRn%>xs-907385G}N|7e3eraB{t>7IE z0#Ez)hO(Q!e<5xP>2so|&5GMi+k2+4+_NsX<8HkikpCw}N`##j%;aDBYA3FA(8ImB zB(rnaM#%wM?mj&OEZFiAT*J@_{ z>F0Aos>U~7-0~DYyfXUZH38WDiofv2vT$bhrahJowL0p_v+D|R{=mLtbQk-MYbS7$ zhW@?d37!FxODJY&V$#{FI8`6s1DBpR!%CWz;A!P-vHFzdedz&QK~ z!{0_m9j%{>7d`Mv${cFUNG_fJ$ZU~2Y@=2y@8BO z`CFuxIt-cPNosuI8R(vUPltpT(ar#4;TFvNIoV5fRI=Hk$nunJ*D8hETS+g_DNy)C zf9N|BcOF$;Qb*m+5?outvF=#tBKL?@y|AESJ0OX8U@QGI9kg=Z-!e`|waO z^4bSUU$Uw*wb9xm*G_p>q_InBJ9*?jP(`% zYUx|WvCkot%ppBO)bNM9j46#-C?N~+hQ~$o9`jBoysx5&CQiYR4%v z?*(TMDos@VZ+Qyx`R5x#p$@jqNY20HFOa|Wareg@R(Ux1auEF2|rYwyUD<$oDz@h$y@CbM!-Xq)*twS}T= zlY0&_S|nv_4M@RKZl4asx4WZUG`3Ebe$-WYIR6HWT@G^4SB&vN}jX85q*TdfaY9ijtR(t(6*{k36EQp9);XlDJ`iHQ7 z-&;=)T#d~X63%y&Aw)EML+ z^;=^26Bikr*Cy|Unndtr-21djIwzzl?3B0?A{R-l_lo|^YyT)ntNP444yVLdC%0dp zqy7UAlcvbR8UMgQ1gzi6NXSHehRX0F*i4)5ZaTVowGK8fo3**4us3zX)HlJqx{YHw z1s9VNF3pbvPug5A2!p+jD6Ytr2aCmGe#w0vAo2EtB-yCzwbV-9K6{?a^S` zuEJi^+H$VB2=xg~h{Jt9q%W8{Mp6cH15m>#ivDM&(St%@It58omIGFXlM;`W@n_OL z!$0woa>csf#1W?sedM>5e4C?TyODcqepXf5@Lz&_dh$_;EB_AbmesCi7jL{tr9&PA zrC}DlqrG_SrcYdD{ewI6d{p|_hU{t*dRv%oPNL=Qm;lPW^SVdKHBa2tyugExQD=mT zgN#Sua-8PCxCNYTZGCDfqOt5soY!m~Y5{!jmOqqao=pEUn!$z(j{%>gzp)lasHh(_ z|BllF#)pXEcTqjzP>a-d(?=iyrpso0jmb-!C(^|QG*1VQV1J$cp`AIct(B4czE&dr z26`q6ONGn;#?5fJjl|_K|Fql3i=6T;*DmPOoxy6U7KvI%j1(HYvhka_j&V)D=isfp znWCdtW1VnDK=pr7z?nL%yFVomzJW{_#eS_7@{wL;@74lFMm!=j$Nd2A7 z=J}Es;zWxMveYY_MR8vSMJKuWTlK>ax0U=#)hfH{ipDC|H4{Iw|4e&$sV5DvIZ4UV zK`={op9_80#mm<(Q+tjsGG>`>5 z*22jfPfdDB^CS*UbwtvXJd_Mq)&;j>57(^JSwMVGc!!9dpBMHBE^r?R#nr{{;d>ps z_`Ka+avzr5?IgwfLq)8q;jWvjCQ?+rYoh%0oLojoM`}KyMkeEkC452bsR&3mjRXol z6h*6Pz|JVc}!VhkHuny($2PUJUFf}w8M zLPN{Tk5y8HO~pI1Uc6Kh7zGLdtgz#>gT~V{y(3FO37RkIg#xE46i@CIDQafK6#c%l z+3>vlRX@M;EYmXkCM;T9kRO7u#5waD*_cQ6Fv}udqG4P{t{m4CWjrng@s@n)4m(_3WQdU;@0GDK=jIDXV@5w}DNk5%JXHKt3%alDji;a+{^Mop31ltNd|6*64R@q9geYey;0Olv7#B& z;CWtC-0;zJeu5z@$?v%H@J&{R8^53fhubIjH7BxL$)WpJfSBtE1-+Rl0gq^rK zAMd`D^FwH4uiMBGq75gWe2buD?PA{*+q6mQ@+mR4=AiuadR+L(`BU~F5u{fYu-1OK zseXPGWweKqYBLUH0^IvAZsb>eZb5}L-X3}+86MA-R}UwvSS&B$913P@VuA0E#?KLl zQN3;iKZJSC>m|2nHjzhRu%vtBY-Zm*Zs+66AM@wj@j`q;+BJwf9J4QPI&5HLud4)A zWSzVI7u?2ZF-k%U9N*A%*y>o#Hf8lLsNv)Gt`xX9%WG6rT>FuCumpsZnzV|qA3Bn| z83sq2M)}+3-`qN57X8MwpBj5F`!QPqsLfAEV@#8;Q5N1@nR7DQmt(QlJ)DGHJc1=# zZyV~@ke#(GnJj2v_*~1#XgK_Z#%j3%f{pi(EJGw@WfC6Xg$5H>Z=czHV&jmv=Y0L5 zR;oN^Os{t$@EbVj%pGLKhKAgW58Ty?M;4o_IBty5mRqkVIx2sydj2lLYtlfR3sZD@ z+Y$1y-)5vo^9vPbek8PfKxTE4k9?+5%?YzdUJZOCi9>-C>nmGDap3K*Ijepay!i-! zl=e@?{E7Jj)+az9n^y+j?aDP63h)#I>K-zC^7h#i`j7|yBI+NBLbZmmX&P*i6z7#&5tB-?qCcB2(1P_`rpz{2toMrYZ#m1L z&t2~&3k!M2OluS!Gyhp}RqGM$?x}*FNmdT+E9>fTt6y*Sr$D7`2R4+Apj#LiKK(a+ z4gqwVe&1j9`jNG`2;`MaLj$R>**=DU;f|bM1OBT&@`7}SrnDnn-WA+`^A27o`)mQ! zn`f`*CmkhZeBw(#bi#zCJZK;#@HCD7510J>)cRpeK+aJ2D&YRx4kz-dhu^XZ<#rJb zbL2B2wZ@LDu|}-33DW&;8FBH`PihMvc-MV3n6}I`_Qa96Otc`^O0KHqUgcKWOq0U$ zsQU{oi)~DP0^NV-1j5!wCDOFPYr$Rl*W6XAkZ5?U;<9n(6aGa_S>M>JVMCH33yb3dZ13ooA!o}~C2VbWTJ2plG@&}k#p-}A;3HA+o>*rP7NdkwW z!5WJxFu3@qmarE+6j~kC$*1}ztQv=CxzGs6G{6-P#S2!%DY#En4J!?82|FUdtt{r- zb zg35M?bW!_Nn^-ztl$igR2^<`L=dOW-OBai{i*6zAbH$H}XU;wQ2~8|gjW^{YKim+` zJNt%cIJBiZs$J0)`k6Vu+&tSw7`r|WDg|$JQPl3VjCKDW$Y!_c{LcGlw=!ijV2T8g z>DCCAVHMis!HLYcx}HA;T7U;hRJ`P~Rh?A#cv5ag11X6@W@Y>gt?up#7wn&CZQprS zxgi;)RjIA>V_Z491RZ0bEL;vQ+1~{~(2ZvGF1@PqF zX15lL`~-q^s!mcxC-d(0L=KOG)4e|Fw-2Nx$XS^u?CpFbm680`W&`$K`S^3ik{R^? zPRRt6?9rxyyFzWBIyDMu8x{}#UuoswnkPtIjDV-ihFeDyU8buWkIgR) zj`cAe)>zm*hKk8!cCMVi?9%!E+Q;;MX?5c#A0P%y`9KT^WKnsooIzWMdd-u2k?}TV z>nuRa=mdL~P&UFOdu6%(%hwV_v`hM^&%lh6&%zf6J5i+Inoo7D-oD~uJ9~R*i^XVH zXpQ9RwR|fY9bbXqvinO3xP(k4v!p`Y#*BAgYx}LnKYc9i7}{<4apk*r>+X8V$x#KF z)~PF|gk4K}{S%tc0gB{%Chmo=wKN=ig3=beC?M<-lm_tb2u{#(Nh)*L7BIHc>c}ZF z=l)~a^Frr2I`Tx`u1+?+=u8zyoDwMT`21q^z~pQ`a44pU60?u;m>dL_-LSefFQcrQ z5oQj}A(IIig&4Ij>QPq-iYMPdJqEcFe?(}$UBq%?d@-Qf0O@1b_CP=pLUX|Qwd6>@ zyJRwZ>vw{uD#0O>!N)`vr`+(Fnq_&=hkV2AmyBZQg`jF&p2>0 z?AqD@4EBO0v~0*_$>N4cr$BK{p}qa25WT_KL@c;O4VUbnZKh+>Ks zZ#i-|pJ(}J!44$$A$ZnJpqI6I-RrJQh1wAbD0!urUO1Xb8PPD1S;Cf2?4$E?!eNdjX(H)~9mmI)#^bCuDBgo$do9VVV#1X#=iSOU z`ZHY~NC&rD521Cw)WH^>sIccEZhp?GOm68BdL;x_rt_J<<0b+AhCkBnSFCGuT?YP+ zK8ZFUYCwXr>{h%H-Xl9v8dWJyMPf_=wUQFT;q^Pcnpm#gyk2MS_zqZ4G&9(>dOX{> z>pr1f$JJ`{-SbNio*cL>AZVmVwpJ3+WAHp()z{aW6WV@c z{6nh!0~Db{)XIBK;(sRNN@p`z*Vwf)Zqp0VljKx5zp5TUX= zj|Z1{&!QZ7mZV+v47OUul8UxV74UDslSuBs{Cht_d!d3`D>B%>T#yLG-B?;nhdd90 z6f_&sAN9oxz9*J+b|u(Wq*h^No&qdvNEEa|d@#1pf_)>K?hR4Nn}jPutm^(q2ji24 z+Hv!ttdoK#keYuyK=$&*KML|Cp1RKPQZ9O)&~j2jDfnBVjeK5VeclSa(}!j&2Z`YS zLs43d56<4MH1Z;qlxl893Z585A>cN#R;wtrfPQt4EJtpvBZUK-NYWH|}!^ zVV*Q}9U(%ONwovdo7frW3+NbNBN+9H+EMEuEAsv7m?EE7=Lcl>F)3+UD8(%J5`A~6 z4sXi0LPH47@<)_w3c7)Yzfa2fRbL`mV@99~*rIpBdS85p@yJLD_#z+U9Tn@oYeQR< zB|^iAP45u&6UF{FKBYj3snrjK@|H{wsY&P8l`FqCZ0Wq+fcM6@Zlt&F+sG2dqBM zH8L8w!Z#FLI^8!okTOXs@pR3cPQ3GT3F`nbNd0_Bcz{kp%bbwRJ?{#2dhS0Gk47z$ zt^Z;R2_%Sx_x8TE^4uiS5=OC&Z;{9dfKx>9Q-oAfZcsKsk$0(c3?Vp%1>pZHS~9Jb z%UDy+&&-F=`9yCRg)*xsL_^$BzX|u5-StE??dsTQ+f;8Mp^3%h8N@|iFD|nX$tQ22 z<8f%))sashVG~G+>D@Lwq72IeHd5H-KhS6br~=sA8lP$G3YI zeH4|9dKi=0wIYa zE$Wbr@$ma)XL!2MD=YUHhMYrh+GdIYlFFY?Z&+9o>56e-8%~wKp7JtFQ5%nJ@Xqud@?iD6S>h!@6UB?lR6rz?AU8P|R7C(43>} z+m$L$_1?Yu-jBZ?mTA67laC=a1OvxfqrHQIm647Q#CI<=gwFdrL8F({gVSMxn)Bt- zH$2oIq&=3cjT2&Fe?gg^D>^8E9`&YpM#RkT=bO(dSx?~EomU%-B%d}^#X@@r&m`uk z>x$++avpa1y^jf4n+3obR00=nU9>6RDhx|qJH+g9`<}D>Mt&p06#;Y#M~cY}&ddfz zOpb9Qk4)&d3z$&#sC{NsY(n?w8uC`0b>s;nMl#<^DxRda)*!g0?K1yHP8B zw4+H?YLnk{)tStU0cYPoXEUw&G`|U|c{B^9h#vS+(0Zl90SYW$yU&ZH*iG8}KjS3< zq_@w)QD9OmBr7E0*q-gZSO<*4NA^gMUVdF9`9w6XfYVNPIYzBa7tss_2m!bChhZBi z5wBUC-CE|bJJnJ^k8p+bjOL>s;gN9=8&E(4nBzCY0(YI{SGn|v$-@$C@<(b2Eos{)a_pPFP zFPV1#HUOTA2=IsTRo_|sj$6tN0CorK_eFMGUhr4YG$2srMCk1tMl&|IPSoEbeCUHl zh;f%A0n&;f^#1_HcL`d+4wy_$RrE~ifW{=bqc^UYl!LOoHu?dK0aGp5Dg#rdji@Vy zM!Cy==lg<|5TEi2qo77Ie)dBUU3CL5fE_KA$V8QPDDGPQ%6;d8dz>I%Wcmx8 z`cxF03V0@XBOMGSb>^!}lt8g+e2M3|qV-JNb2_`BfDiXvw<3qEIG?@Pkaj1uYAe?@ zfzj*zuz2)~Syb}^$Kx5Ei3lck>5$3>^6&55dZYQgYkW~r> zPEh*`+IMLq(gtR&9q%OP8?J$uvO$9>Z>pn%)GSrrgJ{rD+g_0t9T36}K(>P(dlQQR z5`DsM{H3=k6^4_bZiu6TB>cl4BzaQoc5nW7Cjn%wfS}+MQvJ=cCK6y*lL1mlD3T+u zsvgFN9=A{}Oj6pYmb_!;A4^OYUA8yODaUPGs1^u@+Hdp8qBv#zOQyd?jMt)c7c$EG zO~^~E-x1xJx;~x-DVlUD3HToO-$-`iH%~}pmqGBE_uIortMe&?Zt5?|t|$=%kOy}5Si5HgYY1y58bBR5^ridZuAq0myCi#HC|{>C zI!GxP<~h0+vhQquG>4;a?BQB*3dx)6=$P2-jP|0tEG)ebzhj*C%K(6!&heC_5oOtB}je=`ne`4ok1QeVU8Xp~gEthG3j ztFQwXuk(i?J3P|vgl0jr$4Bef%@2wF ziXe+M>9{yw`rh}EuT4mhbvGDvTC17hl4}(XWNP@kAi(1$7VZVZ^cg`cNL9uwcIMwM zB)f$j&1&5Ma%Mbk!Sn`@HH}<>!wzeWf0TUAgIQB5jO>-NLV23@$+ZcKFLIHypbVAUkxHRo7a_2Y<6L#VDi? zvt4g+)ec?#nIXqB5FJ&Yij*s!7~6_Q8FTac*+=9?K+3+h#r+F|+Ga|kKfv7=9=UWy z@}IVI&Dn4^pz#2dPK$j*5an4+kBdGY7m4XT(}|1FKa&iPqJ{qX3@PXJPzhntdXVra z?H}YT1qP3~7m?6HCbN&%#6O*_ws$o8zCG|pA_7JHJETK^C|2C@?MXP$Rm~_Z%?6m( zh_m3k?cj0_xsqoI^C&(Zg_B5<0{Jk}FhfkPOS&U{xUy8{9J>tY8;Cz%Dn-E=y%E?! zTUsvkPY^-C!Iah23jUZig23kr^i@#oz1z=wWIBuPg(H0mSW!0(qz5$O0OGo{=m#G2 zi-W`WnKm!PO~S9J^iq%Bu3gZ-(B&)~f8P8nu74{G_i-%5#-^gyPa*4UL(IDo$M4+B zqD(3Bh1KJp<3e6SXp17^{M0MU3R^oaOD_5W5M8Cwd~ zytqTz$jXSvZ_J6i3B?c^dKv3}IN~($P>Xf?q`2x;QWv-c#{gX(&tExNO6j9sLyMnT zE?4qh?_X+|S$Ui{ZPWJAYCnVhXL-n?vRxhV`SXR=b{q4p=$+B<6k_B5NXTP>J4_$n z{Qc7=IXO0|RtGwHL`fVeM5)|w3YGxP0B zJ>S-yr*A>3F_u+Tm*UFJy)2d(4P%hWRkfG1L#B$H)EgUjC4m90P2XzTNQ|3toNBvC zcqoFpDYK(0VS5b;8{!U|MgQY9X>r6B=3=gSS(%B-e{m1dIhSEQT^BmvjlCA?ahQ{?k;c+UMxtpJ(~ zPT{Q?ZL`u-2^?FqoXpRuT4j-Dwt}Vx{4HR_@^m~D9|qZaPBs@@mSU_H<`&z^t!c-s zVfsy*B};`?yG57qF#RcP%_sgM+FH58ZlU?`(d$MU$-f0tEUxv3MOV#uV#gg||2)ay zdk)Tv&>n{j2b+kKg298)E+?|mr&7x0RXp;+m$Ey>Y!oh(`Gcp08!uNgHL_cvVH#8% z<`g#hY+ab6ZDpdLw}e^GUAc9+t3(*iysW+O%iif{%24X0F{d}40K_Y6(!*vbc${yD zF=s1LA3ykQj(W|A_s_TMP;)83b4K@qlKY2v?tr6TKl07n?H*2P>ymKt$lSA1{h!Wf z$&Wt*HREkmePg!uzgCN#(j93cx=3dpi+t^3_m2!rimlkHL{-@@ROLshF15m8-|laE zY}Q%-4g?1ty@N?7z%>E9bMqXG309j`_5eS9KHUrfU`v4ec1pR#BarrV|0~E3p4yG( zU`eS`If>b=9ILGzNR{5P=(f7Zm#Rs6daBg1FAz=cBg>abBsGrmKw2>Bk?%9P+dJ|S zl}1}Ro36p^COrWDK_`cS_(uk?o2!L8bPPD{ZhGaNu#7*URe;;bTkv9wVs72e)i+Z` zE^0;^X=nh9B1m0C{B*ixt4u2NL6t>2VORxXmr2yRTRwvLI=u?kEBMn(lq%M2+ubni zz-G0W_c-m&<{SO!kcEao^3vvPkI^$~i&suM<6Vk1#;;S}HJ;BL%VzRCh(vuJF@$C- zAaK-FDNH1DK(5|3<|GF6=Fa5$9xE1i42FZm#b4D!5^V;75xUl;heC1cOK7AC#h+9o zOaM9k%+8)J9vx}=;TUd3uku)cC2RqDC@6&+_Qfgl$;PFqu1I}BF#pWUdKL=hx{5D| zUxl3m*|&5y^fO95(PkjWU0%5>h2|+p{Cw@X4Dj+-a(#$y2tY#@ z$2JMWFpGmqxm%7^S=(meBGOzAY~vSc8{hwj6~H9$`p7i+t$s^gR-1Zm^`}PwvfD{} z!;2JSppEck0y`OTvX9C;ouTYM>C+^Zu_4i?RMa`oh|2hBk4<3q@jHa^G|;`FSBA8{ zt4PgJ)JAd=-12buZ9VDKgSN?A{`@7VP4@cr8+iGecL3?{(2fRI%)NUgTE{3~_aBAY znr9>*xLb_ibYaBI>D$0ooETerRUwnE98I)S;RU&`UsK@5ka%Uj!+-}m2Mkfz7o*9z zX@kCqe@KIUBBW`0eEK%;gK$5J3ZIYxIfMRjhL^i52%dO1;sq!^G@QHTZ~f{r4_bmR^R@KGO9gLaQhV zIuf)42V3&G$k+d)XYnpL;qU294F9qzZ+TbhXx8Q8=3Mo;d9RzfmI>E@1BS_H4K#nr z++}y3VuqW?;2g%J6Se;HOx?xucrn{bfp0mLpxh-c5Urb>jfG zbpI2CyuS%G{$-;@{VL$mow|t$^%a)H?q)yb%V>BQG(>HfQi~eD>i^K}AI)U9%}r<$ z$9xJ^#hLpTnV-e%1UQ<8+7qcoRkq47m_kZ)PzjTEu^+~|)+u_nbf9yh!GEYnAddKh zs7ItZ&ZQhFW)SJh{dR3wPJXC^P3zhrYQQxt`Uw(wdC#_;6zjT0c(~&x zT)g>CN&8Dk3mT^ZCB8Uu5P4QX`?^%B76G^2w9a3Yc#V|nSP+AvR(`pnBf`}7{zrJm zRE{v4H1;v9Jn$^my;!w)@BPJUZ=0Xpbjvu9rlgH}p1o_jApeic?GgCDn3ly{5slNx z7cCBFJJ%ChaKp*LPXJ4+Fp}Etc6-QMpj)!yplgZx zB$r9nu-yrV!f|Shg?uo-hZ`~DGmA6ojQIhLx66%S%~%N4$f&)y-3%L zABQgf@Vho`Pj!aL>tNI3xg<^);f?8k z?LnNHZvZE8^p%UbihD_JPIRaIS_+r+dqbezpAgYjVivFeebgkRC)I1Vh{4%;kMl%q zhk!blXVWF^Eeyi@L$66O#C8oX3_q~M2gNqA0wAu>|?CzGyu!;c|8B#XJPx0wG# z#J85&t`+`d`*=a&WS11R$1$?uBpe zX0j#+ce-e|vAZmes4YFx{^f->H$Ea60M?N?+~ujedVla7hK)Ro`T2BiC_h_rrLx*t zvMN_wp70rWoSWKoa0m`gMK4Am6n8LfWM=ViOu|oz1z*)<##dzQAeP2ydlKn9p~pqD z1Upa7hm$M?F90H)miv|9xf|ZgJ(6CHfmQ7OnSpS{@ZG-6e4RVtm>$fPCR&%wv(d6M zAh){cqGEwzc^S=oDm`6!3#oHM%Cl_Ge(r^nlC~>RRW`!dq|A~S$-Y!49y-deqI)aj z<*#w7Fn{0$1E%>-vvL8m5{zzTGw7w0j_1-8y@?g`8Ms+^5hsR8xRJ#SozE@vGA^7U zhd6yqVtHefo34fj`dN$y^+fgC6wgMD_@M2LHVMm^L{t}S!3t-gG~<-6_*i!+FRE&KjFz40+!;nA$*|yUflXmpo@Z znbyF<_6Z!EGeOI@bWlUiw>qMV3LChcGF>=VSj|x6=aPAfHC|(oeMhuoug;dOBrC~f zr!otguS%A3x-2@mY|DIbOL1zTK*- zMlWdY-gP44%FkPaf1?8oOCNTbNPL^mOE}+cix#r^eE7^;B#`Y%-*<9i3h?i8A zL2jP<7o5m;>SAg#=geU>-C3*I?xAeqR~$=ci+kao~jl z#;Nwl$UWwo5EO@#)uFzDGxrArd{0~_cs+^@())nh?cZY&1OAjn1WZWO$~m;+r%bDd z4(`#v3n#uZi;bmz*%|-<#nv*RD{ff56lCSn{(T^UXH+z4#E%Z^`0>lDM6YkE9L{F$ z*#|e>(=)$3zU<1erp1z!K8DbZx7w^6MF^k%(We$WYgSiPxtqM*O*$AVsr1Q&07UzWs3lQ_dUZSM30zfD+&VO!zZ+PF$-y#+_` z?!12+t$7o)>_nuwmZC@i`i4JbmYogLac#XCuaX$yOWL2ktigAa?y;l2zuN?S~)gOuM^o@&Mj z+IPy(8qP2@>+L zWU|1#e=&d8azxon2^wr_A5dVzZ7~0Mfi^ozWd&OWqqIFz_&H_K4>D^PS^VJ8>Jb%X znY*$(Kd0JefJ-)(kKeVb0#KoPh_e^wkjaQd$4bUALadlRuD!|s6KWHuq7xZK&@{D6=1jG5VUqQk=U;XaoIhjA*~NDDbE6Q5(-I1L z3xpT;-yTE$qn{{9_a&w+(npYX6RBk@3>c3LBDHo1QtKm)v8fbL9J$>Gdog4eMKJje ztR`aDjm)`mewz{mXhHw5+Ku*yba4>LWgv4?T`uw;ECSG>l|`zMTnl*)xj;oVY<#@+-Z&9zYX@j zh^s1I`F3MVZ;-MP(7<++-Lc>p4&O`i+X*-~dR54(%&ZCMm@%Ap%6n?d=d^iNC(}A? z;1$rARCV@T{my&i=JN007Y6Jo-j}UAD(Qk-pb?I$3Uig)(yY6hc0*33UspF{7sMVM zeF4o_;p2+f-6I~-yg|4}^vyw~#4isL&zpq>pbBr4Bd`%#|ry)Rl+Na+TaVNmTSK?)tFMk}qaMh98fn|^s}KBw1CR@0~!Gs4y7=^aXNpngLyL%PjxO zwYa#C%272NA=3kI^oWt&FJjKdd#vblW9AFygyJH&R^RG_NB_b`xc8rkizxgy-m(z5 z2!sj4GOL^C?D~O3}8n1B8q~Yeo&j$S6t!r zPTunPhQpT0fXI*z&T?hnfvmb{zNO;yX=kytpakys)L5Rkn+l&+uB`&>OJLvUmp{w( z^~!yzNfaN-Y0F@bL$7_UQ?`RU%0pW37oDxM0e1c7%Px0`RWNIu$DLUY&oc%NC;N>M zBbti4+-N*~gF-)2w)p%kefbmyi4H-YLYZQ`2beyPM&y@Tbml^ev6@m9eXKMGVhgB}JGG@cI?ROPCSr(7-1zsjR43x9ziENor$Y!{Mxv>_2XG0zeEtj(ca>7Yw%yXNah%w#F!TCj^5Jt=tjXFNlpDtZbs~ z@%dS6JA6i{Gvg$yj|!jF4sjlT{-QJ|hTp;R&v4(FSN0=65oeUA>K6PZI#v0?_WMtS z#^OU|!T#ejvm~4zxJWT>b?T(w%IO6e>xe46)lGdG%x+11A!Vl+<28GhIu{Z@;(_icxO8j@@koOtE@uhP9tZL|>G>`v z6PCW~L)nXeO%YA!UYNdNUR){8_Y4fQ9%LL~1^8^qkKYBs@E_c+aYcn(jBK-vJ_`Rq z_BoAU$GvIJy(^HMAd`ZdCCtjN@KohaAv-#HPP~4kJ8`%NUYU7VL(Jkt=SnA>6u1lJ z*{ye#^G>omtWLT3TE_z8&(adeXW!XoJbrijzIR{(vFPqGNL4W@*uV)cjwg0&$2aE2 zBx6$yW&B#~&3&rMvpsDn58RF#uOKj9B$(V&6N-}bz{Nx^N%qS5ucTEe_X};?-dv(T zb9!7Z?r8dzC@F;yhy>f)dm&Nj0@n2uyR~R#y~NSs=>>$Azu8t~@)}T|uzUyn=!kZv zBu&|V1nroa7Z3s;UdX@UF7q8c0qIeI0s~_GTJ{o95r6*AdQhF~^*66Z2(YviaK~CC zyJYOr6CwWQqjdOxDbDlCA)nn`gViTSmMAjd==!-wsdKz>pj;n;B-Wm0ZLv2}OLT|N z&q>%qh8xMaF?uABkzWroWHN&1RAukl?XSS$H!$R7c!v*hiG~*ngpH0=#Y+>F_gKyf zVjZLHi@k^mqM%Gn)X1mr*au+`(h1jc#3fWeDNqSp!LxvzLG0?5-(FT)iiG(I`Ws`69KSgLa9h13xrT%TA3 z_)a?AwY6oOT3=MeA8qW{8|UiNO|}+Oxlw5qP2Kc~_N-t}gc~ztO(0@KFS_JhOCc*( zsp^oIoE{dcW%ovw?=LM74_anJxsx^`!3_DEbkRb32ohTF4TOL<)srk-1*%YFe<_9y zQNU*^43*Q6>N4X~;#1YVpe!ray~FpMb<#wU_T6y-N2gncy6VTEPEKT~@`e(A!@#Bf z&U^`Gi;+uW&dVg5zmtDx)d-{noLPyzBM^}i9nE!-(*9|kFeTpGE?ZsVaLDBaLM3>* z9!e#6PQjTkJ@-caWk$-XZ)n~w(H*{jRH!ffdI-obGn(_61w^F^{t{pd09NOd^O^h` zGl$Z*HeZRNvP7l(ix>tLzXxnk|AgoC#I8k@n(7}aS#XH)NLx?x{zF#~P6d!~%32hq z)Mi0)ZV~@s=3ClccEw3z*Xm8a=Yx~LS+!G2sD$0224Gwa>H+>2@>y^#vbO4U0Phr` z)q}J)KEzA%KdW7)$oK&^?2lKj97KK~b)u?(K1pDh43hM>XfQQ}sMikMto8i}$Sj4( za#PLzCVRO)|JL~8TTOjtz7<$*uZo@^sspy5Yiv)p7}e(T3XWkQBB+d%U>2Ny({Md$ zFO(q31|<&==S1ndW!riPvHkURrxK4jRD)QJAmGXlBkj}Ak+ViJO4W{#U%)FhaF#307NJ51jpl(|99 zuVLjKi$7989Td4@p59Q8_*?;JxKrG2#=EAAe_WNW4NUX z({3_#Iv&Y(_8vGnFjo}nhJ&skBV8iUXolyjc}!GMuMxGDF(eb93-;j9p)TF!Io^_T zXrKC6mH=EcsAjx?0!XV0GohHBuW^&!iS!RSYJSD78b_P@bnTK~<7fw@z^5NSw#~2? z+qVxAhSD!>UkU?|qi$-U4Htkj2R|of^7g(+U?|RFRl52WjTUA{wBR-;oZm+jVq$6sIt@q z@*OQq4Oj0T^IVQc+exlFbM72c5-VI2tYZsb_&INVXLJFWWcLVewr+B~vy=Mf1=;!>1dH7V~mvwQ&D1!Xy12CkOO9JPFjcZf~zIN2+t&lLeOn70cfWYBjiAS~)!h=i1c zlrJ&x3(c4gjmJRQP&oy;!E8M?Orw&@+-39@GFJlSd2+Z{0>SEwI%xwdat4w8O0O*QoS2{aT77K|bA z8!QxLESynp+^*C)sVFC5g%n6B1yp)_Tc?mHYAhw?(;Le5dEHLmg~Y>3w*5#_f-=~| z@B_|f(*AJQLd#smp^{q=B_;h2M}&ah49Myo-re8H-?d~PDZyAj{f1%GWDiY7AzEGv z=kwD>chGG={afmmJ?kYX&C&Il==b6Va(y$$$e!p0nO`doJh4R)L>JBfccxWV`kF)8y{YCt!268>oVKV|z4A`aw>b$H zeQ()mo?NPr=2-=}2-?B2-vs(!jB` zrLsS>Ug%*3)a4XTj~%lt(jd< z&hQRU0JJ19Eua9&S#|ORm?twqbZT9A7aUvX+}p7*Su?SK}tn=8#9>q968qhG*|nXZFrB6^4Vm zcL#tJ7IQ8sc>Xl{R~Emp>a29a@c_6}1HpaW1AYw7kbHPwJ#%|1yrPv}PkNZS;5kJ*9-n0$DjN?Uw}^&KIr zeNdH{-nBgq`ZHtkQ;w&@To=$VSHYb!Fo$SXJ(U&a5uc`BYg|zR0vM`wEaMBG5(FlR z<&d>WY1x+SfI8HI-1XNNPGo*zc&>YQNG*V9T_R<7;J(|NmX9&0w}cJ7h1_4)GQ!+B ztGGpl2tUapUn$T$F8V?619kCSWugKIqXW0WYD%4piom~61z;K9x70f~oNa`NnB{45 zCQAB;#s8-x9NUjaX^y5k-2Ov;j~@rB2;PHHsg471fX8O#ipE}dc>%2wo}P}72Z5%W zuPtXX?^csxxPX&{)n+LL6S+{o!O_**Bp4G~*VN;vhG}~gg;GTQEM@LyR24wK39*L( zewBUD6<49ss}8xY06+SdAdN-v)p{SVv!c}i2Luc zN1QP@y0Rj0!^MwD} zKJC^)(_w6rP-L}?-RN1RXj760TP=J5Izc%bOsa$;;IQEZSR|?=^YZ+*Rv__Wx%iU) zBJI9?x)F_X8h-;829)mBEuZUb$?BigO*&ycIf`C1H75muh#FUIHr3#y1J{vDEiX%= z21X6xPpsfngstG$!NfSd6YUA$EWnE%j}K|{Ls1nY;4--b!&eDHkN>QCZ1ZXrLvEHS3MGO{c z-{xQSc=OHp|6LHY&Z9~})-!N4H|>~o5Kv>uo)>{vXy)`i9+~d*e zmi~5u)`ivSE6UFJgn9^2^Dc3J1kd{^wDFvGX;PDf*l|HcP7gRD;X8v~sBfmJewpw$ zR+9pmp1CDU9Kl*ILEC4k=6Op)a}auJ5T3pvsIsJwK-)8+4()7s=`g)i_4PaHx~aMB z>I|n}qRSfkA*rFb7wq}x$=eKjz?jdp7Sc_5QJ7SfsrZ5$v#NHJ|xkPo;Yl0>_R&$T6P#53?xX(s?ZD`(|`AP9ljzT5H zlFs4vXSxYXdNZ|~H`xyPj*|-@teNj5Shm3at9>_DvAMI;ksChq=rW@OEF{)-<}H0p z-`qOQgmnv3^19W;m-^KZ0T!HAn#&%zwlz2>XOG&TO6j6VwL9ro4#D%Ik%S=ZH|#r+ z;1GT?YiI$}*47?83La~JRa=-V#D)_cXvNSBHWUYhGEYOI%z+ai46C`O(pF9yhe2>+ z0CO+7&&dNiSsl3w5^Q>Yj{B`>;xbXqr5%q{6>@=%TnWA(pUnphL(1-?yV;gs=pv;p zV6?_|(Ln~N%hWr>mfR+0NLfD|)@}CV0A-#Ok9zv+;<3`Zv_=;4!+E$+hM9de$+WGvc%icV8c5RxG=YF> zVW(Noe(vE~41sb5!xa|oRFDBO-oTdwnqgQJDhN=q2=O@dmI`BI@qfT?2FP6bh>2G`MtQE|9hgYDTATwZhTtC&NLc3} ztH7~P1}62xQX<(_b0wF{#uk~?nu|1N*&;-vWY~U`eOj8yv zaPl-KQn(O~sww|bcxse?Pdd1hn2j58>eRi#%qUXH`|r95sH|dC73d=(1OGYvf8Z26p`3Q zK?5{b<#C60^*8`9T9GgLFbVd+d4zM;b*Kg`iTXM{NNbyDvD*=q+<1oWEA+Z+eG{-kZMa;$e6l=8%NEl;-c2KSXH5$^c9UN zK}}HLPH91m;8Py|Gkg35HAaX4&U(h~0N9rq5)5inZ)(!Wo*!2V>RU2BX(Gtwyt*r# zKU8SP=O7CweZyaR>4CEnbK{A#r!6NJrjuXh4%I>UruY2mtb`eng+=Gl4Oly6m}21< zH_WwB?p#cd5zXW04#jbjTC+Mnj+Xk7Xox7*hg4f`z`t(Pjw9K6%{UylfjEgtIxH{9 zxZjiz+pM=O(t)vL7O{}WY&|Cfhno|FN+fc$kU3`beSq$88d=h+X=mp;pp1yEx>ikop$~=DO z-+WkJkfcqADK=0T*4kqD*NW?8mXcG8-^X8wE#D`FCxzZQQW5p>sZD0bh~yNaH$Ui; z99(P<`8Z=gG#8=JpwU;e0Qux?hv_*O;oQDm$)P`fC`craW<(AcIT(-UuV3w&YqJ2O zPGfy@-pSC9AE_s6z~1H?;{4b3K5?yqf`@vBM>!C4Pufdiw*1|-=91=eG#7M1@PU~T z(BQbT=`l7XZEgn((MTZIz)GxkX#NNf7Z@&fgE>3UC%9iurhot%<#e1+84%cQLJi`O zV+$=-Q%KQcVb`cwu!osS+h1QiNX&LLb=0wbQVqkaQ}lw+p3Xy|KFKn9pg?s;Qs@ap zM{9gv$Qzr`l*dWcWo(y!^{7YuwpL@AlFU7kSsW|Iqpzt82t zbN~qU*qGQK0W|E;-02Ni0pZjig%WGIOF4O?v2Vv<2J|JBPW<$4UgYh-BA`A+!x#Po zipcojWia`Su6pMC84_QhQ=@f$^>#hzr%J!QKtKrerfP$57|5uM zWqMG&>%bo(?uIjwkn#quLoSocy<21ju|h9{mt z&cN={{Yi8MFBf!fA?$<79_XJ)B6W>Alr3L6n0LKjncy~P{SBzCplqMr2 zfuY=SDKsSwo)_*1=^Uo!p578cg;~}OKtq|;iEL7b0|-+xv}?(eBI{Nk+eFmI_MvFf zGS#Tw$LY_;bKWe8`RkI@;8EPU?98!Kp9M%A!439u5LJB{axl^j z`4?-dsrae-9XbghT3CvQLcx+D2uFidO*CQCqDVPS8IZXd{T1dFj}}B-r=(UAs&Rm* zJ>)7FZ#Z}oGb^;+f=)G8j9nSx3996`Vv->^y6)$YDzP{M4CDC)*F)nUsw*@Q!Q(qh zDsWtncCqSL0qc*#-b;P4IP3E?hK5y@CuX> zPk-_=`+cJ`k;}KRI2{Rs32Ef(ue&9i1BXRQ3=$YBL7v|P#U^<)@hN$hwWjeEMvHY< zt=H^ZD6eUff*TstOo)cqNCK#7_gy2t^wB{}icOLY*r)dTZnjaY?_*KAIGczLZ z7ojkC1KkO5odd0v(jez02+ud%bA`A5rpG`?0jWn`lys9^wuJS=1Q7|Fv3>Y+5;)~5*q#UI^h4x3lDawrHq_H>vmKvQ#x9*ykN(k^Dd!`H4D)qVqz zbu^Viu4J`Cs(N9d05U>ovMvmooQ&-9fbxR?X)kuI@%b+emcapOnYb8(VPyBka~jCe z;2%FGVQ#L3a0)cU9zTW_%(HAnK-ZpSc$+nlXP6s0 zGbQ%*nA%~4crb9UID_V@kq!-%N`EE{25zjE-%Eht^*}2WRVD|FNOLkU#RAqyP0FhC z?Tu|NF+@{~S#^26cM6C# zscPu0JV`01#x&QL$-S#aqbqtnd84PQ^UwPbciKlkd#}Huru_6=qH(f25089EjZMc} z`BbVMrrQ?rG@^8WTWgwbF%>B&Bpz#5m&)sIppIKnGROa=W>kA-{Ou7Hrn=O-yEhA5 z-Fr&tuZL-eB(5aM)e3BU8gTvg+lrd5E86w5GCM@fmd;9Ea5nr>LcMLzWd9@=-7Eh5 zu4Y=D%7u!ZGFtH!ZtrP_v8K%XqXiB< zCm`WQLKh+9SZa)M&QjcGc1*?dtOX3cRQ_s-5w%$&&3s)XAHR+!&n$gWo{XO;6)XPv z_{|={`R{GxRg(Jo6V_@&&H|eOePVL=w+ip-6ycooO8-aj_#Wy4pCbWfwV5X7mLu$* z@vficf%e^~8#pK{4K3vT@{@L6S&6rDFAuH@^lba*%JN9{m zLyA(XcecZG@B7X*sM@u-IPDfntUL98biH{zm+ALEeoN&&ZKh4x8*R!`M9Mx>QOOcQ zrBa9pNuq48rfHF_jTTuVLS<>Ohssi9DI!aV5XzG5%j^5R?)%kDGr!O8kN0CfpJsa9 z*L|IHp67X<=Umr4tmau)t2Q23dTw5qxXXBRgHpO@%U3(>gYT(>2i2#tTJ$?Q{2#c9 zoG;nqX5Jb4ku>@g4u200*yM2#!)Df9BrLo~bkvdk-Q4U!k&V;#3_ZV^3JDpty}d_-s4Z91)3Fmg zo~p9VCl^p3c76NH}p{t&xT3LD+O)+2&=gMGV`gGEk@|F?5qzFZ4a z*Pp0#bx@98+rQPqxDC*sGdgc;OBip_40>a(c5Oq4lPZ@Cs$(ch zIl+L7)&H8a8E^f0c=P=mlt{yzvY!1Uw!>|)Z2yf{Fz^urPZ`Na*zBz#AJCs*IZKub z$cz*x@LNqW;fjjy8hYJKBs1#q)#@Yz$F8Ar!o@B#SuW>h`6IS^-DnRjh_aRiy=#R- z%y|-P9C7@%zzHK=EUqY`PU+VWrE?Kh>{H@{I3EP!wSkGBhId@||8f)c6gxS<_N4dn z(ocK&3&*8a;4dF&h}5|P?pMF6RA77X$(EaLoP46*X6gsKVMLd3}BlT zuee&+dtju@*!1A653nL)pokVB2c1I;@ddrBw1 z@avG}az{3}OLnXwS2wUPZ*e8`8kTpN5 zqzhUZr7i{wHn6rGcP4EVxuwmrRE|wdspX(bSfmi#><@ z2w9^{oCTE>6=y~H6W+_J-HvrNL~2HO?DoP-;rM9L{t+cUjL0+M^$@MV*_rJy`a8X5Z@88U~3%=mkGKM2aEP2Zn`+SqN z^FG(DC&S<2OCQg_ustlq_e@GOk(=(v&Ghi_=#J(r!Lj>=?#9xI1~LLf-M?I8C=aib zQR`I`%TT-qO{dcLk65AIJmRPcHR1QQ0=vM_2Uw zNfEOf{a@66MF6_eE_IQ1sXE(f)csWqOj_@>_n(Asfk@md&Kq-WdHNeawJ1d2*aiAO zxo@yBs94vq>0LKeXs*Oo0;k(MzR=akoV}g{zht{jr78pcgoTCA>1z_Z

+*t)Dlj za+FNuQ;EJEYZf#WuPJDH{?euC`C#^3RRh#_QZOsqP0fHxbpLjLHnxxsy1(#RfqjuA=8THZW@Tl$7;qlSao&xLheMmk zsyRC)mSLkjmD;tXt>pISYOh7fvu0%&@lfvf@mrr2vRnJra%4w|87ivD&WE?dja+-% zPty!iIK@Vq1Gc0xk;2d2GtWHDCf{kfVwO5T+v2;aCanvgq|%RY~{gS##| zb+lW(PGwB*u&|K-AMGW;NAb@@Jm``6a-@mgrg9!)q5IPqn0ovd0}O4~ zTp`}S_I-(>DjJaYUU8k!?!g&E+rO#q(fUW`*q7!@R{cN*+kB9Njcq05)Gc*0^pXKa zs(#)k_p5O6rqEUNgdH_Rov1ut^e)>^VedKt670j?TO~8)iMC};kUZ5?MflMwzm1{t z5f$&|A`7GSaB*`|q(sfy`OSsHZ^#2RfI%k*P}m@4e#ai|4VXlAKL6RP&aGfL%imtr z24C!J4!j;VBuWP{H}yCMyd=rFl>5yi_8>tBZ@ki6fbvp85R$szEPj*bO5n@qMzB1&(iX6XdC;~@7we|;+#f*SMTk_*Rm12%=e z!z1qh!lVGYGPXYX4;oWO75V=D{u`J+Mq?Pr6IsCaj&Ia26mtQju&v%!jRAo-%RE{tT8>- zC2%xh3YAk}a)00WaKAN>gTpbj4-H<+64`ycfegJ+l~FKB=80C5%z>v?P2U1^1TS$> zM%qTfKKS$3S#on=Kz(CfU?6$Ww3@o? zW;edLeLr8N2cK-tTgw;pVwVwx@w-c;jO7{)9DFfmh){@r{C(8oh0{; z2CMn_1kNS#73D4iugV#P-w8i5Uo12FcJs!==LhU+BOmB(Lr6+SCjTK*)7=q(L8z&j1<16=$*01T!pRU;qr2MpV zF?nJ^;Uf*x^=bmwNTuu9cqHJ!7=TLRP5o7V)0()_bxukz8=5AQkAM$7%lCC%7Xq25R>2nTeQT9>cv zZM|VGgQf=HU+iAm>{*n_Zn&6fe5il*H4U)?8y-8Vo39vADL@~X}L+a3P`pKc0Da{-a=V(eXSjdR8?xP;lV)EBaN%+Cw&i3qTJ8pZO1kO5aC9(WmQ!; za!(&2ue=6!G%BJpr=@-mnNbK|3I_*)a!mSGrK7=3rE0@3fzanOM2C)ERm?DGL&W}u zLJ!GiF{0dn+O;e8o|0^$Z%T7mgaVTI9ydtfPrmo|-@c~^55c{^o*N1;JMSq=nwlwPLjy7-b(6;-sf)?svTb!e@oPlFyKj) zi2_3FwZGrOATHLjCzgYEo8MMn(Qx6i-@qhE0{I~5+IQ^?WDXW+km)f=2_~E0zd(4v z;7fxyM&e`+o)wMCB=y*Ug)a^mW49ty*W7@(X|`uHFYz@jT{F$awQLS`bK~yY3%|Pb z7qoP=O?%++v7>KtpL@GWD(>t;r(@ zXAG4mN?(r_U#GFTXT%(m+;3OxH<=0^F`7IPQ>v3sk{MjW2tHe~XG2#5euS!$)^RQV zg}T3j$+sqsr}v$Z<Xmnjofh8wsLD^G`cpB+j7ZE=gsm2?Jw~9Z=4(dl_G{ z30avAo00?LHo#wUwAHMd5%$bBeRb_I`#xT|fUqES+5gi5jm!A4_pN0{;u^Kl5jO+dDddX;hiv1>xOI2U_T{YD;ELm^Gi|vEyFto#fGG@5qvk*=tLL)#i{ohZL<_ zV5rmkM1s8E|N6BynU3A*;N->QZV_vD_}yQNS($K4OXD_DACMl6HW)?$2;?kpeCB-y z$HM1178Z#nXG>mz(dF}I3DRYhnxZhG9ZfRIgbPVqqD_-{PSW?116SR;9i`?`M%&xs zZ)i);!`^F(&1WYKDZ(KfJJ(}lx2#BZ^DlCm)_ zsQnxsbw9p%{4|Hn>@xa^eah8L%Zt(8A&he}_c@1^OxA+rHPxRJrAteR1IND@Vs~mC z_kU8N^Bgyr_!>!W>Ru$u*QDQIX>DZ1%Lwtq!tRY$?9fTjLDWY1Uv+My%%nK_#6#dq z0ioF(d=Jt_u|bM6q!AgVwN3MotLzI`@fN|bbe>)UL?fybDL1UhK})GaV2WV8bR1~h zu9%JQEfTudn9LwiV=@f;e)OGM?`ksEj2-r=()Coz$2?Ju;X7`)YF!Yr0a8>&bR68? zQEEOFv@>lm6xk9raoJ_mI&b=7x#=npHa&x?#`ONJ^Mq?n!LKc zs0^!i?N9#0*_|ALEALwC1%p-$*3ZrJk4KOsE`viZE){@-iD6()3keFSNmsIR@q2Ea zClx;2sE&_`h`J#7)d856{9%q%5r{(kgk9!H-E9JV7Ej2X0n~lRop#dFAs(vO_p75* zBlK1RzF9#jCq_f@U)`DeiOc?X0Q^Z+=N#L)6f10ZVL|~X6aIJ=DGAgV^$8+mAymo| z6yboPh3ujij>DRFv0OUttgtPFoWp;EQ@XvuE8$d+@BfQm?$QTo>ujV4M56VEHm*ln zAwBR^DF*vemm-@EjDMVc*8m02QLLDDC{zM)y^viqDfOdO(aW3u32hm;S4s@jl?ton z<{ux>5Pny64L?}m#VSm@^geR4u-Yf!xOyg4_Lj+eN3;e!`;QRXK{jpXJ?$OMaIM~v zR}QpfIt!O^mHsQNO}(bDc0a~;0X{4@E^Op`Y`0PaOozrPknU$sr$89tsMp$r@{}cRo9*9cX|m<;FcQ0s#@I5~@lPq_tzdui75j*EN)>g` zf{dFi>SS{#b|`kW?E3}bJtW@~&M_rCO0vc0O_rS-G4fq>S5mMIz&VoCcQfaNS%ODP z7l`<9P@>TT73GhblcZQk*R1(a7lFd-!^dCXZ0JDt@je;V+7}>| z(M|G5j^cpNZ#SxwE}`rp<9*h63ETMC45;)e_w~MdgoI3r` z`Ya#9QvumsfwVrz#oWiF)d7-2-gG{6*z>s zn+S}g&>%2ndkexXwVB8Sa35KsJD`Yw60(S-i+^M|;NPMd23F_*gS&iniPnAka7pS7H150HwL zBpgYRFg~qu>bsvJopdJ#qQdrQPNA%)FnCg#1nuL?ct@D)IykZAesZpIS1nYa)Un8N zf=rCT%<>3OA>#1{WK&V`l7IPQxexDMBSt2GE64z@89s{rIZOjSNjpLgx~~h>2E3r= z=aDd^n*k`nNR1TqA#CmDSvfb2(FaC>1s^6M2;uifgwN6Hmdq#h8u7C`QqDxurw&2b zD2S-bpzjosvL{^B^^@5xrHNw}#wy1k_SxcDOZU2un7#H7A=By-U9?ck6>eD(z1N#) z(m?S!V6^&(z`Der+69T}gkN^88XqN3e4NNh>IaS=Uh?h!B{CLk!u1bGAvOpzMBnd% z`0-<#nEnBIgCrD5&X@sV@{^gZP;Q~at+*2mXS#P$WK-%Tn8S%S56blAG zOsoj+0qzr&RriVI@E*u^A&;S>be4hikRldt39wdP2Zg?zt^$9Ml1+!t6O0yyeAN+D zzmo(SaXnR|-5s-rcMk zic|{?gp~C!NQQ*KV~g*_JGJR`OosD3Op1(b)?=g?qrGV^i>9(0U(r@LkIhI38#{hW z$#V&D5&V3(H}XD7E=aZL$cqzY1l7S>RGdU`LaauoAnuaPwZMR`Ks++r0WbcijG{yt zjVVV_kF@PXMH~&(pyo(`kAECxFC_&6_Lq4Qr;mp+Peg2rSuu(hO`~8ZyBUpBC12|n ze0af2@WsS=y2BnKTGRVA3{59(u9Er=V&sAtP=A{OHE0HK5)%iwia1C&TGN~ z{IY`}Wn&Nr(9oR`Sq8$1lE~zwl?+k{!dV+V+aD^(jva_{6r|+RfJV+SBoo319UB(w zZeofw)-pL%Hy!(w`C&tXm#kU<97#z~@{m#P?g^e69_0+e%4l&|^vzxWk`)qdnDTP* zy<&2rZb*-VMu{E)6S0EFA(PApf)iVVdA{bBL27!q+tpT)C{|E!G$F5mcpNF(x<`fC z2z4#Aac<#@qGmf_DzHaF2i(P(y2-YJG}rgIc_|Pu(k2BB8Q6e31sOcV>S^i+fB^;h z2S$&SqyMGkDAG6;DK)aqfQPQ6xG0lNf&>ZBD9brl^OJHIWDE}Re(*CLB-W&r;1ihb zui$xvzHl^g79n$4?EYA~!#;^pkisg^4uw2i_MIfm<71&-tGFs72mGeXqz*qBvKT$~ zrQKCZOiZ;}Rrsr5gZ~*9)!Ax$KL?P1HdlErUgnvu}@HB<@;! z{Ma#}t$+&{>l;60;iNP#T_XO?2}bSHzbcd<2;op*+Y~{1i0#B#Kv?cVWORR!GVi!l$utsjqmTVxZ?bqx+9ul0JW55JRfTQy9LG?tIc~#70uXxl&r+pA`@0@m5Hw<+2;rR99E51)mH=+g!)$<7^>1V_w21| z)~|f@rGeXMD>5;WE=h{!wV3v&f{ZEa|DghP%47b2JG4d zwt~L0`+ZPyolu^kg(r*xG6p8>@i&9sR2`o<;hr^mWO@!c ze0FG}kd46(9yVf+gSY~T&CgDAAcr!l26DuC^;)1)MwbxSgd^qH8p6}h=w9U%eLa`3 zdm{5D4=se^uUUF`7TsZ*lp{M-Q=%5YKGE6mpkwfhL~$Om!kV4BriCYs`rTeUz$C{J z>Q}1lDpTS-P6cb3G4YJgk0HS8akT^+lC(N4&m2FfG56JK(tC@F8x!wPQ7z9Z#+gfr?_!*V4(g`=R z=N0^KjBomsesErP@L_1`)&4srr+5v2LU3b-3|Q~am}trNmSWd=>mc_(BWTc{aV~-+ zYGUj8kAUCEndsINT>x*)Z3jY0;Fb|kqaP!a68)k}7{`DxFqPfeGE@~dmJqw9{?Y%j z0~K3(RGA*cfh13~|Iw4=tYkas6c1jT@E~4iWCHp@CZnM4D~F?;y)OvdM?Oz-?m1;G zF^}xQ=)dD|7l;O@i;=pewrgl~sKG^TZg9=FiWS)r333al@V%hGR@RI6J3=-s4#f6R5MRzxQ&t7cfr9j{w2oG|uDp?M@DSbzEN8#hWm zeYEh}y}KkOWd<;;fKQHyz(hdCU6k{`k!EV0m(m=foudk&wn(-306gYEMJ3co2;ih| z9KbV`OEn)NL&YytUHIu*_Da=vu%EBs+yAx>|!a+8# zW%s%M6mn(6=ZWsp*~m?rDtPhp=h3GX{kxXZjzyiO5~lboR6Cedlui$g1KC?c91Ye! zY3Z$&icD%QX?JZ|p3Ps?@PXVpeIIQ;G6zALfn1d9kGCgNjH4na3X8wN1l#^G!Ez44 z4qt&RPDIVa)#L4;)wme~ixGG3Co(M|b~_*@GF4sU;>QlA=h@BmYJS>wCpxa~{;?bJ ztL!dR31;BdR)48XXPJ0yT_QdHy@aEL&k!B~dL$W1&umQ8UZNW=NCws?;rFECo2Rq+Iz)mSv)4hgM-I!gp(BA`9JKTPdFh&8?pyXEF_i$j!tE|F|6pT zR({x!jUdUy7L9!$)XtZ>WM5P?uRfg7+}%w6|Cxef%REN}W1UQ_NE-*TI#GNwnZqIK z)5(C+Q+Cl@`wRGJGB@6d5H4wVwi4mx>*0n{w$NBpfre`bc{P|6&}o9PqxU_JW*?$F zcOVrp4V^!A-M^6a)^x0-a_hX>_<~$rFJu;5)4#QXMPgz#`aGnH>iGy#pVT}!i->%U ze5elgsttc7MO277sftKg@!4|!%bSvj+o^}ZTtca7=B zcM|T!c{JDd>NWKBT5ZtHAK!xsz5O#g>z*i(I@IlFg2q#ehKM&h83cvxpP|%Mh0Xw+e$p*6Syyu>H~H_Yr|R*fk9>z1REkZDTdjwy|q2TQ>|H zcR!Ace(gP_z)VHe(k5(Uuh3)^(=`V+^HSE+7`xy67b90dKP&HEgU3qpvZM=5e6JG& zADB8jHcu(p)nq?O)u@4T!l zy}8?b>~02wz)5F`ypEm+B8~an5$m8K#D-^TuH=s>g?2L7cCSI@(|+97&dS9Kh*}S8 z6?Kgw10|MVYJleaQ`O6f;zCM(x;>gG{S_2_qTjzw-%2h}H+i2g$;21zs+tR?kDs+P z;X|&X{RDLh&AGy>A;`TSJg3PiCZNwm$I%@F(j`CLNPm z(Q-&iCn_-eh0-J0Z&H7Aqx2u?-ZgcPa$3Jv@7{lK8J_oH3G&~;0a;f6TdSI zNGcLu@90$}@o&I+%Py@SFR0wpFdXW2<t9on z+ADn(V}o7SIPxXV)V^SMH0SP-!?RkKd{CRTLeF!_vw^lTSLHkBbFY6M))dQXyVc#+ zA2s!i>oY@g?ddq(<&p>{8z?XnWfx~nJn1ZeyE?jiP5!cUE46_UjimfrI|YKc|m6D*At5 zza#H9C`TTqsCR3jH~#e7hw&uIN3w8wXUVFh-$KV?-&mm6PHXL1Y{Ql_P z4&U_eBZ;pycl+nw%icA+^z{Snkd1*SJt_pKMe`YZdOQETnY*tpD=`0*wW`2ralna_ ztD)6~++0p4nloIdC1b?A!-^+TKNPt{Z->#^ee;duzhYud&!y6I z!hNhIKcV1)B_vnpuiO%QeLHvi=NH?G1f;Do#_KCw^(oKa#KpS9@Y*F;f8OJ~@_?>@ z=7y)sTea2@(TFm!2>fc66|_4NJ;I?#5xKc^ae#WEl95Cv}bWM<;CRY*}@u z#CoB2(LPl*$+@$852vZ%RLF(g9fqf?8eAq*n;E%ZUF^nT>5+-gp)V~6DpRi7hmI<1 zbd5d~Jc%a?ysj>ES0266t(hF9a#UCaWkt+}H2=`+Bh}Ahu9Ew+4gI#BSPPq9vOA=O z_g5fW?S7}ly2JNu#pAJ-&+MG+f`SCqc@o#23SiIWCCeMh7OAPxm-MQ0GrfU}FVlm& zeJksq(azNhhbZXnuAkc%b%W;8OZ4H$uji+w9<_4-#CGT{s}(x)hSc#p?m9mjwW9c* zYd_e~TU~;-x?JY`qLpuI$2N}X3bW@J@0)N}?)z4AU8x)g{w4=m8fol5_R>Qf!!%49 zoAc87dv2=|xxE)R%8P6J7rgY?=ijD*4=bRSQJ?Sn@zsm%(mF~N({lbSa{j*2@xYS8 zJkqq@5h=HxF9hFUx4aREjkBqFqP@IX*KHt=Hg@K-C)#(LuGj_Swn~JZFHd-^vES3V zV@c3^pXIt6;hjiYW#Js7mkp+62Lu*N5miH=o@4Za@^I|G>y@{kOs+=fH^uphIw#I& zO$oF+6cGFIH2%}L!ot>jO@&X0c57o){w;0Dhqqqv!8OpwGcJj(-`bj^D+`)i443=*4^e^+YCA+`L?%SFuf&kl607U?1JQNQ%&8&S$#$HF4X+j^<$30 z7la^18KwA-7dmwfJ@E3Evh}>Gc-rUjft^cF28W&3O;(}Z z;dSVt3(voOn2VDx8ZWGX3EZHtMEuiX7F=xK%pYseN_p-UDEgZF9tTW}y`Fo?P^kuKVsf>S~SU2&( z?Ld|s5x(*CW683jSPP$Ve5W5CjKtcJ6WDpOxuT5r2aOX472}6L)E?M*&=MTu<`KK{ zS3^Y&6DRMR7;Oo8KIzb|iK75RIaSa8+b$fZl2-q>qi#xsan^oXMn}rXj^)Hh&s!CJjeX=QT4J=< zk8yR@RF2&Bi~A67wt%*6C865Z;&;!A_#)$Kq^%2~cSsR1A-j@0=g*gW%ID%=B3J9l zBU)2iUFuU!iocsJ}&z+|Jt|qC=&C#HAjG0cM7>tvcv zZKiTAGbovO{-Kmz((og%`9-o4h&)Q}uh@_0n%|FSn1dOhgv2LhyYj9?0VFcOH!TsKIhMV$iAQp4`)#$M^XeYl_ z>1$13B}SrlTvzgtt^D9_1)?A4L_4Kl!{(6R=kIhH?sI&x;~W7gTqyadWx%!HSYnQG zuTDwmjQ|>gVS?-Oskv#DZAq#^4$s9mY=}MH7lZm9e&7G2f0qtSnZ@2NwYO zst7kNI175h%U?A0{CT+O%jCyythqUY_tVCPhdNr>Dn^p`?&#c}CJ&ezQS0gM?Y+mF zS7@1_u|G?`wL~teq)961feAn3NB^v2(Usyn=Y8)x^BpPHCc_n_`9s-bqP@B7p;fbX zVI>z0)7A--V2bY=^$43u9eIPClV82253AdP1Su@-lbYUb9xFk`{Xt@(R4`kF0qvD3 z{{brGtz9oJ+2_6d~0j&3`5wRYW#)=C8AX;~Jxy$(xqpq|~H~b^9g;s?0ao<9xk*YfZGlS<5&* zB!BEAFqk!!(*-_72u|}|Hc46P9~23}94+a(zN(l^$WttZ6CQ@GBs&ewXZbt?@gsXi zg9$8;EiVYPCG*is`lI3<3N&|N5+?jCe^9J+OVi^jclZpa_mvYr+Wgdh4n&N}KSdWg zUw|_gZ`=xir!=>{6l>`dNxB8CuCfyzWzlCxl1_)t{$t{^yv(iU zEm!uyXKPs<#w)Hhz0BIP{iYD^MpXCP=~c8Rb}sC8;$Tg`Tv?*Mjqru9RSQGRK92A0 zZ0_ARk}c#iR;hMZ=g{Uf|1Kze>ZfLhRj?a*=h$ZCoB}IdAD%Op-P|cN zv5GBKV$@2e(5aj$O1o*#CmH6)t7;(P4*bn9FV-PnJ+>#RU|G>n^ z8_?DiDW%y|eN=N^tMOeO8*xsSM!?Qw21|1$4flw8$^%H~ch-Qx{XMK)GJ8IKq|yAy z7T-eZ$O_r&R&if4aj`jGd5v(VeHLzJ%DJ4D+-@<};^4&z9WHmXJRbsMj@6d4BUgDt8&@5phOaC?&XXL z0t}On@G!e~?go{cX4?*QgYQUs+Xn088?W_{x)}tXc4q3M@tfTR4PKue%!^xaq1&PV zfsvB!=KguRrHU+^dOxbw33ITrA`^h6ku`2iV4HC)KAPQvPv1YBRd{jEin@>aBXp{#DeZ7lX(G6+A|n3l*Ns$}@=S5`Ueb6Jk*HGow9i!c^$t zm3AYq<6l}BleTiq2^eCiN(J{6I?~NVN-}k8+PvknzIc;&yi4hE*YG53Bu7c1jd%1+ z^9F1qlo}H`?V`4(1O*SNUB(!QJu|fmlzONzIw4Q{!C9#V2Fo%EZB;05PBWJ^ytnH3hXqwBLR&(%r+nKp zj`(6tWu~kf=Q!89x>@wu*W=8FA+qbbF~5+%y+*49M0CM{aVeK59mYwKun_*!JGAyR ztX;NY$LN8o6xu&ZVzfB^ahe%eKJp7mqZ2{GBm!(I4R}2y{6AzXt;E56BHwJz$1itB zVXZIDQnO{`-B&wL%K39xzbd79a2FYz?v6Wz+wk(@9q!_%2?CQy|MAmnKRCLOIf$Ev z(mDkn9lCrCHt;e?8HX>uum&G~tPH_Rl8jz4-CVZfGKtyfO4KsWD@dJ=Q%4ko@Y4FD zIcs%!FgQ5*zc0Tdk1TOn!@IV`-^zNh@Oc`-);u=2EYjwUzjK2RJT`VshfaLta;S!7 z6hq@o|Jt^;t=)m5t~JJ<%bH)%)gT;rbOp^C43|dAQklX1yN}<&X4=~f5gMy?aNF5k z@8EE$SuLAUJ(H6ICLHlk`Z+2Fu`7FMs~>lM`;NS2>FyTLQ4E1i*c9@ z+b1D}JN>C6+kIc@@xoUx$X`PRcM_D@4yFGmD$z3uYvc>1FFvQ3Zb>eiOGhN?A~I z;KSF0d*L&#VL;8E9ndE*i@+R+2=$Ly08n>J*6|UL7Mrz~*@j_akboW@I<62%s)jFmagq z0RED1!I~b_`FRWMQL&z(T-~sqC{_ToQHBo)-9w^K?k{^vQ;cx?5J)m~ZsbxAv=)6Y zP#uJChf=SuT1D>><=%1k(6A{HB5+}xvbIBJZ@uqa$pzI~xEj;_n4lGNnY z&VDGv-*}n49p4qkS5lG?=$Br_lUroH*?)Hno|kfC0RE5 zNL~1hizP%ukOQ*}@qgpxP$RsKc=c1L0g$c-*#` zYC`9-hogSHI;#q=zDNvN*_OzKpmN(+T#Hz3V>s1h7s-O`^pAVZW#w*#@g*Q$Qpj%l zFwzpJcEiKP4^Jr6a#5LGxu>3z9VK*?%Bh9W0*qMnV?(DE%q*m|+K67VAOoPlrPbBu z6yi7%_zpN`lyy%H&Z2jhW2t3OT^X)N?YIpg^F>1)Kal}Qextha#UKh=_alPb z8tSsR?*wKL_3qvbD$OID$M02HndiEvOP&AN@Y1Cpa6@%4_fWCUzvn>RE0zATl&0KM zD&$=#mX@5nds)m3_0xqG#+%yw2)=HPIN~l&s58HM$e~@T3%s7T+a|@g8+Odw+@ZIB zHTi8+^8fsH_(z10kEtfjT`m6W&5a&6QU2?ye18DzE+HTZ1sA)sgtI%j*Js@7i+nzl zv8MpJ8Q)BXsVN0^fj-Pk!2w||>f$y+M!(d~&-0F?qmDJ@C=i*$9<(H{?3qtQWa5Df z#~nK+hwCi7a%$gb_Vm5`B#DBXbAU<0a*vV7HgbS)4#^`A9gKfeiq!Nk*ce2gKp9dP zftT&;ROdlg?U)gsk1KljjSiby`DQ(qan^Y8()pMsDV4aW3g+q^^%sSZ0NWbB(oRB) z?HOJ!hJ{fyX6J~lcQn(Tx11buO#JKNa56xVK({xtU&Sx#GuJHRb{II_E)_dhfRmc_ ztQeP`Y5U@V5zO&>vteee7A}k6_*c0=)v!p;j5icf3 z;qgS)v!diblW6RIqKb#d<@4T6xuau^x`rm&A{y3yA#=sZ*h+h;5`_+jlXkQ)?ncT$NoMq4{l!m3SuKL5T} zZ8k*CJ9x-9#-O`6+2qHsTY{KwVq7riGWH>1!vXwkPVhW`Uh=zmYU1Q?dpu?>o-{KS zp$K~}o-iL}2P$&K6@JzdqzU&Lfn8Y{s|fP__*E29KN7NJI!VZ1GL%T0hF)`ASw+t? z*O52la88p+OD*7al$^`R+M9^z#vOB%rtXkFS7$5Enm-ltJNY!k-#`7= zz$v>+-5n%^5@To|5maJSLHv9S7oj9gxtIo__^KIBlK6?ij2&GNBx2`=#|f>hyQp~T z{L9u_G}}li)$tddV8gp0u$gf5piT1fV@G~wJK<-A5SWO0veD;vDex*@{GZ${q9)E| zt#p9~Rq=}+L>|-GYS(KnE1s?*1d!wcrbgBqRCLHxK>(9v<4zlsGrWXG^u57;l0@((br=-wl(v*|?sDM8A5 zUP3^j>EM~CG^gOs5yM{>(N%k>M(XmF{s9}oC4lCEyQ+?@be?4B$6e})e>z;`P&($G zC-ptMp<&p&Po+BaL0@S`2y9wGm0d(?a+v7foxkVd@Blao@&V!n`OBw%AlZIrY6Cc} zNZ`?S?R^rZ+ceYGf!IBD%#GHUM%8}!qZnFp#)wOZvAMLuF^}T=9o`R@;TPu-*t-9F z0jNu(3c*#K?1z|Dbs)`j6LFf*u+jrNKRX)ykJ7tFXBM!(YAR8^e%-9T{QcJzJXNaR zm_HtncTl=e-oLhCNP6*F_$gTC;-+HrHG$WxO4*sFw-njI5?Zol8UAK@aUvL4_sBS^PB0}0fiQ@ln_60SzF0#g<{&0D zvhFoltEmeYw$?)UHACEEK_;>&?c?oErO1OLSVNERn6Wtyk)yG%=DOnP6huk@DFFB> zbw7sLRzy`6pT-|!rIV?c-|#S!%b0|7(KPEGyG|{m^y!3}!mn3+4r>M99e9T}%HVKV zcKOh@C}lU#!>#>-hxdtwU~y?@C$W5vfPuX;cMvfsn+>`BUf-mutSIMb4>Qz#;kRMBO`PC5anjyyY_Bph;^pCw--9ZC z$(&B39cNL2+tw~X4~Ja+lW0bl5Tu~wr;faW;BcupB}~xogx%`ax+>pi`iL_pFX;yR z!_f$=mL@}BRfMvWy=(qQElS{bv~oz&tMb^NfG>S2z6EIv_57YKSTb6t?2wgdy@<3CnYBIw|P&ur=`=m@O|?BS~~c5)^+6_j`!rnp``i+*d;VmKLA+rMj$c(#$6jo ztRH9FE1aS90>Kvn!6od~LK_wr0qTR<#o65Ob|>yNFc8oUEmPPhVk_m!MsSS?O${%3 z3`Tlaw7UDrw6b6E@6ZSLG?!f1$Wy+%$!E_XzWTd#kj~AiRGPoY;4Chx+lR><_V<~x zi0a-=*YICCtYw|r!YiGhz1@wyuoha}m>#z`K2ixN*yib2l;#C4LpB5m(rQ3f^SlpZJFmUc$Leu{Ns(z0|n{bo65gkGq{^` zncw4(x$>Hnwa!x)EUJ2Qg_L*9?M7t96^Ryf zlIbE00!RGlZy*0gyAS$ztQ z<6-U+q-1h}0K)KHKBf?pO#k-!7}DXTVx{XdGpoTKbBiBrU@8ogV`qlnIl8u{SMZxI z3i~*T>b}O_=Yy_Q4}t zc~&!kKhnSd=Wv~VIi1}S%>y=C|*X-LOX z_|Y~V3sT!&VXefgDJiV9!~>TeLvpE#-#$}tQpLl8QxAH-N3vC*lwcG1c0l5P`zdvB z$0?WZyt-Y!+3cQ%O`U~ph=e~!X(ToM6LGik3z1(cm@_}>#T}WwsDfz$J=I!QEnO|t zGq)HiJVjyF%TF1jV!?xx6&s%~yv}77OX!{{`|e=6(Ucg+uiUkn4m0KcYRcV?yry(h zseB&0=Tiw0>-LgPgnlf@!>| zQCc{=ZI(YDk`Y3it9Um*vlX^cj%4X zf}>s&*^IU_sk;^xhTp4-R8vhd%-wjIDaCDcU|V<`eO6r@508JXXZd3mz;zEA%6*Gv zNn>k0l^`ti;bfOeijo*OJt@Y?ddO6Z2qh22#SHiv;nXt#Vrd$I;=)x_Fvbmtg6S-{ z3%WjrX&cTsovzccjB!3UO#z|kLF(#{L*yc?@TrU>WmAHUh=v4_YKH2Z*cSl?*1hNw z>;-9gpWtQ3&0H~_SP|;<0;H9VKy%VMxwTG!{A0y6{WdPWJmlf}s(7zGgOe&EQ0KyV z>r2VjG{x8&HU7CU@OI_=R^u?X)3&cz1Ccyzt}<{GjfX@&sXDitQnw?GeWnwL)#uEC zZzT8?+!DO>3f`$RDb)gqOcYQe*LKlEL@33e$T^2h^B&hAQ`(Czla(}7Ia!cj4R zb~z^iP#v{8riB|}5kcc-Hd-B%`N(inWH28uj6)43 za(H)WKG(#=A~1Uhl>zkkXyZIqAE7cNAW$yk&*!qy12&+xh)`aGISI2juAv$q zk$DtS4_YNsGZeU~Yc?mGocg+$TO1caWTbtcT*cpkFRq{^xmUE)e&q1sR6DEt8SN!V zK?Q}aLHN>w78JQW^m!va%yu8EPgS{DdEX~hlAek=L}9MrruXpMZFJ9aQqIye$-S=M zj&%HwwJET>E8wuti|U*+ZeX5hSQhk4X~f3N47ioG-g8$bs`cr2*2&CiiTJ2XH8xr3 z^N`ZXG=`%&^n@ZyR3t#PC3XVEemtPKaC(o%Kc>J)o*&s19U<}$qG$F|o6U%O;x9nX zM7|_FmSoqFOXKk^r_uqP0(3)N-EY0&y20TxB1M``>i&AJe7FAcL>9>K_FJp1r%S($QE`g@o$Dxak{H&=2C6Px` zgQmvFkhMai;pEJlO+%Y=rD7#myDSXP?&>K&;C0K>(z#;>pC0K8LIya^NEh<^nI;HT zzk|uvEi6fOVM2QGGUWHFGX?vo`UH&AS$`Flljbf9CuEZSv1^lY4psV4l+#rH+lPpMMUBBq|0U3{M@Xr3koJ6w!Y1yD8&Ewy#kYG$ z%g>VIs<;3k=Jrt!$&kcmNBL7Wx3=@gZNb3cSaUAM(i1lWIAvT$EC@QDWxzVl2s=_7 zHZH+4O0nWBwoMH`j(0L256bX;NmZ~=h+qiS4@J_O^x^z<7%BiC!x+|G9d0wO%%0qj zsp)KUX1?~wDG+gS9bb@glX<9)h-Qy`#p7G3CpBxV$)LC5FjQZ@SEe8DpeoAQ#7KXJ z51bVm_>Db!Mu_ugKf~7#`bip04^|TtWVfjWlsv1&HP02{m}`8KequG5{{aM%ki?8I z9HsjO<`|%rHr;GUBd|y)sR%P`lW6^8o|LPCD^m8(S`^De1>U83nA^RicxDqBoxn^k z5rb^bGg!S6V$l1s>g=QMd8rshAXc0)Vg@6?B4lKVpcNS%wRo6@+wmk>NFbzm7y$ZU zsFVzTAbIZH9sLXwEF-FH1M)~<`MLNg|$wf#xqJ`cqtBQHQDa{+%n&ac=Kl7V@r`Kuaes(U)jSU+pjB#m;VoxrMKx7 zqp~oivSA=S;Al=x(y;olPj@h8>oaglH@CIabA9E9V^$zELTjfq3X_TfN$#B{)tlhLj*Qf}u33z|)(n!<0Xb2*hap<2V<$^9MaA;_fLcwLrzwOJ$AM zF_TiDe~TGt-(mUU0ec0|i!g4n=ZLDUB;{kABLJiKeW^Ch{UmCabLnkB$&8fhYwJ0f zY*OvM<244L$b{H*Rtl+64{K$Vo}=RGFbj8G;+8Kh%d<8e*$Kbb66UH{5h*K~%oF-l z3e+$dOF5eS&1Tz7a*~~@IKQofI1j5T#I0itnj%Hb-oI_?qBBf?G=K1LlVL(4)iCwC*1n>t)(BlsxkVW4-xnJzDM0@o=#|kuA#l&lZ^k z?ZjQ0EPTwg)%g&Fj>+x~PsxM~pSWL)2$7fwbfr6im`h05l8SlHq6NT|pp~ z2&NQ7m(~fRESqrwM!kqpXMn zzKSueHAzUa*o>Lz?oR(0-0UUfWOb7);Y4d1Gp9z`Fz3P+rD=zy)$ad;TC@TN@oBDO zecnS#aTFddi{`Q7u62IK4RjaZBm1CGCybT&#P8+rY&mn(d=>P7U6u@)G}Pe|$R(UW zc}Isornq%O{+KyJDo4`Z{ap-o$&a96Mp)GK+h#53BKn_PB4=KL^aKp3jz0kP1ml9c zQk6(91fb|I?`1!7agi}UF70j@tuaT0>zEyhf*wq zE^91VaQ0=b{ST6RS=x6|M3uLP&0p&VgqqhS;iVlt^{k z3)#RoudM4#`#rhLuE^Wmo6Y`G-hGL(R*8xbdJ^TccR9DPRi~&!fLcw~8YS+}Vzw^H zc{L$+Ng0EG9G_-)aAJhL5PxK8b@9&UuK~8*C06LG*)gtp(Pm6ZGxGD-Qg}tW;Uh;S z&)zGGx9>~4jI);m7wRV#p8Tl3ZLEbYgDlTNdD`GYgp2KuSSkX$|_&hN(uEd$euWUeCn%8X{p?h zk>W4lOL-oP@%cV{wRCUbrB^Y&Ukb@0uEI6_w)5Zc?LDn&UV3SGp=Yn&)eE6tmf!<# zftU8k$h#>=A8d<|mAw(dT_(?`D-v+98NW;Mu3r6g)z(exmAK6|4A=kq@~Hg-0kyp& zdyh}GYpSd1tN!INwfHra^J1uVO)0M1QzG*ipLLoW)^|*wNi8~qr!H4mtuj|>K-B-^ z({mBZ>w-eKso)6g*s1m#cukGwRvS5+d(~Sg;Rn^J!kxC+zG~I|3#wO8$~kzSp>zB8 zV*T$|7w3OnIMl~~dZ+pY-O7}Lu(3Y&9Is^O^-5>>l8;;M9&>FUt>%Bs>VL9N!>;`R z7GBoG_t&w*OF zmdXz{-NXv$7GIjc)p5%npRM|${O0Zkqo~HGy&9etz8Z6E! zT~EynPd`3C_L)jZ5KGYdB{0&pd5qK5-kfVzigw5*6T5+yqVDoc`7ESe>7v9-$-BA^ zFAyHg@is5TDAa|%{~L31*Dn8o+Y#Ku%t4z2+4RAnFtYJB1GUY#l<@JORbO8B2kwuU z?kxHK10Q$CW@)T#J+y%3behRH?Sk?UY`bN;LGQ~wrhOLp6Ne%wo0}TFH&i^d$Nke6 zH&iYED%?0f>qq%uYqz)kngqwqKdZ`q9J7nL9kk&>4_9Mw7KtGe31^0ra5;&DcaNVs zl--mR6i51P%lg=~Z`(UL>~Cz-418KRJaoZXQ`O0C7xS_O9owA8akBTQo>IN-UtTIh z^e_FDAhr64&xhmp}5FMD`<$+)QQ8N0zL?LP$(v z@wT6+?N>S0lY71iuYa?^ySakXcN9#jl?Zqh9QFf9kekP{a`O+27HxY*nCiN(v*rp7 zzAo8~pKE#I^sThJ{e9P?)tjBZ6eYC3>HXOxx%X1n0ZLHJ2qK%?$Y^rq<;QWQano4t zqY&2*dwjQW$@4{(J^`pDI8tS0WjbTxosY~`Ve_h28mV1Ea2+_hbsD*fBjCh7rsZAU ztn)*yJ*zfZ%t9PliP{3&ahFb;Cr~A80LNHHIRW0n_HVRblkfiok*#(6yC+s3cO|T* zy0=wptyN)}X%=2zes}>@VL90QwjDcEDr55~Nu~U=cc38_F7kVgl{2P9Kd1NPuFjwF z_98(`=sUW-1~KGYr99Z?_Dgm*vUqH$mU2GKzBivOOB3xypndanY1N5R(Dfv2p?I>=$ ze64B!bB#k05C5o-%a{@Jz#a*GCdOp%IhD>p>^hW-qYz=c z!t?mN+y1W3c=zr?sf(FRJawf@QhW8I8qd${Gxb-~X_&c#?IM{JJ|_ODgEAIAU%E$3 z`MlRs{uH3cbZ%Pr%RL!s^Ht7lf9QE~tGk2aVioVH4IAOaMKe$moI1R@LYoRjq|~j@ zWQ{B-IseMIAcc8s%9bD5k^Ql$-KFK4x!j<*i&k-7o{mS|Q2+=5wz|%x!j(sNufQg# z3VSPK+UJI?GWir!_)LT#v7`_HK@=DSG2Drrc9}rAd<8ZE~O&KW%ROh$GsL5yaG(T>VEaMo+0I8|H%sU!uqN4n^ zXoExbkk6CQ1AbIci|K@fRaFb(c%G8dkIor^*xU4f1Bp)td1PS zHBO4%otyEAmNB*aBs`HLEtyh-UEM%j;F0*g{w;g|UPN;pu7_yKr!l!anTn1`i z#-pG*i_12?kFl)1(%FoRx%|Cm56PHNrl#s``mfH6W1vlVjX>^Nm1zJNErT!lc`QnC zgD4uKO4m!BWxJmNu&4ZNad#Hy6wBVMnid{<2b=Z^&l*~q5`@3KR@7?ePYTYrlgUVn z4Z4G0%Rw-1g%&%qDS zjh2!2?+JJ@?t7o1Pt}{i{#5XN$+0`74lST;&Xv7CgUkC69z_FR&8VRM!sJJjtTag< zYz6bO`VxniD+YIx6~Bw+seKtT^DRiw8f+Zl5RyAhb97nM-fakvsM1<;6AP!*HO=5% zf1mPXIua4J(xe}C5g-tbb2(`)7yQr05kgv;f`HCGLrZ9v$&OntLyVKOSQe)i&FinD#kDfX# zKDu?>kl3N=_vMfbE(bfWEkSGr{M#8Il})O+jOtmYir>re4x_6$Gm>Q!`DxnFVnQM~ zn9H^^0Amu>$BB$=3#N-d2;#e|CH+p`#Lfi>1M;a=1fmMQovx!8>v_paYXS za%BW`db54Z@t{qU>Ar-wAp?*TufM z2k^R9#anTgkvQrcZz^+~vqXX99=MCdUe)!}fX-xl-l6Q1w0yZjj%=$k2Wj&^+v@ao zOhjn<@wEse>&|BP*($7>^GJrWf+tBwpC+X$sd)Q1eJ`%g)8)V0sco{4O}SsDJwh{0 zK#JtH{6i2XGY^BvR;(KWrxzF+$j!Kb@+__4GH=T!Jz%Wxz}fpNGF#;DoFlArG$%Fl zN_N5|Dz!+^p|0t}DR=A{DU2mmicXK`#oc{nhx+T2%RvhrMKyZ^13+U}cD&d+jw43N ze$DDR`T6^;Nq9urSfi_i^pTWAA=@gALMGZz2!lLb*5;k2PbL;u1p(I^_c95(UBr&++uE<-XSCvq@NE4aYE%qZ$uFPiPWYmp1@=`W2S)d(o0=7T$Pd z2!gW8$YT*;Ui&Jjk|FhepNfh+iahSjdk@Sk&3d-dT6C781ww>$J zqd2}u!zRz>)5je6(aVBWgM%o7lxYw^cvhD=Bm|Hi&4AgJ+=03#Nx7PaXt#t(jpI>_ zXxT+stz=eqLA~Ni$}DV+Y%;+Euv{lUJUFWKLC85;=CP=Ar1%rKz0Nj?`bDjU*Lt1P z!MfVCkQ|7{;MXK@ES~-oP7n{YzEsBPk_h1siV*$~*2UL1@D;`LZ~v5k@s#wWMrkB@ z$$`<)8>!JDMLHjsQ!zdiU#ARI>($6~di>{DZU#@s@<;q6tZq~wc!(O6$&e|za{W#YL37rB7b)W>SZO#fTuMDRD|(&fxZqlYh}0rz8eLy?poII4wqXF_Uwu*O9zeB_K*E zp5kuPC(|K6_mK6J0m&K0GwfUTuEnkg`)e*E{nyY?S`%fh_*MHNOYu)255qUiqiL%1 z@BpZe|Lu9F0BGIs?2|1lg9X~D8pW|)4)GX=`tqn#C@K&)` zba$}r_1XIp1kn|fp|8h!a*Xdo(jg0mvm)ZYQOB$9=W_u7R?edZhK2u?WPj){0&ck9 z`WKS?4-de^>fiDn^MK%=|P5b}d zj7Zf)=9k0|IS~06`uXk>il27PH<*-thO+8y@~GRg&+Vi-K*pLBHmHm1455mEANTec zglehGOhRtDYwC@h#ZCAkoiUz48%u~5nv1H_{g(wvjkrhm3e&KElB9MPfVd!^IChLQLnJ-8d2$ircBH}dbi#@>yt zdu|Z7!0U0Hn;!oP5pMs5%R++Nqnw7B`5XqfrNZq)r=xU$zFBF4Hq@;iiwGU4(-@D$@zyt{!Z=FZSe-Ww0WOEpLH-AiVNz4DvdY*^-SbB| zD4f%T+8|2MAiAQ3OC0+eB?ya-+jk;*wQwr(<6xKd0@@=2Fen1O$$k>nKAAW61Plog zu&Kt)^>W5Jp+<2MDYQzT`RU)djG!RS4N@Z9ne&WoNxi?{H!yO40$9EcQ?mc_0v+5Z z%nZ8FqAEl~%}}=1B1nTAc)7+dHVYYmP`tQXZpI1W1govc0=xFC_g%HUz534D#%aXf zU=Yn>*iT{{efJpJbAhSm(VbvUsyZ%a1k_+c-q`~6A@P|A(1?Z+dpP@-b;rBdaB2kK zgkcjlz8NO{2ax4|2NgVK#EjGIkPmDsgaK^#@SNBGyu{kg1wQ!`g$GK&M_xDVUq3b9 zqYH|Vux#E%#wm2P(-^6oRkwNpc1L+XZFJPc6DbP}^Ci4R=YmFVp4C?3Q^Tb?Y$-gW z+BNpqGF}tEXGsHB0<4(p({zEMs`}V|B3L?_o%BaiHAi44OuxobhK0HjTu~QFO=rpC zf&D6Cws-}ua68dTwE3%DzP=~g-eQ3;lRzotCyOZuD*&7Xh6oq)N6N&LIxQ|>k8M?( z468FNvI7B5IVA!J(@a$kJe<+^8zTK2Z8dXrexy#Y(jogqTpPJu0H42kJLtlvX;Ng9oUXz4Jm?ZvY{qdzffI3d zoo|5-!l_mo8G z#&iqXIGQV-P7Z%@V-$?^C&vbZQjQH0S>uMqb#Bs_fl#>FrHJogh~MiXZVv-X=A@`O zi*a-uKaCY$-1nH}6!u>b9N}^u7vs9*VmqNmi3DL9crLS~tXY&)bo%i^uj%}7Fyezp zotK0DlO=1rmGgRe*eZ^3^fD7dkKO=-_Aq>0hm??Aq=d+?@K~V|W)alinVc|5VA)P_ zv_v$=jF84HTq2ak|A!Bp>+TrHC3nu0=WeyS=LLjHLWsPDvRwLqf&h|g{{k^vyzNu? zj`%Fkk{S|L@6B_w&d;{@Ok1YHR$a}aw3rhyHLT&7hR-=lYKz#iyAT@vHTYoPu(F{{ zLnAx5id5r61)(RxWa)}K=qEJp3EoMS`M3VzW!Lp92DYy6`DSmqK%llM?(HTPznt+K zS`5)NAzm#)dMrr_MnrK$3x_SC7j8Tab9uxV0pYl80)^pU2Iq#O^EEIMECcT(e4h#7 zZ}iK03!6{08-u70dW;IE&(l&=OKsK)s)n|xNxHA52#+D{8REO+EW+GwC!_k5{C~V2 zCo)7>2dhTb68JHX;igg8e1G|6g9<8hE1_kMkqVs0>vQ3bq?pZWS@fT)Ax`&u!h(ZX zPDD(RZUMlQv8X83d-&geIh1Y0W2f09tXEUYe;1$Sq#dc)2hqK`3(hcO6z>>77ltwT zDk2gy6DCu_W~NAVNz(2IWn1(8mQqEc#6OF1@uSXpvwo*IJ~*$O7{&-r8cMPPYt6vR@kBW_x%G4uTo+V|3 zMg_#?DjF6zTJEk36O1~DqF(mEz}P=isev@)6v2XtM5#&x>_nnAXj||eKk6U+A}Vz= zZhWGe?-2=)>-0B&mP$KpbQmnf#ay&tqX`u629dtRd^lzNgb=yh5mfi$3-)=NV87&1 zsrpe1i6tVL(daRT;zX>*s$KbyDXbG^DWjT&tFZmew+(I;YYcwtKIGx%Ebbn@!ypd} zH<4L5boyjAg=-L=Z~($PfUqb@{uq$YgI!X(DeK2j{0le*UG@falks&qI#@4JkXZpK zTZ+eqts>Qcv6S4GwZ?$2F~5FLoBon-pd8I#N=o3s3Yi zjhvo2fE#8W+{~FvP_=3z&SY2~>^0VM%if6}sf+=+$Dn5)oLf$SMRkX|g{HB)t za2XgQ+E^0_?HP&B&|vMI4UAJod`5Xo^=Wo0rw_%w%`7A!r{BYNm5PNXs`A94^17E< zhpd+1VMI61TdpMHzsqdk@}W5l%P@7bXyqB(jX#pCOQxs}Z-EC#q%+9t5yh_skv+Oo zYBrGEBP+kI^P9azgz9DB@@EuhKPfw1iwMEo_Vm$ID)WMc#)ILZO}minTZx7dPTme$VWRIfAs!6n98b0P*w4iViS zKLEL#v7f|Aazk}`>bc}md7JdeVRN{WzzVluhA?HbUuo{2!4;)%Q`02(C!Z3@_AACiW zyEvzOPg=sVyutd604S$}DVFOWd;XA5xH9MShuGnbhGi5}y8a=7$S?zA#0@p^>LYTd zls97>KHe*Z%oKUECZr|XklxpdmK(uyPM{0~z=R1G76dG~L8+Et$yToYR=kksuYlG$ zSUkCzUA6Z#%cy|bioI(|Swta*LLqsy7nR22)IF=AHLj*jjn50SSA4pWLdrWT*Ip?m| z2!o8U_qo^SIII5%Ie4;l#lg?Fh8B>=AKTL&2fWrv9^6Tw2+gdcnx5ONP7rR6&y+Ei zoyk2g1L6iCI#!~cC@K{WWH13<4}Bw-0NjYx+V3)9V>#AHFyan9QSuxqsQnfgndScj zoUsVT3T#QC!SYI1Mf~??RY89T6!<`cHKq?af6WC=+?bU)f1Bu?uDE?H^!z8vWSky+aC}O{XtIvEu zsEvwFsJ-P{0(0Zm7RaOawral_$j1!r@1EUp$ANCD+$<%xz zq!H!iU)fS}+FMZ181&TMT%f4j3-RFFDExqDbFg34ouo?)SOBVd{2$!8YyYC*0_PjJ zA?b5<|HZUXFEPf5(`AU66}Yj!2WVj?1wc#k#_fa+gkIz`DO6!Yz~3RfC?*ry^cka*O6NyP5~4kdrXd`V`Z|%}pLhw3BMkB%D{F52nbSI zuUcC+FllUM9Uhtuw3?qiQ7$k(SPn=H`7{v8HmA5D>&V74`67Z z(mjA1^s(uV#J^tGkCUc>7^ghiZyN}q`!d8eXUX?d>nW!N_*)>;d=Z|s1mt~@O`$4{7`l$SA+A(+O9&HsLH@nX8aAjbUSNLo5CeGAFC*WGbB zkxAmh4MbKDF95C*5TECUP7Kh%5+EQ0x&ikmF)8%DJn`xzd2t_8j%ba?P}HWlhSw~< zEM4Du%N6K0K0X3fcjg0715TdPB|3nfKtd7+RWJ=)pLqV*l?f9_IY!cv9g1OgDCucE zo7(FSm}=BT{l#0nUZV2^p$m>jCGub-11dm!ls6+ee$EI|`W+wY-AdYq43X2)>UJMj zGfLDBv0iIOtURhBA`%I7MUjV!v`{K*$JkJv+Oi#JSwOT&6dmdL@kuP0y<+p7Bfd=lmRTg2v#hrDFjT}-kZ3}EF&}6q# z0h=a9k_=^fpNxFqShD&iGI`)6%jBY$$h6SHGp7*gE-uVcm%p+|lrM(K=0gX3E_cV}m~*!43e z$~HwUgw>aNMgz-FOl(=qujjPqlHEPe?5+f{JRMlLH`<^OqSI@R&Yu!SJ1Aii z{=WxlSm4~)OMF$@l`p$!IghuCq=nU5>@Po!f!A^Gf%#0EZQhfgEl+(WdeNN))TvhN zexob;d$h&RZru3avIU8j?I^8JsT7g69)8eSa>pfjErpRWB4HxC3eZAzl_T6hyvbuG z{t|A9*GM}ATX0`P)vkpu2*Z! zWE$R&u`no_F8sq`ElD#e;SQ#mF8G~klMp~cSCiU|*`15TowuY!Kldy>Xs3CL;2!)i zh5_+oI+%{~RuP_5L;xF*n%fyRqAUPNdGje@ION&{EV2sQYHk)j{8aO)nRKq06-)SU z#r(h5M6xe)-qJxkJ>(Rd)3wk-0sv3x*@^B-A2bv73nL$i6UkaL$zAo-E-nd3c0bOkc+0OZ3lajNQ{`1*%yyhGfAc zT!y!tdG}P=|B;J-F)a#Pyp{>o2(8~P@d=*Xw(w()ons{S0reG=T93geYwi0%E?hMz zG1#Ylgr~F9sF!fA&tw2YmzDY&?8Q9NRnX_c^ZA(kVJ4h+5OXAh+U^g-9O+w6qLlQk1mKU(0mw!}tz-lvl*qp3L8hPTQXS>!vVM z16E@~G9uVkYeTsE$yz@wW#TH37|oRtFe>W0^gwlN$@vO5IQ9OA!#wRL-veM{*;)sz zRc}=fRJTRj9ejYHrB{~hHO8(jV+-7S^F0I3*b$UR1+OVKHT$qMgR2Z6ds;=g{Kg>O z{%F>+2>cL@cp;vxoi%TzDPA5EK4p7Qtr$!2;hX;=h>Zx^YZ~_R3n#b1dnFvJg8uCC zuE9G+Z9OHqH~jO^S!x~o4#TaOAoPF7@o_~vxczzc%%9e49xnRd5I6(ciB83QI1Qp{ zE1--8^Z2r|bibBT{1Y?54?IZ2F_DFRhzTq0cA8cHVicRoOTgTp-?|25A$L zqNkC;N5Vj+&O|+|8zbS@B7gIiAK+}q%(gVpSy(pHGrHegMl6pZ{TlQ(K^a_rqvv=z zZ;$Q48-UhsvJhBLrW(ai6hINjdbsjlJ)b}|{tm;~!9k{b3zjpuqM>$W$PgBAw#q2E zb*pH$4^raMzJ{%65oFTDhY z;1tt=ZRSSeHP&FjI0Fy>#}P(D9~uOy97)PbrtbO5{4mz9rHrHrF=Z1n+cc?r2SO54RJm>=c-0>wW3-#UW}@|!qLj$0 z{5M+saIf?9=(GO2V9AiMCoY#EZyb3l@OPS?iwWODxgD#0imXdQt#X+ufiHKX=4#~v zVJN*@wDn-M+GEl^)PW+UeO0229y`xu|hWsLB4Sed3Cvkb9+*1x-GL8raKNHUp?&R2JIpAEml{pW9bRs`w2 zGgO+o=GR2^cARL#F%0RgRQIY);hvM$ejAbU06I*wK=>E9PvvW#q^cSjxW-0;;enxX6_@_l*s&X^ zz#t##?n8uKhmKfNMI}zo_?<ePgBQ(4fxT6j=DN4A?HwVX^ zctau2`U3p>(py!BV%5q$ZT-{2-X_kIO`5&QlrzCg+G?7_(lc*aQ^!dy;KgK;h z5S^kmB=-;ZxEqgG`SSCuDQ-`cceaF9Zr>PD>7*%h^8D@PCw8o;x*dJ^#Djl+Y&ks_ zN0XQNjGeT-_VOxOKgTYeqj6WdeLQn>-MWLaYpX{DH<;dTb?+H?ny!DGMkTYrlC{k7 z2XEaz-3O-KIDMkPyEjh$LV*XVxlY@yYY(p}j(t^>G~e9y;n+mQUA0-IhFKOha+8xT zmn039?$OWyI9=UierjcwRBq35ahGY=4(b?X|L$KT#?EY8NGWVy9MDW+%C)IrL9okb z7iYW|kZK#e7pyWhz{nHh5L9D)@&4Z9HYUdCZi(k!$)9oFv?q>e6qkiTKZClfl9y*% zukcuJ*WVuH(j2}byDR;~qO$Gw&%B%KePYVnpV^!E8gE^ZRn!^o^3V4%22C?g3{9-M zr?_&Gaq*4M_!B68Q3Lz>r^yfYl}@18-|&wL_N9@j>+9LvFT2AOSCU~k7!xOx)0>qh zQCIiPSHV3}F5Iv_yvYBdm=tAEY-g7hbHg;3++kz7cf9pa4A$`^-s2w^C54xyKA!w8 zX-VmNjue*2+~SkgtBqP7u8P`SGyQslOPiKT|7PeDkQv^faVv=RYq3=32-3RStzKdr zQ((MTHkyV6!Fa~>E9ZEWu_P&J@oxxEdOl%teuPQ8-xV=n&YIC*72)_vO&&E?3M+cZ;z)QzbO=8aq(x1ur*Ez0CZT$k& z^fD@cR?K57=qop?XrE;9y!4x95N_eh=x#~!RWN-|{CT;<;#E6l2KogI=KUcxiOqg< zPO?n_Ph7BPzu&=z%}uw}CL<0~>klma`1?<7?CRs#W$2sEPxAb90a;YF$@bvybNqVQ z-eF1cM{TB2(!XsYFJQYGL}RM78I~sUVl#9$#Mmw!5P?1((W-E!AxC-r-6@CNt16eXCcnRdtnLec4WlE37InuB_LF4|vp- zmraNrD(S5g;_~@?EkYaCl}caIP<@8KX@4O-C?pkQdb8{7@mX^`Re%3cP`&<(!div# zdu5eslWwHXh#L$ExtP&gUutFhAS*x_0~lgkB9r2$b35O~ZE|crs@1;_iJWkSzd$ak z-(B_lNl*3ku9&Szr$3VH?X!>u;D+p|!ae?J)jb%_|*YWQFlZj(w|}7N)KyTPsmtu^`+i z*u8%9K6MYI+`=gQ+m$CuGFDO=F$6KFw(EIPEyTup8eXz~V*GXNMhBTMq$p|qPPIPz zF^?)K)fTw@(I5@j4?TJkCe$SN%GVvCk*T*Btp-3VBy3FAeqNCfzD(U<) z^594@I9?NvE?tpDz>(zFNHaciTPHj+RYFYibbZ-1^MnIGLxQHT`V^F4-s)dl>U{{Zg#l`F6JBS&NdNQAf2Ksj-V*klR)k0PZ?o{2LdSqZv^v(XcBUt1jOI%)lQgggUO z(qh=3igkUZ-3eUxg}bKW9l)%|F}U9p_g*V3?9#`vD!=9Y+_}j4RX59Aieq2l+CxSS zyo_cB!ZCB%YHd!$84iz;r~*1k?PU5=A^-bymmUzf7znz8rs^A8W>=H#+2NuPLQMed zd8%l4sRad%=&~(tN!~}=Q(-NP!pg!z6xU@S*x2ua808&WvL4MDs)Knww6p?% z05qRcOT+2a6& zPo$pgO^MVvWb?ZuBH`E(bj*&Q@M&-vrJ&%8jy1I~VT&_)ytXR0g5jpY4oe&bd_=pRtMpD#06ud&4W;<|mKsriqQ%B7SH^u@Ed+hp z6drlllA8Z<&wj+7^g^T`LJl|v*r45+*-U>&{B`U=KD{@>Bh}}PS7h7_V$F-XxSeDt z0^@N!&82PzX!K?K9JKg!`m!<<6p-!0g8I){rTN+s9t%_?Jhj71Ce%tu>Wfu21kV%X z-`}1KEJ1dGaUuGErIS^3a9N(52*^Sa;hWx#Jw+`H!?rxN71 z`uiCYOW~h}Mfx9tf-@-sA;T>;NzQr>;)7qn3FwF0rT=EVCQvX(}diNqS#?OnlQ}+Fx?);#T7XAqSbh4{ga*&hwo#y&c8)Px>j%fh|y0qG*?mH z1lvx}ZFzN=)3V?>QylM>0f*c`>wriqxaShuo~| zj}?-4zm^TU5oCwUk;Ez=^^zr0#KCV^8S*f zV3u{PNl$xY@yLvCuaVK#=j0ldYm2F&KTp`KQX%JcQIN4PSPF7t2PO58sH!cuR#KWN zf4{vE`Uj07b-*8cB4a@)WPOn$aJ7e&f#>)i>4(i=t4cMJcvBsCq`tS}yl zJz1sl?a1%gV6v-o=Jace@kHO4dh^s`GL>UF@HDyP<-=3oZ_TAahb74e0Hb*9dHVgu zF>FD(KG2TBC!6 zj8CuC`K2^+vdW{?Fkexv!DW%n9ROa_s&!5jfp&#VMco_|a z8$s07ZGf)Cz_ZI)bLKEqx}a)VU~DM>f42SK z)jEWjya$|gj`?w{{neez>v?~FNi=TH8IxUmZNNpOFt34-IS1`Z-NT(UcIN&CrlW6IZbZsagTiIbRfrmd^L9nax~7v7EL?RD!8735_#e$QZ~{WW44BXd`3+Ma^eA zlYf6?7^XzW2hCNOW$@@a-`#&B_V-<^fetKlsA9a53VU-ARvM*qea%>fS40!Vd|Q!? zx)&?;+lOO|mzs*+Um(~ldx%9+gb*M<`I{Gr8gpED zH&k&V3O)vyQ||#f%ig_Pcyumi1)^r$93j2|1%pXw_=u+&PQp826oRA8RQa3iuUp%{ z+|tWj@OygOMe|j|4?O=WYhV{tP1H-$7t$i@{H13uS!?rERR3v0b@9Io?CcIl=x?l* z@SDP_JOS>DJbb^C2HDW0c+z1Ue~nBfP#w(sTIdOf$3DqYZ$5Gd(^`9n`UVmLy5dK_ z-oYrEWUUvM2({07@dghVDbdez*-eC7hno&vch3C1=qNJzCd%ZNe@LoPmttw${b%*Q zag^j=UrSm@<(6QUHv|-m5OplQa~#>t>LbOU9YfMeB38}%fMVNDjg^dIA~(-#E7kG` zg5=tnNrW2_>{@^IH3RWl%FWkVYsh#sN10uZSmDGm$*&%*hfXVR!wI0deQ)k=?%nL2 zf?329p_wA9<4-X48+!5=@T*l5#|S*E!NH;8Wn%mTOBY?$0)HbFZ#8e?%G;yH$HwuT zg5;&cMpELM@r3A6jiSc3C6L*yP4)GqZCbBRmL+vpG%Wx5P*QpXc(Tj)TY4ZJ9*;c_ zs4pL=x=hF=-i0n}4L!0opSy}KEei=f2XQ)&pw}WLfpzJ=vf`~EO7clj7MgrCT}Ub* zOZRp$JtU*BZSkhHU)`{d0)F=%k459SZPs0D=lpOE>G`_C>B(=>Yp#V(XY@5c0D=&n z#c7{zR^xpW!^yuNZBYy$PFCB?T zs^91y2$U5Oiqh0U%=~atY3x>bg(5;I7>Gs5mmX#zMdcVSCkW=m9`y>?U$Wl^`{<(i~JN}{>O9%~3knS=hT=GJ1 zB3{|sm>f`e<8zxtM$xqix02mH4SA}jSN9t19tsrHfJG3#>7@F8ssm9(xe$5y^M|YCl6}!b2H8V zCCO{ig&OkFD>5T2)u)8q?B0lP%C$KAc3Wl)O~aQ4#JuYO*)7zRs};Rxda$Mopw}cE_gOhC=lX!hoD$ykp#SN_vFnG#01Fh!$>UP~I*NWhvKY zM9RAU=W!=IwCbA&3qyvEyJwtuW;x_Bs~GkNQ|vo7gAC&mZ4T-bI~)=jP}1GldS{Kz zYY!A%OG>QqHjyZhbJ)KlsHCufkQ|axUKP$|(n0d+{K=H-xBnBKjbCo&R_fx# zNj-JJ16^MG{>w$R=#2nVyEIT&B+s~Z{e;4JF2-dw-EdQVSgchyPL)}97x0KOXGVzBsK`5)3J z#kqfRf%lVRyq_g)rETFd9y^GFHXrvuSj%lu0uVs-=0}2v@g7Cds#~DYb{4*MaJKw2 zS^4Opv~K@`A09H7Po;6cXFTZW{J|T%<=PS{Va`8|T$_bxzdp|h(DtXQTO*%;p&JOD0=uQ+&Yt{29+ zFO%bTNGzH;^kFFV@9$4I_9$8A{Q{iV5pIIvu+MdHGjzrk%%@6O0N6AKDy|?4!*&>< zmGgHA11-~04ek+I1@xTEVCMw}JN2DWwSlE2*KrlQ{;bBH?#|0Uj?7(UpZchM>#^EG z`+JI1CH4^>-X252;GtA<)ndd^R8|85Gm$7EMX@Ck*{TSAkLt%fB3O0A{B ziy&p8nM-9PAAT*dli+*Bw)c&e-bJfr!FX#%m89fLQu7U&;;<^3);j`$TYo`XyKGoP zZLv_iJ0hzZ1n_`e_n71y-CFiXJh* ziS8NAsmA8}D^rUq8i?tT%6$ByV>46?1P07U$p;(c|2}`N`^D zG618$i}7m&f?ibH!7n<4f6VB;7B{x(CQ5!Qw>ch4^DSYv#M+arnN4=@{SO`V))W$9 ztYy}H&(nd{t$B|DcBLAw8$0+N^}Wb^45q3P?;!-l0ZhlPEG3gTrGE} zOJ9xBlD_sSXlJ&woA*-TTH|8;#>)r>Rd$^HNW`QabB<6=zdC`8&WgWqp7Hmo4-rGj zDD=GL(}&|+z()O|=_I^=oG6gQS0JB~WJXCvCO`gHMO=Cdpd0B*@g{ppu{cje3Dovb zk$P#TBK1Z~tN%@>R907dADJMXj|G(G9)$(sLJ_Edbfoi=Qg~Q3R}e~X(ks1u1Y zlyT@FD8t@7)|OdY8DmD_UqZz}v_v~_V+z)V5F$3U!=`KYEv11E`4#AQrT(+S$o%Vi zAL|1uM{O(owq~51$It_XHvE-PR26U5!EU+6>(Cx36&+|7%-R}JNlqR>G5*77DGOM$ zkaxWFJ`sm2ch#!jzFX0Ak=;}B#ytL3Vp|!=f={;?n)eX>jrm5NEh&~0CK&pRT|6<~ zdMM!-DJJ6jVtAa;59zhd9|ihwl@1Z^bMtj9MV&Q?clx1Q5V}<~a$fCBQ(Y%IKpuv8RX`H<>?z#B*ah+U8X6J<*;s zm1LH0StRit&x^{*FOI73$>lcG4HL(Og*S6et3BrA4FnF6>D&#&=pgVQ!LYQDo{u)tlO3st!vDru^)sg;y-d*m_A<4KkByI#Cga6uN=J{^nOwX_a|N= z#NSGYDRFVd9ReV{h|ftYcNT948d!w>W`*2-t2^6(_$rps#0@m$*@D0^GA(Ng?~?(9 z7O_1f&0epu-|z3TIF0zn3oi^mCu3dOYT4#2r3B|GVkS73KPX@X~^5WSq9-H2Y!>Mu^olbV<+v86pTX8|m!u1HvcK=tWp3}P&Q z%Mp}HgIlweX!}opBKi?RL&7b8gBs{svmb`&#c=6_gye+(7+dyj5?Z5`=>dUx*I{`zfgRPHN_a2V>i8G7U_-X=%-jqh7$;$8a``{1{-9kgiR z=YRd6z0RuzDF!Xlv7I&@r8l;Ov#HYlsbakE%(RwN)DELC243aLh>-;=>2^@T?xC;w zT%6u^*=(3U)1P$2*e>W02a+~|z0pV>>jT|MMDMCMGSVNjS*frym}Vr$)MYA)g|v3? zOV+QeykVD6S2I6Jd9GEt+d9~j@f62zR8RMI*nlL<`D{o!;qeCHEBTO7$9Q9+d|_7; z4pVF)&jXrXE!yo5>9$XChKXt%M=wv?B@{k=@r;f!)x$S_A(WsIO~MjsrR^dCqRo&dDn$YT0=z`~Zh^`fb$FhBIg|-@-QW4c21~Pbuws2c zpw0#~)Ux<9=%O#sgj0<^#bqQ5uF*OpcEhcV!RITo&S-RsJD0=)))<~yGZTG1UUNiJ z{MSCWb07a_X-wB?jp{IlHwvPSFC|Z;pP**8i^FfcN8Q3&y!xpzy5~m*-tnV)8i;{l zXstn)MA>f~RsIqBbx(#u2-cgn^>}}a!etZVuiyu3yqJ3LQ}f)K%K6-`!@U$FR641$ z>CWJ^8wf;+HJ)b6eQ!&29}pt${D_4PtJmkDr>8pUVOR>yuO0NsuobO@Bi@$FZvbyJ z^2GsYg+q)5F1Qq}`j|+eM&@iv8xQh!+#@ml(ekeSnB?;4e4ng7zS@3ZAz>B3Fos}s zF9J|JyCsJxsavuj3=~?$TP~XRfWht7Y910eL^5u`J;rO3T%X}h4X-`>yI?J4(cT@V z@fs_X?_Txt|3I*M@$@azCb&?a!F$AGY96Ca;#MO+TQ~>P^$G$&dmDxhN3x!_% za1U&vC@=ZQ%k*qnRn|hE7UCQ(__M$P%{=r0frTCTz5w4CGcm|sA^n|g-htYa@gdb4M+PcP!5|odgSZYh$j;3>r z|Gk;?UFn!+Mm%d+OiPUy+Z%9_3m|Dw|Ivf)WQ{uM6sw)Y5J9&whCa=@MPpRB;mu^R ze?hF8rJqzI>nqDNTB0{X|h<3)ZylA=vXiZ6?A;Amb0vfo(HP1ByHDGC1K9t z<)m+E3dZ>Z^b8fcaS;vAl)#yJXJLz`ZL($zb+uUOaZZo+*L-)>0al9CaMc$Gt(S&e z==?Fxk4=W2=fjj`JP7FxQjND-`y-@YS)V*KS-aWBT5l z{AY=sQjaF3(hd?M)f@td+eAhUSenrQ&t2)qS3zZ!Xp_Goz%1hgB6X5RYJIU7|DDxi zIZ>zVMl?n+o;Z#Yyki=dZbs$J4oj%IlU@E8gS>vo=&5tNk7!1R%twC!(|GYTwzx(R*qxp?(u`;{6e!&H*M;)!43eG zLx6KLR}pKbKT8uMGbA1bL>d*Hc*(M0WG}cCBmVsQN-(^oK+1Y~2op0Boksx9k^w_5 zUAt?0zscw2xhx#Y&crN8iVn7iDKTfsn)79Ut%&MB=vSdpFdXMmXp~|0XWqWugc(Y` zr9JHmZ`xF(ItsTM56x2fbT^l40m7c5Lno&|-_>y&U68~m8%OLH=?Pl4UGgnEm;6r5 z1o9{JNqyUD{r$1G=m2SEUTjmrMoLKr-gtc2{F{&b>pU0u@;aZhqWwX1X)((4smQWz~o9aBVy+9e*-YCMm6)e0~2KP%?b0>E~e@NNQ#?AP+}u zm{00~ITefdk|9kb{cs% zt6NGL2B=0DAT4PnuN7r&M;viXAGp^c6r`TNRu?Wa#!F*?f@GA8_Bx~|Vtk2Ccr#hX zmuUr7eBm#lG@Dyw1F}PG(ZqbeN`Fvbed46m&4!>EEb@h(`@Sq`Wk zU546tNb$<|Pfr0$`Ie$;FB)2)sB-32!!A;Ccf@(;;*`d{!WzBJb1~)i=G0=()Zm@s zWck@GpsV{&eM%v59tUMtx9n^A)t3dFWEfOdv3TJ_4r&$|oI;&r26hCR%xHJ(UB<)s zf@<&5|EAun$*-&PR=%5xtR&h>em>jz`Q(R{Zi-PJn2R|`#^l7M_z)Y6|0B{pY6s}| z08l047{qdOD49ZeHvL-js=~4^?s~hYWiVcEP)%kO%4~Bb0FkCAs=P8g zt}H9{rTq^61JQaYNE5k$W))HIZOfS*RZ#fvmQS9j4sjB)W$dYo$TmY zVFD#nh3d)-F!An^!A($C5M_!C^3XilA*A19MB}{}>vRd;_o(ensG{UQE~X3&nz)!v zr##=2IPWPQJm8#_-+i)ru#j{;(CMgpiHRvjNNC;#lw;k=>-q)8Wa5>Co@9$R`#nbq zqb1r8f>TGV6(4(cV^c%gI=R-{#Kr%lLsAZ} z3c5??vWQe8Rt-LA3%6E=c6c2^W=y0~ak5o^SL_X*r&SfSBqnIro8P7$K4WUdN&WKX z=0K{=ZtCE7rG!aU$Ie1t_27JR2dmNb&{Yl+zZA60^ z|5@T%S!0cU&e4}>opC$r>OP8Nrk9}mfyfE2G5=NRn&1?>QuX`hX|N0?9--G+cp_6b z{~Gno@}?B^x;|NovSHLL1_x;lV`hAl+bn<2b8u^Q&dfwsRXN?fU-*nn(_y*kjwa=x zhi+Erlfy=4X>DB-`Nrt%bBDa%WBQJX7(%#8s+gtLYv-+@Dgw7AYfLXT9damZ;Qo|S ze6KF+Voy?uW|CK{XhZ4&sUHTWz^jIy= z*8I~7?vgskj1RGy8AFZ|jR*Aop?Y+A@6#{_D%!OadosU#qVhzpGe!p+@72Ep*``by zTnUY!q{&O$@XNNqf~q+qsjg2b^B2FxaZ_4D`uwkDly~KiXnBWuAA0rqA4wmNq&xaV zQn^XFKWlhvgxkqT5`GfhD1hd8AYwgvZ;KrB!dRC;rqbJ}jN$eedkp1Ho0?vUU#&7l zvXkYNT$qIA#Vi%(>_N0iqL9{{%+mmR5`%we##!;4EJ~i-uEJ|(S<6K-$F^bkcI`vD zf}EEP4ur)0Z2rM>ONQetx@D5+q?15?oEiSCU~BddC%V(ly7-wirBd5T%q9FGX2P;6 z%e#PTGeZg{_x}77?b9@mzNIx)pli$|XgH=Nsy?Yu{59xWgpq+pZ%~Cb(?>h$G1W5m zyC0TV-??Q1Tkw-%u!Fk*CX3G`>#zjgDTBQ}gj2P=wS4xltvTI&oC6nRt*CQNhdu`1 zAB2Pew6Ob~V@z}Lxl_j6nYi-r(Uil`uY($jo%kS-M#kkkT|wC)nr!1$vVS;g+FTKg z(7(c)dYdiNb~tbQU-gOVdv2(08p_)5c%!u{A$yP6wCjniBH_WS28z!2ST9G7oj7X! z>!Ya)-bf!>5cF{T+VC>|*rwT6cyiXBmzwI6gYOrAJCq+#B)_aJ5qIhDs-2yVRsEPbcv;MNgIWxa}`B&f!7!qjuBabTrf*mna+T zuBl$u+36Mi&pXj%Krb)QxucR425=M2t`q(M9&!eQlU+f z5H&?5MGLw#mO*4%WGRo5(xOm$qV&)pA*IdIs=a96*Wde`bDzxT`+NTR%*>~I?>X=D ze!t$Y_v?M`xg!F}D#YEhtZz^}Z+sdYYiE;}-S)d1E{UO$sYWTBotJNo5+lwm=J)7~ zc-~DI$ut^1d^bVb%KH9iqm=HlOF7I-ulBxWXTBq5b!^V82Dg!i^%JalTjOpU=K19y zx!Vegt5uPDp!X(xTjnuM?hQM`5w=*rg6O<(~=R%?kA@}&Id z^`r=7b9s4a+LOTIAp@U=%s->A?@qp1bfo-z4zv22d0m6X@m}#f2t}Q6z3aF0hu(*4 zd2rzS#o<9!ne}6sQ*GAcEfzyo=iY99xesjOB1wW>C9>d430R$Z+1r$GTu0HTuju*C zo{XW&hUYsc@xye=`&9Hea!q+_l<#;hIuC2dUtqL+1dOU-9M>H&i8(#z6&M>y6$Yy@+c^VlHbBhNCVy+w?e)vk_*rp1vbA`t(%a3X3iOMxJv<7G| zlUt1eFB;t9&Z{!f!p+ngA+Byy;R7d{WoqQk?qokTQ`F?NKhm)B@69N2?W>(ngDz$q zb=;NEqk8GM_pXGo>MyUi9Gw`7{Ir)&i&T|vUIjP)%cH+FpnlBL6MuZb_VC5_Xqa4D zPb*4iQmVpjB@ctud6$>>*Bp`Uwlo;5cb?L1Dd>`k9ZGsS0IQ|_A>z{VjU5iMAU{qF zS5Us~dUR^&vqO3>&iQ^2Jn!?8);`s?%;wWR)l1;bqyEe5^1A*7Oq44eY}4n2jviBu zp+P&Re|+^kJ3^(yC-E*n?QQdM-?IeedKsNjUZdJ7sSqe8`|f=PcdOgEp!zYq$_u#| zNX>;dpo6`S2YYWw&ZoUTWs0$g`g(BUrY9|{?6_5dRezj`;-D?-^CNb}WUq4*dq2l) zAa6W3Gmshp90CqTS!mFKxK9a?<~o%6%`-YL!pzjek*2e=^l_2nU}cogL!O+?&)~aY zH~Sl&?qN#DI(LEZau4Wxx@hQ~s9U0m)Gc-OE*K(m>%If8-843h^c}g^d1S<`x}n%u z)%j@$_^EbIr$@-0HJSAQ`IcABA9lL+9&{6%0>I=ZlFO~B*VNM)X6Cg}eTY?yanf4U2K0ou_?pv7h_AbIsBwFP zk5O5AL3!Vce4mWlaupAL#Mxdi{f*!Wc-`Vdo69d5R*2k;MdueU^}JDA=n4NB|F|>g ziY1ryPCzl5S;wZyr+XlTt^}XS`X?}gU}HVT0^YL=tsHb&&zbK$ z4!N>7Z=_)sG~6Fsz6rplE8C!_VoIodG{-t}t6P42dQXHFZvhz4t1|+k4k6pU*6%(v z48vUhd2hzlgwO>$+x*gu<{?1>X_au*QZI|6uY^8 z*upXA+aVnC)2@+Q)-|+Nfi;6|;nv@5Cj@(w(>OhBi^-XFS3!BTU6QqPqGBl0F8Ee`&`m^; z+_gB}oO%`1BXIxt{5g;BKDj>)9v9yo(2%mT*V|wuHb*iTX*_u9ts|3;v-)$$>X(jp zrsL>}BW{Ik)={rVNIi@wN0w1f?#lou#P>8bJkc0TAL%{=V-rcsy52GjL8JXLK)mMz zJJkbT+8ZIR^dh>Bz7$daLwEUEL&Sf?f(jI*juff%(%*f@h>{K=6yKTOkmA!(T(_#f z@4ddCY!^Cj-{7eSH%-mjdB5?RUZOsM z0hn_Zec8z}O6TL>h6u3JXJP4p-_C*j%hwOwl5+Z^LczOL)p zLQ&8D!l&0&CP8I8S2a@5TSQ}NUYRP~c~ecDBFduYy<0}o;mz;u>@3|mFyhm>W<)1? zOs~1~(*Z#E+;OJ5>x`62aJII;5hZN^Q?bFPwxu>rhQY$%HssBD4F4dIq0w%tep{5A zj(yQ*p!`h0@%~pny$ch?W(Gtt|6i1D9s`=@GTh`wkxi2a@@vg=9sIF2#Ulng~ zm}6~KEo&hBv_E$OCUPec(u3~+#D+9*xQ>%!hfF&A!50LFhQEF5`4FH{oIk8#7`~NP zSahUiwJLW8PG|38w_%8kfN1&CI&_U)(m$2QZLaQ9UH$4rnoMl-I!`L^tbNt-raQii}ww0Ritig+Q+z>g*evwD&okVZx8lz$t5-IWQv_wp~s z+_)W;c?n~-<)1=aaAy+@)KFR52ulR5p>i-z6!p>NP~-bcqAru5023h5TD)W$w4c0L z7f$-zX+JeIS8w?tPVnol-70pW&s&uMO!bc!jq(*4%2z7nNqVr4ALZF`n=YLJ>|6;( zdg0BdAn6e{JO*`P8jP7cV}{pN@5OpIFd8`?7K|1xV!l}Oxg>M4iG z8~ps25zo{PlfTa4g(-BOT!KB?kcq>ykY5GgufI&Vxj>}vu~hZjWpnXG+#S@7+q`@+ zI#e!@($i`e+?HrB%$L$Apb>l=$6zwdc|Jas;rKxzeB-ddrfa{k0xTS zwJ(?CZDS30d#f7;bogyqPm?Nn8Q+Mc1<>liCq9*I6Lg}kU^8+ zRTLHT=B)((8)^5!ujq^0RxMO;IqLKu7YK3kkB6G<=!gi4PQ?}<)jO!UZbz6Ga zmS5o)w*VrGrU+^Vr`>1NaDn;S7icfYaCd;6>v5Q|A?6QFf?n<*(?<8G65v-@ z`tclw5;(wx?@4`T|1!nn-|pT7z&u#W;pxU`QP3N$WnNN}MW#F*jY|Y_0mIe??8!fK z6UUm%cuZU&)^`6kAEtJ$r{KDaoAb_zb(OVhlM*AVQ)8EX~)W z9{P{!9ue-4NzHGjb^*YBgSLCOtWo6#$GCzvLb935DzO8cYrBBiD;81$sLs*bhC60dvurBslj5_5*o1toI1geCofkkimYY@6?TZdx+vYxsH zDR*r+`&1)@7k{JRxB0RBc1#Bj!7@VS<|dZ(k~)$hI1(ug5R!c>n8($63si%oVZ=R5 z-FP-E5%0LZq=J(n09JAeW;aXO)wEehpLOpwcdEa9&pl!tc9yFfCj}xd@Bz-r18-!P_*wBM%Kq{Gq%7S6pq!HN&M47F&P=2WR2On%4aJe`YzA z7KatOVIopG5T{Ux+u{;OJ7FKj7xm5Vi`SVp%xOMfdrjIUAVzEFD+)35X}8!9_rDsW z2*x>14LtGYDqvGXrV)uy4kZ^>0S7fzbeeaO3&ZKE;!FfC+SycF06sd z{hB#Qd&9axpg zvIhRHds|WUf#EJssQ4n?$}kbdKTG;~AirFS-eFsD5cAsb@aeL$xsQwE(X+!{Aj?wvwxaoi$gg>AM$CoNUfZOmez=X5@9%K1BK(+Qs!7Fx zxJiTJd!}oAY1xL*-7|ssAAC$o4n}UyD@ZKJ>hdm4#Cxab+E@~T3>v}8W zph-Ging77MP^yr)HwTe|U{J1j-aTjHX}P;QvWry^FSJBe94@yCCi&n&k^DDo%_+o- zxmED=^M=fR*hz>gJTVjXUsmkHtbHm*qd$#)1k5(x;yd>FDg3L-!H(a%tqD;317blN zB7sQrcz!_BK9I0i*$yFh4#QUTq%#j}Qj5x-f++ndL`f(%^7h5FYZGU2kRlhhdePO4 z7dnehM`k<7g;cM8(AHV0N*2UF?^)BKA@Q>qw#K7`NQ*A!H+%yXc6ykUHSRsU&{fMo zJ@51PbXd&Qqw&DI13dt@6-8DfoL{GjiMnp?Bo9UCK7IH+qeCK$SDTxI#eCm|t^Hxk z2f?8cBd2v%#9xD0AD)yzCD2bS3xJJuCu#_nQjw1u=)sIOdO@+~J*UG9F(o6vibVvO zRJIfHhg?o%S|sW_ru5*NG>1_KPzt89BO?dBkG%1p+O6f!LVE^;arjxjO5TOC0_gIY zVErF4b^Oc9!FJ!_tfcY&e)&TK#5Q!B8Fb^1DJ>R9p0qLxK3C=i5##cVpqJkx$g1Ej z;5?#sz$*WdFHeAT*p0=p{6m+V*G-KPSyl^76%Tghe+R37talxbz-d54>;Kx{mm=CI z=&~o%WXx$y2_JbvzIT`B=+YbDVpnBSO)t<`^5>ANDEZw6%nc?W7%R5d#*@a7?44Ia z^yKAbU!Ja{5_WThUS>^M`?f~ON7xb)q}ikmZI1w&T0rgqeGBv6d7>&ie-u~7-UKwW z4DWx&5I;Wz<09`q`#i{?tWwEqyFlRxp=$V89t`yp6vx1F(3B->$Gm~WZT%i29-zoV zg4ho*%uhl`=TThppe>#(T$4s$p{X6ZKk&&)kSDWF;$^gWiVG*?zXdN*(pHvRqpMVQ z!L)>%?h&q|{)#&A^KZ7cB_L*OAQJbdwx{xd;^#~w+5}(ub~t?a>J%EH>f=e2F4;Kd z&3%ew&)K1(3NgY71da!NB(V7>QpF6<4fFx@el&}KeR`fwvCgNJHo7_&gV8qHaGDbt zh}jhjz~5=Ct3eq6OUf7c4J0Z!I+y=#$f$3;bmr+4;W7h8(3sga$E%4fbl678H+eW` z;=eqNo!9p5lFAw?4Ety$WP4<^duu*U3PE(#>z&tIS_G6oH*1g}srEoZGR zJLc2NG+3OSoZf|TR>Y|N^b4))>8?n{E_wvj?z3Atp}201n$e>G~$NEY!t zeo|L2`~2ld_#_pH{3?vg$Tr7al03&lxA+4`h3P!QlDo+O0dFTeETdXfwr-os8obl#ZM=~j7!W|IpG@R?1Zr*G~o zBJQuxk(G4@LJ64q$Q`ctW@Rx zca*Vj31g*$toVh!hRv}+?`2~_0>sTsv4S$zx8!Uw)@7Y+;&MxSACv5gTItb}S*vN) z;;|3J4$ZtLyr1eB*6=+FyWPa_!;-IlNEntykmE5aXT!UgW;GY$(jUH6E z%tHm|GtCk$pa&;Qae$Tu-E3Am z;kz<%3E+p!GBE$#nk>!&$O@V67q770Jn}RvdM>R)0?T}8Y3)Z54nGjumRLVVN&h@k z1T%Vs>X?z#$xL9Mc2q?QbuCE9QEWUjmbApU-*ICG{2|x!^eMopubwLM@GKV@c3x#R zK&p_(%jv&bsJFOR7P8Tl6zupr8Paz%x>-PtCfV;Sp__K*tj$3vMd|K{ci=)O)INpbId}xJ4j)w{gOcbs1@z!fj5<=T2s7f-A#v=%!csqy6yiu zSp<^73f+k*iot%sTv>Q@EE($PMU=SCckv%53!N6gUd!v|byBA}h^vzM9R9ScLP-z) zr(!Pg9%IM!O=tFVc!mV%b{qR-HQK>akGI8VW~s93~3_7=xl z?$pzhei+US>Tud^B7*{rg*eb6BXbCG!m#Zx{LegiGcQaVzC&dKCeG6sx8c!5nhui9 zC=0Sv`0eD22FwF^MNp@+rEIR{Jy8y_y3}+X2D+7RJwYf^$ID=S)p$zbC!lq5x3Hwd z2;+z`Q^RTw8C(JLY@@L2;}C1H2)6#Z!(s5U5hb5+BZkn{^CqkvLiMgyEL`2AOl2}TqD61sk_gP0PV zLEC9M``nm@RRm#*`I(es;3`-cgm{iuNR=Y-5BH`mj)LIKN61*2l)1D=KmD*LV~Wf490qHw)Vd1I4?n8A#BTnloPg-1{jUW-X;~d@;J@W^CT8x?^rJ zq2*v@P4;E^%K#CiX$k>0Zj^#0QG)7lmk7U%1k{{e?mCZvg>?(#>sn7W*U&13h1>=7 z3IE_(Xpr&xr~i+WFlL7&$7Rtz7zF+l%0K71(-M0=rl@1GtSR&zIJ~eOWBEz46$P)C zi!z`EaxtZ@7)gD(b))X`3x>?yHIVgoN*yiqZZ5!Z#_kHSoJ&{@ z?3f`yCT>-+7e^2`jyNx~0a#tZCn9~q5FHPdLKB_lH%r$JWmIrRblBw>0saDOqHn>0 z+#O*G$mXB|&ZXDr#w;kSfh!{JJd9|(bwutmc6=Cc`MN*YI%M-M7!UlITK3OaR}V9k zEH1_sA2(i?{}U8CnCC)wJO}=^LU*|x_7_$}PTm>^C-E?Puv-Hg4R=a84)2Hs_EzIl zW#Px$MzLY(D(7W{@=f>QD;Y`tWq!y5q=MZ+xUoIk4Iwk3 zNfEE`g1!h+4;Ehx*KM-7pdd8PXH15>D)*W5pOS?sxOV*#%oH}X9g!S|Y@XqQiDN(R z=A!?s#AlXYYB?P~)kGQ@ydnjwv5;76dSOEq_7KVvz>s&?nNacyNbkcB+nwAp%gAHw z8F)?NY*F{E0n%)t^AVh+sf3X8V}v6yanyNnbhkn3e4adXX#5NqB>BCg3LgyV`_qH+ zsPSL?0P4K&dVs;5#YVMhBl03|x_+7nI$PVn09CUH_n|wA0VbgC)czkppnQf~MN&oX z#L#fifKk$oo6bmuZlaA2qAB01f@fd_@-2odGa?Oxp{)j3II*8a;|jJd;Yx@(MfUpu z3#7spfO{avwRYD`#LZ_T2}&K-Wyz{+#uQh@QEYET4d|`3bVZf}pZ@)3OX>qLK~Qp6 zla8@Z*&fdMuN|wwAQgJl-fDEvNTO2A$RpL&mJfA}*KDuv3ct;!`*-|I2;~J*Zx-}& zAxGs1jZT>}P8{BH(m7IbEc^2OP2X||2U@p-)bwj1KD0lJ9=7ZNn?x`-2A=~T0mTFM z1aRZ=`Y{$PB}5vOKf~-V(1kU7EPoyFANXT`X5s%qr6Sy+MS=&E8q0-K3J4`8#+%lW zPfvWx7q{!b&n$A-@Em?nAW-arF`(eS7jA7CF8Asbg)0XP9!{9VRQm>;q&B0ae4Vb=SUbMFJbAJ*wxe|qkNm@>%Z zf*`AJKlMN&xTh0(1M@cS`Z->NHvz+_6Y>-2RecuV%DDMy}n z>AwKf`D&G>hzn>p%N?Qkf8j?35#{tXVJhM%HTvZm{G`*rRPMuf$Xb<}?&ZtF|0o;# zR3AGj{3x<5Kj#WtHeqjboJBd{blkLgqKH}2;T0SMBopTioz{sGNagcjKCC_RP$8~v zo9UN={4X_BL;Yy^=0&K>GM_y#)0`JyHoJ*%XWZB>h92KD@5D|cQ`awr>VU zu*?!VQ~ZB^2mj!y9nIh{-6T=M&CG91I6VL{UEI+FFj+D=5FqGP2OUL=*5ZmwT@prOym&M7jtLs&Mppf9u7 z-s-WR2Mf`A#d8@pg6l3-E5eg>w?%HweW;UwiMrW}W1eIKBQO`Di6HJQuT@x7=$)c7 zkU2!WEbAH^x{a!BgK{=mr47J6d7EQ9r%5H9hqry_Ts(r9wfV2HG?7cYSGK!?!bE;T zTpgJvh-?33&t)4}3#!@a=^ospm8+YhA&Pbs(?wEy+XY{tqS6iiGNk;4isQu79xM)3 zz@my^N;as8e?>0w?wAV$v)Nlv$STp?^;Al^+B!xTWu1}OQF+{~X7x&bf&*++1xu(k zd@XgxYowOxE6>cDjK>52nJZ3XKq7rNHV{(!ckQoM@BJpj9PePO%R0d@)ur72DgO%Z zLvKIT0?fK>@ZJPeO5@ulELsG4UNyW3>^I2j&Ixg9)~D@tH^Vh)anD}7Ngg`_P$I8SVgeR&3ttM!z1-X%i|{`n&rI8@^% zM11CMHQPnkRG)+e3|_iA+>jrp+5z9=jJPN5Yn>*adUsm=?O{?j+1$F}jcLF&cAzy_ zoj|SPg;@HtjKY&sadg@Z4p;NUV<}*l#aaAJ#Q)&t3i}u_XDoMy_Zi7h;a~OrFR$c> z-G>zGx>X7kR$1IP5x5e}$^Du5Jxcsqdvo7^Cpf7ZYVjxGCFcMm2#`T#zh!=zUrFu+ zw_y1=4s-9&GEDz+$B;Swb?BKgs*aFT1_57=hx?dyldKatvDa62)!$=yx#<({NsCXZdP)i^eKW{0Pawx`#iNz z7PVKt{?+A(lg){yO{%ZR!Ud6q3!yP38$&OgH`)H=9z0Lp9D>d8Pf9$BoUBz^gYI4Q zEhSjlkHf;@DvX)D)m)d|~fwJo;%!qDIV7#T<@iy~7Wag1On#qxV;& zR!Ae}QIj;3hANK4?_hU?F>cR{4%e<^T2>($qT63pib&;Jiw=C0hBZ=e+`SyAOH9vo zy(5gaIr_X#-Mc0r88$OJmEv(^z*dMP4$72630iv_Te3x(I`%PrW|>_@$-vsTQ7OCb^vSaJmP= zjh(1xIOO#MwHO7+NU!fcrloglGvwjQ1G4enKs;kcaE8#xmpVF=^jspT(F*u^R6o zeCnr8nFhAGj$_-tprIZtE%>k`1I|eb#p@`(`YngIlzQzQn$r!Uu>tIZFkM31t``$8 z>RpfRog30n{qNZsiCb>$6mvDj;mKUAC;1WvswZ?^*!HXQ2xFZqY(~lr$o~ z_EaP=rG&)dpgY#|kv6S+cJ-6KH{z$bK3S?W9S}I~N*VZmz~bj^f-2C9(Q;q~Fkf@wA~?hS_&>VpY*=l=YE7KU!7 zs<@Y3Ntz(&2yuGtTDMN<@7OdDe`eGOWN_DXihqFx5S+UXpaW-_{Z5>JTs|9yi7HOv zklS@urOJfno;`nLMy5cp-u3vjtRVUMK&9`w#ET<0$i85x=bh)s<4U&ce#;5?zUyb| zrI1XBTl^fAkq%pm$K|ON0SrD}NC>RrE`7Z9-=6B(c-&!OJSZS#T9&)b>PM*V+4C01_yZxd7!vIuCn_m*5lVI2I_4{l+ zQI(acc;kvoB&N^6gCxqgL#(o)3V+RyxTpU~NtOjWK>SMY5^zNV-^@g^C@ztNZ`Pm1 zj*6e4N84S~ty;z5i|Jh$Mz}>K)j(359&64%)Lm-|WHe;{(Tk#`+L$uJV=+?0wnK_w z_H>{03O;~aJ24_R)?hK8*c>eB22uc8xu_SlG0#^7OVF1#Z zhrh$oz&io3&6d$57dZ-ne#ux zk5gX`se?Y3J0DN}d3r&iCkD+10gk&V@}FQ)|`;!Zv8x1Due1TlE_85qdSU^REWufZ*|)@9pM8_#q13x z*(5Vmpf*8Y)bkpqc<@BIs-;rGA>$E%L*0${T^JQn2iDwX-&D6G_gIG)*v;+i9^m50 z=4U#>=TO9;&)N6YtrG5$##+TgS7-uP=G1nnhYolD3JxQ2{Jf~^B4SR(4$l;&ttL$_Wr-$FfRV-)~1groJm2}HE=xb_`D0)m8JzSj|LI40Ufm;T`+K=mbh2exR zsv>`2KtbGW!y?cMk>2wK7|9axMl$kqx_iJVMI0V&`l5edqs({D?18kf$oz8am3FXY z=v^WAUL$U|*ZULNi=c1;_(F+@y?(8`JKY?b8}dl9CkPN!U-7hj8_lf4tE5?TOhTLyPhq51}?J(r5Q z7J91Bft;z)OVx0xoZ>pVh8qs_sf(MvcXI_%-R2Y}f(0`texo9e7Q-A2c6+u}iDmS8f*epUXNHHO^0rUD{s^OvryZN5RLx3b$oX-)K zH*RZ5y@mWKbF`R9u2v7d?t&w%7vRM*Vz%O&=bNyg0r;yHR{~pf_?vfxMqz3XHY+3v z8e<-B&b?cr$Q9G&$F!TYp>CEH8H~-(7*wD8El(TN1q5_FFjZXC%$(HOZYv2Kf|)1S z!3VgB{zWIio0Pq)Y{nQE#STzxz**xl(xcv4I+h}v4!(tiiPVMpn%VBM8PS&{-=I#(GRGXEb(8W= z1FAjb{zQsZ0X|~k+LplWnEk?*iaP-T;J7v?{_A=Uzy8qxn{c(j!!tXzzx;o*Nb6Uf&wb>6hX7iuC#W09#l5cz7>*>W3`@?nEw{I5l#$i zTP}l}t4IT6@V*F*bDA1w)8~)N&KmN!ELy%E(+yF0FXqOWtWUH;Iy)UMoAR^?K8GrF z7yekCBzRu+oMQR5vXTBh@UYnGeora?f3B;a*w;k~AxacsXBD@^DEsu6gS)WHgt;oH z{8)upN<+)x9Sr+_s+8b4B+sBkE{lhFTK18^B%jE){jk2NtAe;_?~FYsxLbu)n$0Rw z*4W%-HA1+q{;Y!dI2;35EfC|z2I7qbsNBTw686x8Lkh@0fFeBGu-H&DUJXAGT8FOk zfp=f7Y54Xo7C25(gEW3o`RQGvN9z6S51DOJ`4OQV;ewKr2Umg@4DSD?v{LnGCG|_o zIVmSJ83U5TAtG3a+5&1GhIn;){0&^SefD~?j=J!eEf_9(zqxfjfA75LOm zm9__=2EW?vzomVLI5PV$s9M_X=~21^Pfc)0fPscK9k$1sl5 zCXTsg5-0yb6t!>0ev6oyWGw|B_$olE;pcG{qC;lyw@n_(*26boL6bN;_oX2;sD!EhKfbmLtokjzu1h_Aq$Vn{ zN4)+Y{O}ss5hO@%eA5AZ{tsdNS&WtrFvu)xC^cNC_N?q-z3$r(=kaZ#D_O;2;SDKn zjg!xZSuUW&egn}D@%-#JrYaus)c_gu(S7fU-DOi+R^u`a-vBs>|56=ukxl0n){aHJ zEwu6!{6dfbUkfFy_h#RPz!@+kW_gRcnO3izDmY+*TN*G_qVO;pRL)Ht^B=FvDtlQK z(sqI!ZFM^5akRn5brggx31`clj*DX~n&QHXgDmLDpz51iE}zGh5`^88FCYD;1*C$2 z4{H4M7Rg*wjO?5W%MP#F+V#B6R2&u9xz`Lxp*Z>;y!s{@>8NKnkh26LVxW4S->Y(s zp=GS$BQrF(#i_UCvPvmJZ=^Z>=sxZwWQLC$!w;9K`!mr+tLm(a_izuiYkmc2QXg7Y z4rmR97rDa$ed}R6J;4rYjXkV2-VZiQqS#Ebp8?+p_CP{*Sew54z}*&4J+I54@dWpW zp;&=HH&={WVgaS36XdZ;#4Z%}>6-b>PH;A_D9Hykr!Og9r|f6o-^K>*_%q<5QiDbQ1!l|SYV#pVLG zf>nHQ-`n5o#pe%YEmc0tHx%}|{MjO&xc*$%rVM%X>Cm2;xkK=ktDWU7#H5=1rYc=2Ir*GPZPxUP6$OFfJ(S-{g|8&~^IH&3aitR+W$6fQddEdI4 z8uc--ol0Mhc3O6>oywgAyWh?m2f}2TG!iDr=5HaX;jb#=joneX!WAdjPacCqD|U{s zb6-&|%J?+O3}NkG3prKaSY!YpS-UW9% z8`BqBFu}_kbv0lRr&dWyZ&X!n7}7A9SDno1eZ+cLLpAJ-s^`%*PKCs2ZH`UXXei6o zorVTna|{qs`AgT81Fsf8>>Msu&QA+lJp36Q{qnN;_eL2!|7u4x-^Wl#+H4&nPo_;;ziY!KT1Lh^^%0M;w zwkROVFQc}heidjlF>S?TEB29Oi&DN~8|Pd`NSEkJh<#|HHmx|0`ex)K?RPIe09RY( zlL+kL`|6z!<&+~~98R5!IQ&poKC21%L4i&>4BS_wDn@AMf;$jxbRYAE{LWJqhmLB zZSd2JHgp2m(Q1QYj_m7$Gjfw~mkE~-pd(X)HIeEhKtUw+3zeGh0iy%;gg0}sl27?Z zN2|j}GaQzgi40lya+B0+v9t=YMW#Wh~W`PX}fT zo%Q^p0E0#CmQYw8^XNM?BVnvy=m4h4sAN5rP@)O9LcQy0CwR;#Sylfl$_3s+2)MIa z9O@)u+blNEyrU53E0*u>(}+82){208X&89*FLs-O5O zD-FsK8bH#Y*g2q0@v5K*BByxJ6=8+&Ti{uFv~49E0V6-mu^8tApH<`=0wjUYh-c)q z_zavsU&&c`AJ_L#6(l_x*tP*qJEmHVH_Y6{};==%um(xK|4Cw_LsXH^flIc7T;Fo6CHd)^G31^Uqu{fa6L8 ze=KvGJ;CNG>t#9TA%5D20A=X87XVY+Lr9m|o$mTTR{^Rz%SOJtx$fR5rlJL2eq0<` z?y-J9SSzg#2Y|u$;0`0D@UbYC?ooiPcKbzbcCjMw)j35v`7f({y@ygD(G0g08-ToZ zfd3P7=A*~pF*gxGJlSl*ivUw&Jttvm!5F5Hylf3aDIh-O#`em%`yPeqrx*KRF%e+* zMaTs1>BWazbS8^UyhVXCORyZa^k>`^6*P`!I(2d^-td!w3E4PDzuGzSZCwnS2D8$} zd>>qj^uoE~Bu~ITzxF43C~j2c5j^KJa)>dQduHJ7?3U$vy^yNsdx>WiK>Xq1Lz}Q_ zz0LF+(y$mN=2)EN(p@NcF)IF!bTOc$k-HiXzQd{3J&;m&Czktx1y@XIC9JZSkVM$GYbWynlW(p!IMi;H`4pO`&--V#uk!(IQI)p z9lmtQKp`bfPT}Q1lcIN*x`0E@Io2~Y^0+I*ySuifVJN|1exew225P#kn4@r99ASZR zMKFQqi=`e|Oz55C{QVz;XwuCwjTH39frtB_Yd`#lZ~p7Q7ffShdz&lH0NyM;IAdnw z>>OX%;6R~Wh4w;zIA^leT1g~;)R5BOgMs2w#<1tJMMxW0D?qmmsdsNCw{T6LgyN`f zLSEB@m4rdLnNSKnF`8sJXC=kngay-#Hjd3IC&-GyW@#$fu%H)h-l+)hw`_}vg?b0} zUkdZF%`J9SL=G(T^s36(vr&w<0}vJZjxMS&r9GQ$rn8s{+pAy^2t5MF0C*WYnL2tP`DH-x?_HbYx5s4XRzBOUIXviv?{gl zozHT(m9o#!L)A!;q%7*Bp#T-KFZ!h6yNRnyIKor}Z!FF&$~bmt3epeE(1N}@_*K61 z@WTU4q~{oBm0WDYgkSuTvK5`<91+4y|iF=DxOIf>~^`U(7dDXJ&>p* za0{Gtu^sQebP69M8zB_6Pz6<}(2tpHApO*MDt4ko+mR}t;TE-#5ztgWHvyOfJ8J%h zdv%=p*>%f7Zz#NS4_@Ch9hCust?W5H#cD-caO(zMJpW~f`=A}HM=7ON&2bd;O=((& zZW7s6H;v6aSPLYLJ6@Ik-AT~k8LUe1F;pJu_*6IY1#spGEbukjKnN`W&BVxN*9|y5 z$8`~fUon>M_-}dKf45R_#cDrCy6U+DQ1lMyN_LJ6w~VZV?>NGZ-oTAOhj6i@#lbF? zHxUsIjxm5Og~UCW?XFcb^f%%bC*);;o9ljiGx$|*iFc7n(+n=~+ zs6d3uZLKDuwWAG|#FeBT#sT2lB6S>}JavU^li)=F77|$j_CChk7vIN0BjkBZW;?-_^p^M!HO z-v+N!=$rm!SNQmghm4kAH+tH)tReg2;#=cLr)-cax3 zb~zUgC_Nt7ZaU;OG`8hJDaI2eGuJMa(QW)6<6<@*V#>$ZUUe@4d zVee1MeVJDzjbLv9ulwB7Kd^C(r)=<@0)N`$>W<$#)(#no6-B0IQW|((=SIoT+dyCP z*T>@3jY_}cu2;`)6{V0n;Tc4A*cQ=Y^y7KNgaQPa455G#v zh^h5ohrfn>IMzFX?I0QUNNc9NCcUtJOtgMVuOL%KP0A9=1#aQI?|9!O|4L=UX1Oa7 z0O7X=jTBL|p51lCjEr8HJp2fTFHms#CTOQR%Kh{v6hSd>1hY_U z3;WF0C;RV%aRfJ9d|~#;B)`3?1;}6X_eQ)q+&*#t{eTD0M+!Jhquj`0EVP=?y)KYr zmQD==3eTM}d5G{hRAG?xK;J#(CN!fgeKJ)jn&7qVh@@>ppduC_bD-kio*ZpryTRy_<-MwQky3L+0gd zv&Fb@07K-L!ud|Hr=+q0T0^-mkUyBY@>g{Jw8C3%m{BRwMZBhKFP!mq+P4@kGgd79 zUK%J93b5<_L=T_sCYLjI9^wWG2g}yQjDh`L*h^zl#!rJ6V^&iqEWEGn@iV*!rviJd z{%kjpM7&09&Jh~FT3Y(c@CF5^=`HcFN47?n;RL``CtQW8a>s(T0(;D0WX%UWyaI7B zh}tLa-V5&)po;T@vqaFaux*c2F2JXk-~}1yzSpo5jB9$Yck$BSLfO z{tJYB{Cmv6q9HS0oTHEZv#+fOLPL6?p20e7I`FKY#@1)TG#J9oG0Zg_$@h-owDvROz ze=X$j*gO(xyi}eG!W+&QjkyULFhb@h_Sphu+w-A#!mfBV{63UT1@l1ZR`Taw&qk*f z@I(Ezx@jBn_@;3)r@a(MF*)02p$htiYxqn4jFf(Xt?pU;BH6=~qz?V$2dBUx#8at5 z%x{VJ+9!hMItOCW?cgIl=AUE1K zMxa7+H>LnaM=f|8SiGD+meF&0B3E*;ZhJ@^U3)Ov>LcF8Pi%PIFKaf*h5DR>^L=wh zG~kdNmy6?3uNt^DLLt*MVxV>V%lj0~ZQr|K?)bmSZ&>;s%%b#-glveYr}HA>OsAs= zt6asZLKQ{?cG&n?{n}gYZcq(?CnP10(O3|F(vP=;XH9Ztz5<{Z?hS@k&kLRa*Tk0uJ)uiv%}q&pU1N#b-?hP{_Iy`y~DP zgXaS~dBebe9%4N^+P4z#qCmz%rLh+dZfJF^)s(?& z?^t+IRwAM!0G~2T9TyIWOB%EVJ=Eq`Jm`uM8N^kbU_=@ZKM{lyyq$ov85D*N7%>W- z^D#PBB?+yyWYoxi^NEq)!kNf$lR%mtPmL}UyaD!o>ZZS5zd2`^4I?2dG@F`w)^4Gx zZKNivk;Jugk=_V_0U_NU{;ee*GQqqHK2I=WIXF+iXaU0S<1bI&QZ&6^m}t1Vxc1V!K##^=Lat2 z7{NBx%P?Ou3u8xO`LYg?cLxa=kq{xF4uv$vlA}~Q+?Rdc5k*VGi9<_kT7QMNA?LKj z_io2ACY?yvemBJCV*rO7RLjjP#~axAGIi}7hMuGff7pto>;#=j#Yd>kgM-C1wwO(I z7<(-h4;^!2#>X3#33c+2lSW$V_57CSMtwChSuE;ng>aA<3=I+2^6xtW5Xig z*x}h{a^M8(okH@E0l5emm~2V6XnvtJoKgUHU_L|2vjharK_Srufo%+Fj%T(ncsw4J zoF?phUrgGg(^AO2vUU+aoiHpoaSdl;d5?H~X(|6acwtCrwuv?5kT;jGOgsxh+)G^g?Z!N^@sBT) z5rCL4EFTou5jH`tzt`PQdk7|Fl*Y_z-aoPf99LE7xVo`pgyTnOx!QL865u?t9J%%s zMj=u@w3j(FIvD`L58+d=xnWPE$m!j-nW%!Eu>*^}4PN#-T+%O^A0!is4XFil=RHtb z<_)h7XdaB{n54W;L~AQe*G_vqyl|WrU@Zv$md6780?feq9{r=Px^9(^n#>R`s{dX} zm;G)dh;TP0d|EUR> z<6S;UX0Z@>W-mki@sC`RCI6RpY^DgZ^mg-Qxv z`D)K!lQ&Ip!#)9yMF7$8n$~()M4|6KJbD(T)Fp~-yeV3Wug!qvz(@_oJ4c=8B8yZh zizzR1pyv{TdR&m*4(S^ZNb?x5g<;r03v^HYyS@>j%OQLKiTTUZpqW*>67tc;v8Z6R zZW^Xm4#GXiJ6ZqDb`)Q1Bl}q%=rs&t9633EBqG>UPXh}qeB=qg5;USHj4o*2yUpn z#i0*tm)hI9T#pkn1FmH8YWtzZ;}Ks1;t2naZYI$C*~MOTU&Ir9`W^hH!|L6wcLw^i z5pc~W_?*mpQ+Ol$!kg9YT12-DT?LX104Y$UEDFvo(v*+iKOLuc?4g=lp}xZS zuZ8aH^YE&!T5EHMO$;fuR=*$@c*6kxEJRo1A%|!E08CmEY_}sMR0v^j3qIr=YH>WV z`UyJ;xETEU1nw}rX?joY&bogs|E)N8_P>M3%6A6$75J}H!=X=*pn0=24cK4rKie|M}!f@5sXu86hV z2E|iyB@%yYmRG*y>*lsl(H}Yb7ko*p2wh`e0$j}cwj^a885{)7$9*VcHy@^hSHWbGVkxJ=;~U z;=|zv1=eNTH@&&!cSoe7@>|6LNGo=bC>-_E&3Tn&G&!fK2E2*js!c6bN%&_7CyFP`(x!Rx&^v5o zoMtMlF|~k%2p0MKymSkcb-D#Kf%CPlSaML0kj`!grVoLkCUuB=q2^UNq+>wY(7oqF z37-|Unf0;;!$j?a7Z*o7MN#rrDUBVqKf%X=gvOjljXA*xBr==%fN6JNPyi!v7k#g8 zJz6te)tn`|a(4fA%(#x!u24f2(@AVi-0jg=Wr?C^DA#9x_j|K>?wKoj(=@LHAI5JU zz%AmjtE6K_6>{~VZwe;%4$dC%g=_Lrh1Q9zlk$^EndI=|meg5Ve8a^lAF|t%y4znt zw!AtmOewPc^VFr9-Ym+3LFXTY4;Iq%C*Y5RWlAC0$ehtrS*^BxEl@)IN*Q=R&`U~} z-oAZt1`1w0fC>7CpfIRj_@8NjngE2;J4+U*F&NXGdxP>)NO`N^k@F>P+5v&6Ee>0XX?%z-_%^xh<^7Ivw&y z$ecY3xq(90xB=Uy**9CkxG-s$M-WI*ZX$2zkwjop2>F7#_EnZG-bzbpOqD><_?%S1 zAHrm92o4LSNj6buYB5@1YvGc87=vBpGiXaNS0DF#_=Ee`Pq2~dGvFX&^UO(CEZ{T8 z^yU4DviTeye5ONC>d}45f))tikJ2pE9%3n93{6Ka8aWDZDTTC&Rpm~^irLhPP7)_A zR{7+AkAMM~yu@=L%E&(8dv{1dX(N^Vpyc5cWV`{ZSa0x)gT~~Ot#AQ9AT0$X;^c+% z*Zhg&pL6H{(MjO{x(zrRtUTcN)7zVDXcV%@`37r~_;pyJUqPMeazx6L8yB8&RY*M# zIeo9W1dcpCr@MR=uvZ+@V1xi7TyTVLO?Y_8R`8{G^-DR7W!s=c8?Xkbp24jZFnD9V zN=)!}$I;$3L@l&2OfHs$ywFwMJ{Q^0Q>1`V*wNeroBRK5ax&P&iYSb(NoUH3_9|Qg zOcdHQ9e;W@TEor{G3$gB1iBU{p3F(m*b3Arhe=Hl;&<&vC?P(_>VR)eUtOoV{N$MD zY+cg4m*S;1rJ~P!d62;<7yeG8ycwdrUmnzM!A9V68@5%Z%+|b_BUQEPknro3zzjp~0TWI(2S`TGZ%|HVL8M$%K|X30Ae0xq+ZHS@ z+2ncbYIY@vR{mQokK+z1tcqkxcjKn)pIVGh^trcH!hh3z#Zn3HLx0L3P5LtExT?K6 zs9?JpoR+{lOi-D-{_c|@ZOrsAV#vjSp8MT>-p8+{!o2{>3q(7bzGfP1@jPIocowui zNjOy^d6B;Rqj4yNo=^$Ac>`gP$$LzGj;YFfSWIeDZ7dpnexnSf!wM=Iy}jRkdY1v> zJ!MN=F>D*`Qt8!1kXH0vXso!(M@t4&S2CR1>n+FE18;-(FW%M<{` z%T{L!A)u0q#-$G~fDQUL7EDvI6fQF8E+~M$KBY{b z)uh7-+>8s$n}R{vMW^eIO184n|Bg*#C$2;2>*;G7$VzDWTzprXS_ZlWSxL6%PWP@v_f6>ef&K7jw+gwPT6 z6%=~2Qt+5j#JwL+E2dcB^zw-Ll z$w7m{4jsa8p&K8WA0>kB&?D$>;Y>!eJcnQ*L6$SI7_=cN!FXF}L4$LM_DPAm zi20MaF?aU)MwB~&xVQrpOkg26?W*?zG@*pVb6i1b*(R#(2f4OYD&I|e3LF$q49tcX zADTEXThx$^15K0;lsJtCSODU*opvDJ+wYYCkSHd+XAEc<$ES!4p4vGwkOA~yi#J#v z8T(}BXX5Y()OAA54i$_rfv(5?qK+_kzyX*~J1My)QoC8~)&`pPR5d^vj1W zStE@$()T10nuSZ>a$K8=w2p-?2(6hRVO@@VCZRlAbnp~z7n1O}-F@0_5#mi7d>D5%7jn&*kx9-ARzBvU6UgwU%@) z&>(gnQ?khwvHJJZpNP8nMPz#mK5oZ9OksNfaYt;1dR|aHmhnbI*U{9{Txcl}mI4hx zhAWjFEoCRUK(53|?k!WFWE^OrMew8$2%*NNtdQ@wQ6Umjty zEIL;n$^wB5)A}{vz1{)^8usvHZJD7L%2~Yoke!D@fK7TJ@+@&>#T%!vNtnNmOBQtg z9oqutM|uIh%a{ zPfd}5*LOVWz+siw|3)oLMq3J_5gD_=dtz<5y@oIh!gANz#U}?pCiIe*e0e04TwA%b zTh`nh3S?=8Fm@ejjXUTQ?p0NECY_Omz`7XM#&Jijw9h9KKB$-Et`D?2w_u+?>H)sAAZK&2O)XM*II zllcvCtUQZ|Mxjm&+J?2<;KhJeZ!z#p1tbn33T(7X(3T@GoV##BMcwaP?Bkx}Mcoih zI3M?0qtB4woKDF`_ySS|PR*pVI+i6g-6ivs(J(_s8F>*+AL0ZuU|$AYPKw)i=d@u* z$y(J9LPCe*j$tQhT;WYyJc1yfkQ<`W$ke--)n6)tj+O(*Lf#IK+qX34D6h~^@n}%y z`m0}RluXi>4n#(8oA-PAYEKjV>I>)CWADGvW0U3!YTZiSV|#Dz=3$L#MP$L8HEm}o z>&qE^Mkm8Drd<;g-Y$FWUyU9D=>s~lY!H(^6K$57`~g;HeLwAU-F*f?I7!oB_{4pQBua}TQ-8h2q~gaV`fvqa>vzftGFvKzEEX4r zXd+rjC230lOrEnQ3A`v`|G(S$I~SR?CUS_jJ|ZqngtC7)l)dFpHu#iWrrLISq{M?z zq>@@c7Gqs+v917tP!+h4J|w^hdOVcJsAQ#o7R=^^+1bhcs330d=(%-**8-}x@V95S z(oVLhw};l@^P>((@ZUg&4A!t3na!mc@5wP~N=K}B^vo^5^$}Y?ZcL|xVCu8isCTp+ ztu=*M&utwk8+ZL*%k6b3$IFNcW~-9Av1j~r_OTC2$u%@AwZ_W!bo}|OX8(Ow4agPQ zA3Sl^)j|y$d3#RuT7Kuu*g90u5-`KTbNI$7=R31>K^w!$geey{82Bsd1UwHcHwZp` zJ<%Nxy-?#PHEGy~-4UFSp7ZIT(>U%%TWAwWo{Z*w_pTaCRcrQn-xIo*)6f80K=V4} zC!~D)xpsR0;LUx@0ZiuPOK}e(IDBh)3*od-1$ioD&%YbM$(QUnl$cZlgP4Zg6d56> zI$JjWyRWy1yW)(gP>~i+<@Lyu9Yy`vBr}xZLg6fW@otF_scm?H7V04?bhG@ns$Ho4 zhRS>^Eibr!d!W(hPl-!1oiS+M0lwn(q7oEYI?)R3D`!cAM^|3I$P)mLGRYvGDKtm2 zGvX5FQi-9>+AfL+=OBiJqL4&G7Lnt2Vp?m_Df)H~e3SGoW{|FAOCBwl?;E6^RL|z47H5!j1*pVTCP+n%E1iFsCdCr<9yDmGdQa4Dh&{PF} zd%^oE;lzhC76S2zQ0iXJvIHp!hbcR@jiE|L!MYRZSx4GhNE>W`!VF11=E=uPGP^>K z3xo<&14wtfrVnNZJL>n#NX#c32n1!$95u;o3nCf<_F{A$6)>?EL6q*a(300Kl4LMa zKY;%kDvQY2Y24e!Jm@q0S5RXO#zw0teNf{^O7enD76!|jT`qf^;fc*ZjK<_DS6wd8h( zeEfQ>r^?Q>GQD z^>lh4K5G^-icOJ>WFg@ew{ zhu&UOyk!#76`|| z6>sjp6I#72SZw5$hZ$EJ(e=w_qKn&V#Z7V!PmZm`$Vw=#h}14wKS% z+7Ku2o{0j9_wgUFcOFEOX|kP|pV+g-`3b;C{9~QE^3DIuRnCNPrFV>&>E?kZnBoV{ zL{MGm)i9M5Ati~iN?`DAdxgb$)E&kMW;P-172Xa$`Dr#1v86yd{3e1v3L4p&P>PEosUYd~mNR=7 zyxQpaNcFsK*>7&P=^Hh39Dd((Ceim+&mNnbKl$_>aJ#!o`0u`V4%scY{Ow(*xd!Xh z6fOi!xO4O2;44@3uWEQToGw%|9PFQ5u;-<2Qj_PJ;BshvlKNEm($_t0*4p|q1@cS6 zGhA`D^?{XcE~7BVG-R0Ik9d(SL`Oe3kj;E(cU!Av%k9(aw(l9QkK9@ih;5o;P?4xM zpn-Wr<}H5$(3~0OKCYJ?c^?tG~rX zWYcL0uFmzQ&Yh(GH9>=z@+RZyG7?YTQ%>4&R*fC7s(5y(ytDGa&sW2yc)op-!CbAk zH##p4&)8&x9ZNX5E7uU`hwqrD7@K^!=9Rg#oV4c^-BY29d^h6n&kv-M$dGdxt5Vg3 zM_8umo0P_$xBFU0sBSlWX3AVQtaTcGxcaeR72V%r6Z+Ss-YHj*T*w}8R@k^N9sjAm z#D3gHPA#=%^dI?L zxl3A-q(^)6^P!=HKVKA(sI=?p);C1A@}JTsXpfXtIPAK94EDZY@f{bJTc}<*Mx;WOfL>-oLE&3dGy}Yv|cb&2tQ+BRM45W(~(zF+oB)w|2 zC#xIv%-&Q?+ZbIx*yPQ0@{Dcp&MOu+w~;;=>{I*bAc_dG?@S3m*2+1*EfI|Wa&3w zcY8iwA9gpqvS@J_qDENGhnkexI401FYm-)GqMNR8DqWydbmEPQ*5Z2`M;!@{zNIbc z{OYo{Lv>LdIjeOFQMLe2&MJ3t>I4~du2rmQJj_gWSPl-3|3Me`wiYa z_dnsR+?%XkM&1sq>wHK!i(T4L`Ziu4zsK^qy5wU>jg{~( zRKo7<&c|`t+D9o}!hWktQrm9md&}dmK$#KeER*aO!)bO`k~`#imU;danNZn9Zsw(k zCd|WX^&zXm)sy&}cugzaDjg8tS;P4b&chDg?km}_>uGG5o|6s%pK>3vRHS84%2N|d zEHUzW-txoqCZ0l2?kIIHQ)IPxZIj|Sar+)&O_D*S1&-xj9)|idC9>VG4l;(kCUIFb z<_fNJe41C3%7F=@SDRAjGDn_^F zP240r*w&Mw194SctWR{kbK#EWwPf?F=@};G0nf<0oHjO(ru~OC^Z5xz9+^JQOj%8Q z#0g%F>bLqzrY{_mT3DD?WAY3;T&OkCbu&A+g^jkN0xsm$lEhlb9-CHZHlpt_tMeH||;)2<kAXw z8#}SE3yJkp2zi9}D_Kb89TzqCE)sL|fW%J{{qx6La}vuthnNBLl}`fuJpY1+twOA= znNIm&_wc&ZMc6c>%!X9>gbR;FH!>@>AuFYeZqEJ9Q#@ZB7F?pXYZ$gi+H*|mD8I*S zgj<{S%lbZ2>DldhgmEQ@GVb5V-^PscIjhbWW6>30aei>@n@;&l1NHhA-|#v|AY6O4 zH2cl+gu1&}&n$q{l=q%zu$9K8ewV~BG;k2nSe?uLdl$y|3l4udJ;P>{G)oW5B0rI- zJ?2`p^4a)EN?Ob#6wcOvsjg~gxj8>t?r%bN926if@T{1D{Q1O?(P%4h7no{H>N>K% z?14q_5#$+XxhxxBnj50XZh8pMcT`-n0pNA)ZMi!viT`A^vtp+mB7{b0b}X>ql0Nf2 zg`aWmq;S18veIT0^LP{iwDFBWdgyO&2O>2m2bqx&T0x?_lxN0T-^#Z#lHa+4ws#@V zIj*+cA(sAhTXg{o!!9Fc?tXp;pPZF)JKiPK()pG)%bPQCx{8o`nIvd)<+lxhngrg5 zQ0R=9(pq`=?FJ$5OS4=XpSB+bP>-R_)LQv>fG#$(7SCd;YPcCTOt{H&SxESiV9!88 zoRtFNX!YtqobMlpo(}?FP~Glz66WU@8u;Z@ZsGenGpwfJx{XUEmXL5#1pA;d?@To3 zbzyRRVpVQNY&p#L7$N2w!8uX|1z)GEG%~w%M}XxTAMB~K980dNIc=#pqLVB=HiwEV z?TQaVPRr$izW2Ep=nH>he)L;6|FFd3;R6bqRp9lKj}~LDP%Po4H2=RU!ge>>YUs-r zTwkDTPncYrUHvlhHezxOAGHxyscg`<7z{^p;mgrZRqKUiRW%^2?S3qB?#A+K%AfiH z?qe4P1p12r9kiwLUP4!IsDXy2;Jj`VY}g?zk3hYvR08=7kB0-OQU+Pyw>W^XR7EQrzi0eLr{I@_!St+I z?HQ}vCw)0(Z)3x%41zu+0;Z~RO8tHf+s~wrHdh85cHP=B3t*4|OxnGkZ^r1)8s4j9 ztd;&)WxEN`;YJQ1qddeI`6R?}$>A+PkB7*dw6jH@C&aopGll4{Jq>VHQdlu~DdDT1 zQ0G{O{8fQG%Z8w`o8m$CvkU?p$fl6}YER?wi2LZ?Ap03$zf%+U?$vR)pfcO;H+tw| zI3{^o9)C$dZXPcg=4d%2shkW2FwV43cA9{tX(5=GFaJ>Gf)?*_6d0-hlo}KA;Igto zhOdFQyGMx`-5_`BYd?X#V`l{Dl^TtC=S`&LI7ki1lq;yBzW|jAXWo|oN|ro|mpi4_ zukAm$gc)dM!L!0pW(X^6!Q8DJALf)h?Bv3fS3@$Zb$V(_%m+^zq34uJ4?6->bzODN zCa`r7Jx`-I(+{8ak3sb3J$C#K*kwkCi$;NnGD)oHd@u9f5##H#;Jm%wMMZVP{aJW3afbVSCsX#o`(+noJ z7-+C+&AY=$Geks{S0dSiw@=))@mKvWlH46}IJnAvz(UHQ+w>LwDdaw^1)acaPZ@h z2~sxI^X<+P23kr9MO~Qjl45IJk?8!qp1ad0YYpa$*Fv%eeYn*Z^c!2%>x^Ymg%tTa zVc*%S8WUsP@wTy)eLXd*ngCHj;)0wuo3p1mLiK>VnsTci71PIvpV8e1C4>F5-n%DY z*VEqD5Z*I|zYs%rKi@fX_ttSd{Gk6le8An(6-w~OG_#fT;FWxsPnHO_Zw&YJu0I!g z6h0$hf9_&mGm4(SFFyILK#j-kbI& zPwa2Gg7q9+11?`I^1Q5A#Las8f4)(JP|=EJs9*P`bRNaP!1WmW(-P`=sgYPi_xS?> za8S|U#d!T{VkmN2vv8qxR7pGNz5~7*F18={u<*K2kBngn`;kp&#g-ERxeaoWy=ejk z!^NqdFQ_=(ZNnjM&eD7t3AcjEPh6i#b#H%rkHN1l-zI9BrHd@R4izIzZU41l^V7XE zt!>F&Vb9C%SCMgKITVkRUnyf9EdalC&K?ezznxq+4%xvs1#(giBni4MaSkPxw+JS* zvD!{c9PqH=qKyT!zUZXr9OnL6--%gQap<((W8Ra6UB`+9#Xy;s>8IDJay2SOANA|; z75$my*pfiKL8o(Lz~V+COkJwVzXb}ahy*5mLIn?7bf4bpbP0RG_1*WgTCo~)16 z)I3Lq(z^H`hpJticvHzIRws-nHN_*rF@*%_vIU8#QAK3As7Le@zPd)}pjlfxf$-rT zk3mNPpkv6OyP^UDWfU%hm#k{pZ9tMt7ZmnOE=8omb)SvT&j1jpIROZMd^ScVchheD z2#SE?EKs8AmLjaV7f8oisgWM?_-8slRwM9Kt(1`&y6e|EJ;1q*?7&hGI@Bg2z!>cQ zi2-i#zKK+1;*|MPF*hZO3Her=BAfN6|G*1r0s)Feaink)^aMP653#>+po`m*fWY>O z+uzR8zU_NZ5ve+<^MNO|-a^*5fnRN@rVif7@@Uzv5BctLX8VZ%mhPWekVacPp?VHgWCLhp&dZ+d)RGhwA$ zQUCZpv+W6kRd-L(ZF(m3O1P8|LPj32sYdcG%aZH!BhVK^L)DN!v?<%ND_;Cy}5o?&9C zn#fs-l>XI{k+L6b!T0?3H;0OQ>fDBX7ef&NiyEE!A*ef!V-@p(Ws({mL| zAi2c{>8Ab$VEoP-LGSpAbjK?gmcR>H<=wne2r(0wzH|9p`bFM?w?ynX}OE$uH7%naTa2o)*&w(LVR(pafCthPSp6y=G@>@ zTA;|A0YXy=%1r~wjg{04?NCpm!j!r#L+S9g{1WBzq#Z365m;;o|GK4C6*Ma$d9$oh z9D(K{+dl_?%3#K?pPf8OkpZGX`#JO4qQylJ9~{0ljiROB+P-JqoI%PfPlsqls<9aXg^1f7&Am$6AF1In}pmd8D;k|MF^6G5!CUDf!P#WkTeW8rKx-*_R?V6{Cuk9>x&kzhw`fF89kW0_h8qyf?WgNvYA(v zgMn0eCpsZU#DKiag^R2z^8u{NPLOVVqbcv=6UBi5J>s11w|+IL9ik-$hH)joB$dfw@+H`FCgEP$=b_21(|P|NUTl8b(5-4 zpnfhc3-$Uj0%`y|R%{4a?*ID9QQLcav^gEpMy!l@gZf9qKr8z{CC163a4TXL=jqG~ z&{3+a1oj7WD&3hI_S@0&&tc;;zZ&IV(Y^7Ij7yme_V7m;NyGpZ>WHxqmpd!6xv2j* z!ING-cejQ*6OyEtT=t&g#L3IpzR&8iA$}P2@ZOt7^HiK%7}EAOAWq5BjQztTvFus0*TP{mB4A#~w9h<(KJ$ zZZ^ExBvPfSQ^>~j{|j{eddODQ0}MZW{N1_)7#WrqN2+y}3+Wp0mRdeKe!!IUStuCZ z9JTkL9N|5tgqTnr6-3K=stbVH_8~xRJk0vFb*4bga#hy!=(~Jy^WRLlYu;uAPps&N zKd&stCa%1dYa=y3EbBuPiQ`onZ@O_UrSN)Yn6is92DbA%P<(sdBcOPu>d&Ne`9CNw zSO?~ys^5llJ;(PW<(PZ0P(=(GCh839#Z-BQR8$Bd`tYRl@ok$09N+XyFG>Eg82`dI zrw2ijG$1NSxH92xG)lF}+{yJi((qKK*m?n-53Kw)gaT?60j>^-?IL{X#`7xjxlgcN zDi>osN&_b3gb50#>1sRY>A)qy)nABtB1pu;ucrb>zayF6Nd1awATsisa)lwoAaQen zxN0eWv9RXtQW6QmA;(5@PO*G>Vqc;>@{;LwE%%EB9Z@E$Z$~y-^k#a^yIxFrDu0T3 zYZ_!(z6}sWU*=%G!tc}L-6%0$Um~$Hest~9ETJWh3Nl?Rx%TGN5)4lNh^aolzJ&7nnr zP4K!4Ww>e$cvlKxWI$jhTt|5<;A-5p?I|GnNQ_T2WUSdVuc|#<2CRMdJ{Uz5;LcL4 z@W=26h$h{XFmmjc6N;`g)u|jsnL!-NJbNTX5V<4an<9gtgS2$o-hMV0YGu*Yxylux zlCw}A5%a`g^7TBMoCqTLk=6?1WBp{9;YFbppY)l0xht!D1t$c0ZB9{S1>+G?MerbA zj#x{GWAOfV(7C&%XOVxJktNV6K~sgz;S})#k|%ZSYHxyS3-DWI2^xnK989)BkD441 z5LS9umK97EWypU@1JU`x#wr};I8VIaREAdnc$YT1FErQf`y0kOzeTT#~?o68H!y^s-W>_r7f`SA1);aRYiD)F>X; z6XU6`bd*nRLJ}_yMwY5avYSX657;$wm)vrAxqVyB1mKP9_NE7e&?FL|=WTW00Rv@N7+2k9UI&^1?@D%&sD*;i zhUG5;0pUuh=?CQ9fAgUv2+W9z47&5|bbS?;mrfxjU~ot{9+L7srE-C`XLH&f!6ynN zG6nnQml#qLJn#Foo(jn#-P`}F$8Z;=H^TtlG?gE4I{QDn;VhzcVgia{`k@(M5T43+CSRW+aR#aBH8D)Yl8z(x@j7~@;ys7cSFO0wUtSrODRw+ZFbch;!a#d=){cser z)I6ZXSCuYIc{C(>hdp*0*hgrM$}`|__ZG`3uMnD^C{ucgkl*w_p=Li2}GD3uod{J-!lN0*l5 zbFFEPZDj?Oe6k^Vas7e4v~0?X&?ubcjLty^AxNM;0yKx@a1>sklxRChfq< zim-++KA6dnOv0~Ii^B$d^iQ#Mki?Pnm{8GO!g?vSW~tkCs7}O&?IU{j|LCTM_Q|(sc_ed4!FK7G`!NGB7MvAsH+w z4Q?$uQg@I-+EbFiEEuUj7gQinQmd2tWW`LY6Nr-IUInce6lGldR{kY$-|BrAF*d*j zQVXk25^Fsq>Yw} zAgc+i-&#V6Y}M*RPh(?4i6+ZR4}yoM0_o7TyNKbf^@teZ9*_}?MFPr?g0tR0ah;a} z*c66$q&<0bLVTAbS!9naDl{pBme8BjY-}`Zj30XT&Uo)-GIM}dp3QlY=$9PbB33MN zPehiO^!;Ve=?&Hf`tjjLT;2DZWoeU=RQntvyu)Q(4&BD2Jl>sV)mV3?Zsb1}d znjng`m(WV}Y|CsLH?}=uV0&F;@WaUX+WIs1-)(|urpcaS4i_tD>le(}qR+p4_RSgt* z9O)4yTj6h{vQn);{vyk2_x|f}Jsz{8-+uC?Il8#LsIAF9oHB+xna9u zYr(#rIk$(yKq1RKU+w8fBwU+GWH9Vw9^)TMX|_v)j90T&o8Dn)UxJve@F$plgl^j; z&zdwtA6y)pceQ*3EBLEl(FuU3*C1R);i*iPE^Tahd)-TFB`8T8@yDcQ0_d$nD2b7y zkPKHVZqXgyh}Nx=X{K{~3_p1A@&UE-vZI1d~r$K0e zpY&Snm;9#DAMHDTlu5n4l;nD|ycxp8$fCtwo;q!1R)!eSJ092>%_g`!_9hptJ$;^+ z3-l9PJ}-q-hw!bXzt(K=@j}(fw02N0MF1yc{VN;faZ{s6u{$Gc3j;$}ffj}^Go+E1 zGgI2v62ONug^Nf%$N;_&YG@dP+_x*+-2*9#YIokME09;)?$wW%&$ZMNB%Zq0Q8Z(& z=W#XM4+o=TS_)&Nj4Lb!lRdtl_Ua7|9CB4XrDGNV77c+AOR)u#guWtck>Ei{V6Ukd zj}~2R;=it``)`e=b(A)%g`bS)dy!90;{0EujGskEmm3UK0g*D#p|YP62Ra4kulSwrvt`hBk8#$K*pgJEeVTaw$J zAIS^+gaf#BL|er5uxU|XrzS&=0K!c{3xC=^VFlKo(4KS_NA zvA?C2$l(WM@oEXAfZLQlsJcA2`W7rlEFf1PXdWx_CN}YC}MGKKayLV1Uf7x>ZZr) z?c>_MUh<+DWQ7oz3-=@SZ;W&+CO&6XJWH5)hJ{m)%CpU|bw$f3f3N;v5f6T2Rf@o& zGnk2L|LD~=!1Dpq$|7c#Zgqn~GO&AljaT)xo~-H=pl4cq{4rWYFEKt=@Htc|LtTuS z%7hDoo=RhLTG7b$pz?=^DnYiPa;__rn}^H>enUxzg0+}Q_5V%ZwSmxv#8F`7{`$h}lb7p?8&79cs{b(5?>ncxO1klVdJkV12 zQK;WVt%(MUc10*^o7}ZZ2>;gl!1>(8#&YhHv*Zx?a2Us%$;UVA(YoVx8ey4PW@T*#TiR|(~|1esk(kOsV>QWDffnz{F3gjeWRsf!F$_n z7foesr&_fCNb{_eXRrTPpP$rA-J{|6QhlnPJ7(4LH(vXs2+uSI>0UZ~lQR)zR=BsOnOm)%6DX6l^H$PcWy3vhQLRe*w=@l~e`xRJG}Q^^A3qP$Xjy#q zghAD>W7jNzH%8z5NB3}b!2aOy4EF80y;q&#x~fO9iE%xmd=#Fttn{W)PpccOT80=O z`NPD)cAS$UD?fPY&93Pa&y zR8LK^pFUtkz7ZL9;@0m2idthQT*UVY=U#gLz@gxs*Gaqz8FKw|UET7=NUs3le*}gV zQfAQbsBO1x`z+Wx#IN72MDzHia|31Ghxt!$8PY9(wCi9tCnZXO1&%EnbWpJ308^9p z4O6?VnCR4pl}FrZ^tn07O~*;j=&4@q3;$KlUGRXkfY@a0J>68iPX%0YnqHkFzmIq7 zx(fB;(6jb`o63+Kou)R83a%NZYcwNR?bKu4oFaI9={Oc=L+h`)YatkQ@LR*)Zruzn zpJ3K$ldovpY-sW~NRx5j`?2Pb^0b--g15xt4-*5rupRyaJISis-tyFV+K=l4EC7;r zH;z{Ly}dIJ9y=XQ(>`3z-~HWMGfB!fg{S4sFN@b@=DLNNuXY&HTXJgIqr>EG4keeg zC$93GKG=PO7a4c5U+KJ6Qw{s3Cmh2*{Mzpp1(R&z~UniuBX18($! zd~)iKL}jm!j`Y{qXu9_jI6+=(v-YvvTm*XbwY2s^`Rc5;>TF~MyUO6Uk97^jJ#SnB zTeqdN^uQU0(XyvSdkup>XZ;v_Fu$#IXX!5m(|?Amj8XT-Pf-gp^Sq0~-pZ#ovpF}s zZmG&AwJwY8dB8M93{xi_riY9X7~PDXjzvK2OS6KTu&mymjaF?ZD!a7v4Ad5pLQ^aP}0gG%{bo9&#t)nSg=yBcZ zUhR)tvqzi4)>4qtS#O6TE1kld_K9cUg|@rkHxA!jbQ9+Ew7V9w{Ez^SV`_nT1-q2s zjB)P64mYP3rY3KjwZ_UUu(OeT2WmX7Y|+BDgdOc?F-pd0=lt8ng}U7h&43&)4m=%& z=+~w&*88W~-s&LPhgLB5q9Uv>S(^21(DR5Jvm8o{N|o?!_8zCrQtIa08*AI+zTakQ z+o!}E8uT?waMy9_#;iiULRDM)etxIhC#mJHEc(_K7iy1PK zQ#Ebx2CTF5)I`9|*Vyn-d8$qzMcMj>!M#{Pn}=OxUZTw+6WrZMV|UfDR=CF@xQ9`| zJByM-0m<>bVW{jeQjaHPWzV1K7JW<$UI1ZX*`p>oN*@H7ew&iMI?rI=EH~Hg@KtSI zv%s_1ySl2#%y_8A-`Ijvw{*NF=DC&K!BxjsRwp<74)>ZyPi+#h`l_-{7xtQeoN>py z^e^qc_lIZxjSbQ>awxGDz!fk@_iuuu&UVOOJ`L!TJ%+vCkIdBec&CD))36`ziK7O- z>X+;zQ*{!4@WbQ#-7_s2J=e#zxMQalVH4)B3`ka&y$vB?RlJq_C;Lv8G>utVQo z;P3x3?wPhe-*w`xr2CJ`${fOO9@FbC>HpGj#Sz*3wyVm9Q~TmKHt>kFT>&&0m(;dC zEReZ;Jk(xszPLM+*uD5q_7Wz$+4Kvd%OhzXS8=wy@*WB{faYkltYuvXLP?LNJ zv5-BLy$=IM5U!i+?U-_I&x4BnY_+BczG>|>@;563G9My4z)RWqNMMdeu}$b%T@Zul zgk$SpJ~uR{$jh4OdAlk<)cN{x0fYs8NMeS44xQwRmMzPY`Jv=m_@#0?&3?$>KvzAv_D^pP?jAC|sY*7?i>(el+OLQlrRCGtDODB8hrNH8 z!Abwl=p>0bnwz`6ZImHI{T&0uoI1sTNp)N+uLlc3g+=A?JsCCWdpgB6;+Uz0PI1&C zgiu|6K&Sq>xB9G)gB}VANr}aT;CAp)UkX>a%KGQaEssz_>@HEu!j7c1wdhm!Ge$xZ z5+{~H#{byfbYY(xbJ^Uo|0PoXe8C%W+)wO&7zMR*X8^kIwL0l<3}ZI z|25qM!LHNu+TT!&^iRZ3x7)yjp-B9M!*2+L*wkn2~1nv^Qd0;kgH&rMK=xGO{cY; zHav)QcSK(OO)$u=-f_GK-aMoq8GpA7Rvmqk32dTWRjGaj2aCv=a!= zk|7U1_jPD}5%B!kpvx8#yQ)0P$k||z^TI5UBVTtxRw#i9)>!$!UH9vaWqAk53HlqE zlm?G8X-}~rzatlh59GaVnGF%3-ZswtpLz^V=!ZC8FZ@9Eul=tL{>Bqx{X|sMHh^|) zZ~JCUX*Ha^yNKlIt=_)u`R&*V$V%}S5rN?Qv5Gy^rl|Jr-I#jMvX4wm1W1B^Yop6L z1lYvgFs7p4L*feKSjldXKXCVLv=xgU9VY%1w1P2bDeeCA2u%Xw66K!d?_w+KFAFN! zD}ZTBSv%gC0tI~lXxLp`9v$EqvJY@yc<6NI?4f0U1a>kS1`>kcLD2b-+e~G4VOT|L zU$fcm8{SmDWJe`1eDf~c@44Z}<9OzZ9AvoU_OxPpPRGAyl#<cu-@Pk4^y^K(!NtQ!Wj7w#nHhXAWsazmW=<~E7ZMA`uy=h5P59K zO&!D2dflzx!JjEo0ky|Y$^pGtFwT6FwrapLA3gsngkBjgXfcW`5n1sezJ5XJFXRwQ z9(t_bZrX*#eE7fY;EjEM&%yqKJ@tTsbl*~tP^x?m1nc$gIlX3iifnJ8Ivyu+J6_jg zR{JN5i95IHjkIc6OOzb|@=xV`mK6%G*p{B1+-ev`B!ix{?rD2XFqJ+O2~+lG6D(yt zDb?s6yWL-oz1}2(!I}bZ#h1?p(Qc;noe=CJgt(;t(nqe?Fl|KLS=%oG=9brHRlL0v zF@EWya2fX-AVUB}6Hqtu?+lRgMcVn3M#@HY!9|1iA`BN&+3Z&**n_{&>wc&Mg~x74 zbf{{$4F5+ye7(u9bmzGAAitUU1}F5oNOYQ5B$0{eKL^QCwZnfjXikBWEo&Dp($hioga-T!Ewtl({R8Fx3wmK%8w z1orc4!YCkKThqcSv9#XU+60x=Uy2JnrVT6F<-lz1T~aq7G=bQ5WdsSE9mf*qOxyA$ zpgQBt=9v)`aw4*>_SpEb=-mYwhEDMF%7-t?-(;&u6;U1xhbIS(Vy}-+Q zlu&H%GYL9WpWhb$NH+&jrf>a!oK-CZPJ4K!{JKvij-Tr4A|l7RPt%g!p^JsYrmk09 zFq?MjR$lg8j%*tBL`aJ(kxE=LOoiLn@(0qK z*4P?6rKKx=Tcg;_#G`IYsRAy_5qPS!yYvRwFnJsPCVN?bBAlaS;!`9F%y9xT&O3^| zpxtj~_rgY0KzvAPHjF_vM+hDSQyB59-JA5ZD}d&IJ?9+7H}O z_kXH9@z0E%Hq*Xj9QBn^VI`)t4JIgrPPDh(A>XlpN%HiI{~n&d990<0dfx3aG93$1 z0~p|0Bx*|rCOWw#$R=go;sGMmUD-Ru6No15`OEIuUZo;40fwjRK-GCnIp(?mPpFf(Lgp8ql1u_pGr~W6`m*;4*`LnKjFOnyLkn$h~8{2g*n??@V&jq}V{|l89(k z)-((zD{1ULg33uv+>2NJH7LFjq*RBRBEd~1+Bin4cu(}=3Cvc-MQ+nK)pMxi%&k@& zL<|#k0pVQrOW=gEK@GK%XW*@PupaQ+*MrHiCOm^A#7erQyT80W4nm#K5M?fxlU!fz zHG`(2Hq8@<=sNDb^yYSK&mQ`t6^zOr&y%H)jdIwYy?J*{t5v z^?y5&n>&m=s7w5p4C`m=avT0mYO?CY;!MZ~PP-6Rn*AqC28uXsx+Pb0&!Nt^^oq?x z{!=X|ZpD<(PS24+yBp8ry}Gw+mJU34ZBj2#w}WA^o?s&OJ)T`#;|Jyj^o@vI^T(iW zmWu}(r~)$~g?KeoUfP?zf*B1!`4p81?{e$;3p{Uia_y#4G>dN3NRvF8J%jbA@%xsz zVNgpOl`;~|ERdYQ+b=nFTwSn2R0`-K2ReN0Kg^kioh9B_hZg8xkq8<+(lext7)AXGqzAElO{U zR#ZD!n6(%5)T^`@vwAz__2Flvb%{J3z<=9GZ zFtrL@&l*SJ*%rkwt@h{%e-!!)tUdx16uJTV>OJX>#zsPJN?BtdE$Tx0E6b*b9j(h7 zcvhvg#V|5b^L<+x(qPk*@b;jLACK>!IS4DfPJ1vB@vw0qlb)iO^Te|&Pt_CjnX82U zNYWG=TpHZr|4r2x38e=mkUNc2<=0;Dd9QMeULbXeoTOKGIXU&YS$kvPeZa|7l!LGU zw$i@+Z2ld~SAphd4!#>cF~7v`+9YM9;R?fD0n=F4UBpMZZ}o|Af#s+Xif!iL<(BJ zDL?!O0!89{qOM0uaQZ6JgB*_~Bn_@SUw2%?F<{2hNiegTW@Mz-XP@lGRM$^+ckG(; zTN2FAWIkzX(~6R1(u}QN0*Jbr`1a})9Xcwxy>IXxSvDf-Cw?=>OJYf3605l5n*Krn z-Z_Lk(Ki$crApQVCVw|hXs&NJq1-x)su)>TF1hA7;KBH?_?MX390;s&&beO{j(uYz z5Xm^dfQ^VA#l?xMsP>2pZ=_}di7d={Mbgc|%Q7-&Y3D<>!xM7Wa9b$STil6V-uSe7 zE0aWXoyFq{fCljBcy>n4pa*7qKK8qZ+Ikxu#DPkSe00Ts^1{ko4G5D+5L)^Lzq~t>+vwp1kPPF7+PTiA1#CYz})F3pB|A_|Jk2<+8!Wj#g zh1}_jPBt_X9231A;p`2f=$ouXb#%zq$>0B|TrSI^`InZXx+GyxWswSLq%w1a^DCH9 z^XA3N3TyJu<4BpP2dP$P4v}O-zl*0O5T%85+<#LZB;8+0E!z*Qp+YxX#b^dF;U+{- z{4xG+2?biUMPzTk2q`dXV^S?&iQ4{a5TxhWZvJtNUcY9N{`gk1 zwF{DyZ}2n@707{`b)1YG(faf14xua+Jm2x)XO7&<* zEFkE{#aCB|1f7zs?1_ znGXk&+6gXUJqR32*wH1ZLOzw$!w<$2XP zg%(@mX@w~=7DLC$4Y>rAp9Kb7su0p;Tx)(Y=s5Khkj`IygJ(tQD|*zMm_KqA>i0wl zZX}$!^NGXPa&Rt4*z|X+pI!IC>rT+qPkz@nrp5>Bb9c?2m+&vmfm3zV9$uh|gWX$$ zd``6vOw)0dk)*VDW+6T&jRi3cE{6B6XlIA`p2-;odhvOPyXki_Z%n<#R21lX)&k{sdHFcve{?&fjY z+?}HQ=p>gljJtTbA`m>jvY^^Rq*c9o!@Oaj-ndA|?FYRQ-VT2;aH^i#ir~{Wdo9IZ zKEEkR<+POnImPKZYKQVpMO^x>iOgs#Z<4%=h_S`|;2eACx!GNfx=*ANFIZITxyCOQ znX67wJNe`_d#TG>#>S6`jO1kjJ~W4exAAkYjbQ=F(uD%bqj4LuUpSeEy`Jt@dKexs zE7dGWP^M(@aXmp*rTwFCRN@|f$Y~@rU}FmLgn)jlGGmtpTOY9mFF{)NAeyMEUG zo^M|k!ary!vHRlFn}zh-a0xv!>VDWxbTQrt!UB;N@f5QQ#}5i}xMLugqOWS*hXH7Y zTpqu4+HfEPpMZ%<2aUS~tUGw{Ai9UhfaJ--X%P4#65{ytUG8O1+UP896H z0<1%?P2#WMyxxS&0fB?o7z%M1I8IkV8&F3uP&oIzfv--2 ztCQze{mOw_QIL}HVcy=o!5GoKM-d%8B#aEjtSS;?J=ZOe8*+gfBoLzh?^?ugkl|R% zrt7r*v!Jwd$m|a)CxLV{E&!}N4_{V%Bs1bKgk-~GK^txGg@2IuBlwd#E`pLDM1=yR zcW?OIx=*jsuML?^#}D9uvuOOrvW(y(6p3SiE?F*KN>l)lZ6L!8NEfP7Nu)37|oqoC{uvxl6Y|?b08@1`7mH4K#qh| z4mtbzQ{7tJ%mUqsvidUO1gW%(I|-+zz-L|0J434dp+j#)iKT!svlu@5Xc3 z%(A0R2JQ$1oZZX2UU^FZtSac$^w9~44u?GFqZ9r>u$+KZ(axg_WC*yMHJ*FP3dH(& zYe?;4k>1cL&qt2g(ud&yGV+tJJQdN-{R#9e(PPC+d?optJKC&SC=L_{dr{V6R&)%d zk?<`Xl;!Aqk|bGWg0M6VWYIE$e;JJ;4dq*YutP7!klDyXlt2WhNO{}xkW>vqte&Eu zikYau|1ab4P_J9)@Y~7?h$}HgjUSu;YB?ja`_nA%6^SA;}7$ z-Es^(m83}g@5X=Bdb1*OzH!NL|AVhG_7V^tyx@WypMJA-}ERo z;3yU^t#F2$K)#|v1^8(UdkhqO_U$6BH5?+zyc>%}t42T@glt2%%J%XZ0RN88_3JKo zww@*)>wW`_1hk)?Za9BB=iA8m&@2uK9SW%((WX&mCW11JkKICv)6D72Z29TM)&r0x zJ9bM`*RWIl6Ta1&Xq=AX;}SpzN2RaZ0aPnAu))MnjpD5`p6x= z5zKu<6A(MDR+f#w3M>>rMxqm~V`w9Qx~&R|Luzb+Kw^vC(M_hGq2vyP+A*Lb@cVPd zv$`YRWzf&X`|cCE6Ml> z=ZZyCVoq=>Tl#1nkgUgbG?36Io83X69WS7EDf$o`1dS{!^gj*#xcMT1dp(TjD<3Lq zG|+)30hZ(99|+1=dzt$8lr+nO{^bh=-MOun5vuLzdBe(88$_O23g>}?#q*3`01g)H z4-p8!79boP#L7?cr2RR^k(6Gcdm`9d{@=tOHJiuM=~tY0RGlOiJ7X!vlcF!9R?M5K zuLffa*)U`#`T-7&ua@ynB)-b*wRR+mVZmDWMY{c3uH%CLdhKN@WXuepP1&wT!vy%q zQ+v$nij}`o9PoIuqQMCfu;S$FUUezDj+PRk4(gbNB4*ATZW10gWzs3S7Bcmg+-ayF z8GA=FO2PyL<&7?q35YmVdH-Z;SWDdCIj6yjBz7D<`$TXGTyR66$uhRPu&T5SH3%ynU=0kXrC+7~3Ep{0&m0X!#UA9t_L5Tqbyl4X+?!FiCQfK_@@ zK&UuHf;4>qOc|3tMGFTR1<-{?Z%7oj!XzjjeIp-%0nHKHJUf z`;(Zn682GGHaiBO%|TO11oHK3iCf#Ff5Qo7pUsO;508wbDkM$=cT(022F5s2!#b{| z^fI7|BvepwBBuaK)RhIAz{OPy@zALqfvCens4D=QP~zKLFnOMI)Zj};(A@SF$}j;W zYOaP0Gb!C5CLZlat0!aOE^$496C&4%O6N~GW6l3Mv$u1HmO0%X5p|sk%HncIqEUQR zSrIqhw_TGC)sZ;=1}Y=;F_1>4ua1P5z#-IlPVckj->2OA6D0KJ@FGB%?ITx6I`0%>%Y zh~M=QP`o=5t>q4MZ^=%thYGPIRKP~@ATAhTLN*PN9gj{51}dr4#83T-UZ!TU>l2s1 zncZl#^3ndM%17FN()T|yKOuyk@en#?f6XN99z{e!5IPoUcp@j27*XQ->0#wj@Gu_q zlq}bb03>J(w zuGC{7a(M@hwOky(^pN|5`fA1k`6EY5fGlEN@NEUU{_u3#hF1}qX`hp)_Uk}4Yabq- zeQC=qSezNDL55#%!9z@cZpK<+GeTzXcId-wmk9}*WE8!{+V=0KZ*!)$9`HGMOYYcM zll4M)27~WI!llVm7V|rpA<=4W>{!R8cmd45%}3w3dcb?*uI1;U5PNPr`4KRDILiZ?Jh?fk zOl}ojk48UloHtYx*b3%QY$?1%-k<|33k^_Qdo>S?l?Nx-f=B4kzF}+-SNs2Sp8H;r zJwvI*_@%WEHjRs$z3FN z3n}Tw8y@B}L1==vz4HM zK%0ay9dZHg^39JK`;a0;dGrw7dKp%6W0GD!Xoqli&08p@sL&E-e`%331ld!79-0hN zWbZa0@rvUk5$TD&R(pIl4?8 zc=qlZ__rvPl=C;Vv;J4(=OjNwMiR5^XM#FSle(}bCjV%d0=@13@4+LcO#0%chSc;q zbUu@O`!Bg|xk;m+4b`KlzSuT5GcW0&{4hRwpt%!MaTs6wKH5D@xn~8^7lEeQg|qgl z6WYI9rO~J*=4vnRf&u{|?!BhToCqw?=Jh5te8g=SvRCwh%vjDPtSx<21CI!FI;1-f zF*nT)-N1G$kcHB{{sM`j6NX4S_Dii`#o&T=o65HJMM;(VnaO|neVu3!cKp_cV|veH zMnn$#sxWt9q0Y(3uRVvq9e;lNT8%i(tccN3np;|LTD}kLteUV|a{ptC{~kBnEkoL7 zt{AaVVV3u!8Bx#eX8!)abwhkVu3xdayx*QvqkkK@Om$xLq^+4+`BCdXx6LW{={xFq z&*Q;oKNeM+T{DUb)>3$oKOs3dSKj(FFME1i30ks#yW?m?mo#*DZFi4jPX~Z-YAoEd zK`UeMRHYRgX|5FCm$_8)=72Ab}iQUFs9X;NOaVne|xW?>eXllck%m;bNVvd zU*G*uKeCFl>hk-VK@B^7)yj19ATqJ!l-jInWzXt2k8WfV#vM{}jY$c!Dj`xP?K9T@ zeC@WLEb1TLo0eNPSY-jO9c8<-v3cFQ;t$SUFD@CZ;(OafarV;cYMkc5+(Uw16_y`+ z;82}Hc_Ql1osIvPO<-{}BgN+1x|Rq-eIxj^=fG7P3(wCy!i?sxi1)DH_P!Y+iuL}h z8NO*%|BtRWf#)*)-p3y)rbXHp!lYeNNs+x$ny3^-AxefJTM}h^_i2<}nr{`@JCQ^XTh^U4>xg_aOV)Ye{ z7LQ#Sn73Yd?DcxM1Jp3DRk=%yiiX2O-_X)g{=@sFdY+97`3t|76NaY4Q|s8jb0Czk zM#|8`*#CW$`fXxVWp15A;tNI&s=`=Q-cf}IJKI|IuXY~|1J)Q;BrnVo?edILzk!RY z*!?LVr@zSeO^%$)x9(N=b<^XocN7_Y(Q!TL|DIUlV(H$jri%67378r#CU3+sMLEUr zj=4v&jI2muxARTLU)owL_soo=_>SK>uMl^mRHcYJry?;<8X!qg;+f|Ud-bY@`F+0A zY!ePY;bj?Xe0o1eZen!E^hL@&xHA0`i{##tHav@`B7mOGOW$hScKI%B0Q-w)T7)Uh z%WztXk!zf(m1LGZ)R&v?&=f(lrGzN4xe*SXZrmH~T~@>5FO~~+oIoC|ZW*n!s;dlK*JFwo0C*VL>$Ashi@z6&f7tCE>@O~K{;1KaO z_0vP!zqmvyeGBQpKT-)Z+K;S5bP+!>{hf&hkHKyHRC)AU%X&eq59~Hbyox;50eq8! zhI>mjyVip<@*FnMfix*!((z;Q#RH$-lT8{dV@ZYYo=cBF*=1@ssjK+w&%~0*Jp!KY zS3Fe*4%)B`PIbw^X-ni~Ut&3mr7Nj$MxH9-caQs zO;GWh+spnG*~-ml-ZiQ*BX97OmI-uU^6Y+_rDf;E=UD$PHn$C4c}2M`Qa{mR;trt? z;z`A?nXJ!~9IYeXAK^}|-%9Gqe*CqCrh56y-Nl_7i=l|!07`6`UFl+foY;B@6Ll z5R@WE3Z98mlt}zKx+QAu$jJf;ABD#+a`OnVE@_uVlN3aLw=X6DGq5OZrO%!Jo%rd) zj+x$EF=t4Q=u!xy8)}mXuC6R4$ilwBp#RE4A&dRUYZH@n0d{suz{|M4n)|ErG=I6e zBlHb<)HzIIdK4tT`3iU*hZ+k^JC;CgGgPyUiEs z@aCAKYp6OJbg@rj}U@ZNwh5Mbh);VRYhab`jW^atF?V3vl@{6wDzidW;ay#kw4H`;3q$HigsXJ5-50Lu)ri77h*1H`t5uE z{;;)003!N&Ck8f+U42oHZFdw)<>B*~>1(*Cbmpt)?BWt1m-WPvAD;V{oYW-4mq;9{ zho0R*607t*<{>|&+E9w3z)mE5(glyPuEPsjCozD3{g#{(&1c|I58;1)faDuMq=3Dl z0`5rDstOP2sAgZ-8!GJHxG>D8{iOqRol)&scsS+vE(7oQdi1LEf;xw+pXXb1gN8xOfygrsFIjLBpCJ|$q_U0o>Z3}Xc=vd^B;>%nDEG7_rTgu6_6N&5T9R$`USoLXw zsp*Zvl+J;-r<}!~FI&~T%NaAx`Rg3*&7?RP_ioNRe>zAeM{f$e`#q(tX%EzX7^)IP z&)vr{Y!98w9Y)@S+W?KpErZ=@smpY>bo=z4{L*@o?GsKRC!LWm@5tvXPhJpR)%>la z-nS;lO{X@CY)_!57jP8i+e*Vu;#%>@73^(x-R~Fr$)DRm9GZ`isZr@r`_FSIDveVF zxeqOGxdI6?51*f6xE^v} z8gkeH;jN2dGq5JhYM~{hkuz3Cu)lc=njLTbm<7Ns0W&B1E>=4$b0_}|?mwl>r@PjwlR&+-sY;{TDJklJMo5e#f$|;bNaM zsw6J-jf>aDUh-}(-4wH4Z!rrf*{s*e0;()pS&K&sSg+QY- z^tMAQ_iRKmCpTQjeAO^H0WkBaCV4IXow6jJRR<(#HP%e9&kal9)dkEWEbRYznRVv> zUG#svkz>AgpkDNgk)-MBf^sIa>d12Htl}1wzo}&-GJ>WS$b$?(e9|CkgcYEP+U=_s zuhF+kWmE(pN5NWmOw{DReSAb{gj53DfbdEfyIk}aCw&F#wffY%IYJ$D-)s3B6e>+| zVNMR*<xSP30+7}hZSOlz?xll!k%|E2DjmD|g}!~Nhi}ph z$BQG~5km;S8*(I0aIFIdi*D%&%9Hh%onrV$i9v|xi})~iU*7Y4-q7P}B2gdpv{sEY z)!wflNV*lxXsU)d^UD4Fq>YQw4xDa7xE0nhQz>IT@E?|7eI%&{x#euQRQb36pEsDt zNi}ytF$o^pQl_T?K8k%-$1sFN<;QP68>0m9B+2F|lWcUjz>eF7;DEl_YVZ)6oQk-&Dx#I+y1%uf(OWm^72uyyyfP=VI@6$Quo{XlHx9Rwa; zX@_|+MSHS4f8oh#3b#mIRzCUHRbYy}wfTrGgfpG)r%}o~ka0Zzr5571kn?PY`sP;j zC|iMakoiFBiXQ4Tk~!7+bLMc&MZVrdrOyXzAEhFZ_twDH2MrPPAjQYBZ#!vmId34c9( z(A9M-f|w4zDJcwb2~dTa!GD5_aw#Qsty9%77Ue~pq>aR-iGIT$%0og7s=^{7XAtn5 zM;$E(kqftdT}sCTNqmSQIv(!+5)a=H4?3ilhc0vpdEF1CVKaiDCJLt)aOoVz`x6?1 z#9xtdPW$6GHyWALBJo<%23=m?p0Nj%DRGjRRXLgj+v8Wg;4V!c^QT?Y5A)&6>&xKO z{HBXK3*tUGu%&?%5H5F%K6nXzqsKi@KQpXN>Tjf;^s~ohT6bMWLFa3_{%iyZw!`Te6?aIDfP!Nl6UaYq z0Lcg$Je~lnp&NtOYqb#e@`G8f^nUH*v5*%2&Cmjcz7hE zy6u5q`2j^S6dr-BC#2Pf7quzztX~QC-k6^(LuVT&&;kzqY4pU398UnSbHth#!GqF! z1RC0)GZO}klp-tL?MXD-sqltv1d#I1OBLq67lFUQ$qc860V7rOz7}wTmF?jJRrlI- zRDK-Ldk1rzv8Q89q=_N94{kD;95r;&2+iQq2z{{}#l@=Q&kF!r)!%gf;4B98rVC z+)!}r8OS0AKjxNmRf@~D zSqVTOHU`UG?Kfi=c-$|za79PqK|yuAuupHjgCZeVZIrSK*_UV711I@G<>q4-69_DZ zDdE3cM(N#Fl$09;H$18k(qa=SjAtrqf)CAy^t`OgbM<%mIUFQahR{w|G=#bih*45A zXk(}TYHVgWLPh22e?2jnkch)d=?X-i)ue*=VBxguWabXVb!9ADXi z!6PdFAgy(wUqjH0(fb>gz3qB;3q=R}ua8-8RRO}l7qC4kfA9ywOm42;d8h5_53~s& zPGXf?vyh0XjF$}lMnAY4x<5g}=jnmKwYTZ?(y#-9?WggNxhU3X$!_V&@=VI+h9Ca0-((4}tJM=`t#*u#j$NB5Y@y+=M&-Jz_KFLH;!EB=$bpM5) zYeK=)BovCE=^AS9XV6OsnjWTESqdN>w8C3+fl~yKOA{8j1WG6!Kw@Ma2zulGHdUAS z1YK^^o@2TMSz`RM>jaryWkqU&`Jsix@p3bO0%Y7qln9%MWMw@a*HjM`ur{2=x`a`C ziWTww=){F*af0usP`~H=tl!IJkjZ~_$UQ(65aQxz^r`K$mDP;@pWC5^IXGOibB5$eMPp}~okU$7Cop@j32}YDi11gN`QUKm*Z%FNCjXE6gH#EsTu_TTL3@qkG^KzZ1cmkg8zx)=1mX`nTzZQ(Lm$7CmwO zYEqHYmP7)Xy=L(>F7_dqo8^94Hu6NRLigxL*XnwY6s56d-$P2-;Y!`ry@JhIwvNqj zGU|PY2ILmpUl6Q)_@+-|p28=61PmXuEBxzPa_xKk8C$`5_Z>o|goc!p%H)Kn92I$2 zTVx1x%SE`QKW=Hb;)s2jTw>~2h@WrK==TX-v)=A{L06>_mC?3ub1xa|RFDFRsGvYO zCvmz6%a2d7m^dz-%?+yoV9`s0Hfy+(6!@vm;~MBd6OU$HI=P#Nb=XK9|JD0Q=B0DS zkVBM_nC#&(>m#a0mW4Hs$9i5xD8Z1ak^3$f2Oj#f8FC(>S1bc27ko8mUWcAZHk}G& zAzcI1CTn-0eCqi+mlwykQThAP&_vb|z#By{Y*X+Iuc;Kjoq7S*F0m`rzUhwdeRubF z$DBGAy42&oHhroCu7e#SF9!-oM+SDOJbfclE28b1+;8M+kTm>$TM(9^DYwQ+Wpu*T zN^z0jUn_}0W@L{kMV7LRN#)CFOe5x33hEyFTBRhn zeQr<1(%`Uxi$gveCtQlwjrF;lo!r`18+dENdP5txlG3zZn;o*Te}2!2?Gce!lHHP* zR_=|$oe0r{*yqPH^DpUZNLRkgdudI9wcL3xP7Gm3Gb0Z>pX zU~Hn+)zdX`yi)a&md=s7(E3Fa)$b-wXk7}h?|;_F=^ayfuYU3ZQzro5FA76I0!9VU zIYx~Jkkr{oLND>2Eo#Os z8K#_Y7odcyt^n%TnHa|n5JyRd%)fgd{1KX%p1`Q*(#6!#0J0=K_v4a!lqQna(JYhizt0jw0QR`LMZ?Jv`hgB1%L{DpUdq!wP8L9Hz$3I{XqGHbTb zgVyGHs|o%dLHoR0vTiG_wN++lpS@Sgk~<83GX80zOCLtxTR<~Y>le_TPc+xvBaxMS zdwbb}a7l{qAK6X904AH}F}GjnXp9D@2%|+nxFOv~o=m;K9&Tl$p+LcLMaB1|<*^sg z^m%aP{i>!g7bpAndFjhYVbxkXg3sWxTA4BGbYoC-uu(c}z)emA%n?|-NW@ITk$F&! z5DYN?-{h?W^uZ}fGYW2v2@D*nop6_+&~T9}c9pR&5N93?6CT3MV(?4#w;V4^i)Fey z1IMsrk<3hT`nq43c=A6eBe6Zhbi*ZMFp+(*HT0-la;Iz&=-bbk4g!Q-a{STYV%PqL ziTQxwd!_8+(XV5^i{idur%zgEr?y{V%Je4$CmRA<4)T&bU2-2r#4B)W67~uUG*KUR zBptXZgB~R-3}FP!{5>+yp44Glq+j##+du>q9rjLiKnp*9&hsMO?s?Si){7f4JkSb6 z8};@*)Z0@Whj9qg?O|VJ1vg&JoMgElNwZj!&dCj_zp$~^8$bC{a!iVxu;0I$3Z>Kr z!bGZnkl^&5@$G2Pc#t^&apZLkj*Cg-Y`FAqg*QPmpmtsHCOFbpJusovQ*C}p)1>@w zpjeTT*4ZN0k$d%+bWi(&Ru!iY|D$=+kcvd6lhxcqmj~cgf1tP_5{w|sZb;0Yu1}Pl zzrL;|Y7S}%S~6OWnF93fg~uK*L=RRfN=eUE3^9eTlU>i$sh-E6NZLPuS~z1ponNb& zk*D-!pn5-e*SZJmsvxr1KlOjLnD+%G5Ja5G@-1m6^u?|(hM$j)!;x4id!{jIU4(-4ZP(W)$4Ke$DC%WPRUVFDW!*9)kmT+T?tlGWt}v<3 zfv%)@okL0808F1~3sbT}m^|v#89O}MAB=4tQiZjW)P^({%rYH2Emp-yrc*@nupQ7?1jRV2%^(v(J#0V8a$8a{XrQRB6Y4S!y z1|B`cE5X$eNcPm|jVFv~oqC3OF?KPZLA{XebRJ_v2kRMO9a|t8|~|+C;@jb>Cv-7?9J%lpvo`;9+PfL%x}2Rd=$t?F<=}G zTkyvyAl(t`73hEwrbAD_;5pU>2?}{)zRQp429!(v?Des9PpV%kNQkRdQk6<2^#GXy zBZV~c$)E&NOtKJBD8Uz01B+R2dmbgIqf<-x?Ome9|AA^k z%lPj1M%Q>QDs3ehw|KZh93+VxH=c5REejKN~s2~c!YKcpddKDUtCI{1zcW@vgCB-6ffRe|x zea7GvGPgpuM~O2ZDn!$hc@6CMVj16fQNJ)N?6V)rWbD^8UFz>)3oKs<%;@5)GBxBy|9q~^|=+L96<;h@W%@s;xcLAO&({ct6w@*I$?8DiMl2W0lGN6zXDVnwO zmyw;LYCk(Ehi?FrNwf4{*2kSMN+w6J+N9FOZ~ktz51Y*M!elQl*qCPR>YY2_fq0z#Qz&HZ3{(>$c`P|JtcHEevvjW< z(FtQ#XN)=%v67i+9eFpE{RWmL-A6LJi%P{U;mp+^vj7W8G5RfJrZE>)7WEkEAoCrVz99NM`iB_!tl1<*iC=?=aH0rb z7lFES^jW53okv;Bm`nSNqfhl`+qAcLqafnXLBE-(SAcMxIT`fPXv*f%>bJeHY>;@N z&KRBoIv^uT42+~JA5*|RGW+b{QM6jiFal<~JZ8jX)RE)#7U%?fiL8tC>xStjAQ zHbDk0IwMRfK_^<8_JBJ%9X%?dE1<9P38RFW12}R06MD zq~@yp%j8wgI@21R_|2>>r`~;SriU7p&D&9~o>l7IjzPNlCXPL^gIZPZNr9TWus$ zZ(R9vFfVkG%uIvm=uE2ivMMmJ8c_nSZ70;e^V+g&kV;93kPA;?N6?KODxmTnKfg+@ zErx_y0RyB8xnG(@C=4-|T7T8h<>zsWa&zS_#dmL!;NJTJsX3ovPeRXPYICK* zOY^T0p^RBGz6TgxMFGW_?!EpoOpUosN!IxoE2V((k-Q8SU6 z8Rl3cW7-8V68m$hL@_pPw*Yk?cpAUzNz(GNMSyU|Uw#1H&CY_fgR2rA`#@%+G0y9k zeEWfB7S_{c^F!KoHj7N0eo|j#U^#3ie=ulwNbenjkzshKcZtKXi=4aXqHb4hU*2GZ z4i4$bF>>&g-Jp$N!9=Cps3eV^NEhLJ$ja&KK&%PFBVkBHI!s?80VJHj-+$Vb_sNfp zVUUS#5vRFdClc)^5)akehj!N&@Y-#dI(-M{mA zlY@%W>Te;nI6RZ8)3#~VyR9^O^0vb_bDPFzcXS-ii2)cYL!3a-J8|1&%TzC>%_fIe?FMp#|Ih`wI}QC`0wL=!`-TJji=jh zbiAr|lI5{-RXnDbzNs~?CRCQ!K@glZ`_TOjg0`!QjLwfLXZN@JKTAEk=~+s8zfjUNMa@W;p9$ign@o&KM7rGP-z-FZb7)yr!mJ6^`ic1i zhZ`To=sjskkMGPFEWa#@3k{&hVs*Xyi!a`a+#m5czbWOL|7!ImexG}|4`f?!IUIA! zyQW=E(}at%i`L7WrzAISKvqp=OuODYY~tfNX;p6J6GFPFJIhLI6?=K3c5uh4hNlf- zTZGw5_4`lG%?UV^ab}J0L`xvwd~QlZTap>p8{VjptMs-$53@eXhjapOd8#@J`{C&- z*GL-|3GLr$)%$sHc)zf(o3`zJTUOe;^szgu13k&Y;ia^KGUhgKv_B#LFt)QI(OCD) zsrL)s2lSIe)`WuO&h-r}J@#OdvhnBl8LMPd<;lLxsHM9+bu3>ukA&-qVevpL-jvtW zWJ}W>FbxsUO>IrSN`rq3zM$$qiKTm({!XUJmmnkrL~)0#NsrMBMP@OU?FO0;oHcPw*J) zOhdpmi%c6Uy^^DE9?E9nC-6)VlG(Hgy6Kk{dZ z4-DAidHU4Y*T$f%(~jBc7-3z~u+IBAWt~IbOFMF3;rGD{#Le9Kcc!L1>2RVNoc*UI z!D6p%nu_BKhMw#%T&uY%Trcwmtm?o!RND1v!r;=fv=41t@ma;=pKoVWwv7I!7PrAh zzI3e9fUZ}UGa&M!9WJbs_;BOV%q>uuqgHpE*6cAYYrblpep}YKm@iq_i{E*5)5rC$ z*x~ba!7gKJkd3tslG`f0e(aN>_{+UJTLYX<>SElGi<e1bSof=22+F>xcy!loa&3p$Mx?7jbp9(pytXrZHH{e>nW6u`d^N zHl;6;uHp1%}v$!|AijB^jOkHg~&+6%pUYJ0;x7>}>&`%)t`d-g)#C42NHDQ{lG z_?Ft``=v3P#N=VN)*xI)uTvB5j zS+h-hdXqM8-6C+^DEunGV)I40yRlZ~QQ}mk%rZ$zW9+R}ZObMn3|GgN>{X3B{^;iH z02Rc1>5?U6+bQ+OW+`du?!7eQtPK0F(_G7pw~$wSKa_TV)3pqX795Q^(rfcA(7s^3 zi?c3##?Hd|;zs+^^6+GkWsL&-uTj@W2Lj~t*5CF)fGkTJE=Azyi>{gfXY=E$_nN;O zIBy~@@wCkKTXxk|N+aU@*)EUG7s(8$vRivr--*^5LbdlzE-?h{Hg0;j#c6F`9P^ZS_CADRIMhgY$DT_a^Sx9 zvhBJ*Q7|V=%fpcjX!5;^E2Ks5J`pCF-@jR2#0wLG09y(Y50$0BBH1Ej7*t?*x z-602NAC6mHA6Mfnpo*=FS+rCxp-x4{53qHK-?Jset(nLtIV1+F;>BzMtSMg z7nVj*2zH&BRw99G0N^XDPvJ=d4sd|{^~8{%&G^^rJr|@c5S2eew&hmw6W=uk|D~yDluanbZTZ#R(iARJF=}o?2JLR zEmrTK&srBDqxBO9+Gj-T0TEnAb8_n1CQ+<3hE6Rf&KaZge$hI8GG-~BLAYJsv4IIZ znE=T?;b{|Tg9Y(cJ_{FisfTdemEo+NHH!neSc00jw=P^imUge{<)c+iHTB~=UxqC4 zqcap2_UTUN9lg8R@R#Yc%ft|{&blG`Zk=3#UonCv^CmS02Ejo&#Q_Jo_7AD=el z<}jpv@&2Z$gM>m5q61>AVIvTH>()S|FA)y@3rGQ#1OT3G*4r0yz<*19zh6(|p@q#u z+g;n|ML8lR_cZREkaros-qyUwhRi40#eu%XtA}_8x_sBt8|=P(m+`j{(^HmvLwMk+ zT0uV3RYQFVGnl*IWu42}(eAt8<8VbBiO*7zO+Dq6>)MxxQOSnL+NyrNm}7Pu?RYkK z5hmW%Mx+lhJ0$O&_YJl1=yej;Wyw=F@Vzb*c1O}!V#;my791gYv+PN`O^R=EffsuV zwD&o$S#Yc*qVz&$K=GEHShgi&{arNC;>Jm3u$9l}o-x7}CEq)NdLJ9B=s2uNne9i| zz6@F9_xS0?_!sz=aDq!moQ~@RJyl#4O9<%X=t{R``&ExOku#R4sNH-eB_)o`sD_G4 z_J5*Ot;xV?FoAo6S3|0Qyvl^d#3B<33wMTBX;d9PDbW1n*7&*6(N$$d{!LC~jD;^A zzlen&{W4fz050QTJbW_y@VMPcUAYrGBRAR!Q%;5e%$Fe!5-YIl|mj zT=B_Xo2tA~`@}!Ll1@m}(y;DfNHy*G$S#AIx98dZ?(}=hs|cB%j}wdWzd#a7fei-IiR{aV^3RU+8p1JzAoUrYxFrQbW#Fg3qLqvZP5%6_tyM#Me26a!2 zw`s1@Sk<5R-Q(TBYjot;p6ZI52qU18_ym(O#Y%@`vrdeSku9L&vX^553e#4OFP~Y4 zdWn^Z!=hMb{uUN~sdY-;skx6#KYzF(%gd70MkJG<2$ES~yQ&bWi#jA)|6XWp#}f%l zF2g@$I`U6%%}ZE0-mEnK7jQ%cb<;$>QRz(#j5rq4A7i2%vOb&eKMj5)G&759a|BuY zzk~aR;<}##{k=mcun8*4{N^c5lzHfpu-k96ni!pTa#i>1_EX;?vQBV|zF<&K-DFBF z77>DHi1&GhUm=~}Ly=%ws6CFyALn94r)`lIdVsVR=xT@UozeOCMP}aH2XOUd#-pNS zS`Ka`05CjKE6i2to2v^&TjRB2o#KMMeA9n&JGLe8mHmNmFyI01x2#`3r!j{t%K3>6 z-$DcrwsC1JLv@VjaOj+jS$%@}90J3q8+dC^h;qnh3zwzG#@}Esi@q_MI5UpI-5Alr08O_IF5ie0-YuP<}Ro(c@v0OSwGcLUV!`y_T zuleik`sS~7H0wjyA7Z|{t#S2v#0g^QzJYEuFcMaSCfwT>HQn6yb&pSpfp2{1@Ij_N zh|<+oH5T^L9JvG?Q~>T7t9iVBF2FXP8w?kKWkjg-N-Nq9Af zmUZ?mmZdaa4_hj9X7hGO0a)Rnbd3^CfAWxu()rg{?6JEseGSdj+v!AO)4bcsylH>a z*BZn2B~j!6nW%+UGp4GjAmB~kCw%_!Og%M1!A6MkDrU`~t~`d{C)$kcKp;kKf>3Zp z_JXWb`RPjbjP6e_AIpt(In-`Z|MbLr-YrCF$chZoD{Tv(&IWL9_$GmzVfCST&mI&!$ z@4_VLI9(PB0%)QBZ;xh>4DoH!0aHH2jL2#wpT55cHMkK$2<*O0sMFaek(!AU z4h>-(KvlA9!l?7!@~1$b^diZhTR) z&L!Bl(jY`=RpR+K1$0)~cOf%nd|dyk#bZp=e_PLG-Zr+r!rmEZ+STup{n^xM5P{ z+0HU!-Rfoa@0~3SBO}Z;A8%&~8p-oeQFd_dp&3h9p2@U-$p-!!56d?Uy^I>*!nEH? zCQ(N}M>%++q`~)@y(QxIYwoZmrlu!V9ft@oJ{lTdSmpZ7ngh#f03f_!t4X+&5`@gc zWhbOWn?B^fytQM~u0L7N4~XfF)vs&Dxx)(2PWA$bo>bL|OkwRoK;D++I>9brt1F1j z|I=vrJvU7i+Br{93Sg;F`gq)k{jHDG0ZD;4rb8PW^%KPzu#K6DSWm~Ut^*4r9b2NN|TfC zc}fe_1K zNso;l;v&rh43Dwr(J)2 zsN{nCDRak@y0UkTpF}=OJ)@GEQtP-yRmB-M#CxxT)+c|l*C9-vsGGo4>%QoBV>$7X3+z8KIcc4C&V8=@M zjjvMYIr-2ahYdt^N63aq?=s%k=_pQ+8ceeLj?_eJ`cvH;JJXfmEewEME#9(1Y>J}$ zg9gdolxMV8pepMXVMnRL&sdK>Ie5;|t*7d5o%>|W<#sBA~ZRKILSAJ1h*}y#jo0f#WXLx;`1&&NLd&amh zP}g8AGvdh;|0~LnLjdo@W4^VhfpZB)Jf4z9VRZJI@4D&}&Hb18%qMC1=T3~^<0w?s zZ3CUT6FnjmQg%FQpFX7xEK0__%FY0$inZRfGHfVr+hTST3bcwKb-rSF+Iq_+9%JjB zMf=CTUneC#Itvv_Qsi?1){B&rOq1FP@uG^?)Vn0#M+_n@;0GLW zml67Z6@{JrMZu5ulO}uARIoS#-Y8tt?(I81RZr$yGTCUN1L+1}&x$^b>J23&&dk=j zQP;W<-LlDNYkIUu-8Z*Q*qfF**>TXVS;M5zL$K)1^z}P7lJVe)WUaQU$uW`8bG;q? zQjavaAYjxj@jmy$+M9O+MZE&nkmp}CmlgdM(l|yN0uW64leI3QT(xO!*iF}be{h~d zdg!G+-*b4sK19w*Y9H$YjC$^Ypm+NHxjNekBZf-d6YIfCvzVB~QF2HPEWra3^TUy)krZ5WuY-ewrar5Ab;g1P z+_@?3TIZ;dopkVc|A$%X;6;1frqAwa)p)5}1aFGs1JgOLm?QV$Jh&(Nt?*QwUfuo4 z5B)vDM@SXAT*o`YX9h;ibu+gZjL0G}-_#AR8NUAbQ~`;eM>RBN!j6!x_-OV|A{l)h(^%YTrrHf zFki<|E(vn!SB_a+(|F1B_xp4mm@lb&>z1$8sqxcJW2+{%kS<>3Rt;^mr)H4>A!;{> z?H8dum-SXcw>;blyHIdd6uleKAw3}wx8J6Q00jw7D!QcFN-w8wX>gk3y+(W}?&Z3@Xau;F8C1VY8M-tcs@6F+ zeuQBNjyv)ep!OmHcj*3Z-_&FL8@V;IJ_ztV*T?{x3f#DV8mVScKk&+!Q9kL_%m+^=>IA8)I9=(=e@JSYsw{Lm$}?@m zVMJ$isMf&4E&~($8q%N#{lI7U`C zI|${B^X%%(v$In&K0740^$a>>RVNx|Fgb)^AAE@X9EyfqQdRUo8oN+g#>W3#H0voU zJ+9^dqZ+g0RAWg_)Ku|%5PLHtyzKqKR7opc7@7PuAC3@UOXvytpA(1NNH%ZcN9*eU zv5IjT(oW@6Ykkz4!Lk=Rq|ogoIiI!)Q*Bnp05g%1(1~X6`@SU)b+K4EQblg!(2I=? z8gs>O!TGV*k^EHs*E(0!b;GwUj@}X1_^@~3A@(o1Kae5rn=QWG?2g<=wG%FeMfKn; z>fpe{7p0wX$FT#k(hZj72sFZN(GwJR^7gkm&H8F9KlB!7f>&W}u? z#$>Hvl0Az}YAm_L6)r~)vdIhtam`PgcTIkA7wj15iyu}!3Lm05_&_EiGorw3iF^B4 z{3Ods^xik5+2!9A*m$;^8By2hCDkxd6&EY>y+B)wR7*3WHK20Kf9Q4ia&&~7_qO3; zLTczDw}kA|!H<|S2oz85hB+2FWD6u)^m{G#qE8;o?*c2do7Bzzg-2#Gv%^v605mI$ zc41*Q$Ds2Kt?0Cfm57&8UfY9;>MxE{1CFXytqdo@LJ`26&F$x{LRV#!mNyv(nELg? z*nw(?_Q`dResO$#rUhN;2A@43HfTLZM}Q9YC@u@1h0PTx=HaEI5XI-g| z*0oK%s^pkuio>&)5%2>OB}*ZR;gc5}uI4&XHm><`$-9dyt~11h%sHSw$B&_hMa{VN zcYX!*>@`Wrt^?`-gt#}{A@;|V>hL29+nSo zKZ|Y=l&Ct6o=Vkmp4y(t{gqef45}RR3DZCZStDzEdis&qHIhj3Bw+b3IM@9Be}E#o zVY^UtE%b=Wsdg3^!2zHV>iY8@q_++5u;bBKMpxi$UsCp=k?Bs}A&5U_>7IT z?t=@=a5}!69xRzcMeou46P0t#4G2o52bUiN%jeVy`PB@Lw!b2ds}=~6kx2)QH~1?^HG2sGx^451u_kP!skDTA(S^#M&UyY@bbM(g zlB_k=y2Mv5)Ke*MWNq&H<5QvO3U#DqzF$PFBK8Mi79oyJp=Dh(+nz~jr67|;?qluT zM1pZU300pT=2^%!H5$vaV)UlV4L#Z z&+on||A$Q2nWGx7Q2MTOQkR?=pj-k{j?YbbMONJmU7=0PL}W~2CVtLM)}lZ}K?U1s zExvp|>dZju5*kZ$wU1v-Ib-1oRC|_sUS|q1Re)w{iUN=DIZ=Ad3h!_`()-1pC?P;3 z(;YYa@Doz0?kDtuE32q0_SS+-_Cd&k}3eF#nAx*pp&? zpHR4D!ZQrT5KsS^^Ea=(UQ(nGRxJp1c^<(zBLC3dYItDrjU%%6`V|6XVVU%O^a@kQ zq|v#h+xAp6J{BHbL1b+?*MWFV6Xg;Ng2`b>g`5D0AG3YB^@dI-%S@B|W@0v;6^y

W4X9`Y?yQL?9{7r2|HH2dCb%1s4iYy3Z6vw0~VniR+i zVYPyshd#$(s8{JgU(;H!tRLvWk>nfJVQgm}I=(m?d>V)9U2=vfy35#bDseutfY7=b z=#M%IvSrjTeHo{1$hJ)0EKtYpAIv@G^ur>9EQX_Gmq1~|r?;l1Ns!mGd`{evgclwg zve5}#1tgwBJP(=t*Aw-7-fv8KKG5HE6B8_`hzK!c@iUl^Ukwl~K+_O8%k5}R`(0h{ zsPpIp^=MQXpwSn6!{RTJT}?Y6FVp<(&{A+u0{hIo5!HT>#!PX-nkuJmzVnk`2Y^ zl<8ejcJaj~h57VQ57aE$B!bM&*#>>0zLQ2dR`E5e(e}wYQZR`P$!we0k#_%QA%8=E z?&(jER91F5gH2->*;Mtjz*B5nM{`;&NOpK=wK=bU17>WFwjrq#j$b!Z8rrEh`lTX zP!SGcIGf3-4kEZ285Sf~F+6kesImCefGYCW%Ssi?f1qY4oO4#S*Fj? zpJ1)9aXG%X{qJbI_RCLD=aY(o@?y@P%eM=1YRx(zflvN%;kfThM^iF;V7n+ebhq3u zsD`3ABS&82XTc$4AxFhD>8_zKb)*5`2CebY*5AzDco>Wy!_`SWg0=;6KCaGeHp{mo z_=n*Tgw1xEtr`}`lEtamHd}EGqTizs|;{=Xi3a(e);Gs9$b`JOO}8?W;H*?|EHg>88DS`lxq`|X z-i)=6Fum2@nZ)Vxh*k|5fJ!ERk zZhDqS!-9bj@g%SV+1*p}AEIM058C}t(CapX3odf_IMoPKj0LplpMO_SVd5q%OzfcV z0QlO>?2^jo)q*Jdx|#I4UOZbWW>!1owi)P}2CdkgM!Vh(piLl7E2=*T<2Ixv%arhx z<#q<|qVsOn3J^KCG)fFtx=UJ_5DOAi7{IP!AvZ87EZ`41RucZ9W$j5{w;-9+^uEtY z1S4c(1_$8$KLIIfz2JXGPs6!san3vX;4fh_l~MN6xft08BuD&uE{&|$WP4s@p&;od zV;dkU@=ah zvui+QZ|@lFHBioUQ2t#7<&YzJcG`qfpr204S8+*`rBPF?yo>2ct%HX!&Zfed;W6~! zyvJ11uGmKf=y1$5?$Q-L^YFI$|1Jh6^(YGw#RxF_ba=#`11&lyIaN((cblS_dMgjc zuhE?iCgebe`CJxO40mL-1H-9(bd{@faOBwV$@a+&vOn?UG{mkQ6&>$-&HGxrB>Q<|L9~9AX|Kr+0?9+g@TZb)0Q}CgflL9ZD=IJukb}) zrh*;MSFTu4b+9cVe0>dn?EpxgFA>&IZ~~YOJ+@BYwmBFx)}w*bb(_D|sPYxpu%Rv! zL6-I{b&hZS0}UV3I>ak6lDUEiAy~(o&g-2e?Iu}YgaAwCnSW`WAm$1H%cOz)c7}@> zKqx^ySdvr=nd0QVo;bOV2ELD_$Bb;FkD`CU(HOBkk%5h!8&Hstp)p$V$QM0|t|rO& zivbCkBchuIr7$%kv5`Rodg^T}i6X5#n5;m34mO)is`h{iS}t+eKcVq%2|}o-F#{m8 zxw$g8h_0qzFkcwT7u^aSW%#nwr)eoqxx~!gAV?Ft?}g!Nh$u@!~ct zv8!krx4q5^kFDa)q%{2W;&!Z%CDTS7HHJ$?Zy@S^vyzA%C4sIPai@YGsnYbve-`bY$ZWNhz z3(jdMuT&o<8(|4Y$@E4Wyq(z|%-Xr-KaR5Z6K1qQptPpP*Ud3d>LrH{zW4`I3Z&(W zz*iyjJ|r5HMe*8X_2@}9bbMvckkRo*L=V7*ZsPCL|E0mf`kM&`@e3s8Tt6k^DG*U6p-q zTm6+CGFubx=WI1x)qF*!_~4gqi7n^lTG+?M#O1xW+xuI;sXbJbGy3S$HL=>Wsf}Up z7w=DZrXUR8ZZD4_6GMCzbR;;dDznK_!fPu_7_ zQOJKj?>CC-W2UzA=LFr7S^PRY-hn;!(90)l%!*%Za^5IejfLIKKHeGmsWwHakiW~P z$@~e}BYt#vVim>`jjKzaozy0Q$lf4wMQlK63`^$*{@< zE&Q{$lgXnUd&%T&)O@Re4<|mAtcM;o=lMzPq8$bw=3aktxw!_PJfUM<{OT!NHTl6x z+ltCu@?jIoj-8w?w(sTHq5&^AncjZx`?iDrO*b2Zgdz1}-J_Uu8>xV2+s`RVED`iv zS53Y?R>S7Y@B%!gu3i`CX%#Yp6U5ltljovV+AJPijLIpivum9qa;elo=F=P79y`B0!4SCfPb~@f9~l6CqR-^QjShMW*IyanKUQB^GWN8De!LFO|ND z%CQ(uwdi~=2BuI<8doP(B>!dZotIqZPv(1BVy_Orn=LrPBYIt*lp#?E1M46dv$xNO zpEMU-ZgxmCREhMw(e`K4<3xIS@?Nj|I^XR4#t>+i(pLRlcqXZq}{5~Qkp%c+&)=scTaopqMMz* zK0!Q;bV>8|R3VHPaqKsWn0 zkvxIVD^MDwpfiFhp}YNma{sdol2e-o3NlNg|D7VXrmA#dTFP|L&JmGaj;IU_pN!SF z&$(=>vXM6_Y(xsP;unartx)+)4NsgkA#%PdM>nmX*fxAUAwr*Ci4G31ogR#(oHG%&1V zoP72KC0TZSi%*a}r}k)Wz}=SXjamOqn6|^fso7Cid?&i=fQnF~M^>C9aJe|e1dib9 z5ZUaPWD3z0$Qi9EjkU1?B`JxUN$KYz4P3MdB9_?M zD*4t~pp5X1!Wj`?iEC8^N80cPHMCa+$>g1}6WsXG(X3|bXu1maQ7w>IQ3$W(KCG>~ zWN3Y0Z^x{}P0y4!p$--BeBuL(M?n%KnY|CZT)ne*j-1X3#9)H611=0*LMB^%-S$v; z?4Py5@ma|3Map@6YxQnRTw81Lx#EVvYOvcJ!Y*U;*E!pXYdZ`IRF%THy!~^sPevS` z;XHW74onIx(2zr-Suopp4jJ!FT7P(lM#ERXbdan^9*1PBkYt5#UXAyMQdj_u zXlh1acU(~V%z9#CaPp<3=Q2JUmq#kXcVaU8_v6F&C{1GOj6brKTSEHSYXKym)p^6go^Tht;P0v`VGSJ(f$@Dg za!e)Q^&BMWMbB$GM+{bDqgmPUfYnqPz(_&|e`y}kn5<`$GD z{Hoqk#|@CGyM?IlU&?d&5cbTIfAGio?W2IGx(N**cXZmCJHeg?wQyQ>H>XN`dBqR! zVp-?yOvud6NnaTy?S&j+|BVj@n~w$laN}ti1rW;k2Ps5{lnG&DUVDi$^h zeng>l^bd!Ui=A@s;@(Ett+!UHv438=v~J7NhD=>sU3**Q=9L@suN=7sh=A{QXLuhi zuBb>{k2kh5q=gFK%bnKTQ~f+$0LoPHWAUwOj?K{thm2V3?Jqpq;;9E2^XUw^;a|^s zlj|I|)h3JceogWtPGrg2x}z>psBWn135oKyXqtQLhE4A+Bs08LwxN6PH0kOwes{bB1pe_^sLaxF3k z0kOsVOV$I$b5sP&1SiM+8#%~5<*(E5Dm>8bQEwEMMimg2&KUzI+SmKQL6^V7y+-1d zU*24@iTK)jwJ^f=G?-U@IlA*zbI-eY1}@Kz*w5nut9V(~pPWa}l)r_YwTCNsL60m4 zvohR&{X77PW>Dr+zPURyv(qmb3z?$AzlD0QsuWM*SQ}h#UV5K~{5t<1qWo{>ESF6z zPdI{(Oe4W*#C%Q@Re4&2moR|SJT+O+k4!9!**ThHMQ|Aasj2aUmt7` zeooksq4&}wS#@axDDhXm7P!JZnRrRiwFaPHT7re)u#6W--nQQjiFMAhBmB4vII3bq zaFjG9M9P;4Jg>1#{T{(379%CLsL69St}i}@(o9*DgDh4zYD`(@gaWqW*%3m{Rb4$; zWv4qn%#cYa$=VjY(HmGm_bdmjthmUPp!qUs?Mu_D7*rL{a$*`-!^4g z5B9j^vpG@36oI|FP)qg4=6>E&Tv^h7eYNmyhxYWJQx-32&99mVr&T^a z{4#Q_z3rTAFbvE(Fwaa7255Y#@Dk2fLU$&?{b69i>AOZf`nzU~<@sOt>YxcyXTux$ zlZjDa`{+s!IBcO!E5WR02p`A_uLW!m1^jKE9Wg8IJ**?s8SaxmwGnVtcE~rwWq*70 zzJNdA6u9t$oFB`^wgI*wy@eIfN=P_Axm^y}K)nxC49RwM_*I5dz(ElwX?`n%=qg)B zpUmpaNG{1(NfeB!2^`+Ob7$K=ymgj!O)ntGBT*nEGEE3LAGsovD95Q2}To>TP zFMk>~j#AZ2g}D7gInsk5vQ-&IB>n!yAYkE|bRNRJBgk`xs}iz9{o0q_o{ zCa;5R`6HEmkHC^w0d13exo>lhA)UBsRS!Cc49XYV(4)~grp zRUaxtIjvlHdB}^qZevBGV{&F$l95(EdnX%sscYwDhl{e1gEf01Sr(ta)Z3PFu8*&Uh3wIIaW;F z5&Z4cvnL4bfkTP|%l+pZ9Ij0%ZYBz-0E&RP9n68o9WkYr#=_CBTN<3}emUoiB;}Uw zO3320bnCO6Wbbd=r#cC3BlvI{%JBtoDVe_N8wyWi{|q$VwRCFV*d91`=>{NFe`Rh{ z=!?6sgeKM$=^}V}5m~rm1T7&5R*CQJ&H;FPyE7gg7$0GWekfJ^kFa&NvLx7gj@vYW zZEF%B0y3OoGo%7BnXu~;4-XHALkjS7s-w78CvinsLsp`B3+&5gydU5zPg~jYOf*uo ze$QxKE|2B6R#kBET8BX!4Qq^o+!F;1%o;Q>&INmJkGIX*<2>w~om^<<1|tB1#?Tb+ z>8kS(HFuVfXy3)1w50uELDFXM;0-D2s+qRy0)f--9)4PVPvWnJ;jzBsAwZF^g@pcQ zn!kz+ZF2gy|vTnn9@1Rdpfp;~StcRtuQ*M+Fmkx-JG#r<|Q1+c0#u9s;*296( z>2RX-KSD~K3Q;)*ImymS0skmN?AqXfBV?YiELdCEe**$h@sZbfRS#nhNd>8`g(iUL zI%C4lP<|UFm{r$147=6(ZJ3t*x(oB0LlAb3Csu*kKI5h8x3dr*|7?ZAt?jG8E7 zK2q^*DV;+Dd|@iCV*Ae_q6U0CC(O3|VZkMnjfL+s-`4$f2bYFsGWaU(bNJ6);X#xP z)yKMbdSwNfA$8>g7QLiyFc*bV8h4Uh)e-sUJ%rSs4ThkL*QP>{18ZVawx9r@p?KH= z$k%gI*#tH#o~>#4lVv3$W{GbgQfs9Bs$ZK?FE-xgYCVC20qxnI)?*Nn$aM6m7dli- zx0o{v_VYVgm@hugO{$C;uB}q;Z-L3W*m4l`C3!LNOk*IC0qu)?to)aR|O)WSI?Vqq$m9-nWo@%6}66M+zK z7H|3#-EdR&{pV$$i>-tG+#oq#9W>K?x6YZl&IqzEfOAb5R~#Tl;^#*i*|1Pc1`-W} z>3@NBKao_Qf!<(Y1=$~1=%gu>@#~-FKjG3?M2deMK=(*K27bbucAUrkQ7MEfBKG*8Xd(lm#>&dqkaH7Joa5Qz@7htIoJ{{z01ZprQerTb2uTflQd+NrsK-8N#3885a# zrvrp$C6)d)ABC52E8LDyoFm)81r(j+{`WrxrVbE{!eADx38mDE(Q6Trg#QRui86+Z zE$*JFmQ+z8HXk0qGhTh;`B;BY!*SRTEuA89AjRy8;PtJON4(mNvULcYRg^kzi_7em8AawxqrEqHU3si~)R z(8X;Ho-P2=ILXC4eg>o{G3GlafRZJ^l2ICirR*0Z!51?!($!h}`XjCcE+7jDgTLL` ztml5!+F!u7eGpz(tmKyTJ+;Xc<<&|zl)AR7$Swc|57X3qOx*!0N_?&fn*&{+bUnwf z3pRr|%i!J1n9QkTC1a-*fqX`s1q_0yNcv!)KWGtneL6X?CuHb!=)F>|A5(nYT+&T=WKEJcOlEqA93!H${{Mv0g^Ol_ zLm&$U+b^8N+mfw-&h2;%CNui2N7nUtDqblUjWU4Xz1}`O>yoh9Gsj`p5qx(BLm6%u zfTY&rfyl1T$~xJ0ZUIp0o5Vww#yU$&E^KjX&rKA44u@-LJcWScQC?JMhqKr%i7P95 zYJRM5C@q9VyHi-}5hy$zN?RYQk;)aee;HO`#z#@y`ZEY?fwKe(7QRPxjY?cck{COF z0wX`T@tFJ2OD8ZVjTb@QI>7ndqE$x#6wekR-UP1%qE!C5AU_YWwa^na-|Si%{Imq& zs4>LDHFLl(P-^oEMjPQY_Yv2rhaqC8Z=qLfRPx;{|0enU%@kX>lrqA&DcW?~#y98s zkkO}V0kLMy$*W0`(RLW!TC3B)H`Th6#Lp2kp-xXR?L`2dbnUG11u?9gWW$-KO@ zcq{BEJoRi&XHwHuk~ZF08#?frD{+jEKcOK2^pzPCWG_Q9>QsV0fDsDdQQ`*;pLzWb zxHJ#1i@RvzM8$qV6u0714Gu4muEufV$_IP%&u8ykU)BiG4-l;-?T{SCd~+=Qvm?%j zG^u3)a~Fr~KUhfOUN*=8Sc9m;w<>x}?yTzS`GqC9rNRyP0+_CRlU zr4_*SLzr7}teMr9Ha(=iwS~@|k>WN#<~4Xq@zZYgo{%9s z7SKBM>6=f*vsNJp1XIAbe#0LBof+~zPJpDKnN03~a+0lEYE?-P{L8*0vPswLU!9^V zsij`ut~H7(uU8LOyM!2xqt!4s8sEBc`qe*%XvNw6fOshKrgKQtt)qg+U2mGpa)15g zhe1?~EYV@`FZa>gsK7MO}F_q0KQ|*Gw_Na zah$6_c@c{@vvc7e#|lVbI1T~8JSKM+FeP51vIj~^P1*4S7(2Kwuh8e6rsu1z%2)>i z`U^5MydY8n!7T(|in7bP8^OwhH=-aN!cL&iIQ9rO$XC99J#H$>8Nk}O?F3J(QHBJ{ zE%GFsPp!Uw=zJ|4$he{q1;^iwrTisatDXWhqJE>?036A+`rA5{EC?(JGk?#L`8myNgC2zd^}5LH6a^cCuw0g!3-5GvQYrMulm1xWF594RtwV^ z!B}>T=Xx$5Z*M)ip7YDLPgmdCE+z7DziHm?jwl`;#X>R)#2`bXVqx_}X&Q{c^sTad zleDZS?9{8as`gU_RqS)wN;7>HqLi!P8R`f;#brWLl zXk&+y4qqf^;$+w3Y#`R?9c;)>JVy|*V|7|eFRq@7GIdb+5*pg++qf;jL)LT1>UQMq zvt_pHMlY`1GWcdWP<)sdkoe-;vQnmSa5lyCztt{`gheW8;I*t(G7{Ib%L$25?03MB zQ6U>MJgA7!(*hZq>8H&?Q9u)ayAaY7!SbjA11VDxX|#(22BH~4bXN`=0GPJsJ*bAV zlbwmJ^b2%Qf$rT)wB2a`_qw_HY@^<%`J<>X(EYOP2(rMl2QHYcgXGUPVbB=X4z^CU z{t!b>-C`Cp9B^59qJ2mygo=(F^1FzuM$=;tu;*EVyD;16UJiDR`JD1EfkA29VZ0?)h%J*IuC7I4OK+Z~tIqQ{#se6R;dL1tbd?1n*10>rz+v zAxJnR@}db!axm|0bFw&oz>62xA7dZVlMu%{*eHFoT@bddZ&Wnz((2ynkp)s}w$c4I zBkP*sSY`G#V172(Ei12 z-4c)uqPR#t$QrDC-j}ON^Wh5RRHlRxmWs_ue;uSi(09ygW`LyDy;lwJ0-R;zpMEYu zj`~GA;%)c#iAF!8_+pM_pF->dY78jZAcJlnpn~l|#!!fddF(>h-Gdljf|n4eb1D&;f|L_yQl#MlL3gwn!7wB{v@=h1_eY9LDKKTx7I ze6S@BuL5Ygis}~SDl&X1?Y0=ou_&3e%keE9!ZZL)T#JLW4xLDax-v&*j(vs<$%YyOj3sgPRa8UyaOQ0Lbtn=jq=&*uh#=?9^aNg9 z4?}6bMWkb<)h9ZH^>cRc^e>0+Ai+Zl%=tRj4Ve$FNzwgfs_7{3G`n{KI27|MtObQr zW9FlU5~ysAbi-^Dzz-k*J1@&zLsZ-14QD=)+VK#0!|>lu;!d&` zA`I^6Tv2A``SLZKda{&lpv=0hveq)$bAPf`nE8dCg#GB@TD{At+Kt#WGJsGYLrZ6~ zUoXc7!?HASjSN=dm<`3up+3Gwn#sBbiFJkXwI1+=0t8yF$o^4%7J1tx`$a7fbswm z-G8kxW3L6kMSB=7Ydww`=`}vV1~g#2gp^y*yZel7nAvW(dDFNJWx0p4c0=*p6e53_ z|Ds6_4uk{}Y8|S-2!5#O5_UBW#RN-898D$cB@V}ebo;HK?+XFqI7zZVfWzH=@ z8BoDQS=IciAe>oI`w#W;4A74QP<;PU9E!Pu*^~c;4AvY}S%I}XcsrJwB|Rlo(522I z-(GZ53FV{4;a)Y;5Cee>R#_nNNfwJHMTFyx=t#p-Gu~rU;jCzgr&0Fi?(pXmUgu#m z{bOXLFncegpGl)su@$@~aUVjSU#&)=Qj)hZBJk4){*n$BcuzoE0cf6~APH2W^s*j^ z&j8`OiE73{M^NGwA~y9&u<{PpYN9`}8!&+s50oi!CDrE>NmFEqB30G+cS0yvJHR&1 zLTPu%5LTZ7Q~?Cw!SR7SJFY@Zm^NwB!dZH3G2(|M!3wY0IqPB+}_% zB_tt0Wwws{6!*Wnh}j?l8~WHX)~$CC6(U}S+#&ke>wIwPFxk9QV5-W|GAb--Ra^i< z9fT!Hkb(z7gMI2_PvSLv_97BLk}uyX zN|5pI-V0pD{#PsnO;R`hL+eM|I22?8nK*| zZwC#^xlldXf`}0Un8y_m#DFPypyp6?3zIE}GO>u?0L^0~M8qG+Y`#=MAqsvKv7CrU zaUHP7LTRxt{DArvIw;&mvY{i5(g3imUjbK8go&a@q%>-KC&kLarwL@QiUe>&g%Yg9 zS5eRcD?MR3>0OeOJK}`Yr;aW5_rD=>-{(@6Ov2i$jQM#De`0*eXBaA2-@S3OKzH+7PDmzZ(CJ7yiIVJWxpzsWC@)tdZ+P*-~hS zCOga(TRG?MHkmuM1%&Bj?0*F6c}oaDAA}UG2j_CFM=S)M+wE?v#DN>=4TA?|alXu(m(%_I z;E`=mssBqJ604Nr8BR;mBN)usn`gVU;lY+x>@;D6(4^VrHh`nYDW=kI6Cp;+F%B)l zLCbytb0HHzV=k;sVvis?*99Py@;>=bzXiJ*2S+#Efw8~_%pT}7mM;8P|0ZM)@YBVG z!&_0<1vVIAh5H>Lhf2 zPBGj@b)W_OvF8FVq+fHsed-l!He>1^Yo=|GKQuFkOY!PwgW&TiiuW``H`{(la=A3y zY)|=^<;n1y&p5k|fQB8m|9WL4F6Y}cx}gX4TFj@f&$DscQQP%BkZziJ z0M8Zw@^62C7ig;JxXa!6SO?_8_foHZKiQHtg^`ySrsOM9C(z4x&gC6NxkoC+FYWm>aJTUUol#Z!h zTvy}y{-b;|QjL`hh$7yIuGVrL%y-0JhMV1;XE0VQ|5J8U1bXu@5H6P95TRVS_u}c* zkD{G;<9~c2L((`^qhcDp)8MXABmzqdIW;g8in;r-hKXfsOhTx3&;e=aYJEN$(BksU zuUQd=!kL+0_m)3{o(M*rka3w&=eT6x)v;zo@ud{V?_WPwF3gILF%J)frz|#S!nDil zglW6EzH`f2?WfeUyLlga+oP)kt9BmYzG-cHNEIHeUq%b$h1b%tm3+>fns_ixN^n@c z{`S&GO>+gd1v#!5@4T+`uuX0qdawK)dTRTwWe8_wu6K(xf^XM|^rKatDc2o27E|JdM@Pk1CUS}_E>jBP4DTdVOx*jvxgeHJGwwoTs6Oh8!?CU8R zz|rI~M;qr@!txDu4gWwldP6B^{Z0KZ=}ed!JSZe~IOI^;4SUE> zvEF`fH{;7b#XH7@eA|uJS2u1~p5>d8ZT@_x%ytx`K)~3{E6ScpSUHY-G z@k0-pZ-EUhHU?vzUU6f_0?*o1P%{EyJK+{uzbl|*Ts?jRcxLt2{64cCE9l1Za*kKK zgM9DU!%VXj-wipSmIhOF&_7^+OsgZ2Wu6TpvpB`Wb>@Qe7T)33N2t!v`1?OZ^CimD z(-)!HK-M+$#NBzvT;7QTx4QH3xm|wd`k>^iNSf=n=Ff;rs&sjpavMp3GFK$h{SXPN zZF0QM6{!Q7#49DB)*}ksabx{CZ|C(M0$sN=`RI0=Jy(PI|24^m=Q@0N9j(;S`ov&; zT4BOqK6I1f?xQg=9()~1dy|+S>H*>o1I_PHf;nHed5b(KKTQ3>DAh-x3GlvhvuL@P zcpvm5YW*5~D>>uL#TWfe{rzq+hYI0a!7HHPqitoDy6a>s_Dlaoaaj@lxi-bm?+O?c zX({a2m(c*bRmP_&gVb-r&dhy=7lDFh~ak?*o;2;k~hy5_<^i-Ty5 zJ2FY*wB9BMfE0KXPQ$q`8t3fWw(^Qf^7Z&lE4N1Wc7KwN%q8OFnx%e3)@y$`H?|`8 zu+cV~pR$!8hr=gp0zkW%3;0(-Z>i8dcoL-OU%>@^mZCD+a7sJECBt)f=K-pZ)I_1V zgpL>7WFmENe*_|Z|MBWpDVVB#M3c52<^->mxkC=JGfqoXwSm5@?Q)dLjz0=Nr3CBXpciYm4MT z)H$R#>L0*#o~Br>^jrSzC}6AFf8h0vcKM&J%9UZFgP->DnPm#b zmy1VMdiY7s%*_wSR_`86^sl7`I$=npNd;PgG}|g>0V2kL8hIf$lEb4rMclSQ1Ri5m z*>}8GM(2AnMTq)Qg|zyG(%?=Bn1a4f6&)$C0VCmpSvX4F`PU9*wn70N_om|)6$B$rb z7WlNFqvEr(SMEL8EZRk8$?##hwwH|+rZv>Ug_vY8k0kc?1Yp>M7cp|@biF#Ef7({) z*X5wgqLll~Wuf3U(!`g4gze(UWLaE`(E7Ml#Gg{fA`eAn#b?y^uAUEnyam9NG5>CE zvZP4<^U~*3c0t$YyOrHA4)xuo=*Mq!lLPEJ-XFnAyY&yEwDmjAmRNyPR$?nHlk3~% zx5<*==dTazZ~1jO4Ws@vwVAKc%rOoUh?C2Oez^RHNYPG!Q;MwpUq^x(x9t|CVh#r# zb!pDZZM1W@Koh5S5+ly=En=>z4>)NnC4RGR=6jekK4D5J#@=$<# z_Tl_*iWNz?tXx^mK18MfKC#(Q>pM8v3QLk3k0i&^&&f#g>VLnkz7OC=CPf)a)C^ zXi%&e>@qqw5(Kl@4M+j5X6?q0voHl@B`xxwQm`9*<;M2o;_lfRCV(+;~{--}>2iEGQqfmqc-eIy}V` z7^w+;n{V~m?`g`SH_mfBYwMI=&MC<2`ulF)&@#`UBQWYTJG<7$fWf9H4@H?PAjiwR zO-a+eUaGBbq9eta&nlf$ z7tCwl6ozOz;}vnntPOs^Bc1yT!?0Uoik8*Y$T$VUeAc$}I0snU8uZ7!vZ_n5j;`WT zF3dU3&)@u9c7{IqtyvF10QZfN-NBN;o7R$u_95O4Q$5l8;E1#1@{F;*Eg1PI`O+?t zdoIUkoZ)xk33H_-THwehD>*x*^q5@~vaDt6k2^-m^gJmCbb_A3i8H&g0Bl#22@3zo z=}4I(H>@SMj4<|<)m&)3rVJBxBz>%TY^Wz=O!=eH(%L~IlM*Y?qt`7NP0K9rE~|6s zDMUuay08$TRlOdq)Efx>S1NsEq>jQ9@|n4&y*=x4M$KaUY(T27J^udA`>lnZZS%J) zA`2%UK^O2e*ZO=i5mU1>qU~+E90Wt;!xY=`J2hTQE1c1qmNq%I$H&iZeT`cYKS#H6 z6{5P#+e5b4r;ygj#zu|-L`|(i;R(x+RzIt4`Ol#l!@Kh^iy-e>xB$QBDf>Nn z$&#HNwTE!4_zA`z7{cz)pk25TurFh=BOE;`I2aSZ8NVrMK5;*P0{;S}Z-;P3H z%-`OQ)t3zFK0k)5LnP#8W?sFp6s#v}E8jz;;`B3rkY{M&EVyB9+weJ!*b;t%X?w?7 z5|4e&TB5!JAsH!4TE8iA3`-|3IRMfh50PQA?$VuJ@WCW13ucuV4sJ~4`_TiZ>EM0q zH{v4oGyiw^Vr2M&bk7%*nr&7nYsJlzZl_+w+7l{Iv`4=5+u1#EI5Yi`zf1l~LN~D4 zt%VRUd!CTtQ1||6c;NEhL}lD#fsda5F2~Wx71GE7`W3q1AJ+Gg907UHzvKZ`^1#gC za&0#t-Pjl42e?e$I1=4^ON->cF#W$PdG;;b$X;3%rhPEm?1sZt)B8UXN#2Tl;&t~1)E!Z{UmoPqKimA5lzqn&+{LqD;vbBmRuppnA zIL>1}qm}2@nX!v&e;3yU+|N67XmtmRD~Ur~@5n0-)34b5MhYf&7i51>{*8RNc9Ayx z$XM@afOh-_I#R^xJ)>uS;k(I*vUMRAC)W)6E&ohL6y`(nz%uf{*69dN{uxu5u)7;X z0~_hEAUpGaFtorMt5+j!C)tWANy2bUJ^8RI<|EY8UrD3h-7{jop&2QJP2flFZ@@;9 z%{Gz;;2-!o@s}xzb3ZI`dYB^H^Ph)K6%83znmcZ!11iKwvj0C*R3Vl|`*SUj6nNtK zaGW{$1b1LL)YIVZo@8_o5eNJceEhV^)p@h?1_YNBZA6(TG#=IH?wimE6=6t1S;gaQ zDe`ynbjk}-?`dWCCLF~Te%wsPIs$YnTjI+aaz`X5l#gYC*(l4L43|3A0eM)kX$}SU z#R|;yrJryLtI=ILJ{0{mZ_hlq)!tA;Bq?OrAr6yp+ zWoNtK21B62JbB9}JOzvE$As&mX6_~PX(s@A@ZV74{w(rBN&1D{i^fM^FNN)jRYl82 z1$cw_({XL|!#1= zG(XThgcvbJU5^A5c5#7hkgG5L0{%yBdh55mPCsP3^{r_%15(ok*6_k#Xkv2JEDVs^ zTG$ipVcLTK+8YxeIN@}st;PBgzMmevQR}y76WX(W9rgSKUEca% zq5*Vrp9(*F*GS=-liWCG>-WRvcc0E))R(%BJB5x^PSa^(kTHobny_8DmDwj@Zc)68 zh`+utu~5ngTzF?k)!21ThsykubY+-UMx%|g6_OH**NmghR+Yh&DlFj}bVAAE+p{*$ zart2d%R=OH)dE`KbthEW{n{}T>B{$CF-OujrC-t{Kkl!CtMU6+v5iIdR1MFyt5~w| zKS~ibx|W+!8daYC@;2b4Kpt;ROmFx@j;#7BS_|atqjrqr4RawzOlI++ebTsTuH%Js z7wRR~DyE{g(T90^1ug}#!)MB_z#$R}i|}cTn#>SX7$`8H2&X;PkXB0UDsbS(wUlC$C|ugW zyH-0~xS-5b!&>|-{EyUgC=mhh5aaxVP+Ll#W8S_40D2W# zp&x{{v)}8qAG8^eWx`n*D&w(U4`;vaos^{ec1R~?qK%jMldZ}@=2?HSO_lZJh5x(X z#<2A;p6uYnjkIV}Rp50C7NC65_)G*>gU9Br)wvFtU<@2A9omp){ynZX^`>*W{zlBX zrHx=+>g7HfO6=4j{_y0)+u-xRMgy@y-JzpnHLf>Ok`jp%T5h2YRD)f5fRq}UST(^3 zs|pW?+^zo;50rF62*BDvhzd1RVWL+xPRF##vX zAWPoxN=yu!ss3yl55iWB8+k|7)q(o&x>+h@XuD{2QJNs<^?Q#dEE>}tJ%qI(+l3a; z#t8vk1DxOp#i{!ObVtsQcZ6wKZW85&^0KVE^qeZ~83;He^$sGQ5@>DTEL1?A2~ zKlF3rp}oD3OqFNH;#))h#GZf|H=&@54}y)2=l1pgrIFNd|M=Ji0oS4Ap#v62_ePxE zn+Y8Z-3)mJ@MBPkBbvj`KDkSe4@)1FYo+?D-b2EZeuy6X{PEdleFs`$$idA7>7g?P zCZ35vQ8Zuxwg{Fh-DRl>dr^!6G=!e1lv!@T0}3;EyyRxSj}=IyQkK;wGTf95&7Pi#Cnai8O@8 zVF>YozrVx)VsVykrItDIs>(n!M?L@@TwS9CmlLrw#U^I&z7I!Phe_wTDe)=eGDjAT z@N87pp^tAx-^Hd2!>z{m4rGVwt<_X1%e3LI+V}Noq=&z3wDwEq7Z1N|?NbP`SXo-z zJ8#d=D~F6v`pnt%8>?4Ct5NVL_>!JPkNur>WWOqPj$Bzm zv3L^CH=mUIcS3Mez1PXtIj5r^=}VL4GF0Loc{Y_|I>#K-aecmJj>3gDxThGN3Vu&N zr8RBhspC(C3*^rjtl8+cjO;rY zjd#J~+S2Rm4Nk*RCWr#DW+V%dvlAGS5#<{*1~s33!ppEx11ugvW%m^*R;5vaS9Z7q+(0t_UXEdd_Q%tXugwE>oz!lD@2_RffG+4x{sGUuX@|+4ISX}qcRE$63A2nZ8zv8LkMPS~ zX86EPiVSBHx!Z`kabsM=jHP-im0*qx23ieBFLla_KYiMDpPu)CO+iBPrY_%`;gHX1 zbPpO&pM1tmx(AOi-aYpAZ9w{cG?nyB|6?^if)Xa7?(osn`Qcu3_olssBLV4AY9E4L z*ABY9$wu^_=|gURrGo#Sn7sHnHL1=g?XWKvIZ^6x&DGrxECk4DRutc6xXuAtOnZ&b zc%%2UNyjaJl;#pmGWo`~3C9=qQ43KoK)T}!xjjnz!UiyuZ8(g3K`*?fl`9JFcm*N%jI9I0dg`C6gt)RMA7|>Or9`d;9PKs;Dc-+7bSA7Yo22;1Cq}Mvi7i2S<6KKf)(= z<3_jVWH{`82(d*OuYQlq6<-R8A7zy22nM?N<-%FU=)dQfb81X{{F(M;q0Sa~|2=-Z z`We9W>2)~`2+*<0-`h5Lm(Tk+ZW_UU8cX5IUy+3%S^CLrPUC0^ePV9=vr=}Y z-_PuE9qzd4Q@A9wJI{DrfXehf5r%&*2pqtgzHtpqKKAsw|MDM-PDXfj6H&d$~m8WuY%7e zfNXr0tjVS-wZLQW49(4Xl%`7l1iuLO_)QbF#|82OMgbl|P7d9Hh(G!@A5DY~!UTLx za3ey|mRB$`YOR&N!ZokK^!}*-gZ`{6;@p@%w`gv`Fw_#{ z^#arwxx9xo2tP2koRzgmM0A%AQLVM-1Q*W~JEVYnP$d2)S+74!PYBX?HX)hw z7%&5@W5R{MjOTy>Mi+tsL?Le1++{;NbNc`E$b{XaQ^+xyj9Hvau_qi0I#on%4zQO7 zqY4?2h7;z#PBn$P_Ejqmhs|W{o>6iiz)3P`0GjLEc zuZdG>710s{l`00$4l)~8sSfk}1L<6#&t!VE=q)$kwkKg-5q0)!nE22Ki*f7G_ohkr zDpzoCSdrLMTv;|*DW3AvQ?Tawh58>AaqZ(nWWM|#Ej1xW6jRhbYuTOiM=mSya~MtV zSLAGe){UMijTR>EU*Ch>lO)IX-iml<@n?F{<65ml=;0h~? z?VRFxaR)H9AbtDFuSue$0j(Ld_>2XiBL*e4>+vorG&e8){c4pZn6_<2k2W2cv}6+3 zLd`MiVwJ=h){&!t(8J1403QY-I{}5s1+Od#n&U9`G&`BD8l2%Jw=PA5jVBEC$VqY7 z4JHX3s1=9PZ5@z5{1hEHZtBV>9@y~%GLX*QxWRr_`+q@}@#gU)568@;?Wt%nbR!eF ztOC?$_!x@=VLNDlYC9W{sTJ&g+{^xUDI73l!m?NF24w}Jg~avi`J6u0Wgu@0l)`p&y7!HS+e`SG@`5o`S6T|6HBF{ZNRtzH$^Zehx6w~-Ko+Mx_qC+djJGfgM1s}jc zi6Vo@5(PwWZ^_=>X-Dwk9(yLWRVH({gs0EMxlzOHn3Zvx?7E{E`zxh7&i|~i9j9X~ zWq)Twsc+%gcuyu(L~lK&ik?jd+#(C!#HN4F*InGjI}|WB#O>@Hp1k&ObCP)9I|Wn; zV+=)l`-TAVy3Kz|EJU$@Bw$34yoJPIyM$x_iPu-l4UL#|+ zPy8EOk2`Iw)g3iWRj+wA_X>D6ClpINos}9_GwpGq9-GXhv}}_+&klzxt9K@~@$K!a z@iia1ZmdyQ|x7QL=?LClHe>p}zy0Q6jz<}4+ zn$CuwM?&nS6V4jJ=E9Vtla=GYY~?sSS$eUjZEOO)WUB`+SKQOly504pyxn7&Qel?l z$4w^x$N*$K27FOkBoH@jL!0_0a-R%Eom=m4T=nPRhXj6hl7aPDGPdWd-7}-JdToQX z)xM4Izh1IVfqh!7 z8a32B(P;>eS8j0r#fP5%MW^(_l_Nf zAwE=bb#dnL9;ozdm+P_LjPP`g<|N)1tZaSW#%3-#ABr? zBqgt@lc<9sFa*kT9TFMIFIzoRw%L*c_$%Ln5_i52sPm}(d8ql0PH#$3|6bFI`Ic?I zL0HqCJUV>r`tvOpg`5WG#bi13nBXn-RQ*)i+cW*uAa5f(maEe#sl)4*BH6w_07Y>x zE(HWMHG0YRG6_a=VQl2TsbHCQJ%!C zZfc*dXH0wl6M#R(fM<^a$2!Ft0-AkL1PxJ@4!0 zY_Y*H;eU9JsE0pI`f3v7F1u^UBjQ*g?p+=9?svVkWizj*x7y%rP)LN1XY`aCtZK6C zUbYWKik5wp^|8&db^fJ*bg^D6pEEI}uX&ym6%MP7gu8Wi1*I;@c>XmmF{LXVi-2)i zveZ^M(5WN7ba_kk4ZZ8lJ-Yc%$p!KU0cU=+%ks(kkDeezL~93UEL5Zf_nhI8qTCuX zzar*8JRkc91q|ar>zeO8(a#;Vy7s{W1QknE;;QDrW@e zRa@=ac{)0~TXyUidZV^3crul-_iE#*Hp+xP6p4Bo*VkO$09ZJl=hy{ZWJ%;%YY>C| zG*qV)@FCXbOB34-A!la%HwrIDgT|IOjg^nNfWO6f4&f4KD$ID<vPgb>SqUHTjYo7%2{e zFI}$uwWj%7zYT0pBFY<_i<~cK3WeJDo8s9(yjhtclbXCb1QK8PBy;^GUnL%Qe&liH zQSD8=DG~N#xNW`D0xuXiPy%G}+v6Y~9fq)zCbPS+~Iu7~Y?@mAa8l&@T zFfuYFyVbxOSvA=j=48V!lv@A*a;}{Q>w*N`1bI~f^ z(bt#k;6QxhNfK6p_lS|*wp3CQE{!;|t8lEJ8*u`}hgWD##?I~ZaRXElV=}jJbVtzM z;x(-KMI!o<%9(v1Tl%tRg*Cb70Tx~QR>$Q9;TTB?7TuuXl5rVjUpXCQ$LhJJ7k35z z^iv8b{#6@g=4tL5=ms{Aw0DIZXfH~~u*-zhc479Yf`1>?SoQ=9d#G}To6{#MKAjXA zQgaIFIC6U|#Q4Qy`D*Mm5TnZBnbAhnC2M(|!J)5(*=|;FzAEr?e&B0Jt>T6D6HXVr zf#&7R>AsUvXO$liP8$t*W)&24MXFccgJd(aS3M?54r)R^b#S8-%2q(o#*qi1`YjNt zDSYJ21qN0<_Pr4+Qg9Szh0Z61XMPGR-Egk`LG^Co<3lHb*`xXh(>fLkG$?^F9^@6B z=ky^=|J&^~yvTT7K7nhG;I%Ja=ql`(drop`&<}~*5?7p?9ITFeiuq#oh?Pei2}b#K zS5Ow}W4#UA!LPf`9^{WKRMXI!5` z=BMO%vbks>Ahh_4RxQUzEl@UXZ$%XpBcb%qnaTTA?nbrvpfUos) z@Z5y&lai;D@h&>#3Em4;w%;--_g>$YRu}27e{!=W@t(vL$qqAXE60@X`$!YkAx8$e z%<Cr8npkJV6yLWy{2^xw(o5 zF)g;Ptmy+3LljoWYF4DW7Or-$Km-d4eiJd)izRHUe2wmZmK9xGBRPY}bRqDd*)sjw zQlCVC#XIei8&%LtJMmV`W{XVqN+xITWV_6eE zku7)=l$B97d%T#=sG>u3>Kuf5(kSV}PDN^||JId%-2tYa#(?{Rs2!JOk!?^fq}f9` zjkFX3Svb=fzBg$=f%fx@IsC|T&z!@8MH#F?NJjUdYMgx(U8|&T7o>`OCZpvOet_LQ zRi0T!$q?5+c!eqvT9S|tM!)}czwk49dyyhr5dL{5yAUp&Z(m6Y=C;V=aXEH;#L>;} z%iCZoW0QfxqOt=5T0E7k8Eo!x&I~eSNWaIkUe&i|v?Z-9?B#I-+*S`S`g1fcRr1*V z5hab7CNx>wl(W?kfKBPJYH9JNNAxBh<*LljN=}Vb12=g{U%B}R1xg2 zz6#NWQjEJidmRK-Z%890RojOw?oE&^98C)2l_}*!?Fai%%_?yRz@PA9FC3a8(nQs- zqcMnb+H<6??Yr}7L3aMJUJR6rL}=4D;W>GavI$^Yu^!30}l$+yuwaT~PE-a0!;=ZR3{R92c{hcB)4o!AJ zOi6kf0~}z_nR5xwLVV_$&T&r^`Zq7GL?a6h_?WjJr5f|B$Z63O^@PppElMCu8w#wR zommQ=Chb)B16aKlcJ2@6XvWlW*ZB`ArCOgtT$X8#au=KkL>=~{LB8Inl_T0`@AZlQ87MAcJ4{vh{-Tw%m*y*$hPzhl`&k&ff^RV0FK*|Okua02$ zJTujNxMpnOn8OcrwlH;A4@&i@B`KqPyf9pQygM%;ib6`0Y&-bT3|?XO{x|PjKXqi} zmr)~}9g`7fS=5)_lu>H7?Z8i~m8bY6UH{st{9MHQKfg}!mHy}OYyO!bQi|^4UI%XW z{=F|uFL{pfw2*~|p6}8QuyW{o8f_NQmLz0W<84-)RlHHox8<$zUhmJ}XUebnL$YQ1 z%Kz>^?PDq3l<7BnSECJI_kFXmgSYgD#GH@CTko{JGp=orI`A2HB!Nvu*CXjHH#^Yz0>{ZS-M+kV9kEyg-*` zCA4^fr7W~mC2Qa|RJC--^)1xXMQn|J{5}7CR-i(VXLXSx4pCN=wtn(z$bIE&U}OVp zvvd>OK92T}CcOc7%6z2mC(GTW%Io%Ze>+_fFJnttU$e(+rTeAg&hPoAIAg$=y1dGg zf3BJH4(sXFcbfB}EWcEL?vJ%cg&}64W{jR|*LttLaa+B~_!QZy7~8zf-@&l3LbO=a z?g;MELhj~3xApCI8~)*5svgc-c1SO2hf~LM^PHbwYY-KaDY;U<-r#e0XGFOVZDfD3 z5u@zD*xOeXJso7xlj5qXgC21_G1;TFuv{bdv6J}Nd%I96bh>A~?H!{o%h4}!5u3cR zRG|dVw$#{1$uBRi?RA%4q*v93;g}_nWYi@olHT?8?Kv&TC7CpYe~rm;>%xhB7wxdc zO`|1sa;HYgLNF@$5*q-^vRL2kW=n0wId=A&At`b?eHr|;-LbW#%N z_$6zrxJte{S=$QmRI$-Zf8{j3?v^Vxfe;p%*?4xJ(z2Zz+9=Y&B6+~Wf4>Y};nnWA ztL;1w5Yfp5PR^w(hA>)BCa)gnhQ8ksEm^V+)!CG1u5n@5@HIbU@V{ zPn_9@-*jn>CYm_$F}c+E^S1Cn@U)K`!^E9`Y27jUMfYkQxdQ`MoIDaOSppp^mi-53 z11T}%2#3w-Lm!>FKcVn3_{6ZqsDG#Yg*6bp7x6d83&q_Ca-UT~7h9R@=p{mwVziNk z`Ogd8Ki+nCW7=IWQ62OyCt*6eU&~k#sr5%S;rrK-GN!$}Pn6MkH+xK1i~N3EM_SEC zs^U!9S9fPSrU&_C@NOxPMYV6^-QEMwk>Y5jcRS%>Q^Bt6XNqh8QN769pb(XnXs(<`%}4wCfB80pgd=}8-}Cwvzp3*? z4=Q;xrfR?C^Rjgw-mUSikY0eG&5nVCPry_P0?hcPr>-9T`I6jTd~O4n+neLkOOre@ zz-*L%1im+FXw9qXoHy3pg!_9C0Hu9+D7dABcb$-0;X*EllNg^pJ(EKDDA)Hw1Cwfe8mmW%TdOyv#JHC={V z%Wslxiu`{hTlrerzs@z|r!3g!y<;yM&zg?@lFiofPC=ORS1DH2th+-QqdQ^ZNVpz6 zr~28&q2A+RWVI8OxIp?cU{|QB!lQ68Ka56kTzHU3e<&aSiTQL0AZ;h@uM<*)>fru`xYN%=3WQV0e7>kuxw-f%I=*9uLwdy7 z!ELx!g{nD}H`Zzw$mxFR>^%Q4sU zYSca4{Flo!hmV;(M^|(qAcNrabd0vffGaYS-BiCCbWQ={ZnT|riW+3vPqChAe9~BY z(Dq^nX3_M-O~3T247}v@6?+-*DRB0;UORaj6Iz7D3D?{_g@AwBRu^`QfxY5l^)ZU^ zQs;3PR7?=k=in3bE;)@z3(;ho?cf>a0(cypzj(y0sopgkKR0uYFz$@<>KfzI#?mVn z&&C8#0p;ybl;uHCuxb_`k}=-=_o5o^FzC)1!(oZc48l!Bvts>xJ1kOyV&hH zF_K%0XR)J*Hibo#w-B|SdB$(qVlpaAE*H7hq>j}sP22Z;LiKD>9vb8{{z4Ym4(oOU zfI~<&EWM89W@AXWk}g; z!df_-vRvHwAlbK6z+hU2AE$dy3c}2pKO>(--~7!>wU@CZz};aor?WC6zLdyniNfWduSmuj;5H4kf`zxD=wxSTPx9q6qIHRwY<7og5tgb|;&fGjF!Yi*k5G0eSP z8H?zdiX=~KZ4)Y^6Ya%Wij@jVt@ zB)f=ZQ4b2|4V$aOpLEEW6gl#03XR2%(aDLtAL36IdRUwNJTZ)2*g%diFMNyNts$TTUv%EeP>kxa2mVu;jdpN3 zNhW3PjMYF7yj9!C79ZF1~uB)-J9WMxaAvi#MNB+wEUF5}Hmn!+vk`P(JqB5a>|8BO{-*>k+ zecE}^0+{regvM|=8x#|&jQR=9z<|5$?ZTV#+Oot6D+-F2Soz7|Rp6Yf*xP{}%G7uZ zp=*F3f2Tm0VBENA-GI(C;?JG(4ovBQOYFxvnpLN)K1Hp^GUko=WLNlNq_+eA^9cfP ztla{}B`K@VeGEo4uAP2r_=K3@Y$u}(=I*mAH+M@kiHt1L{}%`-Jm6DHvJnd*uuJ29 zj#t*|>3O3|L>eq6Zj59>o#{}WS&<-m+O=6H&P3b5-|0MN`&Hp(S8KtJqrzaHX^EB3 zlKUQfm%Yh=wFK=u&wq%5&;oApN^C0pZo`!8@$pvtCS4|D6?~>gy_J=J+%I6P(Y5o6 zcd%<7@-8{&t1U6sH0i;LMJv^emt==vBEbvJ&)c=(LcNhkxmBYloG(`&V~kEu;NAjY zbVkNQnz#O$-+cY+O~~6&oM5Lji_}16j23Oa44!v>-q#0ZCA22Y3t{IrvpR}bhR>Z! z{Rv#XBix{EIHB)&m@&8DF`mhs0hV9iU=_e4qo1J7 z-MX>FsYAwRvT=k4y3W5G0lf^~F_7KhE4{q#UiHTu?*rWKekL?`qwV$O&u;7tbFBTF zC4~DB93=N^44DUtGj)Bb(|yZ4>M9YuMYsXKL5CS|BHF=J%__UvE~(0dmE#3CoT`?15(BnO=% zU%>@0)uf|j(Z;BlVw{l@Z3>qZ7r}RqNva6cs0nFQX{-p(;umOiSqtXIbi=LQCfDeY z3nbB?#-wv33ml{IYCJ36B8d06XW3>N-Lj{v%@FeT?RYxCfXH}zTCPwB!3gZf;bsWt zPQ2~sHI4u-!?v+xb$-CHtnE+wX7nUGh$+*OJUxKs^GJN|u%Vwbk4l|28Tac` z!z*@&B>tQnWi^X!yM|of+N>ncx(ociUZy;2z001P(L1~7MWj+Ifs56Rx#cc{2x;4? z#Y)n=C*%zaAduK$0=ACWZq!jN<*MxU9Ho?AdDfg@F!6G!Eg>brt(YPZU%u?_UdmS^&JZ9o<0b`^ zL^ud5QQEg3?_G7dEH&)|n)2B9Nl&ct;Qh@IlXLrDA-wI`(UkY%ah(MNU8Iv=o2w#Vxu-juTcu}5#o?q@T@p?phodDE~rZebb)yB8v`8}&NC01WrCgPF(K z4butxLhm%(&~9*hv3?dv1{o$HC*RX#%XIyRk`Sn z@N4|IDas9lxqSKP0=eDJ*nh#n*Dl{vD{^EO@ZjPYKq&FM<;zBJ&YiImwhBsn9msmo zuWc7iWx{nTE(M#nSiSe9*J_RjP!JEjn`p;*m(a_iVXdRM1&hxTB_ecZiCKQy+<3sG zZ}#1rw^CnSeq@i8H#bGFOOdw7Fqm`v`)e@kw=IJ-sfrp%a(dYpv$GSHS|nIx^@D@C z3Z^LwKKm`zkxu0L0z%{^Q}g8>j%u6ymjm0vr_@cRuNRF@k{9)NYSH2c!#* z@i3ecD2UFyWetpy!<- zV}%Y%R?jz-O_N9#IMPc)>gWHv;VX1Zof-}$tg?kXmv!dz`29&(w#-YBVve5D7 zNNsP*rM&1loqGYZUU-|2@rXG7cjBe@FQ-4e{BaL?CY>H}Se1%nV+{}`m-LKV+xuKY z#fQ!0BpCH5oS4@KteR+?B` zc%#Gtk}bI2aBuHyF}9QEtEBI3sjE_)C1__#ZCc1ptItRA?p?^rW|LSFbhkbW)KT9} zh0jt++X?(r+V2rkI*psV=z@N-$x4JWYH=q@ueo{@&*8G3^?Q)@bgfP-BXddr9Z3ob zZd4u5&>L=y^u3T=k{(&kj-^{oaV47!WT6tC|HbC?RE`nJ>_#9hvi+HNPcV2cJ;VeH zX%i-?E34&I$P!quNj-N7FzNi0!`fz3ss9618n_0dQ=@KUun93bl11KLQ|kXmd*y(n zm+d)$u6>_!t2VP>*7zsb4YhsJ{G=^W^!upMjm>x1uald~08m@Rv8wze#7#1? zV79H=l7KY^6H_)NDAsMp9*>N$7Pc;(wBY!IREh({*ht!o>zqQt6xkV(S_+RzJ=^Ah zUr;8rlQZ@+ow&uN3bS11F^?!m)p9Yi1rTL2e@C*8JcYob6h0h(PqH%f`o>i*iYt!mX z{E*5eVsJWRNK1OO$Q%*QuI*>f+z;X2T*WsI1^AP;vnI8!)uiMA+_us+h23XO?VXyw z)}L@){f=65$A*e(=||9qv=5H@&f*w`aATkv0FoUXP$*^~V^3uOy1^k(P2zb7NJR3( z)P@WCl##*ellRQ#`RMKKKFKXBV-TlNyW|ehV~qIxAK2%IsKNsw<-@sXMH#{@xM+Ge zWRcFl=KoBHlo+E^NE(wH67A|O5-y~jtRxIiUSGdj)A@}(2i*4axImR+D>8Bl2LVva z1YNM72^2@5Sjeub_Fk;&bkE0R>y&sYzLLqrQhy6Pj2Q$5vtykC@m%y9~kqW#gTO26sv_)GN13;u#@5)tcn_;4H5-N z&`I)yW`?1-1cBT3$ElRE5)(R16yK!~ z1@@C^v&}x|Zyo@isZR+NNGtSLo2ii%bMrTTC_A=i4GPMWqr%FLPE({<4#=eti76pZ zhtwrsbpnOq-c3NH&3c1c>nm%Q_+9!ql+9$Cn*X3Z+IyPiQEz&~Rv`ke5mEON*9HTV z#UuDPg{00Rh+iomdqdUpM%|#tkB?BuVc!+m#_?AQby@g$sX}>y(`icnTscjFK#-tl z)NHn3z{V&{<}pD+*~T2-RHyDw^=O0Y;rG#XB3rt0+Nz@6sGop50rEW=4Aj>e!48JT z`vpT`!yDspk0OWioWJ@%bv@%aMX(^H=;>dw54u+87KgJXBAr3dJfuqt1qQ;Ip*&R) zn+VrU1e5ZY#0wQ=nf|-SUun>N5wv-N^G_j#^4QQY^uH#ixJ#H=tsO`rfXHMh0FFEsBb%T%$##UY?4~xF{I5tnALWPOG4~an+8ddQ;Xt zYG`}0Ghf(&iBoQr3p6sy#e3P218m&4^`UygSb^|$Sn-WAIXKF3JJTEq61Y>W9{);+ zG=hJJ28BceyNHor%cgY&9VeosbA%j8%9Riw7J*l;ZZ@)Z7#Zc#fx&Doj8lVPA#KaV zwSh)aJjGWmq}u?)%h#m(vYhMDCa*<}?g^-c0g4tZ{4bMHn$%WE{!g95BGNm6LTL+`RbZY%h+F<$pOs7{IZFb+bp-ppCI`vCSyHP)@c?4132)W0~ zcqFdlh?4so^_mjfhauBSxoJvGb#Ja;GiM#H$3Dfo=@M+llPMr;jJ*HPh(ReqSIh#g z&SJrTGOJHIx>`}9lP|;Ul{FWbS|U}oKE$URuz`g1{d^ESZlZkj82V9&jHD9}#%~xx zSe0I2!N#)A2fx8@we#|rzF@L4V8rOuBoe;i5S$=4kc_8o+% znDhO5Td%3}jT>8ITe3fbs0WNhhvt_@+z_^ip?x? z=3|Q1xtx(S-xR3^2%k_?Tua7FNbfU-CsA&_Hq}5-Sd3fhywQ7%#Cocw|BD^i^{(I` zHzKcD-x?94wN-!#d*+*1GAa4BcbA0DEM%xLiZY)cR+@DO6_a)(o^ zB)F{5KWru;3D7pc0Hq6ou19TcA+#NpNE~vRzR>dr>^_mqMCK1m6z|oB;zJS|{14eB zJ547;v!YBwVtD~5_{$fsNRvPf3Hl-$A6{=}j|h6hvP=^6M;1 zI!Uk0J-45>J0MHXZs}m70w#)YnvJZO4YCSk=5PDu^`?v_U{j_@g-C>KsZ=vP9d%@I zac6e_#02EJu>!D4O0~;V6^!_jC^kR8I5wDcR-EJsh`(?#3b2&<*gvsKF8*JBf0Ir; z2u@92Qj<;;;!w~o9yBQZ&gTPVihY}J@QAwJ!aPG)fGAT*5Ux_ru9hAXEwmTKbF>Tu zY$);*c4Xeh4v#+Ip2Vt9dwc&q-uZVhj+Lo8!`bqI>_(!6k#%X06vtE zDclxqX! z-bw8M958gY*&G$*<6^VLP_=zPHoG34U2DPUGGqdH z>-2jM@Fwe&Ozy*Okby!*{rQd27YDe<|D{gbc2F|4w@22GQ@LhFZ{J-oR4KkNf?N_i zUoIIg8$qs0)L%A0r)^0l7I%>EvNuHSn`&(Gv{c46VSG$-IKYkWMcjqRo#*_C`dJaSh29;Lt+R@msW z=jc==O3tj;)(OXU11d7_x0_=i$2j|L;&juW{#%8ULhCksAzVUnBs%9z_THpj>48#=EYv}??5BLxp=h6+OtaYiF$wF$TJV~H&n@RYRo~isKuF|m8JR% z{$&cIg||ac!6g)-o536e6fw${6QbxlUx;OUZLGM2T<){i9=IRpNU72&7VMa8QG*LQ zzURb7RZPLnul)y%wBkHAoSoUc_V+Z}%Rx!ms|QOh4}4;RZ&jY^;x%p1gw0dr1W>1+ zIOV8dgcF1%1f7(gBCIpwnZsIl^9T=zSqUbM?)Am5C|WzWqtB6B46}(9G#nJyq#Hz;zbxLgWKy* zc0-JWBlLs-EKKGS;%@*0frzhtglX@~xmepiBpr2~&~^j-z5DJP&g(G8<7E<|!o!%W zBZf@w8+3;UI27)Rvg$AFw6Se2>RP@VBc45jgz_Q->cIIEMyfrRU7h{H;NFRY`;*w$ zHwMhaUMiX;sdv}4t==eSGR#^r90*+o$4n($n$@wxMrSwFHa)-Qou~GfYrqX{3zPf1 zhNpDzu76!&Zq^Tm!`FrMcCsq1e!u>gB^p1^T|Q*i@#S+PLgRku;2U#iZR7LFPOi!w z9xU6V|21g~<=Bq4;YT?@eG8w}keQTNq`#_skn`5!0ry=})yJ7fTx;vn@&6p*a{jBr zPy&Axl?u5Zm;^X z4^3e%({_iIoJPAXw4|HN-@SnU5H`*YJ&OzSo*Ll^4~Ji;ZhZ`6@Ef)I!=d3K4tmD> zo<&+?Hqf=Bs2w0PK$w|y4*!8aM!KeF?}oP)xi^cBTs7&e&)w{TA%C-i_u_S<4s2bw zB+0E`;`@a?f7fPjSMY<+ZT6W=y}H1_25iO!pQG2(9E=|tu`{o2Gl-X?Hvg$_40B|V zW1iQK_wRG!Oggb}Y`EnEJ+5lTc=6xBhENVp(d&y_(xaa`{pn81c{I^B(QjvRlONvp z)2oY}!9E0u0UQ3?iwt9TA?~d~S&7P+`~-hN{%JdW*cuNbyT^ax8Cj;;>mCN&Q+F)H z(hiIC;`+}WOLe(g;p*M5YSoxQ9Qc;=-39UNq!UVhboE<{^qa_&q0gR&-)+sj{FG=1 zYO+ShZN4`m^uVPn?VFO8U0r>(PI^B+`Bg>ZBvy1d8N+w3L8!eo^5y5QLGOCza*YIs z#a7u$_FYT%cb_;%n49oXVTAQt@~TF;o2kDdnI1g_Q9um03L6~M-s`aw!0By#*`1N? z?nWahP|{3__jh;e9Z=S|t9oT1D@I&r#eV`7JvDw{@ArE7v$cg7WXV%aXOp-F zMi}E*K@%9u!*L|Y(-G&@(}jgS+F4KlX}%#^3^G#dr%^!_domDZWAcp zru*3)k3E;Zaqp$~l%1t?!spu3Nq8#sAlx&X`Yh5R>nM&<H0!lq)We%y!@K zBZ)pjxN@ZYN>xdr>Qg7hqNV%UsSb@^IJWR*nKs>e;aFjz3J!=0qjc$2m?ZJskvNY} z3)tb8{k57`0_A|3tCvtXsC~Ao~=9lMBaoX>(YVhSo!(E(vz_8}un897Y z(>Bqg7j>q^A(im$G+=-#Q}n(J95uloBj<-Lje$Pzbe)|TY(Uo6f%{1hu7Mf;;S`~@ zV}<*(7{Q|K!Yy<45`$A)>Zpx~1=3+-zCMhir_ ziVOEvS4a3f>106Q|+-^21p&J$k==JNwqxz&2 zB`SCS9>z|_48Gz_kP8rCmGZ~O;#6w+W)}F7r7ou(t2CO6Iphjp!}UJK7h`#nZMcR@ z^ZAe4Lr7*Ool_oiIwCx(Y5s-kibJV&i`#H4fH6KOKml_h+H}8(zY0lum6|U(nD4Px zKi++rgYpI=^Y+u*Cys~=pUs(puo!q~59gJXp_>9ldwk+r*bFC8b zShm`MnAcXshfeR9rCn442~aXz>2rpji^SF>R1$813Db}8pfnEF8i z@-crs9e($WexRp~t;WxX@Sgo(=wte;@ekbhvF-ja1e;&^)G^?n`d2dVVyoVC4LED| z$zBctItRsPYb-_|L#Eul?+v-&$d}@9+!~j#Kwd zAA#W>lD!M_V0m1(K3rzUHwVC98}%k0{pmjB*gLpty(D1a0|9oS6m)oqy1;0MD*1)z z31;T{d;F%l;mR0_ZK4m22Ad<;A8Zvht$xe;abCR+4~Iv^V9I9&S5{$tOuidiSS&mneqJpFNWuJS+Qt18C?R}I?a5a^D^lysDL6lGIz;CFVRxfSQN z;^B6xAnDK|5XVrwYrtm;bOho@E$i zx*ub*U+;6D?!FM-cJGxukDpRUm4z)FyBxleZ4rnog%2u2n%#ggHykC0`%;Ho*}mtO zb38R{Cu)ChbJLo}kBJA!XQ0cf>YagVrkwJ-uCPk6#)Pv^8?inAaJj6Th)2>M(68xB zBNUCpmHfg1N9o@&1w(yP+1oPASW&ka)p@M(dfMCmNt$OhP?vX~)sgf95;`%f0m%Gp+UZ10qr`ty9rrKT z34S_#2~8T5&ZLSa?b@xXq_nFa6sq~%y?jLdE5A2I?^>7pegKsq$(9M>=!nQ9odv@@ zFR6Zbh-NySlmlnk!;9~egMXB-s$O<@9UckL9|ms$N*2RNCp^kL0s|!~qve3L$A9xM z4)$=JD9&Eyhy}v0fC0lI?{&EZTU^MFz_ zI5P%!reS)`O}q}+(2cRA|LS6WZ4(WJwQI(inT5mK7f!d)N2Ad_(-%8-K5^(qjF%6Q zMQXf=LuZqG>@jmOa6CcLD9m*K&Vt#e-%?A%tTSdVB%$*XGLRZCQ0U`=<;oWFodj^F zG|P!cOSeww-2h=hQp4KQ3Ka}{dM^I?Wg`MQ`+H_Qe*Ynin zd5(I#Ju<3tAS(X{5m?WsMOsrn8K{G%4H^{ra!7?LlQtvqdR_U^vXFb+0QK$y4A>)q z?>B{iNb><>J2Pfa?qgtLQQB4wvjhIQ0W@qAHNRipIlU(5hByDSv_Mf;W!9z=nvAxW3NFUx_W$7ZNWi$p_wX-`OQg^nvP5Cj*WZ^J0AhgW;7;?_-?n?f? zNBNK8({TJY?t)UWVp8%OJSa@jc<`X=vfXR%x-oVytw*W6GF6J;R>`5ED`-iPCVvxE z(K|CvwyZ!-z3n8MW1o2stMeiIjj5WgTkmcIdkJGSk&m%J0+w^%hjKVn5w>SjC;bEtg!rLb z#lX&{+PG_s<;~KDNt+xt#8FI*L%M|jnmztrz8HY2tGluBt@4%$dd_`s>3Mr65o!c} zY->C{#u7oA*3sVS>19{Db%QbAspbd-qq9o1VMghg=aM&-kH1)Nd=rBmzr$cee>!~c zl^dO_A7iQ1=&iivi)PnPZW_G4F|}g64flRR(YQ-7lrBw@>r>n3cwg=0k=K(wC9k6` zbNi=3#@SzAKF+O-iT^q<`}70HT;U$Yx&kc#FQ|6&!NMvUH8yzF)+?{CaO;YHvi>H$ zDLLQk6W5Rj5p-wypvyaQiW}o%rN44eW8Vm09}d0Da`f}E-Ftj|{3a`&g#bpoiA91kC@267 \ 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 497/936] 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 498/936] 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 499/936] 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 500/936] 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 501/936] 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 502/936] 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 503/936] 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 504/936] 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 505/936] 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 506/936] 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 507/936] 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 508/936] 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 509/936] 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 510/936] 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 511/936] 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 512/936] 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 513/936] 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 514/936] 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 515/936] 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 516/936] 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 517/936] 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 518/936] 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 519/936] 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 520/936] 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 521/936] 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 522/936] 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/$%7BPBS_RELEASE%7D/$%7BPBS_ARCHIVE%7D" 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 523/936] 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 524/936] 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 525/936] 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 526/936] 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 527/936] 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 528/936] 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 529/936] 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 530/936] 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 531/936] 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 532/936] 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 533/936] 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 534/936] 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 535/936] 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 536/936] 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 537/936] 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 538/936] 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 539/936] 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 540/936] 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 541/936] 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 542/936] 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 543/936] 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 544/936] 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 545/936] 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 546/936] 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 547/936] 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 548/936] 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 549/936] 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 550/936] 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 551/936] 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 552/936] 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 553/936] 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 554/936] 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 555/936] 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 556/936] 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 557/936] 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 558/936] 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 565/936] 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 566/936] 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 567/936] 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 568/936] 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 569/936] 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 570/936] 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 571/936] 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 572/936] 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 573/936] 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 574/936] 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 575/936] 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 576/936] 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 577/936] 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 578/936] 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 579/936] 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 580/936] 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 581/936] 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 582/936] 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 583/936] 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 584/936] 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 585/936] 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 586/936] 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 587/936] 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 588/936] 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 589/936] 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 590/936] 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 591/936] 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 592/936] 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 593/936] 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 594/936] 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 595/936] 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 596/936] 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 597/936] 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/%7Bpath%7D" 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 598/936] 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 599/936] 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 600/936] 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 601/936] 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 602/936] 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 603/936] 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 604/936] 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 605/936] 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 606/936] 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 607/936] 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 608/936] 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 609/936] 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 610/936] 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 611/936] 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 612/936] 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 613/936] 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 614/936] 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 615/936] 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 616/936] 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 617/936] 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 618/936] 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 619/936] 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 620/936] 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 621/936] 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 622/936] 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 bb82090078542b45a67330b7d2493e9d2f11244f Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 28 Mar 2025 19:29:27 +0000 Subject: [PATCH 623/936] ci: mark many rust code blocks as no_run (#5019) --- guide/src/async-await.md | 4 ++-- guide/src/class.md | 10 ++++---- guide/src/class/numeric.md | 12 +++++----- guide/src/class/object.md | 32 ++++++++++++------------- guide/src/class/protocols.md | 12 +++++----- guide/src/class/thread-safety.md | 8 +++---- guide/src/conversions/traits.md | 16 ++++++------- guide/src/ecosystem/logging.md | 4 ++-- guide/src/ecosystem/tracing.md | 2 +- guide/src/exception.md | 8 +++---- guide/src/faq.md | 6 ++--- guide/src/features.md | 4 ++-- guide/src/free-threading.md | 10 +++----- guide/src/function.md | 8 +++---- guide/src/function/signature.md | 8 +++---- guide/src/getting-started.md | 2 +- guide/src/migration.md | 40 ++++++++++++++++---------------- guide/src/module.md | 8 +++---- guide/src/performance.md | 8 +++---- guide/src/trait-bounds.md | 14 +++++------ pyo3-build-config/src/impl_.rs | 2 +- pyo3-ffi/README.md | 2 +- pyo3-ffi/src/lib.rs | 4 ++-- src/conversion.rs | 4 ++-- src/conversions/num_bigint.rs | 2 +- src/conversions/num_rational.rs | 2 +- src/conversions/rust_decimal.rs | 2 +- src/conversions/uuid.rs | 2 +- src/impl_/pymethods.rs | 2 +- src/lib.rs | 2 +- src/marker.rs | 6 ++--- src/pycell.rs | 2 +- src/pyclass.rs | 2 +- src/types/module.rs | 10 ++++---- src/types/pysuper.rs | 2 +- 35 files changed, 129 insertions(+), 133 deletions(-) diff --git a/guide/src/async-await.md b/guide/src/async-await.md index 27574181804..f9fc33fe3c5 100644 --- a/guide/src/async-await.md +++ b/guide/src/async-await.md @@ -4,7 +4,7 @@ `#[pyfunction]` and `#[pymethods]` attributes also support `async fn`. -```rust +```rust,no_run # #![allow(dead_code)] # #[cfg(feature = "experimental-async")] { use std::{thread, time::Duration}; @@ -77,7 +77,7 @@ where Cancellation on the Python side can be caught using [`CancelHandle`]({{#PYO3_DOCS_URL}}/pyo3/coroutine/struct.CancelHandle.html) type, by annotating a function parameter with `#[pyo3(cancel_handle)]`. -```rust +```rust,no_run # #![allow(dead_code)] # #[cfg(feature = "experimental-async")] { use futures::FutureExt; diff --git a/guide/src/class.md b/guide/src/class.md index 90991328fe6..460fed6b34c 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -788,7 +788,7 @@ impl MyClass { To create a class attribute (also called [class variable][classattr]), a method without any arguments can be annotated with the `#[classattr]` attribute. -```rust +```rust,no_run # use pyo3::prelude::*; # #[pyclass] # struct MyClass {} @@ -812,7 +812,7 @@ class creation. If the class attribute is defined with `const` code only, one can also annotate associated constants: -```rust +```rust,no_run # use pyo3::prelude::*; # #[pyclass] # struct MyClass {} @@ -827,7 +827,7 @@ impl MyClass { Free functions defined using `#[pyfunction]` interact with classes through the same mechanisms as the self parameters of instance methods, i.e. they can take GIL-bound references, GIL-bound reference wrappers or GIL-indepedent references: -```rust +```rust,no_run # #![allow(dead_code)] # use pyo3::prelude::*; #[pyclass] @@ -866,7 +866,7 @@ fn print_refcnt(my_class: Py, py: Python<'_>) { Classes can also be passed by value if they can be cloned, i.e. they automatically implement `FromPyObject` if they implement `Clone`, e.g. via `#[derive(Clone)]`: -```rust +```rust,no_run # #![allow(dead_code)] # use pyo3::prelude::*; #[pyclass] @@ -890,7 +890,7 @@ Similar to `#[pyfunction]`, the `#[pyo3(signature = (...))]` attribute can be us The following example defines a class `MyClass` with a method `method`. This method has a signature that sets default values for `num` and `name`, and indicates that `py_args` should collect all extra positional arguments and `py_kwargs` all extra keyword arguments: -```rust +```rust,no_run # use pyo3::prelude::*; use pyo3::types::{PyDict, PyTuple}; # diff --git a/guide/src/class/numeric.md b/guide/src/class/numeric.md index 4f73a44adab..124bb8d27a6 100644 --- a/guide/src/class/numeric.md +++ b/guide/src/class/numeric.md @@ -31,7 +31,7 @@ own extraction function, using the `#[pyo3(from_py_with = ...)]` attribute. Unfo doesn't provide a way to wrap Python integers out of the box, but we can do a Python call to mask it and cast it to an `i32`. -```rust +```rust,no_run # #![allow(dead_code)] use pyo3::prelude::*; @@ -44,7 +44,7 @@ fn wrap(obj: &Bound<'_, PyAny>) -> PyResult { ``` We also add documentation, via `///` comments, which are visible to Python users. -```rust +```rust,no_run # #![allow(dead_code)] use pyo3::prelude::*; @@ -70,7 +70,7 @@ impl Number { With that out of the way, let's implement some operators: -```rust +```rust,no_run use pyo3::exceptions::{PyZeroDivisionError, PyValueError}; # use pyo3::prelude::*; @@ -124,7 +124,7 @@ impl Number { ### Unary arithmetic operations -```rust +```rust,no_run # use pyo3::prelude::*; # # #[pyclass] @@ -152,7 +152,7 @@ impl Number { ### Support for the `complex()`, `int()` and `float()` built-in functions. -```rust +```rust,no_run # use pyo3::prelude::*; # # #[pyclass] @@ -415,7 +415,7 @@ Let's create that helper function. The signature has to be `fn(&Bound<'_, PyAny> - `&Bound<'_, PyAny>` represents a checked borrowed reference, so the pointer derived from it is valid (and not null). - Whenever we have borrowed references to Python objects in scope, it is guaranteed that the GIL is held. This reference is also where we can get a [`Python`] token to use in our call to [`PyErr::take`]. -```rust +```rust,no_run # #![allow(dead_code)] use std::os::raw::c_ulong; use pyo3::prelude::*; diff --git a/guide/src/class/object.md b/guide/src/class/object.md index 07f445aac60..3b9f1aafa40 100644 --- a/guide/src/class/object.md +++ b/guide/src/class/object.md @@ -2,7 +2,7 @@ Recall the `Number` class from the previous chapter: -```rust +```rust,no_run # #![allow(dead_code)] use pyo3::prelude::*; @@ -44,7 +44,7 @@ It can't even print an user-readable representation of itself! We can fix that b `__repr__` and `__str__` methods inside a `#[pymethods]` block. We do this by accessing the value contained inside `Number`. -```rust +```rust,no_run # use pyo3::prelude::*; # # #[pyclass] @@ -72,7 +72,7 @@ impl Number { To automatically generate the `__str__` implementation using a `Display` trait implementation, pass the `str` argument to `pyclass`. -```rust +```rust,no_run # use std::fmt::{Display, Formatter}; # use pyo3::prelude::*; # @@ -91,16 +91,16 @@ impl Display for Coordinate { } ``` -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: +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: 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 +```rust,no_run # use pyo3::prelude::*; # # #[allow(dead_code)] @@ -120,7 +120,7 @@ the subclass name. This is typically done in Python code by accessing `self.__class__.__name__`. In order to be able to access the Python type information *and* the Rust struct, we need to use a `Bound` as the `self` argument. -```rust +```rust,no_run # use pyo3::prelude::*; # use pyo3::types::PyString; # @@ -145,7 +145,7 @@ impl Number { Let's also implement hashing. We'll just hash the `i32`. For that we need a [`Hasher`]. The one provided by `std` is [`DefaultHasher`], which uses the [SipHash] algorithm. -```rust +```rust,no_run use std::collections::hash_map::DefaultHasher; // Required to call the `.hash` and `.finish` methods, which are defined on traits. @@ -171,7 +171,7 @@ This option is only available for `frozen` classes to prevent accidental hash ch 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 +```rust,no_run # use pyo3::prelude::*; # # #[allow(dead_code)] @@ -195,7 +195,7 @@ struct Number(i32); > Types which should not be hashable can override this by setting `__hash__` to None. > This is the same mechanism as for a pure-Python class. This is done like so: > -> ```rust +> ```rust,no_run > # use pyo3::prelude::*; > #[pyclass] > struct NotHashable {} @@ -213,7 +213,7 @@ PyO3 supports the usual magic comparison methods available in Python such as `__ and so on. It is also possible to support all six operations at once with `__richcmp__`. This method will be called with a value of `CompareOp` depending on the operation. -```rust +```rust,no_run use pyo3::class::basic::CompareOp; # use pyo3::prelude::*; @@ -240,7 +240,7 @@ impl Number { If you obtain the result by comparing two Rust values, as in this example, you can take a shortcut using `CompareOp::matches`: -```rust +```rust,no_run use pyo3::class::basic::CompareOp; # use pyo3::prelude::*; @@ -289,7 +289,7 @@ impl Number { To implement `__eq__` using the Rust [`PartialEq`] trait implementation, the `eq` option can be used. -```rust +```rust,no_run # use pyo3::prelude::*; # # #[allow(dead_code)] @@ -300,7 +300,7 @@ 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 +```rust,no_run # use pyo3::prelude::*; # # #[allow(dead_code)] @@ -313,7 +313,7 @@ struct Number(i32); We'll consider `Number` to be `True` if it is nonzero: -```rust +```rust,no_run # use pyo3::prelude::*; # # #[allow(dead_code)] @@ -330,7 +330,7 @@ impl Number { ### Final code -```rust +```rust,no_run use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; diff --git a/guide/src/class/protocols.md b/guide/src/class/protocols.md index 8a361a1442e..3dee8db137d 100644 --- a/guide/src/class/protocols.md +++ b/guide/src/class/protocols.md @@ -50,7 +50,7 @@ given signatures should be interpreted as follows: Disabling Python's default hash By default, all `#[pyclass]` types have a default hash implementation from Python. Types which should not be hashable can override this by setting `__hash__` to `None`. This is the same mechanism as for a pure-Python class. This is done like so: - ```rust + ```rust,no_run # use pyo3::prelude::*; # #[pyclass] @@ -95,7 +95,7 @@ given signatures should be interpreted as follows: If you want to leave some operations unimplemented, you can return `py.NotImplemented()` for some of the operations: - ```rust + ```rust,no_run use pyo3::class::basic::CompareOp; use pyo3::types::PyNotImplemented; @@ -155,7 +155,7 @@ Returning `None` from `__next__` indicates that that there are no further items. Example: -```rust +```rust,no_run use pyo3::prelude::*; use std::sync::Mutex; @@ -181,7 +181,7 @@ In many cases you'll have a distinction between the type being iterated over only needs to implement `__iter__()` while the iterator must implement both `__iter__()` and `__next__()`. For example: -```rust +```rust,no_run # use pyo3::prelude::*; #[pyclass] @@ -274,7 +274,7 @@ Use the `#[pyclass(sequence)]` annotation to instruct PyO3 to fill the `sq_lengt can override this by setting `__contains__` to `None`. This is the same mechanism as for a pure-Python class. This is done like so: - ```rust + ```rust,no_run # use pyo3::prelude::*; # #[pyclass] @@ -430,7 +430,7 @@ cleared, as every cycle must contain at least one mutable reference. Example: -```rust +```rust,no_run use pyo3::prelude::*; use pyo3::PyTraverseError; use pyo3::gc::PyVisit; diff --git a/guide/src/class/thread-safety.md b/guide/src/class/thread-safety.md index 55c2a3caca8..75c0b1a7423 100644 --- a/guide/src/class/thread-safety.md +++ b/guide/src/class/thread-safety.md @@ -16,7 +16,7 @@ By default, `#[pyclass]` employs an ["interior mutability" pattern](../class.md# For example, the below simple class is thread-safe: -```rust +```rust,no_run # use pyo3::prelude::*; #[pyclass] @@ -47,7 +47,7 @@ To remove the possibility of having overlapping `&self` and `&mut self` referenc For example, a thread-safe version of the above `MyClass` using atomic integers would be as follows: -```rust +```rust,no_run # use pyo3::prelude::*; use std::sync::atomic::{AtomicI32, Ordering}; @@ -75,7 +75,7 @@ An alternative to atomic data structures is to use [locks](https://doc.rust-lang For example, a thread-safe version of the above `MyClass` using locks would be as follows: -```rust +```rust,no_run # use pyo3::prelude::*; use std::sync::Mutex; @@ -107,4 +107,4 @@ If you need to lock around state stored in the Python interpreter or otherwise c 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. \ No newline at end of file +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/conversions/traits.md b/guide/src/conversions/traits.md index 848dc041ef7..4ffdf802a87 100755 --- a/guide/src/conversions/traits.md +++ b/guide/src/conversions/traits.md @@ -550,7 +550,7 @@ _without_ having a unique python type. `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 +```rust,no_run # #![allow(dead_code)] # use pyo3::prelude::*; # use std::collections::HashMap; @@ -574,7 +574,7 @@ For structs with a single field (newtype pattern) the `#[pyo3(transparent)]` opt forward the implementation to the inner type. -```rust +```rust,no_run # #![allow(dead_code)] # use pyo3::prelude::*; @@ -591,7 +591,7 @@ struct TransparentStruct<'py> { For `enum`s each variant is converted according to the rules for `struct`s above. -```rust +```rust,no_run # #![allow(dead_code)] # use pyo3::prelude::*; # use std::collections::HashMap; @@ -618,7 +618,7 @@ Additionally `IntoPyObject` can be derived for a reference to a struct or enum u - `#[derive(IntoPyObject)]` will invoke the function with `Cow::Owned` - `#[derive(IntoPyObjectRef)]` will invoke the function with `Cow::Borrowed` - ```rust + ```rust,no_run # use pyo3::prelude::*; # use pyo3::IntoPyObjectExt; # use std::borrow::Cow; @@ -642,7 +642,7 @@ Additionally `IntoPyObject` can be derived for a reference to a struct or enum u If the derive macro is not suitable for your use case, `IntoPyObject` can be implemented manually as demonstrated below. -```rust +```rust,no_run # use pyo3::prelude::*; # #[allow(dead_code)] struct MyPyObjectWrapper(PyObject); @@ -673,7 +673,7 @@ impl<'a, 'py> IntoPyObject<'py> for &'a MyPyObjectWrapper { `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 +```rust,no_run use pyo3::prelude::*; use pyo3::IntoPyObject; use pyo3::types::{PyBool, PyInt}; @@ -700,7 +700,7 @@ In this example if we wanted to combine `ints_as_pyints` and `bools_as_pybool` i 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 +```rust,no_run # use pyo3::prelude::*; # use pyo3::BoundObject; # use pyo3::IntoPyObject; @@ -757,7 +757,7 @@ All types in PyO3 implement this trait, as does a `#[pyclass]` which doesn't use Occasionally you may choose to implement this for custom types which are mapped to Python types _without_ having a unique python type. -```rust +```rust,no_run use pyo3::prelude::*; # #[allow(dead_code)] struct MyPyObjectWrapper(PyObject); diff --git a/guide/src/ecosystem/logging.md b/guide/src/ecosystem/logging.md index da95c4a7cd2..e7d5c3c1e01 100644 --- a/guide/src/ecosystem/logging.md +++ b/guide/src/ecosystem/logging.md @@ -18,7 +18,7 @@ Python programs. Use [`pyo3_log::init`][init] to install the logger in its default configuration. It's also possible to tweak its configuration (mostly to tune its performance). -```rust +```rust,no_run use log::info; use pyo3::prelude::*; @@ -61,7 +61,7 @@ To have python logs be handled by Rust, one need only register a rust function t This has been implemented within the [pyo3-pylogger] crate. -```rust +```rust,no_run use log::{info, warn}; use pyo3::prelude::*; diff --git a/guide/src/ecosystem/tracing.md b/guide/src/ecosystem/tracing.md index 341d0759e96..c9fd355173a 100644 --- a/guide/src/ecosystem/tracing.md +++ b/guide/src/ecosystem/tracing.md @@ -37,7 +37,7 @@ 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 +```rust,no_run #[tracing::instrument] #[pyfunction] fn fibonacci(index: usize, use_memoized: bool) -> PyResult { diff --git a/guide/src/exception.md b/guide/src/exception.md index 8de04ba986a..2517ebde8b4 100644 --- a/guide/src/exception.md +++ b/guide/src/exception.md @@ -40,7 +40,7 @@ Python::with_gil(|py| { When using PyO3 to create an extension module, you can add the new exception to the module like this, so that it is importable from Python: -```rust +```rust,no_run use pyo3::prelude::*; use pyo3::exceptions::PyException; @@ -77,7 +77,7 @@ Python::with_gil(|py| { Python has an [`isinstance`](https://docs.python.org/3/library/functions.html#isinstance) method to check an object's type. In PyO3 every object has the [`PyAny::is_instance`] and [`PyAny::is_instance_of`] methods which do the same thing. -```rust +```rust,no_run use pyo3::prelude::*; use pyo3::types::{PyBool, PyList}; @@ -94,7 +94,7 @@ Python::with_gil(|py| { To check the type of an exception, you can similarly do: -```rust +```rust,no_run # use pyo3::exceptions::PyTypeError; # use pyo3::prelude::*; # Python::with_gil(|py| { @@ -109,7 +109,7 @@ It is possible to use an exception defined in Python code as a native Rust type. The `import_exception!` macro allows importing a specific exception class and defines a Rust type for that exception. -```rust +```rust,no_run #![allow(dead_code)] use pyo3::prelude::*; diff --git a/guide/src/faq.md b/guide/src/faq.md index 83089cf395e..8efe0b61bad 100644 --- a/guide/src/faq.md +++ b/guide/src/faq.md @@ -86,7 +86,7 @@ You can give the Python interpreter a chance to process the signal properly by c You may have a nested struct similar to this: -```rust +```rust,no_run # use pyo3::prelude::*; #[pyclass] #[derive(Clone)] @@ -126,7 +126,7 @@ b: This can be especially confusing if the field is mutable, as getting the field and then mutating it won't persist - you'll just get a fresh clone of the original on the next access. Unfortunately Python and Rust don't agree about ownership - if PyO3 gave out references to (possibly) temporary Rust objects to Python code, Python code could then keep that reference alive indefinitely. Therefore returning Rust objects requires cloning. If you don't want that cloning to happen, a workaround is to allocate the field on the Python heap and store a reference to that, by using [`Py<...>`]({{#PYO3_DOCS_URL}}/pyo3/struct.Py.html): -```rust +```rust,no_run # use pyo3::prelude::*; #[pyclass] struct Inner {/* fields omitted */} @@ -179,7 +179,7 @@ However, when the dependency is renamed, or your crate only indirectly depends on `pyo3`, you need to let the macro code know where to find the crate. This is done with the `crate` attribute: -```rust +```rust,no_run # use pyo3::prelude::*; # pub extern crate pyo3; # mod reexported { pub use ::pyo3; } diff --git a/guide/src/features.md b/guide/src/features.md index b48c138b287..252b0916637 100644 --- a/guide/src/features.md +++ b/guide/src/features.md @@ -184,7 +184,7 @@ Adds a dependency on [rust_decimal](https://docs.rs/rust_decimal) and enables co Enables (de)serialization of `Py` objects via [serde](https://serde.rs/). This allows to use [`#[derive(Serialize, Deserialize)`](https://serde.rs/derive.html) on structs that hold references to `#[pyclass]` instances -```rust +```rust,no_run # #[cfg(feature = "serde")] # #[allow(dead_code)] # mod serde_only { @@ -214,4 +214,4 @@ Adds a dependency on [smallvec](https://docs.rs/smallvec) and enables conversion ### `uuid` -Adds a dependency on [uuid](https://docs.rs/uuid) and enables conversions into its [`Uuid`](https://docs.rs/uuid/latest/uuid/struct.Uuid.html) type. \ No newline at end of file +Adds a dependency on [uuid](https://docs.rs/uuid) and enables conversions into its [`Uuid`](https://docs.rs/uuid/latest/uuid/struct.Uuid.html) type. diff --git a/guide/src/free-threading.md b/guide/src/free-threading.md index ffb95d240a1..6df9ac01cfb 100644 --- a/guide/src/free-threading.md +++ b/guide/src/free-threading.md @@ -72,7 +72,7 @@ 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: -```rust +```rust,no_run use pyo3::prelude::*; /// This module supports free-threaded Python @@ -85,7 +85,7 @@ fn my_extension(m: &Bound<'_, PyModule>) -> PyResult<()> { Or for a module that is set up without using the `pymodule` macro: -```rust +```rust,no_run use pyo3::prelude::*; # #[allow(dead_code)] @@ -208,9 +208,8 @@ The most straightforward way to trigger this problem is to use the Python [`pyclass`]({{#PYO3_DOCS_URL}}/pyo3/attr.pyclass.html) in multiple threads. For example, consider the following implementation: -```rust +```rust,no_run # use pyo3::prelude::*; -# fn main() { #[pyclass] #[derive(Default)] struct ThreadIter { @@ -229,7 +228,6 @@ impl ThreadIter { self.count } } -# } ``` And then if we do something like this in Python: @@ -309,7 +307,6 @@ 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; @@ -331,7 +328,6 @@ Python::with_gil(|py| { cache.cache = Some(PyDict::new(py).unbind()); }); }); -# } ``` ### `GILProtected` is not exposed diff --git a/guide/src/function.md b/guide/src/function.md index 323bc9c8f87..a3121835215 100644 --- a/guide/src/function.md +++ b/guide/src/function.md @@ -4,7 +4,7 @@ The `#[pyfunction]` attribute is used to define a Python function from a Rust fu The following example defines a function called `double` in a Python module called `my_extension`: -```rust +```rust,no_run use pyo3::prelude::*; #[pyfunction] @@ -79,7 +79,7 @@ The `#[pyo3]` attribute can be used to modify properties of the generated Python The following example creates a function `pyfunction_with_module` which returns the containing module's name (i.e. `module_with_fn`): - ```rust + ```rust,no_run use pyo3::prelude::*; use pyo3::types::PyString; @@ -174,7 +174,7 @@ annotated with `#[pyfn]`. To simplify PyO3, it is expected that `#[pyfn]` may be An example of `#[pyfn]` is below: -```rust +```rust,no_run use pyo3::prelude::*; #[pymodule] @@ -191,7 +191,7 @@ fn my_extension(m: &Bound<'_, PyModule>) -> PyResult<()> { `#[pyfn(m)]` is just syntactic sugar for `#[pyfunction]`, and takes all the same options documented in the rest of this chapter. The code above is expanded to the following: -```rust +```rust,no_run use pyo3::prelude::*; #[pymodule] diff --git a/guide/src/function/signature.md b/guide/src/function/signature.md index 431cad87bfd..1e011a27840 100644 --- a/guide/src/function/signature.md +++ b/guide/src/function/signature.md @@ -10,7 +10,7 @@ This section of the guide goes into detail about use of the `#[pyo3(signature = For example, below is a function that accepts arbitrary keyword arguments (`**kwargs` in Python syntax) and returns the number that was passed: -```rust +```rust,no_run use pyo3::prelude::*; use pyo3::types::PyDict; @@ -38,7 +38,7 @@ Just like in Python, the following constructs can be part of the signature:: code unmodified. Example: -```rust +```rust,no_run # use pyo3::prelude::*; use pyo3::types::{PyDict, PyTuple}; # @@ -79,7 +79,7 @@ impl MyClass { Arguments of type `Python` must not be part of the signature: -```rust +```rust,no_run # #![allow(dead_code)] # use pyo3::prelude::*; #[pyfunction] @@ -108,7 +108,7 @@ num=-1 > Note: to use keywords like `struct` as a function argument, use "raw identifier" syntax `r#struct` in both the signature and the function definition: > -> ```rust +> ```rust,no_run > # #![allow(dead_code)] > # use pyo3::prelude::*; > #[pyfunction(signature = (r#struct = "foo"))] diff --git a/guide/src/getting-started.md b/guide/src/getting-started.md index e2cc040bbd7..99cffa25895 100644 --- a/guide/src/getting-started.md +++ b/guide/src/getting-started.md @@ -145,7 +145,7 @@ classifiers = [ After this you can setup Rust code to be available in Python as below; for example, you can place this code in `src/lib.rs`: -```rust +```rust,no_run use pyo3::prelude::*; /// Formats the sum of two numbers as string. diff --git a/guide/src/migration.md b/guide/src/migration.md index 35126dfcaef..402b22ab134 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -70,7 +70,7 @@ are deprecated and will be removed in a future PyO3 version. To implement the new trait you may use the new `IntoPyObject` and `IntoPyObjectRef` derive macros as below. -```rust +```rust,no_run # use pyo3::prelude::*; #[derive(IntoPyObject, IntoPyObjectRef)] struct Struct { @@ -103,7 +103,7 @@ impl ToPyObject for MyPyObjectWrapper { ``` After: -```rust +```rust,no_run # use pyo3::prelude::*; # #[allow(dead_code)] # struct MyPyObjectWrapper(PyObject); @@ -145,7 +145,7 @@ This change has an effect on functions and methods returning _byte_ collections 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 +```rust,no_run # #![allow(dead_code)] # use pyo3::prelude::*; #[pyfunction] @@ -237,7 +237,7 @@ where After: -```rust +```rust,no_run # use pyo3::prelude::*; # use pyo3::types::{PyDict, IntoPyDict}; # use std::collections::HashMap; @@ -282,7 +282,7 @@ and unnoticed changes in behavior. With 0.24 this restriction will be lifted aga Before: -```rust +```rust,no_run # #![allow(deprecated, dead_code)] # use pyo3::prelude::*; #[pyfunction] @@ -293,7 +293,7 @@ fn increment(x: u64, amount: Option) -> u64 { After: -```rust +```rust,no_run # #![allow(dead_code)] # use pyo3::prelude::*; #[pyfunction] @@ -326,7 +326,7 @@ To migrate, place a `#[pyo3(eq, eq_int)]` attribute on simple enum classes. Before: -```rust +```rust,no_run # #![allow(deprecated, dead_code)] # use pyo3::prelude::*; #[pyclass] @@ -338,7 +338,7 @@ enum SimpleEnum { After: -```rust +```rust,no_run # #![allow(dead_code)] # use pyo3::prelude::*; #[pyclass(eq, eq_int)] @@ -528,7 +528,7 @@ impl PyClassIter { If returning `"done"` via `StopIteration` is not really required, this should be written as -```rust +```rust,no_run use pyo3::prelude::*; #[pyclass] @@ -553,7 +553,7 @@ This form also has additional benefits: It has already worked in previous PyO3 v Alternatively, the implementation can also be done as it would in Python itself, i.e. by "raising" a `StopIteration` exception -```rust +```rust,no_run use pyo3::prelude::*; use pyo3::exceptions::PyStopIteration; @@ -577,7 +577,7 @@ impl PyClassIter { Finally, an asynchronous iterator can directly return an awaitable without confusing wrapping -```rust +```rust,no_run use pyo3::prelude::*; #[pyclass] @@ -885,7 +885,7 @@ fn x_or_y(x: Option, y: u64) -> u64 { After: -```rust +```rust,no_run # #![allow(dead_code)] # use pyo3::prelude::*; @@ -915,7 +915,7 @@ fn add(a: u64, b: u64) -> u64 { After: -```rust +```rust,no_run # #![allow(dead_code)] # use pyo3::prelude::*; @@ -1105,7 +1105,7 @@ fn required_argument_after_option(x: Option, y: i32) {} After, specify the intended Python signature explicitly: -```rust +```rust,no_run # #![allow(dead_code)] # use pyo3::prelude::*; @@ -1553,7 +1553,7 @@ impl PyObjectProtocol for MyClass { After: -```rust +```rust,no_run use pyo3::prelude::*; #[pyclass] @@ -1646,7 +1646,7 @@ let result: PyResult<()> = PyErr::new::("error message").into(); ``` After (also using the new reworked exception types; see the following section): -```rust +```rust,no_run # use pyo3::{PyResult, exceptions::PyTypeError}; let result: PyResult<()> = Err(PyTypeError::new_err("error message")); ``` @@ -1712,7 +1712,7 @@ impl FromPy for PyObject { ``` After -```rust +```rust,no_run # use pyo3::prelude::*; # #[allow(dead_code)] struct MyPyObjectWrapper(PyObject); @@ -1736,7 +1736,7 @@ let obj = PyObject::from_py(1.234, py); ``` After: -```rust +```rust,no_run # #![allow(deprecated)] # use pyo3::prelude::*; # Python::with_gil(|py| { @@ -1853,7 +1853,7 @@ There can be two fixes: ``` After: - ```rust + ```rust,no_run # #![allow(dead_code)] use pyo3::prelude::*; @@ -1951,7 +1951,7 @@ impl MyClass { ``` After: -```rust +```rust,no_run # use pyo3::prelude::*; #[pyclass] struct MyClass {} diff --git a/guide/src/module.md b/guide/src/module.md index 1e274c7c953..deb516d9c97 100644 --- a/guide/src/module.md +++ b/guide/src/module.md @@ -2,7 +2,7 @@ You can create a module using `#[pymodule]`: -```rust +```rust,no_run use pyo3::prelude::*; #[pyfunction] @@ -23,7 +23,7 @@ module to Python. The module's name defaults to the name of the Rust function. You can override the module name by using `#[pyo3(name = "custom_name")]`: -```rust +```rust,no_run use pyo3::prelude::*; #[pyfunction] @@ -108,7 +108,7 @@ It is not necessary to add `#[pymodule]` on nested modules, which is only requir Another syntax based on Rust inline modules is also available to declare modules. For example: -```rust +```rust,no_run # mod declarative_module_test { use pyo3::prelude::*; @@ -151,7 +151,7 @@ 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 +```rust,no_run # mod declarative_module_module_attr_test { use pyo3::prelude::*; diff --git a/guide/src/performance.md b/guide/src/performance.md index 5a57585c4a0..3c9b63b648a 100644 --- a/guide/src/performance.md +++ b/guide/src/performance.md @@ -6,7 +6,7 @@ To achieve the best possible performance, it is useful to be aware of several tr Pythonic API implemented using PyO3 are often polymorphic, i.e. they will accept `&Bound<'_, PyAny>` and try to turn this into multiple more concrete types to which the requested operation is applied. This often leads to chains of calls to `extract`, e.g. -```rust +```rust,no_run # #![allow(dead_code)] # use pyo3::prelude::*; # use pyo3::{exceptions::PyTypeError, types::PyList}; @@ -33,7 +33,7 @@ fn frobnicate<'py>(value: &Bound<'py, PyAny>) -> PyResult> { This suboptimal as the `FromPyObject` trait requires `extract` to have a `Result` return type. For native types like `PyList`, it faster to use `downcast` (which `extract` calls internally) when the error value is ignored. This avoids the costly conversion of a `PyDowncastError` to a `PyErr` required to fulfil the `FromPyObject` contract, i.e. -```rust +```rust,no_run # #![allow(dead_code)] # use pyo3::prelude::*; # use pyo3::{exceptions::PyTypeError, types::PyList}; @@ -59,7 +59,7 @@ Calling `Python::with_gil` is effectively a no-op when the GIL is already held, For example, instead of writing -```rust +```rust,no_run # #![allow(dead_code)] # use pyo3::prelude::*; # use pyo3::types::PyList; @@ -80,7 +80,7 @@ impl PartialEq for FooBound<'_> { use the more efficient -```rust +```rust,no_run # #![allow(dead_code)] # use pyo3::prelude::*; # use pyo3::types::PyList; diff --git a/guide/src/trait-bounds.md b/guide/src/trait-bounds.md index e1b8e82f1db..18476eeb9f6 100644 --- a/guide/src/trait-bounds.md +++ b/guide/src/trait-bounds.md @@ -22,7 +22,7 @@ Let's work with the following basic example of an implementation of a optimizati Let's say we have a function `solve` that operates on a model and mutates its state. The argument of the function can be any model that implements the `Model` trait : -```rust +```rust,no_run # #![allow(dead_code)] pub trait Model { fn set_variables(&mut self, inputs: &Vec); @@ -63,7 +63,7 @@ class Model: The following wrapper will call the Python model from Rust, using a struct to hold the model as a `PyAny` object: -```rust +```rust,no_run # #![allow(dead_code)] use pyo3::prelude::*; use pyo3::types::PyList; @@ -116,7 +116,7 @@ impl Model for UserModel { Now that this bit is implemented, let's expose the model wrapper to Python. Let's add the PyO3 annotations and add a constructor: -```rust +```rust,no_run # #![allow(dead_code)] # pub trait Model { # fn set_variables(&mut self, inputs: &Vec); @@ -161,7 +161,7 @@ That's a bummer! However, we can write a second wrapper around these functions to call them directly. This wrapper will also perform the type conversions between Python and Rust. -```rust +```rust,no_run # #![allow(dead_code)] # use pyo3::prelude::*; # use pyo3::types::PyList; @@ -328,7 +328,7 @@ Let's modify the code performing the type conversion to give a helpful error mes We used in our `get_results` method the following call that performs the type conversion: -```rust +```rust,no_run # #![allow(dead_code)] # use pyo3::prelude::*; # use pyo3::types::PyList; @@ -379,7 +379,7 @@ impl Model for UserModel { Let's break it down in order to perform better error handling: -```rust +```rust,no_run # #![allow(dead_code)] # use pyo3::prelude::*; # use pyo3::types::PyList; @@ -456,7 +456,7 @@ Because of this, we can write a function wrapper that takes the `UserModel`--whi It is also required to make the struct public. -```rust +```rust,no_run # #![allow(dead_code)] use pyo3::prelude::*; use pyo3::types::PyList; diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 2c4955dcc6f..8b3d0a6d592 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -1146,7 +1146,7 @@ impl FromStr for BuildFlag { /// 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 +/// ```rust,no_run /// #[cfg(py_sys_config="{varname}")] /// # struct Foo; /// ``` diff --git a/pyo3-ffi/README.md b/pyo3-ffi/README.md index 3fada2ffab6..e817a1a73d7 100644 --- a/pyo3-ffi/README.md +++ b/pyo3-ffi/README.md @@ -65,7 +65,7 @@ fn main() { ``` **`src/lib.rs`** -```rust +```rust,no_run use std::os::raw::{c_char, c_long}; use std::ptr; diff --git a/pyo3-ffi/src/lib.rs b/pyo3-ffi/src/lib.rs index b14fe1e8611..64c831c1043 100644 --- a/pyo3-ffi/src/lib.rs +++ b/pyo3-ffi/src/lib.rs @@ -129,7 +129,7 @@ //! ``` //! //! **`src/lib.rs`** -//! ```rust +//! ```rust,no_run //! use std::os::raw::{c_char, c_long}; //! use std::ptr; //! @@ -356,7 +356,7 @@ macro_rules! opaque_struct { /// /// Examples: /// -/// ```rust +/// ```rust,no_run /// use std::ffi::CStr; /// /// const HELLO: &CStr = pyo3_ffi::c_str!("hello"); diff --git a/src/conversion.rs b/src/conversion.rs index 82ad4d84977..073a1fde2e9 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -87,7 +87,7 @@ pub trait ToPyObject { /// The easiest way to implement `IntoPy` is by exposing a struct as a native Python object /// by annotating it with [`#[pyclass]`](crate::prelude::pyclass). /// -/// ```rust +/// ```rust,no_run /// use pyo3::prelude::*; /// /// # #[allow(dead_code)] @@ -103,7 +103,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 +/// ```rust,no_run /// #![allow(deprecated)] /// use pyo3::prelude::*; /// diff --git a/src/conversions/num_bigint.rs b/src/conversions/num_bigint.rs index d91973b5572..b3a1ac12531 100644 --- a/src/conversions/num_bigint.rs +++ b/src/conversions/num_bigint.rs @@ -21,7 +21,7 @@ //! Using [`BigInt`] to correctly increment an arbitrary precision integer. //! This is not possible with Rust's native integers if the Python integer is too large, //! in which case it will fail its conversion and raise `OverflowError`. -//! ```rust +//! ```rust,no_run //! use num_bigint::BigInt; //! use pyo3::prelude::*; //! diff --git a/src/conversions/num_rational.rs b/src/conversions/num_rational.rs index 6c48f67fcf2..d86a99f2632 100644 --- a/src/conversions/num_rational.rs +++ b/src/conversions/num_rational.rs @@ -17,7 +17,7 @@ //! //! Rust code to create a function that adds five to a fraction: //! -//! ```rust +//! ```rust,no_run //! use num_rational::Ratio; //! use pyo3::prelude::*; //! diff --git a/src/conversions/rust_decimal.rs b/src/conversions/rust_decimal.rs index 7c6eda21a66..882efa847e2 100644 --- a/src/conversions/rust_decimal.rs +++ b/src/conversions/rust_decimal.rs @@ -20,7 +20,7 @@ //! //! Rust code to create a function that adds one to a Decimal //! -//! ```rust +//! ```rust,no_run //! use rust_decimal::Decimal; //! use pyo3::prelude::*; //! diff --git a/src/conversions/uuid.rs b/src/conversions/uuid.rs index 6f6775bd1dc..44ac0bae25a 100644 --- a/src/conversions/uuid.rs +++ b/src/conversions/uuid.rs @@ -21,7 +21,7 @@ //! //! Rust code to create a function that parses a UUID string and returns it as a `Uuid`: //! -//! ```rust +//! ```rust,no_run //! use pyo3::prelude::*; //! use pyo3::exceptions::PyValueError; //! use uuid::Uuid; diff --git a/src/impl_/pymethods.rs b/src/impl_/pymethods.rs index 8f869068463..13039912762 100644 --- a/src/impl_/pymethods.rs +++ b/src/impl_/pymethods.rs @@ -260,7 +260,7 @@ impl PySetterDef { /// /// Elided lifetime should compile ok: /// -/// ```rust +/// ```rust,no_run /// use pyo3::prelude::*; /// use pyo3::pyclass::{PyTraverseError, PyVisit}; /// diff --git a/src/lib.rs b/src/lib.rs index 863fd010ade..68a7daf6a9c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -186,7 +186,7 @@ //! ``` //! //! **`src/lib.rs`** -//! ```rust +//! ```rust,no_run //! use pyo3::prelude::*; //! //! /// Formats the sum of two numbers as string. diff --git a/src/marker.rs b/src/marker.rs index 4fe91464244..a6a03a2b011 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -18,7 +18,7 @@ //! closure and the return type. This is done by relying on the [`Send`] auto trait. `Ungil` is //! defined as the following: //! -//! ```rust +//! ```rust,no_run //! # #![allow(dead_code)] //! pub unsafe trait Ungil {} //! @@ -95,7 +95,7 @@ //! However on nightly Rust and when PyO3's `nightly` feature is //! enabled, `Ungil` is defined as the following: //! -//! ```rust +//! ```rust,no_run //! # #[cfg(any())] //! # { //! #![feature(auto_traits, negative_impls)] @@ -803,7 +803,7 @@ impl<'py> Python<'py> { /// /// # Example /// - /// ```rust + /// ```rust,no_run /// # #![allow(dead_code)] // this example is quite impractical to test /// use pyo3::prelude::*; /// diff --git a/src/pycell.rs b/src/pycell.rs index c7e5226a292..50a3e59ae15 100644 --- a/src/pycell.rs +++ b/src/pycell.rs @@ -22,7 +22,7 @@ //! Usually you can use `&mut` references as method and function receivers and arguments, and you //! won't need to use `PyCell` directly: //! -//! ```rust +//! ```rust,no_run //! use pyo3::prelude::*; //! //! #[pyclass] diff --git a/src/pyclass.rs b/src/pyclass.rs index 0315ae5e8e8..086bd2435c3 100644 --- a/src/pyclass.rs +++ b/src/pyclass.rs @@ -55,7 +55,7 @@ impl CompareOp { /// /// Usage example: /// - /// ```rust + /// ```rust,no_run /// # use pyo3::prelude::*; /// # use pyo3::class::basic::CompareOp; /// diff --git a/src/types/module.rs b/src/types/module.rs index 99dc1be233b..cf0213a98c2 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -234,7 +234,7 @@ pub trait PyModuleMethods<'py>: crate::sealed::Sealed { /// /// # Examples /// - /// ```rust + /// ```rust,no_run /// use pyo3::prelude::*; /// /// #[pymodule] @@ -270,7 +270,7 @@ pub trait PyModuleMethods<'py>: crate::sealed::Sealed { /// /// # Examples /// - /// ```rust + /// ```rust,no_run /// use pyo3::prelude::*; /// /// #[pyclass] @@ -323,7 +323,7 @@ pub trait PyModuleMethods<'py>: crate::sealed::Sealed { /// /// # Examples /// - /// ```rust + /// ```rust,no_run /// use pyo3::prelude::*; /// /// #[pymodule] @@ -359,7 +359,7 @@ pub trait PyModuleMethods<'py>: crate::sealed::Sealed { /// Note that this also requires the [`wrap_pyfunction!`][2] macro /// to wrap a function annotated with [`#[pyfunction]`][1]. /// - /// ```rust + /// ```rust,no_run /// use pyo3::prelude::*; /// /// #[pyfunction] @@ -404,7 +404,7 @@ pub trait PyModuleMethods<'py>: crate::sealed::Sealed { /// /// # Examples /// - /// ```rust + /// ```rust,no_run /// use pyo3::prelude::*; /// /// #[pymodule(gil_used = false)] diff --git a/src/types/pysuper.rs b/src/types/pysuper.rs index 81db8cea869..9f52f9c5b56 100644 --- a/src/types/pysuper.rs +++ b/src/types/pysuper.rs @@ -21,7 +21,7 @@ impl PySuper { /// /// # Examples /// - /// ```rust + /// ```rust,no_run /// use pyo3::prelude::*; /// /// #[pyclass(subclass)] From a1537222a337e6181619b20c880a2a44a91e4e2f Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 28 Mar 2025 19:42:43 +0000 Subject: [PATCH 624/936] ci: save caches on main only by default (#5020) --- .github/workflows/build.yml | 4 ++-- .github/workflows/ci.yml | 28 ++++++++++++++-------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5fb2124836e..8bf88ef087f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -33,7 +33,7 @@ jobs: with: python-version: ${{ inputs.python-version }} architecture: ${{ inputs.python-architecture }} - check-latest: ${{ startsWith(inputs.python-version, 'pypy') }} # PyPy can have FFI changes within Python versions, which creates pain in CI + check-latest: ${{ startsWith(inputs.python-version, 'pypy') }} # PyPy can have FFI changes within Python versions, which creates pain in CI - name: Install nox run: python -m pip install --upgrade pip && pip install nox @@ -52,7 +52,7 @@ jobs: - uses: Swatinem/rust-cache@v2 with: - save-if: ${{ github.event_name != 'merge_group' }} + save-if: ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'CI-save-pr-cache') }} - if: inputs.os == 'ubuntu-latest' name: Prepare LD_LIBRARY_PATH (Ubuntu only) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 18367f353a0..91509e93104 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -71,7 +71,7 @@ jobs: python-version: "3.12" - uses: Swatinem/rust-cache@v2 with: - save-if: ${{ github.event_name != 'merge_group' }} + save-if: ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'CI-save-pr-cache') }} - run: python -m pip install --upgrade pip && pip install nox # This is a smoke test to confirm that CI will run on MSRV (including dev dependencies) - name: Check with MSRV package versions @@ -157,7 +157,7 @@ jobs: architecture: ${{ matrix.platform.python-architecture }} - uses: Swatinem/rust-cache@v2 with: - save-if: ${{ github.event_name != 'merge_group' }} + save-if: ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'CI-save-pr-cache') }} - run: python -m pip install --upgrade pip && pip install nox - run: nox -s clippy-all env: @@ -413,7 +413,7 @@ jobs: python-version: "3.12.4" - uses: Swatinem/rust-cache@v2 with: - save-if: ${{ github.event_name != 'merge_group' }} + save-if: ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'CI-save-pr-cache') }} - uses: dtolnay/rust-toolchain@stable - uses: taiki-e/install-action@valgrind - run: python -m pip install --upgrade pip && pip install nox @@ -434,7 +434,7 @@ jobs: python-version: "3.12" - uses: Swatinem/rust-cache@v2 with: - save-if: ${{ github.event_name != 'merge_group' }} + save-if: ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'CI-save-pr-cache') }} - uses: dtolnay/rust-toolchain@nightly with: components: rust-src @@ -456,7 +456,7 @@ jobs: python-version: "3.12" - uses: Swatinem/rust-cache@v2 with: - save-if: ${{ github.event_name != 'merge_group' }} + save-if: ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'CI-save-pr-cache') }} - uses: dtolnay/rust-toolchain@nightly with: components: rust-src @@ -477,7 +477,7 @@ jobs: python-version: "3.12" - uses: Swatinem/rust-cache@v2 with: - save-if: ${{ github.event_name != 'merge_group' }} + save-if: ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'CI-save-pr-cache') }} - uses: dtolnay/rust-toolchain@stable with: components: llvm-tools-preview,rust-src @@ -520,14 +520,14 @@ jobs: 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' }} + save-if: ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'CI-save-pr-cache') }} - name: Build if: steps.cache.outputs.cache-hit != 'true' run: nox -s build-emscripten - name: Test run: nox -s test-emscripten - uses: actions/cache/save@v4 - if: ${{ github.event_name != 'merge_group' }} + if: ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'CI-save-pr-cache') }} with: path: | .nox/emscripten @@ -541,7 +541,7 @@ jobs: - uses: actions/checkout@v4 - uses: Swatinem/rust-cache@v2 with: - save-if: ${{ github.event_name != 'merge_group' }} + save-if: ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'CI-save-pr-cache') }} - uses: dtolnay/rust-toolchain@stable with: components: rust-src @@ -588,7 +588,7 @@ jobs: - uses: actions/checkout@v4 - uses: Swatinem/rust-cache@v2 with: - save-if: ${{ github.event_name != 'merge_group' }} + save-if: ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'CI-save-pr-cache') }} - uses: dtolnay/rust-toolchain@stable with: components: rust-src @@ -625,7 +625,7 @@ jobs: python-version: "3.12" - uses: Swatinem/rust-cache@v2 with: - save-if: ${{ github.event_name != 'merge_group' }} + save-if: ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'CI-save-pr-cache') }} - uses: dtolnay/rust-toolchain@stable - run: python3 -m pip install --upgrade pip && pip install nox - run: python3 -m nox -s test-version-limits @@ -649,7 +649,7 @@ jobs: python-version: "3.12" - uses: Swatinem/rust-cache@v2 with: - save-if: ${{ github.event_name != 'merge_group' }} + save-if: ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'CI-save-pr-cache') }} - uses: dtolnay/rust-toolchain@master with: toolchain: stable @@ -701,7 +701,7 @@ jobs: - uses: Swatinem/rust-cache@v2 with: workspaces: examples/maturin-starter - save-if: ${{ github.event_name != 'merge_group' }} + save-if: ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'CI-save-pr-cache') }} key: ${{ matrix.target }} - name: Setup cross-compiler if: ${{ matrix.target == 'x86_64-pc-windows-gnu' }} @@ -731,7 +731,7 @@ jobs: - uses: Swatinem/rust-cache@v2 with: workspaces: examples/maturin-starter - save-if: ${{ github.event_name != 'merge_group' }} + save-if: ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'CI-save-pr-cache') }} - uses: actions/cache/restore@v4 with: # https://github.com/PyO3/maturin/discussions/1953 From 27178e86848149e84fc7f5f31973d72f789942d8 Mon Sep 17 00:00:00 2001 From: Thomas Tanon Date: Fri, 28 Mar 2025 22:13:42 +0100 Subject: [PATCH 625/936] Allows to introspect Python modules from cdylib: first step (#3977) * Draft: Allows to introspect Python modules from cdylib * Do not use a custom link_section to get WASM compatibility * Introspection macro: generate less concatenations * Fixes dev mach-o artifacts introspection * Fixes test-introspection * Support cfg with module members * Code review feedback * Revert back to TokenStream inside of ConcatenationBuilder --- .github/workflows/ci.yml | 47 +++++ Cargo.toml | 3 +- guide/src/features.md | 4 +- newsfragments/3977.added.md | 1 + noxfile.py | 23 ++- pyo3-introspection/Cargo.toml | 18 ++ pyo3-introspection/LICENSE-APACHE | 1 + pyo3-introspection/LICENSE-MIT | 1 + pyo3-introspection/src/introspection.rs | 244 ++++++++++++++++++++++ pyo3-introspection/src/lib.rs | 8 + pyo3-introspection/src/model.rs | 17 ++ pyo3-introspection/src/stubs.rs | 52 +++++ pyo3-introspection/tests/test.rs | 77 +++++++ pyo3-macros-backend/Cargo.toml | 1 + pyo3-macros-backend/src/introspection.rs | 250 +++++++++++++++++++++++ pyo3-macros-backend/src/lib.rs | 2 + pyo3-macros-backend/src/module.rs | 19 ++ pyo3-macros-backend/src/pyclass.rs | 21 +- pyo3-macros-backend/src/pyfunction.rs | 8 +- pyo3-macros/Cargo.toml | 1 + pytests/Cargo.toml | 2 +- pytests/README.md | 3 + pytests/src/lib.rs | 76 ++++--- pytests/src/pyclasses.rs | 16 +- pytests/stubs/__init__.pyi | 0 pytests/stubs/pyclasses.pyi | 5 + pytests/stubs/pyfunctions.pyi | 0 src/impl_.rs | 2 + src/impl_/concat.rs | 30 +++ src/types/mod.rs | 4 + tests/test_compile_error.rs | 3 +- 31 files changed, 887 insertions(+), 52 deletions(-) create mode 100644 newsfragments/3977.added.md create mode 100644 pyo3-introspection/Cargo.toml create mode 120000 pyo3-introspection/LICENSE-APACHE create mode 120000 pyo3-introspection/LICENSE-MIT create mode 100644 pyo3-introspection/src/introspection.rs create mode 100644 pyo3-introspection/src/lib.rs create mode 100644 pyo3-introspection/src/model.rs create mode 100644 pyo3-introspection/src/stubs.rs create mode 100644 pyo3-introspection/tests/test.rs create mode 100644 pyo3-macros-backend/src/introspection.rs create mode 100644 pytests/stubs/__init__.pyi create mode 100644 pytests/stubs/pyclasses.pyi create mode 100644 pytests/stubs/pyfunctions.pyi create mode 100644 src/impl_/concat.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 91509e93104..978f5dfe2a6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -757,6 +757,52 @@ jobs: with: path: ~/.cache/cargo-xwin key: cargo-xwin-cache + + test-introspection: + needs: [fmt] + strategy: + matrix: + 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: "windows-latest", + python-architecture: "x64", + rust-target: "x86_64-pc-windows-msvc", + }, + { + os: "windows-latest", + python-architecture: "x86", + rust-target: "i686-pc-windows-msvc", + }, + ] + runs-on: ${{ matrix.platform.os }} + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.platform.rust-target }} + components: rust-src + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + architecture: ${{ matrix.platform.python-architecture }} + - uses: Swatinem/rust-cache@v2 + with: + save-if: ${{ github.event_name != 'merge_group' }} + - run: python -m pip install --upgrade pip && pip install nox + - run: nox -s test-introspection + env: + CARGO_BUILD_TARGET: ${{ matrix.platform.rust-target }} + conclusion: needs: - fmt @@ -775,6 +821,7 @@ jobs: - check-feature-powerset - test-cross-compilation - test-cross-compilation-windows + - test-introspection if: always() runs-on: ubuntu-latest steps: diff --git a/Cargo.toml b/Cargo.toml index 84c6daba30e..102fc8cc536 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,7 +78,7 @@ experimental-async = ["macros", "pyo3-macros/experimental-async"] # Enables pyo3::inspect module and additional type information on FromPyObject # and IntoPy traits -experimental-inspect = [] +experimental-inspect = ["pyo3-macros/experimental-inspect"] # Enables macros: #[pyclass], #[pymodule], #[pyfunction] etc. macros = ["pyo3-macros", "indoc", "unindent"] @@ -145,6 +145,7 @@ members = [ "pyo3-build-config", "pyo3-macros", "pyo3-macros-backend", + "pyo3-introspection", "pytests", "examples", ] diff --git a/guide/src/features.md b/guide/src/features.md index 252b0916637..08696683d74 100644 --- a/guide/src/features.md +++ b/guide/src/features.md @@ -59,7 +59,9 @@ The feature has some unfinished refinements and performance improvements. To hel ### `experimental-inspect` -This feature adds the `pyo3::inspect` module, as well as `IntoPy::type_output` and `FromPyObject::type_input` APIs to produce Python type "annotations" for Rust types. +This feature adds to the built binaries introspection data that can be then retrieved using the `pyo3-introspection` crate to generate [type stubs](https://typing.readthedocs.io/en/latest/source/stubs.html). + +Also, this feature adds the `pyo3::inspect` module, as well as `IntoPy::type_output` and `FromPyObject::type_input` APIs to produce Python type "annotations" for Rust types. This is a first step towards adding first-class support for generating type annotations automatically in PyO3, however work is needed to finish this off. All feedback and offers of help welcome on [issue #2454](https://github.com/PyO3/pyo3/issues/2454). diff --git a/newsfragments/3977.added.md b/newsfragments/3977.added.md new file mode 100644 index 00000000000..58f116cd837 --- /dev/null +++ b/newsfragments/3977.added.md @@ -0,0 +1 @@ +Basic introspection and stub generation based on metadata embedded in produced cdylib. \ No newline at end of file diff --git a/noxfile.py b/noxfile.py index 61e8dee71fd..ee5911e4aea 100644 --- a/noxfile.py +++ b/noxfile.py @@ -816,6 +816,26 @@ def update_ui_tests(session: nox.Session): _run_cargo(session, *command, "--features=abi3,full,jiff-02", env=env) +@nox.session(name="test-introspection") +def test_introspection(session: nox.Session): + session.install("maturin") + target = os.environ.get("CARGO_BUILD_TARGET") + for options in ([], ["--release"]): + if target is not None: + options += ("--target", target) + session.run_always("maturin", "develop", "-m", "./pytests/Cargo.toml", *options) + # We look for the built library + lib_file = None + for file in Path(session.virtualenv.location).rglob("pyo3_pytests.*"): + if file.is_file(): + lib_file = str(file.resolve()) + _run_cargo_test( + session, + package="pyo3-introspection", + env={"PYO3_PYTEST_LIB_PATH": lib_file}, + ) + + def _build_docs_for_ffi_check(session: nox.Session) -> None: # pyo3-ffi-check needs to scrape docs of pyo3-ffi env = os.environ.copy() @@ -932,6 +952,7 @@ def _run_cargo_test( *, package: Optional[str] = None, features: Optional[str] = None, + env: Optional[Dict[str, str]] = None, ) -> None: command = ["cargo"] if "careful" in session.posargs: @@ -946,7 +967,7 @@ def _run_cargo_test( if features: command.append(f"--features={features}") - _run(session, *command, external=True) + _run(session, *command, external=True, env=env or {}) def _run_cargo_publish(session: nox.Session, *, package: str) -> None: diff --git a/pyo3-introspection/Cargo.toml b/pyo3-introspection/Cargo.toml new file mode 100644 index 00000000000..d861ec7b6d5 --- /dev/null +++ b/pyo3-introspection/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "pyo3-introspection" +version = "0.25.0-dev" +description = "Introspect dynamic libraries built with PyO3 to get metadata about the exported Python types" +authors = ["PyO3 Project and Contributors "] +homepage = "https://github.com/pyo3/pyo3" +repository = "https://github.com/pyo3/pyo3" +license = "MIT OR Apache-2.0" +edition = "2021" + +[dependencies] +anyhow = "1" +goblin = "0.9.0" +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +[lints] +workspace = true diff --git a/pyo3-introspection/LICENSE-APACHE b/pyo3-introspection/LICENSE-APACHE new file mode 120000 index 00000000000..965b606f331 --- /dev/null +++ b/pyo3-introspection/LICENSE-APACHE @@ -0,0 +1 @@ +../LICENSE-APACHE \ No newline at end of file diff --git a/pyo3-introspection/LICENSE-MIT b/pyo3-introspection/LICENSE-MIT new file mode 120000 index 00000000000..76219eb72e8 --- /dev/null +++ b/pyo3-introspection/LICENSE-MIT @@ -0,0 +1 @@ +../LICENSE-MIT \ No newline at end of file diff --git a/pyo3-introspection/src/introspection.rs b/pyo3-introspection/src/introspection.rs new file mode 100644 index 00000000000..b96daccbe4b --- /dev/null +++ b/pyo3-introspection/src/introspection.rs @@ -0,0 +1,244 @@ +use crate::model::{Class, Function, Module}; +use anyhow::{bail, Context, Result}; +use goblin::elf::Elf; +use goblin::mach::symbols::N_SECT; +use goblin::mach::{Mach, MachO, SingleArch}; +use goblin::pe::PE; +use goblin::Object; +use serde::Deserialize; +use std::collections::HashMap; +use std::fs; +use std::path::Path; + +/// Introspect a cdylib built with PyO3 and returns the definition of a Python module. +/// +/// This function currently supports the ELF (most *nix including Linux), Match-O (macOS) and PE (Windows) formats. +pub fn introspect_cdylib(library_path: impl AsRef, main_module_name: &str) -> Result { + let chunks = find_introspection_chunks_in_binary_object(library_path.as_ref())?; + parse_chunks(&chunks, main_module_name) +} + +/// Parses the introspection chunks found in the binary +fn parse_chunks(chunks: &[Chunk], main_module_name: &str) -> Result { + let chunks_by_id = chunks + .iter() + .map(|c| { + ( + match c { + Chunk::Module { id, .. } => id, + Chunk::Class { id, .. } => id, + Chunk::Function { id, .. } => id, + }, + c, + ) + }) + .collect::>(); + // We look for the root chunk + for chunk in chunks { + if let Chunk::Module { + name, + members, + id: _, + } = chunk + { + if name == main_module_name { + return parse_module(name, members, &chunks_by_id); + } + } + } + bail!("No module named {main_module_name} found") +} + +fn parse_module( + name: &str, + members: &[String], + chunks_by_id: &HashMap<&String, &Chunk>, +) -> Result { + let mut modules = Vec::new(); + let mut classes = Vec::new(); + let mut functions = Vec::new(); + for member in members { + if let Some(chunk) = chunks_by_id.get(member) { + match chunk { + Chunk::Module { + name, + members, + id: _, + } => { + modules.push(parse_module(name, members, chunks_by_id)?); + } + Chunk::Class { name, id: _ } => classes.push(Class { name: name.into() }), + Chunk::Function { name, id: _ } => functions.push(Function { name: name.into() }), + } + } + } + Ok(Module { + name: name.into(), + modules, + classes, + functions, + }) +} + +fn find_introspection_chunks_in_binary_object(path: &Path) -> Result> { + let library_content = + fs::read(path).with_context(|| format!("Failed to read {}", path.display()))?; + match Object::parse(&library_content) + .context("The built library is not valid or not supported by our binary parser")? + { + Object::Elf(elf) => find_introspection_chunks_in_elf(&elf, &library_content), + Object::Mach(Mach::Binary(macho)) => { + find_introspection_chunks_in_macho(&macho, &library_content) + } + Object::Mach(Mach::Fat(multi_arch)) => { + for arch in &multi_arch { + match arch? { + SingleArch::MachO(macho) => { + return find_introspection_chunks_in_macho(&macho, &library_content) + } + SingleArch::Archive(_) => (), + } + } + bail!("No Mach-o chunk found in the multi-arch Mach-o container") + } + Object::PE(pe) => find_introspection_chunks_in_pe(&pe, &library_content), + _ => { + bail!("Only ELF, Mach-o and PE containers can be introspected") + } + } +} + +fn find_introspection_chunks_in_elf(elf: &Elf<'_>, library_content: &[u8]) -> Result> { + let mut chunks = Vec::new(); + for sym in &elf.syms { + if is_introspection_symbol(elf.strtab.get_at(sym.st_name).unwrap_or_default()) { + let section_header = &elf.section_headers[sym.st_shndx]; + let data_offset = sym.st_value + section_header.sh_offset - section_header.sh_addr; + chunks.push(read_symbol_value_with_ptr_and_len( + &library_content[usize::try_from(data_offset).context("File offset overflow")?..], + 0, + library_content, + elf.is_64, + )?); + } + } + Ok(chunks) +} + +fn find_introspection_chunks_in_macho( + macho: &MachO<'_>, + library_content: &[u8], +) -> Result> { + if !macho.little_endian { + bail!("Only little endian Mach-o binaries are supported"); + } + + let sections = macho + .segments + .sections() + .flatten() + .map(|t| t.map(|s| s.0)) + .collect::, _>>()?; + let mut chunks = Vec::new(); + for (name, nlist) in macho.symbols().flatten() { + if nlist.is_global() && nlist.get_type() == N_SECT && is_introspection_symbol(name) { + let section = §ions[nlist.n_sect]; + let data_offset = nlist.n_value + u64::from(section.offset) - section.addr; + chunks.push(read_symbol_value_with_ptr_and_len( + &library_content[usize::try_from(data_offset).context("File offset overflow")?..], + 0, + library_content, + macho.is_64, + )?); + } + } + Ok(chunks) +} + +fn find_introspection_chunks_in_pe(pe: &PE<'_>, library_content: &[u8]) -> Result> { + let rdata_data_section = pe + .sections + .iter() + .find(|section| section.name().unwrap_or_default() == ".rdata") + .context("No .rdata section found")?; + let rdata_shift = pe.image_base + + usize::try_from(rdata_data_section.virtual_address) + .context(".rdata virtual_address overflow")? + - usize::try_from(rdata_data_section.pointer_to_raw_data) + .context(".rdata pointer_to_raw_data overflow")?; + + let mut chunks = Vec::new(); + for export in &pe.exports { + if is_introspection_symbol(export.name.unwrap_or_default()) { + chunks.push(read_symbol_value_with_ptr_and_len( + &library_content[export.offset.context("No symbol offset")?..], + rdata_shift, + library_content, + pe.is_64, + )?); + } + } + Ok(chunks) +} + +fn read_symbol_value_with_ptr_and_len( + value_slice: &[u8], + shift: usize, + full_library_content: &[u8], + is_64: bool, +) -> Result { + let (ptr, len) = if is_64 { + let (ptr, len) = value_slice[..16].split_at(8); + let ptr = usize::try_from(u64::from_le_bytes( + ptr.try_into().context("Too short symbol value")?, + )) + .context("Pointer overflow")?; + let len = usize::try_from(u64::from_le_bytes( + len.try_into().context("Too short symbol value")?, + )) + .context("Length overflow")?; + (ptr, len) + } else { + let (ptr, len) = value_slice[..8].split_at(4); + let ptr = usize::try_from(u32::from_le_bytes( + ptr.try_into().context("Too short symbol value")?, + )) + .context("Pointer overflow")?; + let len = usize::try_from(u32::from_le_bytes( + len.try_into().context("Too short symbol value")?, + )) + .context("Length overflow")?; + (ptr, len) + }; + let chunk = &full_library_content[ptr - shift..ptr - shift + len]; + serde_json::from_slice(chunk).with_context(|| { + format!( + "Failed to parse introspection chunk: '{}'", + String::from_utf8_lossy(chunk) + ) + }) +} + +fn is_introspection_symbol(name: &str) -> bool { + name.strip_prefix('_') + .unwrap_or(name) + .starts_with("PYO3_INTROSPECTION_0_") +} + +#[derive(Deserialize)] +#[serde(tag = "type", rename_all = "lowercase")] +enum Chunk { + Module { + id: String, + name: String, + members: Vec, + }, + Class { + id: String, + name: String, + }, + Function { + id: String, + name: String, + }, +} diff --git a/pyo3-introspection/src/lib.rs b/pyo3-introspection/src/lib.rs new file mode 100644 index 00000000000..22aac933e85 --- /dev/null +++ b/pyo3-introspection/src/lib.rs @@ -0,0 +1,8 @@ +//! Utilities to introspect cdylib built using PyO3 and generate [type stubs](https://typing.readthedocs.io/en/latest/source/stubs.html). + +pub use crate::introspection::introspect_cdylib; +pub use crate::stubs::module_stub_files; + +mod introspection; +pub mod model; +mod stubs; diff --git a/pyo3-introspection/src/model.rs b/pyo3-introspection/src/model.rs new file mode 100644 index 00000000000..73a4c27d082 --- /dev/null +++ b/pyo3-introspection/src/model.rs @@ -0,0 +1,17 @@ +#[derive(Debug, Eq, PartialEq, Clone, Hash)] +pub struct Module { + pub name: String, + pub modules: Vec, + pub classes: Vec, + pub functions: Vec, +} + +#[derive(Debug, Eq, PartialEq, Clone, Hash)] +pub struct Class { + pub name: String, +} + +#[derive(Debug, Eq, PartialEq, Clone, Hash)] +pub struct Function { + pub name: String, +} diff --git a/pyo3-introspection/src/stubs.rs b/pyo3-introspection/src/stubs.rs new file mode 100644 index 00000000000..0705911032f --- /dev/null +++ b/pyo3-introspection/src/stubs.rs @@ -0,0 +1,52 @@ +use crate::model::{Class, Function, Module}; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; + +/// Generates the [type stubs](https://typing.readthedocs.io/en/latest/source/stubs.html) of a given module. +/// It returns a map between the file name and the file content. +/// The root module stubs will be in the `__init__.pyi` file and the submodules directory +/// in files with a relevant name. +pub fn module_stub_files(module: &Module) -> HashMap { + let mut output_files = HashMap::new(); + add_module_stub_files(module, Path::new(""), &mut output_files); + output_files +} + +fn add_module_stub_files( + module: &Module, + module_path: &Path, + output_files: &mut HashMap, +) { + output_files.insert(module_path.join("__init__.pyi"), module_stubs(module)); + for submodule in &module.modules { + if submodule.modules.is_empty() { + output_files.insert( + module_path.join(format!("{}.pyi", submodule.name)), + module_stubs(submodule), + ); + } else { + add_module_stub_files(submodule, &module_path.join(&submodule.name), output_files); + } + } +} + +/// Generates the module stubs to a String, not including submodules +fn module_stubs(module: &Module) -> String { + let mut elements = Vec::new(); + for class in &module.classes { + elements.push(class_stubs(class)); + } + for function in &module.functions { + elements.push(function_stubs(function)); + } + elements.push(String::new()); // last line jump + elements.join("\n") +} + +fn class_stubs(class: &Class) -> String { + format!("class {}: ...", class.name) +} + +fn function_stubs(function: &Function) -> String { + format!("def {}(*args, **kwargs): ...", function.name) +} diff --git a/pyo3-introspection/tests/test.rs b/pyo3-introspection/tests/test.rs new file mode 100644 index 00000000000..37070a53a13 --- /dev/null +++ b/pyo3-introspection/tests/test.rs @@ -0,0 +1,77 @@ +use anyhow::Result; +use pyo3_introspection::{introspect_cdylib, module_stub_files}; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use std::{env, fs}; + +#[test] +fn pytests_stubs() -> Result<()> { + // We run the introspection + let binary = env::var_os("PYO3_PYTEST_LIB_PATH") + .expect("The PYO3_PYTEST_LIB_PATH constant must be set and target the pyo3-pytests cdylib"); + let module = introspect_cdylib(binary, "pyo3_pytests")?; + let actual_stubs = module_stub_files(&module); + + // We read the expected stubs + let expected_subs_dir = Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap() + .join("pytests") + .join("stubs"); + let mut expected_subs = HashMap::new(); + add_dir_files( + &expected_subs_dir, + &expected_subs_dir.canonicalize()?, + &mut expected_subs, + )?; + + // We ensure we do not have extra generated files + for file_name in actual_stubs.keys() { + assert!( + expected_subs.contains_key(file_name), + "The generated file {} is not in the expected stubs directory pytests/stubs", + file_name.display() + ); + } + + // We ensure the expected files are generated properly + for (file_name, expected_file_content) in &expected_subs { + let actual_file_content = actual_stubs.get(file_name).unwrap_or_else(|| { + panic!( + "The expected stub file {} has not been generated", + file_name.display() + ) + }); + assert_eq!( + &expected_file_content.replace('\r', ""), // Windows compatibility + actual_file_content, + "The content of file {} is different", + file_name.display() + ) + } + + Ok(()) +} + +fn add_dir_files( + dir_path: &Path, + base_dir_path: &Path, + output: &mut HashMap, +) -> Result<()> { + for entry in fs::read_dir(dir_path)? { + let entry = entry?; + if entry.file_type()?.is_dir() { + add_dir_files(&entry.path(), base_dir_path, output)?; + } else { + output.insert( + entry + .path() + .canonicalize()? + .strip_prefix(base_dir_path)? + .into(), + fs::read_to_string(entry.path())?, + ); + } + } + Ok(()) +} diff --git a/pyo3-macros-backend/Cargo.toml b/pyo3-macros-backend/Cargo.toml index 91e0009f9f6..874a7cae700 100644 --- a/pyo3-macros-backend/Cargo.toml +++ b/pyo3-macros-backend/Cargo.toml @@ -33,3 +33,4 @@ workspace = true [features] experimental-async = [] +experimental-inspect = [] diff --git a/pyo3-macros-backend/src/introspection.rs b/pyo3-macros-backend/src/introspection.rs new file mode 100644 index 00000000000..17ae19ee8d9 --- /dev/null +++ b/pyo3-macros-backend/src/introspection.rs @@ -0,0 +1,250 @@ +//! Generates introspection data i.e. JSON strings in the .pyo3i0 section. +//! +//! There is a JSON per PyO3 proc macro (pyclass, pymodule, pyfunction...). +//! +//! These JSON blobs can refer to each others via the _PYO3_INTROSPECTION_ID constants +//! providing unique ids for each element. +//! +//! The JSON blobs format must be synchronized with the `pyo3_introspection::introspection.rs::Chunk` +//! type that is used to parse them. + +use crate::utils::PyO3CratePath; +use proc_macro2::{Span, TokenStream}; +use quote::{format_ident, quote, ToTokens}; +use std::collections::hash_map::DefaultHasher; +use std::collections::HashMap; +use std::hash::{Hash, Hasher}; +use std::mem::take; +use std::sync::atomic::{AtomicUsize, Ordering}; +use syn::{Attribute, Ident}; + +static GLOBAL_COUNTER_FOR_UNIQUE_NAMES: AtomicUsize = AtomicUsize::new(0); + +pub fn module_introspection_code<'a>( + pyo3_crate_path: &PyO3CratePath, + name: &str, + members: impl IntoIterator, + members_cfg_attrs: impl IntoIterator>, +) -> TokenStream { + let stub = IntrospectionNode::Map( + [ + ("type", IntrospectionNode::String("module")), + ("id", IntrospectionNode::IntrospectionId(None)), + ("name", IntrospectionNode::String(name)), + ( + "members", + IntrospectionNode::List( + members + .into_iter() + .zip(members_cfg_attrs) + .filter_map(|(member, attributes)| { + if attributes.is_empty() { + Some(IntrospectionNode::IntrospectionId(Some(member))) + } else { + None // TODO: properly interpret cfg attributes + } + }) + .collect(), + ), + ), + ] + .into(), + ) + .emit(pyo3_crate_path); + let introspection_id = introspection_id_const(); + quote! { + #stub + #introspection_id + } +} + +pub fn class_introspection_code( + pyo3_crate_path: &PyO3CratePath, + ident: &Ident, + name: &str, +) -> TokenStream { + let stub = IntrospectionNode::Map( + [ + ("type", IntrospectionNode::String("class")), + ("id", IntrospectionNode::IntrospectionId(Some(ident))), + ("name", IntrospectionNode::String(name)), + ] + .into(), + ) + .emit(pyo3_crate_path); + let introspection_id = introspection_id_const(); + quote! { + #stub + impl #ident { + #introspection_id + } + } +} + +pub fn function_introspection_code(pyo3_crate_path: &PyO3CratePath, name: &str) -> TokenStream { + let stub = IntrospectionNode::Map( + [ + ("type", IntrospectionNode::String("function")), + ("id", IntrospectionNode::IntrospectionId(None)), + ("name", IntrospectionNode::String(name)), + ] + .into(), + ) + .emit(pyo3_crate_path); + let introspection_id = introspection_id_const(); + quote! { + #stub + #introspection_id + } +} + +enum IntrospectionNode<'a> { + String(&'a str), + IntrospectionId(Option<&'a Ident>), + Map(HashMap<&'static str, IntrospectionNode<'a>>), + List(Vec>), +} + +impl IntrospectionNode<'_> { + fn emit(self, pyo3_crate_path: &PyO3CratePath) -> TokenStream { + let mut content = ConcatenationBuilder::default(); + self.add_to_serialization(&mut content); + let content = content.into_token_stream(pyo3_crate_path); + + let static_name = format_ident!("PYO3_INTROSPECTION_0_{}", unique_element_id()); + // #[no_mangle] is required to make sure some linkers like Linux ones do not mangle the section name too. + quote! { + const _: () = { + #[used] + #[no_mangle] + static #static_name: &'static str = #content; + }; + } + } + + fn add_to_serialization(self, content: &mut ConcatenationBuilder) { + match self { + Self::String(string) => { + content.push_str_to_escape(string); + } + Self::IntrospectionId(ident) => { + content.push_str("\""); + content.push_tokens(if let Some(ident) = ident { + quote! { #ident::_PYO3_INTROSPECTION_ID } + } else { + Ident::new("_PYO3_INTROSPECTION_ID", Span::call_site()).into_token_stream() + }); + content.push_str("\""); + } + Self::Map(map) => { + content.push_str("{"); + for (i, (key, value)) in map.into_iter().enumerate() { + if i > 0 { + content.push_str(","); + } + content.push_str_to_escape(key); + content.push_str(":"); + value.add_to_serialization(content); + } + content.push_str("}"); + } + Self::List(list) => { + content.push_str("["); + for (i, value) in list.into_iter().enumerate() { + if i > 0 { + content.push_str(","); + } + value.add_to_serialization(content); + } + content.push_str("]"); + } + } + } +} + +#[derive(Default)] +struct ConcatenationBuilder { + elements: Vec, + current_string: String, +} + +impl ConcatenationBuilder { + fn push_tokens(&mut self, token_stream: TokenStream) { + if !self.current_string.is_empty() { + self.elements.push(ConcatenationBuilderElement::String(take( + &mut self.current_string, + ))); + } + self.elements + .push(ConcatenationBuilderElement::TokenStream(token_stream)); + } + + fn push_str(&mut self, value: &str) { + self.current_string.push_str(value); + } + + fn push_str_to_escape(&mut self, value: &str) { + self.current_string.push('"'); + for c in value.chars() { + match c { + '\\' => self.current_string.push_str("\\\\"), + '"' => self.current_string.push_str("\\\""), + c => { + if c < char::from(32) { + panic!("ASCII chars below 32 are not allowed") + } else { + self.current_string.push(c); + } + } + } + } + self.current_string.push('"'); + } + + fn into_token_stream(self, pyo3_crate_path: &PyO3CratePath) -> TokenStream { + let mut elements = self.elements; + if !self.current_string.is_empty() { + elements.push(ConcatenationBuilderElement::String(self.current_string)); + } + + if let [ConcatenationBuilderElement::String(string)] = elements.as_slice() { + // We avoid the const_concat! macro if there is only a single string + return string.to_token_stream(); + } + + quote! { + #pyo3_crate_path::impl_::concat::const_concat!(#(#elements , )*) + } + } +} + +enum ConcatenationBuilderElement { + String(String), + TokenStream(TokenStream), +} + +impl ToTokens for ConcatenationBuilderElement { + fn to_tokens(&self, tokens: &mut TokenStream) { + match self { + Self::String(s) => s.to_tokens(tokens), + Self::TokenStream(ts) => ts.to_tokens(tokens), + } + } +} + +fn introspection_id_const() -> TokenStream { + let id = unique_element_id().to_string(); + quote! { + #[doc(hidden)] + pub const _PYO3_INTROSPECTION_ID: &'static str = #id; + } +} + +fn unique_element_id() -> u64 { + let mut hasher = DefaultHasher::new(); + format!("{:?}", Span::call_site()).hash(&mut hasher); // Distinguishes between call sites + GLOBAL_COUNTER_FOR_UNIQUE_NAMES + .fetch_add(1, Ordering::Relaxed) + .hash(&mut hasher); // If there are multiple elements in the same call site + hasher.finish() +} diff --git a/pyo3-macros-backend/src/lib.rs b/pyo3-macros-backend/src/lib.rs index 7893a94af98..4baf58907b4 100644 --- a/pyo3-macros-backend/src/lib.rs +++ b/pyo3-macros-backend/src/lib.rs @@ -11,6 +11,8 @@ mod utils; mod attributes; mod frompyobject; mod intopyobject; +#[cfg(feature = "experimental-inspect")] +mod introspection; mod konst; mod method; mod module; diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index d9fac3cbd7b..7ee165cbd27 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -1,5 +1,7 @@ //! Code generation for the function that initializes a python module and adds classes and function. +#[cfg(feature = "experimental-inspect")] +use crate::introspection::module_introspection_code; use crate::{ attributes::{ self, kw, take_attributes, take_pyo3_options, CrateAttribute, GILUsedAttribute, @@ -337,6 +339,16 @@ pub fn pymodule_module_impl( } } + #[cfg(feature = "experimental-inspect")] + let introspection = module_introspection_code( + pyo3_path, + &name.to_string(), + &module_items, + &module_items_cfg_attrs, + ); + #[cfg(not(feature = "experimental-inspect"))] + let introspection = quote! {}; + let module_def = quote! {{ use #pyo3_path::impl_::pymodule as impl_; const INITIALIZER: impl_::ModuleInitializer = impl_::ModuleInitializer(__pyo3_pymodule); @@ -362,6 +374,7 @@ pub fn pymodule_module_impl( #(#items)* #initialization + #introspection fn __pyo3_pymodule(module: &#pyo3_path::Bound<'_, #pyo3_path::types::PyModule>) -> #pyo3_path::PyResult<()> { use #pyo3_path::impl_::pymodule::PyAddToModule; @@ -401,6 +414,11 @@ pub fn pymodule_function_impl( options.gil_used.map_or(true, |op| op.value.value), ); + #[cfg(feature = "experimental-inspect")] + let introspection = module_introspection_code(pyo3_path, &name.to_string(), &[], &[]); + #[cfg(not(feature = "experimental-inspect"))] + let introspection = quote! {}; + // Module function called with optional Python<'_> marker as first arg, followed by the module. let mut module_args = Vec::new(); if function.sig.inputs.len() == 2 { @@ -413,6 +431,7 @@ pub fn pymodule_function_impl( #[doc(hidden)] #vis mod #ident { #initialization + #introspection } // Generate the definition inside an anonymous function in the same scope as the original function - diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 7512d63c9fb..aadfe2332ad 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -14,6 +14,8 @@ use crate::attributes::{ FreelistAttribute, ModuleAttribute, NameAttribute, NameLitStr, RenameAllAttribute, StrFormatterAttribute, }; +#[cfg(feature = "experimental-inspect")] +use crate::introspection::class_introspection_code; use crate::konst::{ConstAttributes, ConstSpec}; use crate::method::{FnArg, FnSpec, PyArg, RegularArg}; use crate::pyfunction::ConstructorAttribute; @@ -1096,6 +1098,7 @@ fn impl_complex_enum( impl_builder.impl_pyclassimpl(ctx)?, impl_builder.impl_add_to_module(ctx), impl_builder.impl_freelist(ctx), + impl_builder.impl_introspection(ctx), ] .into_iter() .collect(); @@ -2063,17 +2066,17 @@ impl<'a> PyClassImplsBuilder<'a> { } fn impl_all(&self, ctx: &Ctx) -> Result { - let tokens = [ + Ok([ self.impl_pyclass(ctx), self.impl_extractext(ctx), self.impl_into_py(ctx), self.impl_pyclassimpl(ctx)?, self.impl_add_to_module(ctx), self.impl_freelist(ctx), + self.impl_introspection(ctx), ] .into_iter() - .collect(); - Ok(tokens) + .collect()) } fn impl_pyclass(&self, ctx: &Ctx) -> TokenStream { @@ -2416,6 +2419,18 @@ impl<'a> PyClassImplsBuilder<'a> { Vec::new() } } + + #[cfg(feature = "experimental-inspect")] + fn impl_introspection(&self, ctx: &Ctx) -> TokenStream { + let Ctx { pyo3_path, .. } = ctx; + let name = get_class_python_name(self.cls, self.attr).to_string(); + class_introspection_code(pyo3_path, self.cls, &name) + } + + #[cfg(not(feature = "experimental-inspect"))] + fn impl_introspection(&self, _ctx: &Ctx) -> TokenStream { + quote! {} + } } fn define_inventory_class(inventory_class_name: &syn::Ident, ctx: &Ctx) -> TokenStream { diff --git a/pyo3-macros-backend/src/pyfunction.rs b/pyo3-macros-backend/src/pyfunction.rs index c87492f095c..68976190fbe 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "experimental-inspect")] +use crate::introspection::function_introspection_code; use crate::utils::Ctx; use crate::{ attributes::{ @@ -241,15 +243,19 @@ 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), ctx); + #[cfg(feature = "experimental-inspect")] + let introspection = function_introspection_code(pyo3_path, &name.to_string()); + #[cfg(not(feature = "experimental-inspect"))] + let introspection = quote! {}; let wrapped_pyfunction = quote! { - // Create a module with the same name as the `#[pyfunction]` - this way `use ` // will actually bring both the module and the function into scope. #[doc(hidden)] #vis mod #name { pub(crate) struct MakeDef; pub const _PYO3_DEF: #pyo3_path::impl_::pymethods::PyMethodDef = MakeDef::_PYO3_DEF; + #introspection } // Generate the definition inside an anonymous function in the same scope as the original function - diff --git a/pyo3-macros/Cargo.toml b/pyo3-macros/Cargo.toml index 25821b84e81..84473123977 100644 --- a/pyo3-macros/Cargo.toml +++ b/pyo3-macros/Cargo.toml @@ -17,6 +17,7 @@ proc-macro = true [features] multiple-pymethods = [] experimental-async = ["pyo3-macros-backend/experimental-async"] +experimental-inspect = ["pyo3-macros-backend/experimental-inspect"] [dependencies] proc-macro2 = { version = "1.0.60", default-features = false } diff --git a/pytests/Cargo.toml b/pytests/Cargo.toml index 1fee3093275..2763cdec2e3 100644 --- a/pytests/Cargo.toml +++ b/pytests/Cargo.toml @@ -8,7 +8,7 @@ publish = false rust-version = "1.63" [dependencies] -pyo3 = { path = "../", features = ["extension-module"] } +pyo3 = { path = "../", features = ["extension-module", "experimental-inspect"] } [build-dependencies] pyo3-build-config = { path = "../pyo3-build-config" } diff --git a/pytests/README.md b/pytests/README.md index 7ced072aa36..1016baa7209 100644 --- a/pytests/README.md +++ b/pytests/README.md @@ -2,6 +2,9 @@ An extension module built using PyO3, used to test and benchmark PyO3 from Python. +The `stubs` directory contains Python stubs used to test the automated stubs introspection. +To test them run `nox -s test-introspection`. + ## Testing This package is intended to be built using `maturin`. Once built, you can run the tests using `pytest`: diff --git a/pytests/src/lib.rs b/pytests/src/lib.rs index b6c32230dac..4112c90400e 100644 --- a/pytests/src/lib.rs +++ b/pytests/src/lib.rs @@ -18,43 +18,51 @@ pub mod sequence; pub mod subclassing; #[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))] - m.add_wrapped(wrap_pymodule!(buf_and_str::buf_and_str))?; - m.add_wrapped(wrap_pymodule!(comparisons::comparisons))?; - #[cfg(not(Py_LIMITED_API))] - m.add_wrapped(wrap_pymodule!(datetime::datetime))?; - m.add_wrapped(wrap_pymodule!(dict_iter::dict_iter))?; - m.add_wrapped(wrap_pymodule!(enums::enums))?; - m.add_wrapped(wrap_pymodule!(misc::misc))?; - m.add_wrapped(wrap_pymodule!(objstore::objstore))?; - m.add_wrapped(wrap_pymodule!(othermod::othermod))?; - m.add_wrapped(wrap_pymodule!(path::path))?; - m.add_wrapped(wrap_pymodule!(pyclasses::pyclasses))?; - m.add_wrapped(wrap_pymodule!(pyfunctions::pyfunctions))?; - m.add_wrapped(wrap_pymodule!(sequence::sequence))?; - m.add_wrapped(wrap_pymodule!(subclassing::subclassing))?; +mod pyo3_pytests { + use super::*; + + #[pymodule_export] + use {pyclasses::pyclasses, pyfunctions::pyfunctions}; // Inserting to sys.modules allows importing submodules nicely from Python // e.g. import pyo3_pytests.buf_and_str as bas + #[pymodule_init] + fn init(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_wrapped(wrap_pymodule!(awaitable::awaitable))?; + #[cfg(not(Py_LIMITED_API))] + m.add_wrapped(wrap_pymodule!(buf_and_str::buf_and_str))?; + m.add_wrapped(wrap_pymodule!(comparisons::comparisons))?; + #[cfg(not(Py_LIMITED_API))] + m.add_wrapped(wrap_pymodule!(datetime::datetime))?; + m.add_wrapped(wrap_pymodule!(dict_iter::dict_iter))?; + m.add_wrapped(wrap_pymodule!(enums::enums))?; + m.add_wrapped(wrap_pymodule!(misc::misc))?; + m.add_wrapped(wrap_pymodule!(objstore::objstore))?; + m.add_wrapped(wrap_pymodule!(othermod::othermod))?; + m.add_wrapped(wrap_pymodule!(path::path))?; + m.add_wrapped(wrap_pymodule!(sequence::sequence))?; + m.add_wrapped(wrap_pymodule!(subclassing::subclassing))?; + + // Inserting to sys.modules allows importing submodules nicely from Python + // e.g. import pyo3_pytests.buf_and_str as bas - let sys = PyModule::import(py, "sys")?; - let sys_modules = sys.getattr("modules")?.downcast_into::()?; - sys_modules.set_item("pyo3_pytests.awaitable", m.getattr("awaitable")?)?; - sys_modules.set_item("pyo3_pytests.buf_and_str", m.getattr("buf_and_str")?)?; - sys_modules.set_item("pyo3_pytests.comparisons", m.getattr("comparisons")?)?; - sys_modules.set_item("pyo3_pytests.datetime", m.getattr("datetime")?)?; - sys_modules.set_item("pyo3_pytests.dict_iter", m.getattr("dict_iter")?)?; - sys_modules.set_item("pyo3_pytests.enums", m.getattr("enums")?)?; - sys_modules.set_item("pyo3_pytests.misc", m.getattr("misc")?)?; - sys_modules.set_item("pyo3_pytests.objstore", m.getattr("objstore")?)?; - sys_modules.set_item("pyo3_pytests.othermod", m.getattr("othermod")?)?; - sys_modules.set_item("pyo3_pytests.path", m.getattr("path")?)?; - sys_modules.set_item("pyo3_pytests.pyclasses", m.getattr("pyclasses")?)?; - sys_modules.set_item("pyo3_pytests.pyfunctions", m.getattr("pyfunctions")?)?; - sys_modules.set_item("pyo3_pytests.sequence", m.getattr("sequence")?)?; - sys_modules.set_item("pyo3_pytests.subclassing", m.getattr("subclassing")?)?; + let sys = PyModule::import(m.py(), "sys")?; + let sys_modules = sys.getattr("modules")?.downcast_into::()?; + sys_modules.set_item("pyo3_pytests.awaitable", m.getattr("awaitable")?)?; + sys_modules.set_item("pyo3_pytests.buf_and_str", m.getattr("buf_and_str")?)?; + sys_modules.set_item("pyo3_pytests.comparisons", m.getattr("comparisons")?)?; + sys_modules.set_item("pyo3_pytests.datetime", m.getattr("datetime")?)?; + sys_modules.set_item("pyo3_pytests.dict_iter", m.getattr("dict_iter")?)?; + sys_modules.set_item("pyo3_pytests.enums", m.getattr("enums")?)?; + sys_modules.set_item("pyo3_pytests.misc", m.getattr("misc")?)?; + sys_modules.set_item("pyo3_pytests.objstore", m.getattr("objstore")?)?; + sys_modules.set_item("pyo3_pytests.othermod", m.getattr("othermod")?)?; + sys_modules.set_item("pyo3_pytests.path", m.getattr("path")?)?; + sys_modules.set_item("pyo3_pytests.pyclasses", m.getattr("pyclasses")?)?; + sys_modules.set_item("pyo3_pytests.pyfunctions", m.getattr("pyfunctions")?)?; + sys_modules.set_item("pyo3_pytests.sequence", m.getattr("sequence")?)?; + sys_modules.set_item("pyo3_pytests.subclassing", m.getattr("subclassing")?)?; - Ok(()) + Ok(()) + } } diff --git a/pytests/src/pyclasses.rs b/pytests/src/pyclasses.rs index 3af08c053cc..1091e6c16b3 100644 --- a/pytests/src/pyclasses.rs +++ b/pytests/src/pyclasses.rs @@ -105,14 +105,12 @@ impl ClassWithDict { } #[pymodule(gil_used = false)] -pub fn pyclasses(m: &Bound<'_, PyModule>) -> PyResult<()> { - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; +pub mod pyclasses { #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] - m.add_class::()?; - - Ok(()) + #[pymodule_export] + use super::ClassWithDict; + #[pymodule_export] + use super::{ + AssertingBaseClass, ClassWithoutConstructor, EmptyClass, PyClassIter, PyClassThreadIter, + }; } diff --git a/pytests/stubs/__init__.pyi b/pytests/stubs/__init__.pyi new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pytests/stubs/pyclasses.pyi b/pytests/stubs/pyclasses.pyi new file mode 100644 index 00000000000..2a36e0b4540 --- /dev/null +++ b/pytests/stubs/pyclasses.pyi @@ -0,0 +1,5 @@ +class AssertingBaseClass: ... +class ClassWithoutConstructor: ... +class EmptyClass: ... +class PyClassIter: ... +class PyClassThreadIter: ... diff --git a/pytests/stubs/pyfunctions.pyi b/pytests/stubs/pyfunctions.pyi new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/impl_.rs b/src/impl_.rs index 7ac71399854..5cf2af4d664 100644 --- a/src/impl_.rs +++ b/src/impl_.rs @@ -7,6 +7,8 @@ //! breaking semver guarantees. pub mod callback; +#[cfg(feature = "experimental-inspect")] +pub mod concat; #[cfg(feature = "experimental-async")] pub mod coroutine; pub mod exceptions; diff --git a/src/impl_/concat.rs b/src/impl_/concat.rs new file mode 100644 index 00000000000..4993eb45bc5 --- /dev/null +++ b/src/impl_/concat.rs @@ -0,0 +1,30 @@ +/// `concat!` but working with constants +#[macro_export] +#[doc(hidden)] +macro_rules! const_concat { + ($e:expr) => {{ + $e + }}; + ($l:expr, $($r:expr),+ $(,)?) => {{ + const L: &'static str = $l; + const R: &'static str = $crate::impl_::concat::const_concat!($($r),*); + const LEN: usize = L.len() + R.len(); + const fn combine(l: &'static [u8], r: &'static [u8]) -> [u8; LEN] { + let mut out = [0u8; LEN]; + let mut i = 0; + while i < l.len() { + out[i] = l[i]; + i += 1; + } + while i < LEN { + out[i] = r[i - l.len()]; + i += 1; + } + out + } + #[allow(unsafe_code)] + unsafe { ::std::str::from_utf8_unchecked(&combine(L.as_bytes(), R.as_bytes())) } + }} +} + +pub use const_concat; diff --git a/src/types/mod.rs b/src/types/mod.rs index 637d07d72b6..19f96d47e42 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -162,6 +162,10 @@ macro_rules! pyobject_native_type_info( impl $name { #[doc(hidden)] pub const _PYO3_DEF: $crate::impl_::pymodule::AddTypeToModule = $crate::impl_::pymodule::AddTypeToModule::new(); + + #[allow(dead_code)] + #[doc(hidden)] + pub const _PYO3_INTROSPECTION_ID: &'static str = concat!(stringify!($module), stringify!($name)); } }; ); diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index 9a4ffc114ba..958c5ff253d 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -58,7 +58,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(not(Py_LIMITED_API))] + #[cfg(not(any(Py_LIMITED_API, feature = "experimental-inspect")))] t.pass("tests/ui/forbid_unsafe.rs"); #[cfg(all(Py_LIMITED_API, not(feature = "experimental-async")))] // output changes with async feature @@ -67,6 +67,7 @@ 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"); + #[cfg(not(feature = "experimental-inspect"))] 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"); From bd1e7d22f4d8375a50891ee77e0d7faebb4ae885 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 28 Mar 2025 21:37:56 +0000 Subject: [PATCH 626/936] remove all functionality deprecated in PyO3 0.23 (except `IntoPy` and `ToPyObject`) (#4982) * remove all functionality deprecated in PyO3 0.23 (except `IntoPy` and `ToPyObject`) * Apply suggestions from code review Co-authored-by: Lily Foote * bump version to 0.25.0-dev --------- Co-authored-by: Lily Foote --- Cargo.toml | 8 +- guide/src/migration.md | 2 +- guide/src/python-from-rust.md | 4 +- newsfragments/4982.removed.md | 1 + pyo3-benches/benches/bench_list.rs | 4 +- 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 +- src/buffer.rs | 7 -- src/err/mod.rs | 124 +------------------------ src/exceptions.rs | 24 ----- src/instance.rs | 33 +------ src/lib.rs | 20 ---- src/macros.rs | 20 +--- src/marker.rs | 73 +++------------ src/marshal.rs | 19 ---- src/prelude.rs | 3 - src/type_object.rs | 21 ----- src/types/any.rs | 37 -------- src/types/boolobject.rs | 7 -- src/types/bytearray.rs | 28 ------ src/types/bytes.rs | 31 ------- src/types/capsule.rs | 26 ------ src/types/complex.rs | 11 --- src/types/datetime.rs | 143 +---------------------------- src/types/dict.rs | 22 ----- src/types/ellipsis.rs | 7 -- src/types/float.rs | 7 -- src/types/frozenset.rs | 25 ----- src/types/function.rs | 42 --------- src/types/iterator.rs | 9 +- src/types/list.rs | 23 ----- src/types/memoryview.rs | 7 -- src/types/mod.rs | 10 +- src/types/module.rs | 41 +-------- src/types/none.rs | 7 -- src/types/notimplemented.rs | 7 -- src/types/num.rs | 4 - src/types/pysuper.rs | 10 -- src/types/set.rs | 18 ---- src/types/slice.rs | 14 --- src/types/string.rs | 33 +------ src/types/tuple.rs | 23 ----- src/types/typeobject.rs | 7 -- src/types/weakref/anyref.rs | 142 +--------------------------- src/types/weakref/proxy.rs | 73 --------------- src/types/weakref/reference.rs | 62 ------------- tests/ui/reject_generics.stderr | 4 +- 50 files changed, 55 insertions(+), 1206 deletions(-) create mode 100644 newsfragments/4982.removed.md diff --git a/Cargo.toml b/Cargo.toml index 102fc8cc536..caf072dbe09 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3" -version = "0.24.0" +version = "0.25.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" # 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.25.0-dev" } # support crates for macros feature -pyo3-macros = { path = "pyo3-macros", version = "=0.24.0", optional = true } +pyo3-macros = { path = "pyo3-macros", version = "=0.25.0-dev", 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.25.0-dev", features = ["resolve-config"] } [features] default = ["macros"] diff --git a/guide/src/migration.md b/guide/src/migration.md index 402b22ab134..e421ff659ff 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -179,7 +179,7 @@ With the removal of the old API, many "Bound" API functions which had been intro Before: -```rust +```rust,ignore # #![allow(deprecated)] # use pyo3::prelude::*; # use pyo3::types::PyTuple; diff --git a/guide/src/python-from-rust.md b/guide/src/python-from-rust.md index ebb7fa1f4da..7ab39161e70 100644 --- a/guide/src/python-from-rust.md +++ b/guide/src/python-from-rust.md @@ -17,7 +17,7 @@ are met. Its lifetime `'py` is a central part of PyO3's API. The `Python<'py>` token serves three purposes: -* It provides global APIs for the Python interpreter, such as [`py.eval_bound()`][eval] and [`py.import_bound()`][import]. +* It provides global APIs for the Python interpreter, such as [`py.eval()`][eval] and [`py.import()`][import]. * It can be passed to functions that require a proof of holding the GIL, such as [`Py::clone_ref`][clone_ref]. * Its lifetime `'py` is used to bind many of PyO3's types to the Python interpreter, such as [`Bound<'py, T>`][Bound]. @@ -45,6 +45,6 @@ Because of the lack of exclusive `&mut` references, PyO3's APIs for Python objec [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 +[import]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.import [clone_ref]: {{#PYO3_DOCS_URL}}/pyo3/prelude/struct.Py.html#method.clone_ref [Bound]: {{#PYO3_DOCS_URL}}/pyo3/struct.Bound.html diff --git a/newsfragments/4982.removed.md b/newsfragments/4982.removed.md new file mode 100644 index 00000000000..61f9dedbc48 --- /dev/null +++ b/newsfragments/4982.removed.md @@ -0,0 +1 @@ +Remove all functionality deprecated in PyO3 0.23. diff --git a/pyo3-benches/benches/bench_list.rs b/pyo3-benches/benches/bench_list.rs index 7a19452455e..f6df0cc4315 100644 --- a/pyo3-benches/benches/bench_list.rs +++ b/pyo3-benches/benches/bench_list.rs @@ -42,7 +42,7 @@ fn list_get_item(b: &mut Bencher<'_>) { fn list_nth(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 50; - let list = PyList::new_bound(py, 0..LEN); + let list = PyList::new(py, 0..LEN).unwrap(); let mut sum = 0; b.iter(|| { for i in 0..LEN { @@ -55,7 +55,7 @@ fn list_nth(b: &mut Bencher<'_>) { fn list_nth_back(b: &mut Bencher<'_>) { Python::with_gil(|py| { const LEN: usize = 50; - let list = PyList::new_bound(py, 0..LEN); + let list = PyList::new(py, 0..LEN).unwrap(); let mut sum = 0; b.iter(|| { for i in 0..LEN { diff --git a/pyo3-build-config/Cargo.toml b/pyo3-build-config/Cargo.toml index b8030cc4304..86456e33cb5 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.25.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 16c8a3374cd..8b75c978c2e 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.25.0-dev" 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.25.0-dev", features = ["resolve-config"] } [lints] workspace = true diff --git a/pyo3-macros-backend/Cargo.toml b/pyo3-macros-backend/Cargo.toml index 874a7cae700..5730df1ae1d 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.25.0-dev" 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.25.0-dev", 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.25.0-dev" } [lints] workspace = true diff --git a/pyo3-macros/Cargo.toml b/pyo3-macros/Cargo.toml index 84473123977..c7bd43fad69 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.25.0-dev" description = "Proc macros for PyO3 package" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -23,7 +23,7 @@ experimental-inspect = ["pyo3-macros-backend/experimental-inspect"] 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.25.0-dev" } [lints] workspace = true diff --git a/pyproject.toml b/pyproject.toml index d757c927f4f..1ff0f0332de 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ [tool.towncrier] filename = "CHANGELOG.md" -version = "0.24.0" +version = "0.25.0-dev" start_string = "\n" template = ".towncrier.template.md" title_format = "## [{version}] - {project_date}" diff --git a/src/buffer.rs b/src/buffer.rs index 2d94681a5c7..c5e5de568eb 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -224,13 +224,6 @@ 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, diff --git a/src/err/mod.rs b/src/err/mod.rs index bd026bab14d..21e89efcf8e 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -14,7 +14,7 @@ use crate::{Borrowed, BoundObject, Py, PyAny, PyObject, Python}; #[allow(deprecated)] use crate::{IntoPy, ToPyObject}; use std::borrow::Cow; -use std::ffi::{CStr, CString}; +use std::ffi::CStr; mod err_state; mod impls; @@ -29,8 +29,8 @@ use std::convert::Infallible; /// 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_bound`](PyErr::value_bound), -/// [`get_type_bound`](PyErr::get_type_bound), or [`is_instance_bound`](PyErr::is_instance_bound) +/// 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. pub struct PyErr { state: PyErrState, @@ -126,7 +126,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_bound`] instead. + /// consider using [`PyErr::from_value`] instead. /// /// If `T` does not inherit from `BaseException`, then a `TypeError` will be returned. /// @@ -198,16 +198,6 @@ impl PyErr { PyErr::from_state(PyErrState::lazy_arguments(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. @@ -256,13 +246,6 @@ 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 @@ -278,13 +261,6 @@ impl PyErr { 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 @@ -302,13 +278,6 @@ impl PyErr { 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 @@ -339,13 +308,6 @@ impl PyErr { 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 { @@ -449,29 +411,6 @@ impl PyErr { 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)] @@ -529,13 +468,6 @@ impl PyErr { (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 @@ -586,13 +518,6 @@ impl PyErr { 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. @@ -601,7 +526,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_bound()`]. + /// object can be retrieved using [`Python::get_type()`]. /// /// Example: /// ```rust @@ -630,19 +555,6 @@ 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. @@ -680,32 +592,6 @@ 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 diff --git a/src/exceptions.rs b/src/exceptions.rs index 66044a6658f..dbbc8c714f1 100644 --- a/src/exceptions.rs +++ b/src/exceptions.rs @@ -653,19 +653,6 @@ 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 @@ -701,17 +688,6 @@ 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")); diff --git a/src/instance.rs b/src/instance.rs index 7a32f4c9c30..a47274e7732 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -884,7 +884,7 @@ impl<'a, 'py, T> BoundObject<'py, T> for Borrowed<'a, 'py, T> { /// 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. +/// - You can call methods directly on `Py` with [`Py::call`], [`Py::call_method`] and friends. /// /// These require passing in the [`Python<'py>`](crate::Python) token but are otherwise similar to the corresponding /// methods on [`PyAny`]. @@ -1514,19 +1514,6 @@ impl Py { .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, - py: Python<'_>, - args: impl IntoPy>, - kwargs: Option<&Bound<'_, PyDict>>, - ) -> PyResult { - self.call(py, args.into_py(py), kwargs) - } - /// Calls the object with only positional arguments. /// /// This is equivalent to the Python expression `self(*args)`. @@ -1576,24 +1563,6 @@ impl Py { .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, - 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.into_py(py), kwargs) - } - /// Calls a method on the object with only positional arguments. /// /// This is equivalent to the Python expression `self.name(*args)`. diff --git a/src/lib.rs b/src/lib.rs index 68a7daf6a9c..3954223a489 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -367,26 +367,6 @@ pub(crate) mod sealed; pub mod class { pub use self::gc::{PyTraverseError, PyVisit}; - pub use self::methods::*; - - #[doc(hidden)] - pub mod methods { - #[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. /// /// Prefer using the same content from `pyo3::pyclass`, e.g. `use pyo3::pyclass::CompareOp` instead diff --git a/src/macros.rs b/src/macros.rs index 53fcbcaad3d..01d035f85ac 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -2,10 +2,10 @@ /// /// # Panics /// -/// This macro internally calls [`Python::run_bound`](crate::Python::run_bound) and panics +/// This macro internally calls [`Python::run`](crate::Python::run) and panics /// if it returns `Err`, after printing the error to stdout. /// -/// If you need to handle failures, please use [`Python::run_bound`](crate::marker::Python::run_bound) instead. +/// If you need to handle failures, please use [`Python::run`](crate::marker::Python::run) instead. /// /// # Examples /// ``` @@ -149,22 +149,6 @@ macro_rules! wrap_pyfunction { }}; } -/// Wraps a Rust function annotated with [`#[pyfunction]`](macro@crate::pyfunction). -/// -/// This can be used with [`PyModule::add_function`](crate::types::PyModuleMethods::add_function) to -/// add free functions to a [`PyModule`](crate::types::PyModule) - see its documentation for more -/// information. -#[deprecated(since = "0.23.0", note = "renamed to `wrap_pyfunction!`")] -#[macro_export] -macro_rules! wrap_pyfunction_bound { - ($function:path) => { - $crate::wrap_pyfunction!($function) - }; - ($function:path, $py_or_module:expr) => { - $crate::wrap_pyfunction!($function, $py_or_module) - }; -} - /// Returns a function that takes a [`Python`](crate::Python) instance and returns a /// Python module. /// diff --git a/src/marker.rs b/src/marker.rs index a6a03a2b011..2ffd12f5995 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -128,10 +128,8 @@ use crate::types::{ PyAny, PyDict, PyEllipsis, PyModule, PyNone, PyNotImplemented, PyString, PyType, }; use crate::version::PythonVersionInfo; -#[allow(deprecated)] -use crate::IntoPy; -use crate::{ffi, Bound, Py, PyObject, PyTypeInfo}; -use std::ffi::{CStr, CString}; +use crate::{ffi, Bound, PyObject, PyTypeInfo}; +use std::ffi::CStr; use std::marker::PhantomData; use std::os::raw::c_int; @@ -217,7 +215,7 @@ mod nightly { /// # use pyo3::prelude::*; /// # use pyo3::types::PyString; /// Python::with_gil(|py| { - /// let string = PyString::new_bound(py, "foo"); + /// let string = PyString::new(py, "foo"); /// /// py.allow_threads(|| { /// println!("{:?}", string); @@ -245,7 +243,7 @@ mod nightly { /// 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); /// @@ -304,9 +302,9 @@ 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_bound`]. +/// - It provides a global API for the Python interpreter, such as [`Python::eval`]. /// - It can be passed to functions that require a proof of holding the GIL, such as -/// [`Py::clone_ref`]. +/// [`Py::clone_ref`][crate::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 [`Bound<'py, PyAny>`]. /// @@ -354,7 +352,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`]. +/// interpreter, using functions such as [`Python::eval`] and [`PyModule::import`]. #[derive(Copy, Clone)] pub struct Python<'py>(PhantomData<(&'py GILGuard, NotSend)>); @@ -496,7 +494,7 @@ impl<'py> Python<'py> { /// use pyo3::types::PyString; /// /// fn parallel_print(py: Python<'_>) { - /// let s = PyString::new_bound(py, "This object cannot be accessed without holding the GIL >_<"); + /// let s = PyString::new(py, "This object cannot be accessed without holding the GIL >_<"); /// py.allow_threads(move || { /// println!("{:?}", s); // This causes a compile error. /// }); @@ -548,20 +546,6 @@ impl<'py> Python<'py> { 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> { - let code = CString::new(code)?; - self.eval(&code, globals, locals) - } - /// Executes one or more Python statements in the given context. /// /// If `globals` is `None`, it defaults to Python module `__main__`. @@ -609,20 +593,6 @@ 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`, @@ -699,17 +669,6 @@ impl<'py> Python<'py> { T::type_object(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(self, name: N) -> PyResult> where @@ -718,18 +677,6 @@ impl<'py> Python<'py> { PyModule::import(self, name) } - /// 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> - where - N: IntoPy>, - { - self.import(name.into_py(self)) - } - /// Gets the Python builtin value `None`. #[allow(non_snake_case)] // the Python keyword starts with uppercase #[inline] @@ -1025,6 +972,10 @@ mod tests { #[cfg(feature = "macros")] #[test] fn test_py_run_inserts_globals_2() { + use std::ffi::CString; + + use crate::Py; + #[crate::pyclass(crate = "crate")] #[derive(Clone)] struct CodeRunner { diff --git a/src/marshal.rs b/src/marshal.rs index d541da7d695..cecaa16f53b 100644 --- a/src/marshal.rs +++ b/src/marshal.rs @@ -40,19 +40,6 @@ pub fn dumps<'py>(object: &Bound<'py, PyAny>, version: i32) -> PyResult( - 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<'py, B>(py: Python<'py>, data: &B) -> PyResult> where @@ -65,12 +52,6 @@ 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::*; diff --git a/src/prelude.rs b/src/prelude.rs index d4f649f552a..62cde9a5591 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -25,9 +25,6 @@ pub use pyo3_macros::{ #[cfg(feature = "macros")] 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/type_object.rs b/src/type_object.rs index b7cad4ab3b2..d181fff08b1 100644 --- a/src/type_object.rs +++ b/src/type_object.rs @@ -62,38 +62,17 @@ 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(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(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. diff --git a/src/types/any.rs b/src/types/any.rs index 7e86401ed9b..00f5594ce5d 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -659,12 +659,6 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// This is equivalent to the Python expression `self is None`. fn is_none(&self) -> bool; - /// 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. /// /// This is equivalent to the Python expression `len(self) == 0`. @@ -718,13 +712,6 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// ``` 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. fn get_type(&self) -> Bound<'py, PyType>; @@ -1371,10 +1358,6 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { unsafe { ffi::Py_None() == self.as_ptr() } } - fn is_ellipsis(&self) -> bool { - unsafe { ffi::Py_Ellipsis() == self.as_ptr() } - } - fn is_empty(&self) -> PyResult { self.len().map(|l| l == 0) } @@ -1443,10 +1426,6 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { 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())) } } @@ -2086,22 +2065,6 @@ class SimpleClass: }) } - #[test] - #[allow(deprecated)] - fn test_is_ellipsis() { - Python::with_gil(|py| { - let v = py - .eval(ffi::c_str!("..."), None, None) - .map_err(|e| e.display(py)) - .unwrap(); - - assert!(v.is_ellipsis()); - - let not_ellipsis = 5i32.into_pyobject(py).unwrap(); - assert!(!not_ellipsis.is_ellipsis()); - }); - } - #[test] fn test_is_callable() { Python::with_gil(|py| { diff --git a/src/types/boolobject.rs b/src/types/boolobject.rs index 53043fa798c..2e7f0369de1 100644 --- a/src/types/boolobject.rs +++ b/src/types/boolobject.rs @@ -38,13 +38,6 @@ impl PyBool { .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`]. diff --git a/src/types/bytearray.rs b/src/types/bytearray.rs index 094cdf3144b..c6a0683f635 100644 --- a/src/types/bytearray.rs +++ b/src/types/bytearray.rs @@ -32,13 +32,6 @@ 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 @@ -84,20 +77,6 @@ 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<'py>(src: &Bound<'py, PyAny>) -> PyResult> { @@ -107,13 +86,6 @@ impl PyByteArray { .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`]. diff --git a/src/types/bytes.rs b/src/types/bytes.rs index a820ece3b45..4e2ce9c96e7 100644 --- a/src/types/bytes.rs +++ b/src/types/bytes.rs @@ -64,13 +64,6 @@ 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 @@ -114,16 +107,6 @@ 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. @@ -141,20 +124,6 @@ impl PyBytes { .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> { - unsafe { Self::from_ptr(py, ptr, len) } - } } /// Implementation of functionality for [`PyBytes`]. diff --git a/src/types/capsule.rs b/src/types/capsule.rs index 5f97aeda5c5..19dde8a1cba 100644 --- a/src/types/capsule.rs +++ b/src/types/capsule.rs @@ -89,17 +89,6 @@ impl PyCapsule { 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(py, value, name) - } - /// Constructs a new capsule whose contents are `value`, associated with `name`. /// /// Also provides a destructor: when the `PyCapsule` is destroyed, it will be passed the original object, @@ -139,21 +128,6 @@ 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 diff --git a/src/types/complex.rs b/src/types/complex.rs index 58651569b47..00c6c58b853 100644 --- a/src/types/complex.rs +++ b/src/types/complex.rs @@ -38,17 +38,6 @@ 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)))] diff --git a/src/types/datetime.rs b/src/types/datetime.rs index 0b0a7324c4b..085d41d8e65 100644 --- a/src/types/datetime.rs +++ b/src/types/datetime.rs @@ -177,13 +177,6 @@ pub trait PyTzInfoAccess<'py> { /// /// 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`. @@ -212,13 +205,6 @@ 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` @@ -234,13 +220,6 @@ 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> { @@ -304,34 +283,6 @@ 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. @@ -371,36 +322,6 @@ 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` @@ -420,17 +341,6 @@ 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> { @@ -543,21 +453,7 @@ 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`]. + /// Alternate constructor that takes a `fold` argument. See [`PyDateTime::new_with_fold`]. pub fn new_with_fold<'py>( py: Python<'py>, hour: u8, @@ -582,21 +478,6 @@ 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> { @@ -661,7 +542,7 @@ impl<'py> PyTzInfoAccess<'py> for Bound<'py, PyTime> { /// [`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 +/// For concrete time zone implementations, see [`timezone_utc`] and /// the [`zoneinfo` module](https://docs.python.org/3/library/zoneinfo.html). #[repr(transparent)] pub struct PyTzInfo(PyAny); @@ -688,13 +569,6 @@ pub fn timezone_utc(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 @@ -748,19 +622,6 @@ 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> { diff --git a/src/types/dict.rs b/src/types/dict.rs index 3f5118aab6b..0ef5c477ae1 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -67,13 +67,6 @@ impl 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 @@ -90,14 +83,6 @@ impl PyDict { })?; 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`]. @@ -776,13 +761,6 @@ 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<'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<'py>) -> Bound<'py, PyDict> { - self.into_py_dict(py).unwrap() - } } impl<'py, T, I> IntoPyDict<'py> for I diff --git a/src/types/ellipsis.rs b/src/types/ellipsis.rs index ee5898c9013..19cb2d2dca9 100644 --- a/src/types/ellipsis.rs +++ b/src/types/ellipsis.rs @@ -18,13 +18,6 @@ impl 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 { diff --git a/src/types/float.rs b/src/types/float.rs index 3c2d6643d18..89001495227 100644 --- a/src/types/float.rs +++ b/src/types/float.rs @@ -43,13 +43,6 @@ impl PyFloat { .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`]. diff --git a/src/types/frozenset.rs b/src/types/frozenset.rs index df22560cd8e..edde7ca146e 100644 --- a/src/types/frozenset.rs +++ b/src/types/frozenset.rs @@ -49,13 +49,6 @@ impl<'py> PyFrozenSetBuilder<'py> { 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`. @@ -100,17 +93,6 @@ impl PyFrozenSet { try_new_from_iter(py, elements) } - /// 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: crate::ToPyObject + 'a>( - py: Python<'p>, - elements: impl IntoIterator, - ) -> PyResult> { - Self::new(py, elements.into_iter().map(|e| e.to_object(py))) - } - /// Creates a new empty frozen set pub fn empty(py: Python<'_>) -> PyResult> { unsafe { @@ -119,13 +101,6 @@ impl PyFrozenSet { .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`]. diff --git a/src/types/function.rs b/src/types/function.rs index 2f4da950b0a..1747c87038e 100644 --- a/src/types/function.rs +++ b/src/types/function.rs @@ -39,19 +39,6 @@ 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), @@ -66,19 +53,6 @@ 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 @@ -131,22 +105,6 @@ 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, - for<'p> R: crate::impl_::callback::IntoPyCallbackOutput<'p, *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 f6709a8f234..6731fff2c48 100644 --- a/src/types/iterator.rs +++ b/src/types/iterator.rs @@ -34,7 +34,7 @@ pyobject_native_type_named!(PyIterator); impl PyIterator { /// 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], + /// Usually it is more convenient to write [`obj.try_iter()`][crate::types::any::PyAnyMethods::try_iter], /// which is a more concise way of calling this function. pub fn from_object<'py>(obj: &Bound<'py, PyAny>) -> PyResult> { unsafe { @@ -43,13 +43,6 @@ impl PyIterator { .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) - } } #[derive(Debug)] diff --git a/src/types/list.rs b/src/types/list.rs index bd770aaeca7..9612aa00cda 100644 --- a/src/types/list.rs +++ b/src/types/list.rs @@ -110,22 +110,6 @@ impl PyList { try_new_from_iter(py, iter) } - /// Deprecated name for [`PyList::new`]. - #[deprecated(since = "0.23.0", note = "renamed to `PyList::new`")] - #[allow(deprecated)] - #[inline] - #[track_caller] - pub fn new_bound( - py: Python<'_>, - elements: impl IntoIterator, - ) -> Bound<'_, PyList> - where - T: crate::ToPyObject, - U: ExactSizeIterator, - { - Self::new(py, elements.into_iter().map(|e| e.to_object(py))).unwrap() - } - /// Constructs a new empty list. pub fn empty(py: Python<'_>) -> Bound<'_, PyList> { unsafe { @@ -134,13 +118,6 @@ impl PyList { .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`]. diff --git a/src/types/memoryview.rs b/src/types/memoryview.rs index 81acc5cbb2a..31bdcd47d65 100644 --- a/src/types/memoryview.rs +++ b/src/types/memoryview.rs @@ -22,13 +22,6 @@ impl PyMemoryView { .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> { diff --git a/src/types/mod.rs b/src/types/mod.rs index 19f96d47e42..49aa4eb62cf 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -11,8 +11,8 @@ pub use self::complex::{PyComplex, PyComplexMethods}; #[cfg(not(Py_LIMITED_API))] #[allow(deprecated)] pub use self::datetime::{ - timezone_utc, timezone_utc_bound, PyDate, PyDateAccess, PyDateTime, PyDelta, PyDeltaAccess, - PyTime, PyTimeAccess, PyTzInfo, PyTzInfoAccess, + timezone_utc, PyDate, PyDateAccess, PyDateTime, PyDelta, PyDeltaAccess, PyTime, PyTimeAccess, + PyTzInfo, PyTzInfoAccess, }; pub use self::dict::{IntoPyDict, PyDict, PyDictMethods}; #[cfg(not(any(PyPy, GraalPy)))] @@ -35,8 +35,7 @@ pub use self::memoryview::PyMemoryView; pub use self::module::{PyModule, PyModuleMethods}; pub use self::none::PyNone; pub use self::notimplemented::PyNotImplemented; -#[allow(deprecated)] -pub use self::num::{PyInt, PyLong}; +pub use self::num::PyInt; #[cfg(not(any(PyPy, GraalPy)))] pub use self::pysuper::PySuper; pub use self::sequence::{PySequence, PySequenceMethods}; @@ -44,8 +43,7 @@ pub use self::set::{PySet, PySetMethods}; pub use self::slice::{PySlice, PySliceIndices, PySliceMethods}; #[cfg(not(Py_LIMITED_API))] pub use self::string::PyStringData; -#[allow(deprecated)] -pub use self::string::{PyString, PyStringMethods, PyUnicode}; +pub use self::string::{PyString, PyStringMethods}; pub use self::traceback::{PyTraceback, PyTracebackMethods}; pub use self::tuple::{PyTuple, PyTupleMethods}; pub use self::typeobject::{PyType, PyTypeMethods}; diff --git a/src/types/module.rs b/src/types/module.rs index cf0213a98c2..cae3554da5b 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -7,10 +7,9 @@ use crate::types::{ any::PyAnyMethods, list::PyListMethods, PyAny, PyCFunction, PyDict, PyList, PyString, }; use crate::{ - exceptions, ffi, Borrowed, Bound, BoundObject, IntoPyObject, IntoPyObjectExt, Py, PyObject, - Python, + exceptions, ffi, Borrowed, Bound, BoundObject, IntoPyObject, IntoPyObjectExt, PyObject, Python, }; -use std::ffi::{CStr, CString}; +use std::ffi::CStr; #[cfg(all(not(Py_LIMITED_API), Py_GIL_DISABLED))] use std::os::raw::c_int; use std::str; @@ -59,13 +58,6 @@ impl PyModule { } } - /// 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 @@ -99,17 +91,6 @@ 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 - N: crate::IntoPy>, - { - Self::import(py, name.into_py(py)) - } - /// Creates and loads a module named `module_name`, /// containing the Python code passed to `code` /// and pretending to live at `file_name`. @@ -127,7 +108,7 @@ impl PyModule { /// Returns `PyErr` if: /// - `code` is not syntactically correct Python. /// - Any Python exceptions are raised while initializing the module. - /// - Any of the arguments cannot be converted to [`CString`]s. + /// - Any of the arguments cannot be converted to [`CString`][std::ffi::CString]s. /// /// # Example: bundle in a file at compile time with [`include_str!`][std::include_str]: /// @@ -182,22 +163,6 @@ impl PyModule { .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, - file_name: &str, - module_name: &str, - ) -> PyResult> { - let data = CString::new(code)?; - let filename = CString::new(file_name)?; - let module = CString::new(module_name)?; - - Self::from_code(py, data.as_c_str(), filename.as_c_str(), module.as_c_str()) - } } /// Implementation of functionality for [`PyModule`]. diff --git a/src/types/none.rs b/src/types/none.rs index 1ec12d3f5b0..da8ef1aa51b 100644 --- a/src/types/none.rs +++ b/src/types/none.rs @@ -18,13 +18,6 @@ impl 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 { diff --git a/src/types/notimplemented.rs b/src/types/notimplemented.rs index d93ab466d2d..040cc19db61 100644 --- a/src/types/notimplemented.rs +++ b/src/types/notimplemented.rs @@ -22,13 +22,6 @@ impl PyNotImplemented { .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 { diff --git a/src/types/num.rs b/src/types/num.rs index 8de116a470f..38874f29aba 100644 --- a/src/types/num.rs +++ b/src/types/num.rs @@ -16,10 +16,6 @@ pub struct PyInt(PyAny); 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; - impl PyInt { /// Creates a new Python int object. /// diff --git a/src/types/pysuper.rs b/src/types/pysuper.rs index 9f52f9c5b56..3093bcf6192 100644 --- a/src/types/pysuper.rs +++ b/src/types/pysuper.rs @@ -66,14 +66,4 @@ 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 ddaddbfe5ed..93c251d460f 100644 --- a/src/types/set.rs +++ b/src/types/set.rs @@ -54,17 +54,6 @@ impl PySet { try_new_from_iter(py, elements) } - /// 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>, - elements: impl IntoIterator, - ) -> PyResult> { - Self::new(py, elements.into_iter().map(|e| e.to_object(py))) - } - /// Creates a new empty set. pub fn empty(py: Python<'_>) -> PyResult> { unsafe { @@ -73,13 +62,6 @@ impl PySet { .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`]. diff --git a/src/types/slice.rs b/src/types/slice.rs index 9ca2aa4ec43..b74a228aae3 100644 --- a/src/types/slice.rs +++ b/src/types/slice.rs @@ -69,13 +69,6 @@ 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(py: Python<'_>) -> Bound<'_, PySlice> { unsafe { @@ -84,13 +77,6 @@ impl PySlice { .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`]. diff --git a/src/types/string.rs b/src/types/string.rs index a389f0df234..f7a724b9fce 100644 --- a/src/types/string.rs +++ b/src/types/string.rs @@ -13,10 +13,6 @@ use std::borrow::Cow; use std::ffi::CString; 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 @@ -175,19 +171,12 @@ 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. /// - /// 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`]. + /// 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`]. /// /// Panics if out of memory. pub fn intern<'py>(py: Python<'py>, s: &str) -> Bound<'py, PyString> { @@ -202,13 +191,6 @@ 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). @@ -229,17 +211,6 @@ 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`]. diff --git a/src/types/tuple.rs b/src/types/tuple.rs index a35f5805c15..c412a81a752 100644 --- a/src/types/tuple.rs +++ b/src/types/tuple.rs @@ -104,22 +104,6 @@ impl PyTuple { try_new_from_iter(py, elements) } - /// Deprecated name for [`PyTuple::new`]. - #[deprecated(since = "0.23.0", note = "renamed to `PyTuple::new`")] - #[allow(deprecated)] - #[track_caller] - #[inline] - pub fn new_bound( - py: Python<'_>, - elements: impl IntoIterator, - ) -> Bound<'_, PyTuple> - where - T: ToPyObject, - U: ExactSizeIterator, - { - PyTuple::new(py, elements.into_iter().map(|e| e.to_object(py))).unwrap() - } - /// Constructs an empty tuple (on the Python side, a singleton object). pub fn empty(py: Python<'_>) -> Bound<'_, PyTuple> { unsafe { @@ -128,13 +112,6 @@ impl PyTuple { .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`]. diff --git a/src/types/typeobject.rs b/src/types/typeobject.rs index 7ab3690b90b..194755fc5a3 100644 --- a/src/types/typeobject.rs +++ b/src/types/typeobject.rs @@ -27,13 +27,6 @@ impl PyType { T::type_object(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/src/types/weakref/anyref.rs b/src/types/weakref/anyref.rs index 741a638c295..cea930b7204 100644 --- a/src/types/weakref/anyref.rs +++ b/src/types/weakref/anyref.rs @@ -1,11 +1,8 @@ use crate::err::PyResult; use crate::ffi_ptr_ext::FfiPtrExt; use crate::type_object::{PyTypeCheck, PyTypeInfo}; -use crate::types::{ - any::{PyAny, PyAnyMethods}, - PyNone, -}; -use crate::{ffi, Bound, Python}; +use crate::types::any::{PyAny, PyAnyMethods}; +use crate::{ffi, Bound}; /// Represents any Python `weakref` reference. /// @@ -321,74 +318,6 @@ 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 fn upgrade(&self) -> Option>; - - /// 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 similar results to using [`PyWeakref_GetRef`] 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" - )] - /// #![allow(deprecated)] - /// 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(&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_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 - #[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> { @@ -545,40 +474,6 @@ mod tests { inner(new_reference, true)?; inner(new_proxy, false) } - - #[test] - #[allow(deprecated)] - 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) - } } // under 'abi3-py37' and 'abi3-py38' PyClass cannot be weakreferencable. @@ -697,38 +592,5 @@ mod tests { inner(new_reference, true)?; inner(new_proxy, false) } - - #[test] - #[allow(deprecated)] - 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) - } } } diff --git a/src/types/weakref/proxy.rs b/src/types/weakref/proxy.rs index 5334f0341f1..563c5b0982c 100644 --- a/src/types/weakref/proxy.rs +++ b/src/types/weakref/proxy.rs @@ -80,13 +80,6 @@ impl PyWeakrefProxy { } } - /// 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. /// /// 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. @@ -172,20 +165,6 @@ impl PyWeakrefProxy { .as_borrowed(), ) } - - /// 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: crate::ToPyObject, - { - Self::new_with(object, callback.to_object(object.py())) - } } impl<'py> PyWeakrefMethods<'py> for Bound<'py, PyWeakrefProxy> { @@ -579,23 +558,6 @@ mod tests { Ok(()) }) } - - #[test] - #[allow(deprecated)] - fn test_weakref_get_object() -> PyResult<()> { - Python::with_gil(|py| { - let object = Py::new(py, WeakrefablePyClass {})?; - let reference = PyWeakrefProxy::new(object.bind(py))?; - - assert!(reference.get_object().is(&object)); - - drop(object); - - assert!(reference.get_object().is_none()); - - Ok(()) - }) - } } } @@ -753,24 +715,6 @@ mod tests { Ok(()) }) } - - #[test] - #[allow(deprecated)] - fn test_weakref_get_object() -> PyResult<()> { - Python::with_gil(|py| { - let class = get_type(py)?; - let object = class.call0()?; - let reference = PyWeakrefProxy::new(&object)?; - - assert!(reference.get_object().is(&object)); - - drop(object); - - assert!(reference.get_object().is_none()); - - Ok(()) - }) - } } // under 'abi3-py37' and 'abi3-py38' PyClass cannot be weakreferencable. @@ -913,23 +857,6 @@ mod tests { Ok(()) }) } - - #[test] - #[allow(deprecated)] - fn test_weakref_get_object() -> PyResult<()> { - Python::with_gil(|py| { - let object = Py::new(py, WeakrefablePyClass {})?; - let reference = PyWeakrefProxy::new(object.bind(py))?; - - assert!(reference.get_object().is(&object)); - - drop(object); - - assert!(reference.get_object().is_none()); - - Ok(()) - }) - } } } } diff --git a/src/types/weakref/reference.rs b/src/types/weakref/reference.rs index edabb6da935..334f136aa33 100644 --- a/src/types/weakref/reference.rs +++ b/src/types/weakref/reference.rs @@ -90,13 +90,6 @@ impl PyWeakrefReference { } } - /// 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. /// /// 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. @@ -181,20 +174,6 @@ impl PyWeakrefReference { .as_borrowed(), ) } - - /// 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: crate::ToPyObject, - { - Self::new_with(object, callback.to_object(object.py())) - } } impl<'py> PyWeakrefMethods<'py> for Bound<'py, PyWeakrefReference> { @@ -392,27 +371,6 @@ mod tests { Ok(()) }) } - - #[test] - #[allow(deprecated)] - fn test_weakref_get_object() -> 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().is(&object)); - - drop(object); - - assert!(reference.call0()?.is(&reference.get_object())); - assert!(reference.call0()?.is_none()); - assert!(reference.get_object().is_none()); - - Ok(()) - }) - } } // under 'abi3-py37' and 'abi3-py38' PyClass cannot be weakreferencable. @@ -537,25 +495,5 @@ mod tests { Ok(()) }) } - - #[test] - #[allow(deprecated)] - fn test_weakref_get_object() -> 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().is(&object)); - - drop(object); - - assert!(reference.call0()?.is(&reference.get_object())); - assert!(reference.call0()?.is_none()); - assert!(reference.get_object().is_none()); - - Ok(()) - }) - } } } diff --git a/tests/ui/reject_generics.stderr b/tests/ui/reject_generics.stderr index 9cb7e6e2068..fed5d48937e 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.25.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/v0.24.0/class.html#no-lifetime-parameters +error: #[pyclass] cannot have lifetime parameters. For an explanation, see https://pyo3.rs/v0.25.0-dev/class.html#no-lifetime-parameters --> tests/ui/reject_generics.rs:9:27 | 9 | struct ClassWithLifetimes<'a> { From a8452c56655f4af0b9e04719b7f06e07d2701d1f Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sat, 29 Mar 2025 00:40:41 +0100 Subject: [PATCH 627/936] remove deprecated `IntoPy` and `ToPyObject` (#5010) --- guide/src/class.md | 7 - guide/src/conversions/traits.md | 48 ------- guide/src/migration.md | 6 +- newsfragments/5010.removed.md | 1 + pyo3-benches/benches/bench_intopyobject.rs | 19 --- pyo3-benches/benches/bench_tuple.rs | 8 -- pyo3-macros-backend/src/pyclass.rs | 37 ----- pyo3-macros-backend/src/pymethod.rs | 2 - src/conversion.rs | 130 +----------------- src/conversions/chrono.rs | 147 +------------------- src/conversions/chrono_tz.rs | 20 +-- src/conversions/either.rs | 36 +---- src/conversions/hashbrown.rs | 63 +-------- src/conversions/indexmap.rs | 36 +---- src/conversions/num_bigint.rs | 22 +-- src/conversions/num_complex.rs | 25 +--- src/conversions/num_rational.rs | 19 +-- src/conversions/rust_decimal.rs | 20 +-- src/conversions/smallvec.rs | 40 +----- src/conversions/std/array.rs | 44 +----- src/conversions/std/cell.rs | 18 +-- src/conversions/std/ipaddr.rs | 36 +---- src/conversions/std/map.rs | 66 +-------- src/conversions/std/num.rs | 114 +--------------- src/conversions/std/option.rs | 25 +--- src/conversions/std/osstr.rs | 60 +------- src/conversions/std/path.rs | 88 +----------- src/conversions/std/set.rs | 74 +--------- src/conversions/std/slice.rs | 25 +--- src/conversions/std/string.rs | 105 +------------- src/conversions/std/time.rs | 34 ----- src/conversions/std/vec.rs | 39 +----- src/err/mod.rs | 26 ---- src/impl_/pyclass.rs | 152 ++------------------- src/impl_/pyclass/probes.rs | 16 --- src/impl_/wrap.rs | 28 ---- src/instance.rs | 74 ---------- src/lib.rs | 2 - src/marker.rs | 6 +- src/prelude.rs | 2 - src/pybacked.rs | 65 --------- src/pycell.rs | 32 +---- src/types/boolobject.rs | 22 +-- src/types/float.rs | 40 +----- src/types/list.rs | 13 +- src/types/none.rs | 41 +----- src/types/num.rs | 5 +- src/types/set.rs | 12 -- src/types/slice.rs | 11 +- src/types/string.rs | 23 ---- src/types/tuple.rs | 40 +----- tests/ui/invalid_property_args.stderr | 6 +- 52 files changed, 68 insertions(+), 1962 deletions(-) create mode 100644 newsfragments/5010.removed.md diff --git a/guide/src/class.md b/guide/src/class.md index 460fed6b34c..8772e857d7e 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -1410,13 +1410,6 @@ impl<'a, 'py> pyo3::impl_::extract_argument::PyFunctionArgument<'a, 'py, false> } } -#[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) - } -} - impl pyo3::impl_::pyclass::PyClassImpl for MyClass { const IS_BASETYPE: bool = false; const IS_SUBCLASS: bool = false; diff --git a/guide/src/conversions/traits.md b/guide/src/conversions/traits.md index 4ffdf802a87..b1b6cd31127 100755 --- a/guide/src/conversions/traits.md +++ b/guide/src/conversions/traits.md @@ -738,55 +738,7 @@ 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,no_run -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 - -
- -⚠️ 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 reference 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`. - -[`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 diff --git a/guide/src/migration.md b/guide/src/migration.md index e421ff659ff..13ca506662c 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -1237,7 +1237,7 @@ Python::with_gil(|py| { After, some type annotations may be necessary: -```rust +```rust,ignore # #![allow(deprecated)] # use pyo3::prelude::*; # @@ -1712,7 +1712,7 @@ impl FromPy for PyObject { ``` After -```rust,no_run +```rust,ignore # use pyo3::prelude::*; # #[allow(dead_code)] struct MyPyObjectWrapper(PyObject); @@ -1736,7 +1736,7 @@ let obj = PyObject::from_py(1.234, py); ``` After: -```rust,no_run +```rust,ignore # #![allow(deprecated)] # use pyo3::prelude::*; # Python::with_gil(|py| { diff --git a/newsfragments/5010.removed.md b/newsfragments/5010.removed.md new file mode 100644 index 00000000000..9b42e682154 --- /dev/null +++ b/newsfragments/5010.removed.md @@ -0,0 +1 @@ +Remove deprecated `IntoPy` and `ToPyObject` traits \ No newline at end of file diff --git a/pyo3-benches/benches/bench_intopyobject.rs b/pyo3-benches/benches/bench_intopyobject.rs index 42af893cd8a..0e1fbad1a57 100644 --- a/pyo3-benches/benches/bench_intopyobject.rs +++ b/pyo3-benches/benches/bench_intopyobject.rs @@ -46,15 +46,6 @@ 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).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).collect::>(); @@ -62,14 +53,6 @@ fn vec_into_pyobject(b: &mut Bencher<'_>) { }); } -#[allow(deprecated)] -fn vec_into_py(b: &mut Bencher<'_>) { - Python::with_gil(|py| { - let bytes = (0..u8::MAX).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); @@ -86,9 +69,7 @@ fn criterion_benchmark(c: &mut Criterion) { "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); diff --git a/pyo3-benches/benches/bench_tuple.rs b/pyo3-benches/benches/bench_tuple.rs index e235567e926..cba7573c9d2 100644 --- a/pyo3-benches/benches/bench_tuple.rs +++ b/pyo3-benches/benches/bench_tuple.rs @@ -115,13 +115,6 @@ 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(|| { @@ -175,7 +168,6 @@ fn criterion_benchmark(c: &mut Criterion) { c.bench_function("sequence_from_tuple", sequence_from_tuple); c.bench_function("tuple_new_list", tuple_new_list); c.bench_function("tuple_to_list", tuple_to_list); - c.bench_function("tuple_into_py", tuple_into_py); c.bench_function("tuple_into_pyobject", tuple_into_pyobject); } diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index aadfe2332ad..b524f667a7a 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -1029,35 +1029,6 @@ fn impl_complex_enum( ) .doc(doc); - // Need to customize the into_py impl so that it returns the variant PyClass - let enum_into_py_impl = { - let match_arms: Vec = variants - .iter() - .map(|variant| { - let variant_ident = variant.get_ident(); - let variant_cls = gen_complex_enum_variant_class_ident(cls, variant.get_ident()); - quote! { - #cls::#variant_ident { .. } => { - let pyclass_init = <#pyo3_path::PyClassInitializer 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) - } - } - }) - .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 { - #(#match_arms)* - } - } - } - } - }; - let enum_into_pyobject_impl = { let match_arms = variants .iter() @@ -1093,7 +1064,6 @@ fn impl_complex_enum( 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), @@ -2142,13 +2112,6 @@ 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) - } - } - impl<'py> #pyo3_path::conversion::IntoPyObject<'py> for #cls { type Target = Self; type Output = #pyo3_path::Bound<'py, >::Target>; diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index a1689e4e75c..a6d692775ef 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -845,8 +845,6 @@ pub fn impl_py_getter_def( #ty, 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() }; diff --git a/src/conversion.rs b/src/conversion.rs index 073a1fde2e9..53c9f3a9de8 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, PyErr, PyObject, PyRef, PyRefMut, Python, + ffi, Borrowed, Bound, BoundObject, Py, PyAny, PyClass, PyErr, PyRef, PyRefMut, Python, }; use std::convert::Infallible; @@ -66,116 +66,6 @@ pub unsafe trait AsPyPointer { fn as_ptr(&self) -> *mut ffi::PyObject; } -/// 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.0/migration) for more information." -)] -pub trait ToPyObject { - /// Converts self into a Python object. - fn to_object(&self, py: Python<'_>) -> PyObject; -} - -/// Defines a conversion from a Rust type to a Python object. -/// -/// It functions similarly to std's [`Into`] trait, but requires a [GIL token](Python) -/// as an argument. Many functions and traits internal to PyO3 require this trait as a bound, -/// so a lack of this trait can manifest itself in different error messages. -/// -/// # Examples -/// ## With `#[pyclass]` -/// The easiest way to implement `IntoPy` is by exposing a struct as a native Python object -/// by annotating it with [`#[pyclass]`](crate::prelude::pyclass). -/// -/// ```rust,no_run -/// use pyo3::prelude::*; -/// -/// # #[allow(dead_code)] -/// #[pyclass] -/// struct Number { -/// #[pyo3(get, set)] -/// value: i32, -/// } -/// ``` -/// Python code will see this as an instance of the `Number` class with a `value` attribute. -/// -/// ## Conversion to a Python object -/// -/// However, it may not be desirable to expose the existence of `Number` to Python code. -/// `IntoPy` allows us to define a conversion to an appropriate Python object. -/// ```rust,no_run -/// #![allow(deprecated)] -/// use pyo3::prelude::*; -/// -/// # #[allow(dead_code)] -/// struct Number { -/// value: i32, -/// } -/// -/// impl IntoPy for Number { -/// fn into_py(self, py: Python<'_>) -> PyObject { -/// // delegates to i32's IntoPy implementation. -/// self.value.into_py(py) -/// } -/// } -/// ``` -/// Python code will see this as an `int` object. -/// -/// ## Dynamic conversion into Python objects. -/// It is also possible to return a different Python object depending on some condition. -/// This is useful for types like enums that can carry different types. -/// -/// ```rust -/// #![allow(deprecated)] -/// use pyo3::prelude::*; -/// -/// enum Value { -/// Integer(i32), -/// String(String), -/// None, -/// } -/// -/// impl IntoPy for Value { -/// fn into_py(self, py: Python<'_>) -> PyObject { -/// match self { -/// Self::Integer(val) => val.into_py(py), -/// Self::String(val) => val.into_py(py), -/// Self::None => py.None(), -/// } -/// } -/// } -/// # fn main() { -/// # Python::with_gil(|py| { -/// # let v = Value::Integer(73).into_py(py); -/// # let v = v.extract::(py).unwrap(); -/// # -/// # let v = Value::String("foo".into()).into_py(py); -/// # let v = v.extract::(py).unwrap(); -/// # -/// # let v = Value::None.into_py(py); -/// # let v = v.extract::>>(py).unwrap(); -/// # }); -/// # } -/// ``` -/// Python code will see this as any of the `int`, `string` or `None` objects. -#[cfg_attr( - 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::*`" - ) -)] -#[deprecated( - since = "0.23.0", - 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. - fn into_py(self, py: Python<'_>) -> T; -} - /// 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 @@ -541,16 +431,6 @@ 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 { - ::to_object(*self, py) - } -} - impl FromPyObject<'_> for T where T: PyClass + Clone, @@ -579,14 +459,6 @@ where } } -/// Converts `()` to an empty Python tuple. -#[allow(deprecated)] -impl IntoPy> for () { - fn into_py(self, py: Python<'_>) -> Py { - PyTuple::empty(py).unbind() - } -} - impl<'py> IntoPyObject<'py> for () { type Target = PyTuple; type Output = Bound<'py, Self::Target>; diff --git a/src/conversions/chrono.rs b/src/conversions/chrono.rs index 342ed659e22..6fd4465ca34 100644 --- a/src/conversions/chrono.rs +++ b/src/conversions/chrono.rs @@ -58,31 +58,13 @@ use crate::types::{ timezone_utc, PyDate, PyDateAccess, PyDateTime, PyDelta, PyDeltaAccess, PyTime, PyTimeAccess, PyTzInfo, PyTzInfoAccess, }; -use crate::{ffi, Bound, FromPyObject, IntoPyObjectExt, PyAny, PyErr, PyObject, PyResult, Python}; -#[allow(deprecated)] -use crate::{IntoPy, ToPyObject}; +use crate::{ffi, Bound, FromPyObject, IntoPyObjectExt, PyAny, PyErr, PyResult, Python}; use chrono::offset::{FixedOffset, Utc}; use chrono::{ DateTime, Datelike, Duration, LocalResult, NaiveDate, NaiveDateTime, NaiveTime, Offset, TimeZone, Timelike, }; -#[allow(deprecated)] -impl ToPyObject for Duration { - #[inline] - fn to_object(&self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() - } -} - -#[allow(deprecated)] -impl IntoPy for Duration { - #[inline] - fn into_py(self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() - } -} - impl<'py> IntoPyObject<'py> for Duration { #[cfg(Py_LIMITED_API)] type Target = PyAny; @@ -174,22 +156,6 @@ impl FromPyObject<'_> for Duration { } } -#[allow(deprecated)] -impl ToPyObject for NaiveDate { - #[inline] - fn to_object(&self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() - } -} - -#[allow(deprecated)] -impl IntoPy for NaiveDate { - #[inline] - fn into_py(self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() - } -} - impl<'py> IntoPyObject<'py> for NaiveDate { #[cfg(Py_LIMITED_API)] type Target = PyAny; @@ -241,22 +207,6 @@ impl FromPyObject<'_> for NaiveDate { } } -#[allow(deprecated)] -impl ToPyObject for NaiveTime { - #[inline] - fn to_object(&self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() - } -} - -#[allow(deprecated)] -impl IntoPy for NaiveTime { - #[inline] - fn into_py(self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() - } -} - impl<'py> IntoPyObject<'py> for NaiveTime { #[cfg(Py_LIMITED_API)] type Target = PyAny; @@ -318,22 +268,6 @@ impl FromPyObject<'_> for NaiveTime { } } -#[allow(deprecated)] -impl ToPyObject for NaiveDateTime { - #[inline] - fn to_object(&self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() - } -} - -#[allow(deprecated)] -impl IntoPy for NaiveDateTime { - #[inline] - fn into_py(self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() - } -} - impl<'py> IntoPyObject<'py> for NaiveDateTime { #[cfg(Py_LIMITED_API)] type Target = PyAny; @@ -407,24 +341,6 @@ 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 - // See https://github.com/PyO3/pyo3/issues/3266 - let tz = self.offset().fix().to_object(py); - let tz = tz.bind(py).downcast().unwrap(); - naive_datetime_to_py_datetime(py, &self.naive_local(), Some(tz)) - } -} - -#[allow(deprecated)] -impl IntoPy for DateTime { - fn into_py(self, py: Python<'_>) -> PyObject { - self.to_object(py) - } -} - impl<'py, Tz: TimeZone> IntoPyObject<'py> for DateTime where Tz: IntoPyObject<'py>, @@ -536,22 +452,6 @@ impl FromPyObject<'py>> FromPyObject<'_> for DateTime) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() - } -} - -#[allow(deprecated)] -impl IntoPy for FixedOffset { - #[inline] - fn into_py(self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() - } -} - impl<'py> IntoPyObject<'py> for FixedOffset { #[cfg(Py_LIMITED_API)] type Target = PyAny; @@ -621,22 +521,6 @@ impl FromPyObject<'_> for FixedOffset { } } -#[allow(deprecated)] -impl ToPyObject for Utc { - #[inline] - fn to_object(&self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() - } -} - -#[allow(deprecated)] -impl IntoPy for Utc { - #[inline] - fn into_py(self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() - } -} - impl<'py> IntoPyObject<'py> for Utc { #[cfg(Py_LIMITED_API)] type Target = PyAny; @@ -722,35 +606,6 @@ impl From<&NaiveTime> for TimeArgs { } } -fn naive_datetime_to_py_datetime( - py: Python<'_>, - naive_datetime: &NaiveDateTime, - #[cfg(not(Py_LIMITED_API))] tzinfo: Option<&Bound<'_, PyTzInfo>>, - #[cfg(Py_LIMITED_API)] tzinfo: Option<&Bound<'_, PyAny>>, -) -> PyObject { - let DateArgs { year, month, day } = (&naive_datetime.date()).into(); - let TimeArgs { - hour, - min, - sec, - micro, - truncated_leap_second, - } = (&naive_datetime.time()).into(); - #[cfg(not(Py_LIMITED_API))] - let datetime = PyDateTime::new(py, year, month, day, hour, min, sec, micro, tzinfo) - .expect("failed to construct datetime"); - #[cfg(Py_LIMITED_API)] - let datetime = DatetimeTypes::get(py) - .datetime - .bind(py) - .call1((year, month, day, hour, min, sec, micro, tzinfo)) - .expect("failed to construct datetime.datetime"); - if truncated_leap_second { - warn_truncated_leap_second(&datetime); - } - datetime.into() -} - fn warn_truncated_leap_second(obj: &Bound<'_, PyAny>) { let py = obj.py(); if let Err(e) = PyErr::warn( diff --git a/src/conversions/chrono_tz.rs b/src/conversions/chrono_tz.rs index 60a3bab4918..d564bcca87b 100644 --- a/src/conversions/chrono_tz.rs +++ b/src/conversions/chrono_tz.rs @@ -39,28 +39,10 @@ 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::{IntoPy, ToPyObject}; +use crate::{intern, Bound, FromPyObject, Py, PyAny, PyErr, PyResult, Python}; use chrono_tz::Tz; use std::str::FromStr; -#[allow(deprecated)] -impl ToPyObject for Tz { - #[inline] - fn to_object(&self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().unbind() - } -} - -#[allow(deprecated)] -impl IntoPy for Tz { - #[inline] - fn into_py(self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().unbind() - } -} - impl<'py> IntoPyObject<'py> for Tz { type Target = PyAny; type Output = Bound<'py, Self::Target>; diff --git a/src/conversions/either.rs b/src/conversions/either.rs index a514b1fde8d..7e8e3bfc2dc 100644 --- a/src/conversions/either.rs +++ b/src/conversions/either.rs @@ -48,28 +48,10 @@ use crate::inspect::types::TypeInfo; use crate::{ exceptions::PyTypeError, types::any::PyAnyMethods, Bound, FromPyObject, IntoPyObject, - IntoPyObjectExt, PyAny, PyErr, PyObject, PyResult, Python, + IntoPyObjectExt, PyAny, PyErr, 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, - R: IntoPy, -{ - #[inline] - fn into_py(self, py: Python<'_>) -> PyObject { - match self { - Either::Left(l) => l.into_py(py), - Either::Right(r) => r.into_py(py), - } - } -} - #[cfg_attr(docsrs, doc(cfg(feature = "either")))] impl<'py, L, R> IntoPyObject<'py> for Either where @@ -106,22 +88,6 @@ where } } -#[cfg_attr(docsrs, doc(cfg(feature = "either")))] -#[allow(deprecated)] -impl ToPyObject for Either -where - L: ToPyObject, - R: ToPyObject, -{ - #[inline] - fn to_object(&self, py: Python<'_>) -> PyObject { - match self { - Either::Left(l) => l.to_object(py), - Either::Right(r) => r.to_object(py), - } - } -} - #[cfg_attr(docsrs, doc(cfg(feature = "either")))] impl<'py, L, R> FromPyObject<'py> for Either where diff --git a/src/conversions/hashbrown.rs b/src/conversions/hashbrown.rs index 0efe7f5161f..c302d28d2a1 100644 --- a/src/conversions/hashbrown.rs +++ b/src/conversions/hashbrown.rs @@ -22,47 +22,13 @@ use crate::{ any::PyAnyMethods, dict::PyDictMethods, frozenset::PyFrozenSetMethods, - set::{new_from_iter, try_new_from_iter, PySetMethods}, + set::{try_new_from_iter, PySetMethods}, PyDict, PyFrozenSet, PySet, }, - Bound, FromPyObject, PyAny, PyErr, PyObject, PyResult, Python, + Bound, FromPyObject, PyAny, PyErr, PyResult, Python, }; -#[allow(deprecated)] -use crate::{IntoPy, ToPyObject}; use std::{cmp, hash}; -#[allow(deprecated)] -impl ToPyObject for hashbrown::HashMap -where - K: hash::Hash + cmp::Eq + ToPyObject, - V: ToPyObject, - H: hash::BuildHasher, -{ - fn to_object(&self, py: Python<'_>) -> PyObject { - 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() - } -} - -#[allow(deprecated)] -impl IntoPy for hashbrown::HashMap -where - K: hash::Hash + cmp::Eq + IntoPy, - V: IntoPy, - H: hash::BuildHasher, -{ - fn into_py(self, py: Python<'_>) -> PyObject { - 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() - } -} - impl<'py, K, V, H> IntoPyObject<'py> for hashbrown::HashMap where K: IntoPyObject<'py> + cmp::Eq + hash::Hash, @@ -117,31 +83,6 @@ where } } -#[allow(deprecated)] -impl ToPyObject for hashbrown::HashSet -where - T: hash::Hash + Eq + ToPyObject, -{ - fn to_object(&self, py: Python<'_>) -> PyObject { - new_from_iter(py, self) - .expect("Failed to create Python set from hashbrown::HashSet") - .into() - } -} - -#[allow(deprecated)] -impl IntoPy for hashbrown::HashSet -where - K: IntoPy + Eq + hash::Hash, - S: hash::BuildHasher + Default, -{ - fn into_py(self, py: Python<'_>) -> PyObject { - new_from_iter(py, self.into_iter().map(|item| item.into_py(py))) - .expect("Failed to create Python set from hashbrown::HashSet") - .into() - } -} - impl<'py, K, H> IntoPyObject<'py> for hashbrown::HashSet where K: IntoPyObject<'py> + cmp::Eq + hash::Hash, diff --git a/src/conversions/indexmap.rs b/src/conversions/indexmap.rs index e3787e68091..d6995c14db2 100644 --- a/src/conversions/indexmap.rs +++ b/src/conversions/indexmap.rs @@ -89,43 +89,9 @@ use crate::conversion::IntoPyObject; use crate::types::*; -use crate::{Bound, FromPyObject, PyErr, PyObject, Python}; -#[allow(deprecated)] -use crate::{IntoPy, ToPyObject}; +use crate::{Bound, FromPyObject, PyErr, Python}; use std::{cmp, hash}; -#[allow(deprecated)] -impl ToPyObject for indexmap::IndexMap -where - K: hash::Hash + cmp::Eq + ToPyObject, - V: ToPyObject, - H: hash::BuildHasher, -{ - fn to_object(&self, py: Python<'_>) -> PyObject { - 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() - } -} - -#[allow(deprecated)] -impl IntoPy for indexmap::IndexMap -where - K: hash::Hash + cmp::Eq + IntoPy, - V: IntoPy, - H: hash::BuildHasher, -{ - fn into_py(self, py: Python<'_>) -> PyObject { - 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() - } -} - impl<'py, K, V, H> IntoPyObject<'py> for indexmap::IndexMap where K: IntoPyObject<'py> + cmp::Eq + hash::Hash, diff --git a/src/conversions/num_bigint.rs b/src/conversions/num_bigint.rs index b3a1ac12531..ebc4058b1a6 100644 --- a/src/conversions/num_bigint.rs +++ b/src/conversions/num_bigint.rs @@ -54,10 +54,8 @@ use crate::{ ffi, instance::Bound, types::{any::PyAnyMethods, PyInt}, - FromPyObject, Py, PyAny, PyErr, PyObject, PyResult, Python, + FromPyObject, Py, PyAny, PyErr, PyResult, Python, }; -#[allow(deprecated)] -use crate::{IntoPy, ToPyObject}; use num_bigint::{BigInt, BigUint}; @@ -67,24 +65,6 @@ use num_bigint::Sign; // for identical functionality between BigInt and BigUint 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 { - self.into_pyobject(py).unwrap().into_any().unbind() - } - } - - #[cfg_attr(docsrs, doc(cfg(feature = "num-bigint")))] - #[allow(deprecated)] - impl IntoPy for $rust_ty { - #[inline] - fn into_py(self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() - } - } - #[cfg_attr(docsrs, doc(cfg(feature = "num-bigint")))] impl<'py> IntoPyObject<'py> for $rust_ty { type Target = PyInt; diff --git a/src/conversions/num_complex.rs b/src/conversions/num_complex.rs index db981a4179b..e9fb1096ff5 100644 --- a/src/conversions/num_complex.rs +++ b/src/conversions/num_complex.rs @@ -93,13 +93,11 @@ //! 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, + Bound, FromPyObject, PyAny, PyErr, PyResult, Python, }; use num_complex::Complex; use std::os::raw::c_double; @@ -120,27 +118,6 @@ 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 { - crate::IntoPy::::into_py(self.to_owned(), py) - } - } - - #[cfg_attr(docsrs, doc(cfg(feature = "num-complex")))] - #[allow(deprecated)] - impl crate::IntoPy for Complex<$float> { - fn into_py(self, py: Python<'_>) -> PyObject { - unsafe { - let raw_obj = - ffi::PyComplex_FromDoubles(self.re as c_double, self.im as c_double); - PyObject::from_owned_ptr(py, raw_obj) - } - } - } - #[cfg_attr(docsrs, doc(cfg(feature = "num-complex")))] impl<'py> crate::conversion::IntoPyObject<'py> for Complex<$float> { type Target = PyComplex; diff --git a/src/conversions/num_rational.rs b/src/conversions/num_rational.rs index d86a99f2632..dfdd80cf54f 100644 --- a/src/conversions/num_rational.rs +++ b/src/conversions/num_rational.rs @@ -48,9 +48,7 @@ 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::{IntoPy, ToPyObject}; +use crate::{Bound, FromPyObject, Py, PyAny, PyErr, PyResult, Python}; #[cfg(feature = "num-bigint")] use num_bigint::BigInt; @@ -84,21 +82,6 @@ macro_rules! rational_conversion { } } - #[allow(deprecated)] - impl ToPyObject for Ratio<$int> { - #[inline] - fn to_object(&self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() - } - } - #[allow(deprecated)] - impl IntoPy for Ratio<$int> { - #[inline] - fn into_py(self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() - } - } - impl<'py> IntoPyObject<'py> for Ratio<$int> { type Target = PyAny; type Output = Bound<'py, Self::Target>; diff --git a/src/conversions/rust_decimal.rs b/src/conversions/rust_decimal.rs index 882efa847e2..392971a0b4b 100644 --- a/src/conversions/rust_decimal.rs +++ b/src/conversions/rust_decimal.rs @@ -55,9 +55,7 @@ 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::{IntoPy, ToPyObject}; +use crate::{Bound, FromPyObject, Py, PyAny, PyErr, PyResult, Python}; use rust_decimal::Decimal; use std::str::FromStr; @@ -82,22 +80,6 @@ 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 { - self.into_pyobject(py).unwrap().into_any().unbind() - } -} - -#[allow(deprecated)] -impl IntoPy for Decimal { - #[inline] - fn into_py(self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() - } -} - impl<'py> IntoPyObject<'py> for Decimal { type Target = PyAny; type Output = Bound<'py, Self::Target>; diff --git a/src/conversions/smallvec.rs b/src/conversions/smallvec.rs index 99244d81417..d4229bc2865 100644 --- a/src/conversions/smallvec.rs +++ b/src/conversions/smallvec.rs @@ -20,38 +20,11 @@ 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::PyErr; -use crate::{err::DowncastError, ffi, Bound, FromPyObject, PyAny, PyObject, PyResult, Python}; -#[allow(deprecated)] -use crate::{IntoPy, ToPyObject}; +use crate::{err::DowncastError, ffi, Bound, FromPyObject, PyAny, PyResult, Python}; use smallvec::{Array, SmallVec}; -#[allow(deprecated)] -impl
ToPyObject for SmallVec -where - A: Array, - A::Item: ToPyObject, -{ - fn to_object(&self, py: Python<'_>) -> PyObject { - self.as_slice().to_object(py) - } -} - -#[allow(deprecated)] -impl IntoPy for SmallVec -where - A: Array, - A::Item: IntoPy, -{ - fn into_py(self, py: Python<'_>) -> PyObject { - let mut iter = self.into_iter().map(|e| e.into_py(py)); - let list = new_from_iter(py, &mut iter); - list.into() - } -} - impl<'py, A> IntoPyObject<'py> for SmallVec where A: Array, @@ -142,17 +115,6 @@ mod tests { use super::*; 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(); - let hso: PyObject = sv.clone().into_py(py); - let l = PyList::new(py, [1, 2, 3, 4, 5]).unwrap(); - assert!(l.eq(hso).unwrap()); - }); - } - #[test] fn test_smallvec_from_py_object() { Python::with_gil(|py| { diff --git a/src/conversions/std/array.rs b/src/conversions/std/array.rs index 1c33a610273..36db5ec640f 100644 --- a/src/conversions/std/array.rs +++ b/src/conversions/std/array.rs @@ -2,40 +2,8 @@ use crate::conversion::IntoPyObject; use crate::instance::Bound; use crate::types::any::PyAnyMethods; use crate::types::PySequence; -use crate::{err::DowncastError, ffi, FromPyObject, Py, PyAny, PyObject, PyResult, Python}; +use crate::{err::DowncastError, ffi, FromPyObject, PyAny, PyResult, Python}; use crate::{exceptions, PyErr}; -#[allow(deprecated)] -use crate::{IntoPy, ToPyObject}; - -#[allow(deprecated)] -impl IntoPy for [T; N] -where - T: IntoPy, -{ - fn into_py(self, py: Python<'_>) -> PyObject { - unsafe { - let len = N as ffi::Py_ssize_t; - - let ptr = ffi::PyList_New(len); - - // We create the `Py` pointer here for two reasons: - // - panics if the ptr is null - // - its Drop cleans up the list if user code panics. - let list: Py = Py::from_owned_ptr(py, ptr); - - for (i, obj) in (0..len).zip(self) { - let obj = obj.into_py(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); - } - - list - } - } -} impl<'py, T, const N: usize> IntoPyObject<'py> for [T; N] where @@ -69,16 +37,6 @@ where } } -#[allow(deprecated)] -impl ToPyObject for [T; N] -where - T: ToPyObject, -{ - fn to_object(&self, py: Python<'_>) -> PyObject { - self.as_ref().to_object(py) - } -} - impl<'py, T, const N: usize> FromPyObject<'py> for [T; N] where T: FromPyObject<'py>, diff --git a/src/conversions/std/cell.rs b/src/conversions/std/cell.rs index 70da688b70c..983a0350228 100644 --- a/src/conversions/std/cell.rs +++ b/src/conversions/std/cell.rs @@ -1,24 +1,10 @@ use std::cell::Cell; use crate::{ - conversion::IntoPyObject, types::any::PyAnyMethods, Bound, FromPyObject, PyAny, PyObject, - PyResult, Python, + conversion::IntoPyObject, types::any::PyAnyMethods, Bound, FromPyObject, PyAny, PyResult, + Python, }; -#[allow(deprecated)] -impl crate::ToPyObject for Cell { - fn to_object(&self, py: Python<'_>) -> PyObject { - self.get().to_object(py) - } -} - -#[allow(deprecated)] -impl> crate::IntoPy for Cell { - fn into_py(self, py: Python<'_>) -> PyObject { - self.get().into_py(py) - } -} - impl<'py, T: Copy + IntoPyObject<'py>> IntoPyObject<'py> for Cell { type Target = T::Target; type Output = T::Output; diff --git a/src/conversions/std/ipaddr.rs b/src/conversions/std/ipaddr.rs index 7ba8da87749..76f6a6927c2 100755 --- a/src/conversions/std/ipaddr.rs +++ b/src/conversions/std/ipaddr.rs @@ -7,9 +7,7 @@ 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::{IntoPy, ToPyObject}; +use crate::{intern, FromPyObject, Py, PyAny, PyErr, PyResult, Python}; impl FromPyObject<'_> for IpAddr { fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { @@ -31,14 +29,6 @@ impl FromPyObject<'_> for IpAddr { } } -#[allow(deprecated)] -impl ToPyObject for Ipv4Addr { - #[inline] - fn to_object(&self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().unbind() - } -} - impl<'py> IntoPyObject<'py> for Ipv4Addr { type Target = PyAny; type Output = Bound<'py, Self::Target>; @@ -63,14 +53,6 @@ impl<'py> IntoPyObject<'py> for &Ipv4Addr { } } -#[allow(deprecated)] -impl ToPyObject for Ipv6Addr { - #[inline] - fn to_object(&self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().unbind() - } -} - impl<'py> IntoPyObject<'py> for Ipv6Addr { type Target = PyAny; type Output = Bound<'py, Self::Target>; @@ -95,22 +77,6 @@ impl<'py> IntoPyObject<'py> for &Ipv6Addr { } } -#[allow(deprecated)] -impl ToPyObject for IpAddr { - #[inline] - fn to_object(&self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().unbind() - } -} - -#[allow(deprecated)] -impl IntoPy for IpAddr { - #[inline] - fn into_py(self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().unbind() - } -} - impl<'py> IntoPyObject<'py> for IpAddr { type Target = PyAny; type Output = Bound<'py, Self::Target>; diff --git a/src/conversions/std/map.rs b/src/conversions/std/map.rs index 8a6ba57012e..b472957d1b1 100644 --- a/src/conversions/std/map.rs +++ b/src/conversions/std/map.rs @@ -6,57 +6,8 @@ use crate::{ conversion::IntoPyObject, instance::Bound, types::{any::PyAnyMethods, dict::PyDictMethods, PyDict}, - FromPyObject, PyAny, PyErr, PyObject, Python, + FromPyObject, PyAny, PyErr, Python, }; -#[allow(deprecated)] -use crate::{IntoPy, ToPyObject}; - -#[allow(deprecated)] -impl ToPyObject for collections::HashMap -where - K: hash::Hash + cmp::Eq + ToPyObject, - V: ToPyObject, - H: hash::BuildHasher, -{ - fn to_object(&self, py: Python<'_>) -> PyObject { - 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() - } -} - -#[allow(deprecated)] -impl ToPyObject for collections::BTreeMap -where - K: cmp::Eq + ToPyObject, - V: ToPyObject, -{ - fn to_object(&self, py: Python<'_>) -> PyObject { - 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() - } -} - -#[allow(deprecated)] -impl IntoPy for collections::HashMap -where - K: hash::Hash + cmp::Eq + IntoPy, - V: IntoPy, - H: hash::BuildHasher, -{ - fn into_py(self, py: Python<'_>) -> PyObject { - 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() - } -} impl<'py, K, V, H> IntoPyObject<'py> for collections::HashMap where @@ -108,21 +59,6 @@ where } } -#[allow(deprecated)] -impl IntoPy for collections::BTreeMap -where - K: cmp::Eq + IntoPy, - V: IntoPy, -{ - fn into_py(self, py: Python<'_>) -> PyObject { - 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() - } -} - impl<'py, K, V> IntoPyObject<'py> for collections::BTreeMap where K: IntoPyObject<'py> + cmp::Eq, diff --git a/src/conversions/std/num.rs b/src/conversions/std/num.rs index 0450c591ae8..40073d8af69 100644 --- a/src/conversions/std/num.rs +++ b/src/conversions/std/num.rs @@ -5,9 +5,7 @@ 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::{IntoPy, ToPyObject}; +use crate::{exceptions, ffi, Bound, FromPyObject, PyAny, PyErr, PyResult, Python}; use std::convert::Infallible; use std::num::{ NonZeroI128, NonZeroI16, NonZeroI32, NonZeroI64, NonZeroI8, NonZeroIsize, NonZeroU128, @@ -17,21 +15,6 @@ 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 { - self.into_pyobject(py).unwrap().into_any().unbind() - } - } - #[allow(deprecated)] - impl IntoPy for $rust_type { - #[inline] - fn into_py(self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() - } - } - impl<'py> IntoPyObject<'py> for $rust_type { type Target = PyInt; type Output = Bound<'py, Self::Target>; @@ -104,20 +87,6 @@ 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 { - self.into_pyobject(py).unwrap().into_any().unbind() - } - } - #[allow(deprecated)] - impl IntoPy for $rust_type { - #[inline] - fn into_py(self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() - } - } impl<'py> IntoPyObject<'py> for $rust_type { type Target = PyInt; type Output = Bound<'py, Self::Target>; @@ -166,21 +135,6 @@ 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 { - self.into_pyobject(py).unwrap().into_any().unbind() - } - } - #[allow(deprecated)] - impl IntoPy for $rust_type { - #[inline] - fn into_py(self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() - } - } - impl<'py> IntoPyObject<'py> for $rust_type { type Target = PyInt; type Output = Bound<'py, Self::Target>; @@ -231,20 +185,6 @@ macro_rules! int_fits_c_long { }; } -#[allow(deprecated)] -impl ToPyObject for u8 { - #[inline] - fn to_object(&self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() - } -} -#[allow(deprecated)] -impl IntoPy for u8 { - #[inline] - fn into_py(self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() - } -} impl<'py> IntoPyObject<'py> for u8 { type Target = PyInt; type Output = Bound<'py, Self::Target>; @@ -356,22 +296,6 @@ 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 { - self.into_pyobject(py).unwrap().into_any().unbind() - } - } - - #[allow(deprecated)] - impl IntoPy for $rust_type { - #[inline] - fn into_py(self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() - } - } - impl<'py> IntoPyObject<'py> for $rust_type { type Target = PyInt; type Output = Bound<'py, Self::Target>; @@ -510,22 +434,6 @@ 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 { - self.into_pyobject(py).unwrap().into_any().unbind() - } - } - - #[allow(deprecated)] - impl IntoPy for $rust_type { - #[inline] - fn into_py(self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() - } - } - impl<'py> IntoPyObject<'py> for $rust_type { type Target = PyInt; type Output = Bound<'py, Self::Target>; @@ -577,11 +485,11 @@ mod slow_128bit_int_conversion { ffi::PyLong_AsUnsignedLongLongMask(ob.as_ptr()), )? as $rust_type; let shift = SHIFT.into_pyobject(py)?; - let shifted = PyObject::from_owned_ptr_or_err( + let shifted = Bound::from_owned_ptr_or_err( py, ffi::PyNumber_Rshift(ob.as_ptr(), shift.as_ptr()), )?; - let upper: $half_type = shifted.extract(py)?; + let upper: $half_type = shifted.extract()?; Ok((<$rust_type>::from(upper) << SHIFT) | lower) } } @@ -614,22 +522,6 @@ 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 { - self.into_pyobject(py).unwrap().into_any().unbind() - } - } - - #[allow(deprecated)] - impl IntoPy for $nonzero_type { - #[inline] - fn into_py(self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() - } - } - impl<'py> IntoPyObject<'py> for $nonzero_type { type Target = PyInt; type Output = Bound<'py, Self::Target>; diff --git a/src/conversions/std/option.rs b/src/conversions/std/option.rs index aac8c7ab210..cd5edec7d6d 100644 --- a/src/conversions/std/option.rs +++ b/src/conversions/std/option.rs @@ -1,31 +1,8 @@ use crate::{ conversion::IntoPyObject, ffi, types::any::PyAnyMethods, AsPyPointer, Bound, BoundObject, - FromPyObject, PyAny, PyObject, PyResult, Python, + FromPyObject, PyAny, PyResult, Python, }; -/// `Option::Some` is converted like `T`. -/// `Option::None` is converted to Python `None`. -#[allow(deprecated)] -impl crate::ToPyObject for Option -where - T: crate::ToPyObject, -{ - fn to_object(&self, py: Python<'_>) -> PyObject { - self.as_ref() - .map_or_else(|| py.None(), |val| val.to_object(py)) - } -} - -#[allow(deprecated)] -impl crate::IntoPy for Option -where - T: crate::IntoPy, -{ - fn into_py(self, py: Python<'_>) -> PyObject { - self.map_or_else(|| py.None(), |val| val.into_py(py)) - } -} - impl<'py, T> IntoPyObject<'py> for Option where T: IntoPyObject<'py>, diff --git a/src/conversions/std/osstr.rs b/src/conversions/std/osstr.rs index 70b5bd152ac..39fa56f373d 100644 --- a/src/conversions/std/osstr.rs +++ b/src/conversions/std/osstr.rs @@ -3,21 +3,11 @@ 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::{IntoPy, ToPyObject}; +use crate::{ffi, FromPyObject, PyAny, 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 { - self.into_pyobject(py).unwrap().into_any().unbind() - } -} - impl<'py> IntoPyObject<'py> for &OsStr { type Target = PyString; type Output = Bound<'py, Self::Target>; @@ -134,30 +124,6 @@ impl FromPyObject<'_> for OsString { } } -#[allow(deprecated)] -impl IntoPy for &'_ OsStr { - #[inline] - fn into_py(self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() - } -} - -#[allow(deprecated)] -impl ToPyObject for Cow<'_, OsStr> { - #[inline] - fn to_object(&self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() - } -} - -#[allow(deprecated)] -impl IntoPy for Cow<'_, OsStr> { - #[inline] - fn into_py(self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() - } -} - impl<'py> IntoPyObject<'py> for Cow<'_, OsStr> { type Target = PyString; type Output = Bound<'py, Self::Target>; @@ -180,22 +146,6 @@ impl<'py> IntoPyObject<'py> for &Cow<'_, OsStr> { } } -#[allow(deprecated)] -impl ToPyObject for OsString { - #[inline] - fn to_object(&self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() - } -} - -#[allow(deprecated)] -impl IntoPy for OsString { - #[inline] - fn into_py(self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() - } -} - impl<'py> IntoPyObject<'py> for OsString { type Target = PyString; type Output = Bound<'py, Self::Target>; @@ -207,14 +157,6 @@ impl<'py> IntoPyObject<'py> for OsString { } } -#[allow(deprecated)] -impl IntoPy for &OsString { - #[inline] - fn into_py(self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() - } -} - impl<'py> IntoPyObject<'py> for &OsString { type Target = PyString; type Output = Bound<'py, Self::Target>; diff --git a/src/conversions/std/path.rs b/src/conversions/std/path.rs index 25765a8084e..3eb074d04d2 100644 --- a/src/conversions/std/path.rs +++ b/src/conversions/std/path.rs @@ -3,21 +3,11 @@ use crate::ffi_ptr_ext::FfiPtrExt; use crate::instance::Bound; use crate::sync::GILOnceCell; use crate::types::any::PyAnyMethods; -use crate::{ffi, FromPyObject, IntoPyObjectExt, PyAny, PyErr, PyObject, PyResult, Python}; -#[allow(deprecated)] -use crate::{IntoPy, ToPyObject}; +use crate::{ffi, FromPyObject, PyAny, PyErr, PyObject, PyResult, Python}; use std::borrow::Cow; use std::ffi::OsString; use std::path::{Path, PathBuf}; -#[allow(deprecated)] -impl ToPyObject for Path { - #[inline] - fn to_object(&self, py: Python<'_>) -> PyObject { - self.as_os_str().into_py_any(py).unwrap() - } -} - // See osstr.rs for why there's no FromPyObject impl for &Path impl FromPyObject<'_> for PathBuf { @@ -28,14 +18,6 @@ impl FromPyObject<'_> for PathBuf { } } -#[allow(deprecated)] -impl IntoPy for &Path { - #[inline] - fn into_py(self, py: Python<'_>) -> PyObject { - self.to_object(py) - } -} - impl<'py> IntoPyObject<'py> for &Path { type Target = PyAny; type Output = Bound<'py, Self::Target>; @@ -61,22 +43,6 @@ impl<'py> IntoPyObject<'py> for &&Path { } } -#[allow(deprecated)] -impl ToPyObject for Cow<'_, Path> { - #[inline] - fn to_object(&self, py: Python<'_>) -> PyObject { - (**self).to_object(py) - } -} - -#[allow(deprecated)] -impl IntoPy for Cow<'_, Path> { - #[inline] - fn into_py(self, py: Python<'_>) -> PyObject { - (*self).to_object(py) - } -} - impl<'py> IntoPyObject<'py> for Cow<'_, Path> { type Target = PyAny; type Output = Bound<'py, Self::Target>; @@ -99,22 +65,6 @@ impl<'py> IntoPyObject<'py> for &Cow<'_, Path> { } } -#[allow(deprecated)] -impl ToPyObject for PathBuf { - #[inline] - fn to_object(&self, py: Python<'_>) -> PyObject { - (**self).to_object(py) - } -} - -#[allow(deprecated)] -impl IntoPy for PathBuf { - #[inline] - fn into_py(self, py: Python<'_>) -> PyObject { - (*self).to_object(py) - } -} - impl<'py> IntoPyObject<'py> for PathBuf { type Target = PyAny; type Output = Bound<'py, Self::Target>; @@ -126,14 +76,6 @@ impl<'py> IntoPyObject<'py> for PathBuf { } } -#[allow(deprecated)] -impl IntoPy for &PathBuf { - #[inline] - fn into_py(self, py: Python<'_>) -> PyObject { - (**self).to_object(py) - } -} - impl<'py> IntoPyObject<'py> for &PathBuf { type Target = PyAny; type Output = Bound<'py, Self::Target>; @@ -147,8 +89,8 @@ impl<'py> IntoPyObject<'py> for &PathBuf { #[cfg(test)] mod tests { - use crate::types::{PyAnyMethods, PyString, PyStringMethods}; - use crate::{IntoPyObject, IntoPyObjectExt, PyObject, Python}; + use crate::types::{PyAnyMethods, PyString}; + use crate::{IntoPyObject, IntoPyObjectExt, Python}; use std::borrow::Cow; use std::fmt::Debug; use std::path::{Path, PathBuf}; @@ -203,28 +145,4 @@ mod tests { 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 = 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()); - }); - } } diff --git a/src/conversions/std/set.rs b/src/conversions/std/set.rs index 3f1e6733f1a..ebb737f72c6 100644 --- a/src/conversions/std/set.rs +++ b/src/conversions/std/set.rs @@ -8,51 +8,11 @@ use crate::{ types::{ any::PyAnyMethods, frozenset::PyFrozenSetMethods, - set::{new_from_iter, try_new_from_iter, PySetMethods}, + set::{try_new_from_iter, PySetMethods}, PyFrozenSet, PySet, }, - FromPyObject, PyAny, PyErr, PyObject, PyResult, Python, + FromPyObject, PyAny, PyErr, PyResult, Python, }; -#[allow(deprecated)] -use crate::{IntoPy, ToPyObject}; - -#[allow(deprecated)] -impl ToPyObject for collections::HashSet -where - T: hash::Hash + Eq + ToPyObject, - S: hash::BuildHasher + Default, -{ - fn to_object(&self, py: Python<'_>) -> PyObject { - new_from_iter(py, self) - .expect("Failed to create Python set from HashSet") - .into() - } -} - -#[allow(deprecated)] -impl ToPyObject for collections::BTreeSet -where - T: hash::Hash + Eq + ToPyObject, -{ - fn to_object(&self, py: Python<'_>) -> PyObject { - new_from_iter(py, self) - .expect("Failed to create Python set from BTreeSet") - .into() - } -} - -#[allow(deprecated)] -impl IntoPy for collections::HashSet -where - K: IntoPy + Eq + hash::Hash, - S: hash::BuildHasher + Default, -{ - fn into_py(self, py: Python<'_>) -> PyObject { - new_from_iter(py, self.into_iter().map(|item| item.into_py(py))) - .expect("Failed to create Python set from HashSet") - .into() - } -} impl<'py, K, S> IntoPyObject<'py> for collections::HashSet where @@ -117,18 +77,6 @@ where } } -#[allow(deprecated)] -impl IntoPy for collections::BTreeSet -where - K: IntoPy + cmp::Ord, -{ - fn into_py(self, py: Python<'_>) -> PyObject { - new_from_iter(py, self.into_iter().map(|item| item.into_py(py))) - .expect("Failed to create Python set from BTreeSet") - .into() - } -} - impl<'py, K> IntoPyObject<'py> for collections::BTreeSet where K: IntoPyObject<'py> + cmp::Ord, @@ -192,7 +140,7 @@ where #[cfg(test)] mod tests { use crate::types::{any::PyAnyMethods, PyFrozenSet, PySet}; - use crate::{IntoPyObject, PyObject, Python}; + use crate::{IntoPyObject, Python}; use std::collections::{BTreeSet, HashSet}; #[test] @@ -221,22 +169,6 @@ 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(); - - let bto: PyObject = bt.clone().into_py(py); - let hso: PyObject = hs.clone().into_py(py); - - assert_eq!(bt, bto.extract(py).unwrap()); - assert_eq!(hs, hso.extract(py).unwrap()); - }); - } - #[test] fn test_set_into_pyobject() { Python::with_gil(|py| { diff --git a/src/conversions/std/slice.rs b/src/conversions/std/slice.rs index 798e5b54c45..5bfa5a3d48d 100644 --- a/src/conversions/std/slice.rs +++ b/src/conversions/std/slice.rs @@ -5,17 +5,8 @@ use crate::inspect::types::TypeInfo; use crate::{ conversion::IntoPyObject, types::{PyByteArray, PyByteArrayMethods, PyBytes}, - Bound, Py, PyAny, PyErr, PyObject, PyResult, Python, + Bound, PyAny, PyErr, 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() - } -} impl<'a, 'py, T> IntoPyObject<'py> for &'a [T] where @@ -76,20 +67,6 @@ 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() - } -} - -#[allow(deprecated)] -impl IntoPy> for Cow<'_, [u8]> { - fn into_py(self, py: Python<'_>) -> Py { - self.into_pyobject(py).unwrap().into_any().unbind() - } -} - impl<'py, T> IntoPyObject<'py> for Cow<'_, [T]> where T: Clone, diff --git a/src/conversions/std/string.rs b/src/conversions/std/string.rs index 074b5c2ba73..8936bd35000 100644 --- a/src/conversions/std/string.rs +++ b/src/conversions/std/string.rs @@ -6,36 +6,8 @@ use crate::{ conversion::IntoPyObject, instance::Bound, types::{any::PyAnyMethods, string::PyStringMethods, PyString}, - FromPyObject, Py, PyAny, PyObject, PyResult, Python, + FromPyObject, PyAny, PyResult, Python, }; -#[allow(deprecated)] -use crate::{IntoPy, ToPyObject}; - -/// 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 { - self.into_pyobject(py).unwrap().into_any().unbind() - } -} - -#[allow(deprecated)] -impl IntoPy for &str { - #[inline] - fn into_py(self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() - } -} - -#[allow(deprecated)] -impl IntoPy> for &str { - #[inline] - fn into_py(self, py: Python<'_>) -> Py { - self.into_pyobject(py).unwrap().unbind() - } -} impl<'py> IntoPyObject<'py> for &str { type Target = PyString; @@ -69,24 +41,6 @@ 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 { - self.into_pyobject(py).unwrap().into_any().unbind() - } -} - -#[allow(deprecated)] -impl IntoPy for Cow<'_, str> { - #[inline] - fn into_py(self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() - } -} - impl<'py> IntoPyObject<'py> for Cow<'_, str> { type Target = PyString; type Output = Bound<'py, Self::Target>; @@ -119,32 +73,6 @@ 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 { - self.into_pyobject(py).unwrap().into_any().unbind() - } -} - -#[allow(deprecated)] -impl ToPyObject for char { - #[inline] - fn to_object(&self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() - } -} - -#[allow(deprecated)] -impl IntoPy for char { - #[inline] - fn into_py(self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() - } -} - impl<'py> IntoPyObject<'py> for char { type Target = PyString; type Output = Bound<'py, Self::Target>; @@ -177,14 +105,6 @@ impl<'py> IntoPyObject<'py> for &char { } } -#[allow(deprecated)] -impl IntoPy for String { - #[inline] - fn into_py(self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() - } -} - impl<'py> IntoPyObject<'py> for String { type Target = PyString; type Output = Bound<'py, Self::Target>; @@ -200,14 +120,6 @@ impl<'py> IntoPyObject<'py> for String { } } -#[allow(deprecated)] -impl IntoPy for &String { - #[inline] - fn into_py(self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() - } -} - impl<'py> IntoPyObject<'py> for &String { type Target = PyString; type Output = Bound<'py, Self::Target>; @@ -282,22 +194,9 @@ impl FromPyObject<'_> for char { #[cfg(test)] mod tests { use crate::types::any::PyAnyMethods; - use crate::{IntoPyObject, PyObject, Python}; + use crate::{IntoPyObject, 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); - assert_eq!(s, py_string.extract::>(py).unwrap()); - let py_string: PyObject = Cow::::Owned(s.into()).into_py(py); - assert_eq!(s, py_string.extract::>(py).unwrap()); - }) - } - #[test] fn test_cow_into_pyobject() { Python::with_gil(|py| { diff --git a/src/conversions/std/time.rs b/src/conversions/std/time.rs index 741c28ee905..549eb4b5865 100755 --- a/src/conversions/std/time.rs +++ b/src/conversions/std/time.rs @@ -9,8 +9,6 @@ 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::{IntoPy, ToPyObject}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; const SECONDS_PER_DAY: u64 = 24 * 60 * 60; @@ -52,22 +50,6 @@ impl FromPyObject<'_> for Duration { } } -#[allow(deprecated)] -impl ToPyObject for Duration { - #[inline] - fn to_object(&self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() - } -} - -#[allow(deprecated)] -impl IntoPy for Duration { - #[inline] - fn into_py(self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() - } -} - impl<'py> IntoPyObject<'py> for Duration { #[cfg(not(Py_LIMITED_API))] type Target = PyDelta; @@ -134,22 +116,6 @@ impl FromPyObject<'_> for SystemTime { } } -#[allow(deprecated)] -impl ToPyObject for SystemTime { - #[inline] - fn to_object(&self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() - } -} - -#[allow(deprecated)] -impl IntoPy for SystemTime { - #[inline] - fn into_py(self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() - } -} - impl<'py> IntoPyObject<'py> for SystemTime { type Target = PyAny; type Output = Bound<'py, Self::Target>; diff --git a/src/conversions/std/vec.rs b/src/conversions/std/vec.rs index b630ca4019a..d77c899eda9 100644 --- a/src/conversions/std/vec.rs +++ b/src/conversions/std/vec.rs @@ -1,44 +1,7 @@ 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::{IntoPy, ToPyObject}; - -#[allow(deprecated)] -impl ToPyObject for [T] -where - T: ToPyObject, -{ - fn to_object(&self, py: Python<'_>) -> PyObject { - let mut iter = self.iter().map(|e| e.to_object(py)); - let list = new_from_iter(py, &mut iter); - list.into() - } -} - -#[allow(deprecated)] -impl ToPyObject for Vec -where - T: ToPyObject, -{ - fn to_object(&self, py: Python<'_>) -> PyObject { - self.as_slice().to_object(py) - } -} - -#[allow(deprecated)] -impl IntoPy for Vec -where - T: IntoPy, -{ - fn into_py(self, py: Python<'_>) -> PyObject { - let mut iter = self.into_iter().map(|e| e.into_py(py)); - let list = new_from_iter(py, &mut iter); - list.into() - } -} +use crate::{Bound, PyAny, PyErr, Python}; impl<'py, T> IntoPyObject<'py> for Vec where diff --git a/src/err/mod.rs b/src/err/mod.rs index 21e89efcf8e..ecdd4efe2eb 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -11,8 +11,6 @@ use crate::{ ffi, }; use crate::{Borrowed, BoundObject, Py, PyAny, PyObject, Python}; -#[allow(deprecated)] -use crate::{IntoPy, ToPyObject}; use std::borrow::Cow; use std::ffi::CStr; @@ -695,30 +693,6 @@ 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 { - self.into_pyobject(py).unwrap().into_any().unbind() - } -} - -#[allow(deprecated)] -impl ToPyObject for PyErr { - #[inline] - fn to_object(&self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() - } -} - -#[allow(deprecated)] -impl IntoPy for &PyErr { - #[inline] - fn into_py(self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() - } -} - impl<'py> IntoPyObject<'py> for PyErr { type Target = PyBaseException; type Output = Bound<'py, Self::Target>; diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index 270ba2de47e..fccbf1610ac 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -12,8 +12,6 @@ use crate::{ Borrowed, BoundObject, IntoPyObject, IntoPyObjectExt, Py, PyAny, PyClass, PyErr, PyRef, PyResult, PyTypeInfo, Python, }; -#[allow(deprecated)] -use crate::{IntoPy, ToPyObject}; use std::{ borrow::Cow, ffi::{CStr, CString}, @@ -1247,8 +1245,6 @@ pub struct PyClassGetterGenerator< // 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, - const IMPLEMENTS_INTOPY: bool, const IMPLEMENTS_INTOPYOBJECT_REF: bool, const IMPLEMENTS_INTOPYOBJECT: bool, >(PhantomData<(ClassT, FieldT, Offset)>); @@ -1258,8 +1254,6 @@ impl< FieldT, Offset: OffsetCalculator, const IS_PY_T: bool, - const IMPLEMENTS_TOPYOBJECT: bool, - const IMPLEMENTS_INTOPY: bool, const IMPLEMENTS_INTOPYOBJECT_REF: bool, const IMPLEMENTS_INTOPYOBJECT: bool, > @@ -1268,8 +1262,6 @@ impl< FieldT, Offset, IS_PY_T, - IMPLEMENTS_TOPYOBJECT, - IMPLEMENTS_INTOPY, IMPLEMENTS_INTOPYOBJECT_REF, IMPLEMENTS_INTOPYOBJECT, > @@ -1285,8 +1277,6 @@ impl< ClassT: PyClass, U, Offset: OffsetCalculator>, - const IMPLEMENTS_TOPYOBJECT: bool, - const IMPLEMENTS_INTOPY: bool, const IMPLEMENTS_INTOPYOBJECT_REF: bool, const IMPLEMENTS_INTOPYOBJECT: bool, > @@ -1295,8 +1285,6 @@ impl< Py, Offset, true, - IMPLEMENTS_TOPYOBJECT, - IMPLEMENTS_INTOPY, IMPLEMENTS_INTOPYOBJECT_REF, IMPLEMENTS_INTOPYOBJECT, > @@ -1319,52 +1307,17 @@ impl< } else { PyMethodDefType::Getter(PyGetterDef { name, - meth: pyo3_get_value_topyobject::, Offset>, + meth: pyo3_get_value_into_pyobject_ref::, Offset>, doc, }) } } } -/// 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, - Offset: OffsetCalculator, - const IMPLEMENTS_INTOPY: bool, - > PyClassGetterGenerator -{ - pub const fn generate(&self, name: &'static CStr, doc: &'static CStr) -> PyMethodDefType { - PyMethodDefType::Getter(PyGetterDef { - name, - meth: pyo3_get_value_topyobject::, - doc, - }) - } -} - /// Field is not `Py`; 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, - > +impl + PyClassGetterGenerator where ClassT: PyClass, for<'a, 'py> &'a FieldT: IntoPyObject<'py>, @@ -1379,51 +1332,6 @@ where } } -/// 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, -{ - 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. -#[allow(deprecated)] -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_attr( diagnostic_namespace, diagnostic::on_unimplemented( @@ -1436,20 +1344,24 @@ 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 +impl< + ClassT: PyClass, + FieldT, + Offset: OffsetCalculator, + const IMPLEMENTS_INTOPYOBJECT: bool, + > PyClassGetterGenerator { - pub const fn generate(&self, _name: &'static CStr, _doc: &'static CStr) -> PyMethodDefType + 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." - ) + PyMethodDefType::Getter(PyGetterDef { + name, + meth: pyo3_get_value_into_pyobject::, + doc, + }) } } @@ -1476,23 +1388,6 @@ where unsafe { obj.cast::().add(Offset::offset()).cast::() } } -#[allow(deprecated)] -fn pyo3_get_value_topyobject< - ClassT: PyClass, - FieldT: ToPyObject, - Offset: OffsetCalculator, ->( - py: Python<'_>, - obj: *mut ffi::PyObject, -) -> PyResult<*mut ffi::PyObject> { - 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, @@ -1534,23 +1429,6 @@ where .into_ptr()) } -#[allow(deprecated)] -fn pyo3_get_value< - ClassT: PyClass, - FieldT: IntoPy> + Clone, - Offset: OffsetCalculator, ->( - py: Python<'_>, - obj: *mut ffi::PyObject, -) -> PyResult<*mut ffi::PyObject> { - 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_py(py).into_ptr()) -} - pub struct ConvertField< const IMPLEMENTS_INTOPYOBJECT_REF: bool, const IMPLEMENTS_INTOPYOBJECT: bool, diff --git a/src/impl_/pyclass/probes.rs b/src/impl_/pyclass/probes.rs index 719976e89c7..001b85f5023 100644 --- a/src/impl_/pyclass/probes.rs +++ b/src/impl_/pyclass/probes.rs @@ -1,8 +1,6 @@ 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. @@ -30,20 +28,6 @@ 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, diff --git a/src/impl_/wrap.rs b/src/impl_/wrap.rs index 7b214219408..9d14d321ae2 100644 --- a/src/impl_/wrap.rs +++ b/src/impl_/wrap.rs @@ -1,7 +1,5 @@ use std::{convert::Infallible, marker::PhantomData, ops::Deref}; -#[allow(deprecated)] -use crate::IntoPy; use crate::{ffi, types::PyNone, Bound, IntoPyObject, IntoPyObjectExt, PyObject, PyResult, Python}; /// Used to wrap values in `Option` for default arguments. @@ -108,32 +106,6 @@ impl<'py, T: IntoPyObject<'py>, E> IntoPyObjectConverter> { } } -#[allow(deprecated)] -impl> IntoPyConverter { - #[inline] - pub fn wrap(&self, obj: T) -> Result { - Ok(obj) - } -} - -#[allow(deprecated)] -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 diff --git a/src/instance.rs b/src/instance.rs index a47274e7732..89e950e0d91 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -11,8 +11,6 @@ use crate::{ 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; @@ -827,24 +825,6 @@ impl Clone for Borrowed<'_, '_, T> { impl Copy for Borrowed<'_, '_, T> {} -#[allow(deprecated)] -impl ToPyObject for Borrowed<'_, '_, T> { - /// Converts `Py` instance -> PyObject. - #[inline] - fn to_object(&self, py: Python<'_>) -> PyObject { - (*self).into_py(py) - } -} - -#[allow(deprecated)] -impl IntoPy for Borrowed<'_, '_, T> { - /// Converts `Py` instance -> PyObject. - #[inline] - fn into_py(self, py: Python<'_>) -> PyObject { - self.to_owned().into_py(py) - } -} - impl<'a, 'py, T> BoundObject<'py, T> for Borrowed<'a, 'py, T> { type Any = Borrowed<'a, 'py, PyAny>; @@ -1711,60 +1691,6 @@ impl Py { } } -#[allow(deprecated)] -impl ToPyObject for Py { - /// Converts `Py` instance -> PyObject. - #[inline] - fn to_object(&self, py: Python<'_>) -> PyObject { - self.clone_ref(py).into_any() - } -} - -#[allow(deprecated)] -impl IntoPy for Py { - /// Converts a `Py` instance to `PyObject`. - /// Consumes `self` without calling `Py_DECREF()`. - #[inline] - fn into_py(self, _py: Python<'_>) -> PyObject { - self.into_any() - } -} - -#[allow(deprecated)] -impl IntoPy for &'_ Py { - #[inline] - fn into_py(self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() - } -} - -#[allow(deprecated)] -impl ToPyObject for Bound<'_, T> { - /// Converts `&Bound` instance -> PyObject, increasing the reference count. - #[inline] - fn to_object(&self, py: Python<'_>) -> PyObject { - self.clone().into_py(py) - } -} - -#[allow(deprecated)] -impl IntoPy for Bound<'_, T> { - /// Converts a `Bound` instance to `PyObject`. - #[inline] - fn into_py(self, _py: Python<'_>) -> PyObject { - self.into_any().unbind() - } -} - -#[allow(deprecated)] -impl IntoPy for &Bound<'_, T> { - /// Converts `&Bound` instance -> PyObject, increasing the reference count. - #[inline] - fn into_py(self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() - } -} - unsafe impl crate::AsPyPointer for Py { /// Gets the underlying FFI pointer, returns a borrowed pointer. #[inline] diff --git a/src/lib.rs b/src/lib.rs index 3954223a489..e7e99318e2a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -339,8 +339,6 @@ //! [`Ungil`]: crate::marker::Ungil pub use crate::class::*; 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}; #[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 2ffd12f5995..a4a7be8c0c5 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -304,7 +304,7 @@ pub use nightly::Ungil; /// It serves three main purposes: /// - It provides a global API for the Python interpreter, such as [`Python::eval`]. /// - It can be passed to functions that require a proof of holding the GIL, such as -/// [`Py::clone_ref`][crate::Py::clone_ref]. +/// [`Py::clone_ref`](crate::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 [`Bound<'py, PyAny>`]. /// @@ -974,8 +974,6 @@ mod tests { fn test_py_run_inserts_globals_2() { use std::ffi::CString; - use crate::Py; - #[crate::pyclass(crate = "crate")] #[derive(Clone)] struct CodeRunner { @@ -985,7 +983,7 @@ mod tests { impl CodeRunner { fn reproducer(&mut self, py: Python<'_>) -> PyResult<()> { let variables = PyDict::new(py); - variables.set_item("cls", Py::new(py, self.clone())?)?; + variables.set_item("cls", crate::Py::new(py, self.clone())?)?; py.run(self.code.as_c_str(), Some(&variables), None)?; Ok(()) diff --git a/src/prelude.rs b/src/prelude.rs index 62cde9a5591..258b522c103 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -9,8 +9,6 @@ //! ``` pub use crate::conversion::{FromPyObject, IntoPyObject}; -#[allow(deprecated)] -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 173d8e0e9e4..2d67786c802 100644 --- a/src/pybacked.rs +++ b/src/pybacked.rs @@ -9,8 +9,6 @@ use crate::{ }, 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. /// @@ -87,30 +85,6 @@ 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.as_any().clone_ref(py) - } - #[cfg(not(any(Py_3_10, not(Py_LIMITED_API))))] - fn to_object(&self, py: Python<'_>) -> Py { - PyString::new(py, self).into_any().unbind() - } -} - -#[allow(deprecated)] -impl IntoPy> for PyBackedStr { - #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] - fn into_py(self, _py: Python<'_>) -> Py { - self.storage.into_any() - } - #[cfg(not(any(Py_3_10, not(Py_LIMITED_API))))] - fn into_py(self, py: Python<'_>) -> Py { - PyString::new(py, &self).into_any().unbind() - } -} - impl<'py> IntoPyObject<'py> for PyBackedStr { type Target = PyAny; type Output = Bound<'py, Self::Target>; @@ -239,26 +213,6 @@ impl FromPyObject<'_> for PyBackedBytes { } } -#[allow(deprecated)] -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(py, bytes).into_any().unbind(), - } - } -} - -#[allow(deprecated)] -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(py, &bytes).into_any().unbind(), - } - } -} - impl<'py> IntoPyObject<'py> for PyBackedBytes { type Target = PyBytes; type Output = Bound<'py, Self::Target>; @@ -404,19 +358,6 @@ mod test { }); } - #[test] - #[allow(deprecated)] - fn py_backed_str_into_py() { - 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.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| { @@ -454,7 +395,6 @@ mod test { } #[test] - #[allow(deprecated)] fn py_backed_bytes_into_pyobject() { Python::with_gil(|py| { let orig_bytes = PyBytes::new(py, b"abcde"); @@ -463,12 +403,10 @@ mod test { .into_pyobject(py) .unwrap() .is(&orig_bytes)); - assert!(py_backed_bytes.into_py(py).is(&orig_bytes)); }); } #[test] - #[allow(deprecated)] fn rust_backed_bytes_into_pyobject() { Python::with_gil(|py| { let orig_bytes = PyByteArray::new(py, b"abcde"); @@ -480,9 +418,6 @@ mod test { 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); - assert!(&into_py.is_exact_instance_of::()); - assert_eq!(&into_py.extract::().unwrap(), b"abcde"); }); } diff --git a/src/pycell.rs b/src/pycell.rs index 50a3e59ae15..1b7463a5941 100644 --- a/src/pycell.rs +++ b/src/pycell.rs @@ -199,9 +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; -#[allow(deprecated)] -use crate::IntoPy; -use crate::{ffi, Borrowed, Bound, PyErr, PyObject, Python}; +use crate::{ffi, Borrowed, Bound, PyErr, Python}; use std::convert::Infallible; use std::fmt; use std::mem::ManuallyDrop; @@ -449,20 +447,6 @@ 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()) } - } -} - impl<'py, T: PyClass> IntoPyObject<'py> for PyRef<'py, T> { type Target = T; type Output = Bound<'py, T>; @@ -640,20 +624,6 @@ 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) - } -} - impl<'py, T: PyClass> IntoPyObject<'py> for PyRefMut<'py, T> { type Target = T; type Output = Bound<'py, T>; diff --git a/src/types/boolobject.rs b/src/types/boolobject.rs index 2e7f0369de1..5091930a6cb 100644 --- a/src/types/boolobject.rs +++ b/src/types/boolobject.rs @@ -2,14 +2,11 @@ use crate::inspect::types::TypeInfo; use crate::{ exceptions::PyTypeError, ffi, ffi_ptr_ext::FfiPtrExt, instance::Bound, - types::typeobject::PyTypeMethods, Borrowed, FromPyObject, PyAny, PyObject, PyResult, Python, + types::typeobject::PyTypeMethods, Borrowed, FromPyObject, PyAny, PyResult, Python, }; -#[allow(deprecated)] -use crate::{IntoPy, ToPyObject}; use super::any::PyAnyMethods; use crate::conversion::IntoPyObject; -use crate::BoundObject; use std::convert::Infallible; /// Represents a Python `bool`. @@ -138,23 +135,6 @@ 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 { - self.into_pyobject(py).unwrap().into_any().unbind() - } -} - -#[allow(deprecated)] -impl IntoPy for bool { - #[inline] - fn into_py(self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() - } -} - impl<'py> IntoPyObject<'py> for bool { type Target = PyBool; type Output = Borrowed<'py, 'py, Self::Target>; diff --git a/src/types/float.rs b/src/types/float.rs index 89001495227..3753572904b 100644 --- a/src/types/float.rs +++ b/src/types/float.rs @@ -3,11 +3,9 @@ use crate::conversion::IntoPyObject; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; use crate::{ - ffi, ffi_ptr_ext::FfiPtrExt, instance::Bound, Borrowed, FromPyObject, PyAny, PyErr, PyObject, - PyResult, Python, + ffi, ffi_ptr_ext::FfiPtrExt, instance::Bound, Borrowed, FromPyObject, PyAny, PyErr, PyResult, + Python, }; -#[allow(deprecated)] -use crate::{IntoPy, ToPyObject}; use std::convert::Infallible; use std::os::raw::c_double; @@ -20,7 +18,7 @@ use std::os::raw::c_double; /// [`Bound<'py, PyFloat>`][Bound]. /// /// You can usually avoid directly working with this type -/// by using [`ToPyObject`] and [`extract`][PyAnyMethods::extract] +/// by using [`IntoPyObject`] and [`extract`][PyAnyMethods::extract] /// with [`f32`]/[`f64`]. #[repr(transparent)] pub struct PyFloat(PyAny); @@ -71,22 +69,6 @@ impl<'py> PyFloatMethods<'py> for Bound<'py, PyFloat> { } } -#[allow(deprecated)] -impl ToPyObject for f64 { - #[inline] - fn to_object(&self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() - } -} - -#[allow(deprecated)] -impl IntoPy for f64 { - #[inline] - fn into_py(self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() - } -} - impl<'py> IntoPyObject<'py> for f64 { type Target = PyFloat; type Output = Bound<'py, Self::Target>; @@ -149,22 +131,6 @@ impl<'py> FromPyObject<'py> for f64 { } } -#[allow(deprecated)] -impl ToPyObject for f32 { - #[inline] - fn to_object(&self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() - } -} - -#[allow(deprecated)] -impl IntoPy for f32 { - #[inline] - fn into_py(self, py: Python<'_>) -> PyObject { - self.into_pyobject(py).unwrap().into_any().unbind() - } -} - impl<'py> IntoPyObject<'py> for f32 { type Target = PyFloat; type Output = Bound<'py, Self::Target>; diff --git a/src/types/list.rs b/src/types/list.rs index 9612aa00cda..7c63c5bf44d 100644 --- a/src/types/list.rs +++ b/src/types/list.rs @@ -5,9 +5,7 @@ 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::{Borrowed, Bound, BoundObject, IntoPyObject, IntoPyObjectExt, PyAny, PyErr, Python}; use std::iter::FusedIterator; #[cfg(feature = "nightly")] use std::num::NonZero; @@ -24,15 +22,6 @@ pub struct PyList(PyAny); pyobject_native_type_core!(PyList, pyobject_native_static_type_object!(ffi::PyList_Type), #checkfunction=ffi::PyList_Check); -#[inline] -#[track_caller] -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>( diff --git a/src/types/none.rs b/src/types/none.rs index da8ef1aa51b..319a01a5311 100644 --- a/src/types/none.rs +++ b/src/types/none.rs @@ -1,7 +1,5 @@ use crate::ffi_ptr_ext::FfiPtrExt; -use crate::{ffi, types::any::PyAnyMethods, Borrowed, Bound, PyAny, PyObject, PyTypeInfo, Python}; -#[allow(deprecated)] -use crate::{IntoPy, ToPyObject}; +use crate::{ffi, types::any::PyAnyMethods, Borrowed, Bound, PyAny, PyTypeInfo, Python}; /// Represents the Python `None` object. /// @@ -41,27 +39,11 @@ 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) - } -} - -#[allow(deprecated)] -impl IntoPy for () { - #[inline] - fn into_py(self, py: Python<'_>) -> PyObject { - PyNone::get(py).into_py(py) - } -} - #[cfg(test)] mod tests { use crate::types::any::PyAnyMethods; use crate::types::{PyDict, PyNone}; - use crate::{PyObject, PyTypeInfo, Python}; + use crate::{PyTypeInfo, Python}; #[test] fn test_none_is_itself() { @@ -85,25 +67,6 @@ 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()); - }) - } - - #[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()); - }) - } - #[test] fn test_dict_is_not_none() { Python::with_gil(|py| { diff --git a/src/types/num.rs b/src/types/num.rs index 38874f29aba..79cd800b75d 100644 --- a/src/types/num.rs +++ b/src/types/num.rs @@ -7,9 +7,8 @@ use std::convert::Infallible; /// Values of this type are accessed via PyO3's smart pointers, e.g. as /// [`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) +/// You can usually avoid directly working with this type by using +/// [`IntoPyObject`] and [`extract`](super::PyAnyMethods::extract) /// with the primitive Rust integer types. #[repr(transparent)] pub struct PyInt(PyAny); diff --git a/src/types/set.rs b/src/types/set.rs index 93c251d460f..8db32c56c1e 100644 --- a/src/types/set.rs +++ b/src/types/set.rs @@ -1,6 +1,4 @@ use crate::types::PyIterator; -#[allow(deprecated)] -use crate::ToPyObject; use crate::{ err::{self, PyErr, PyResult}, ffi_ptr_ext::FfiPtrExt, @@ -265,16 +263,6 @@ impl ExactSizeIterator for BoundSetIterator<'_> { } } -#[allow(deprecated)] -#[inline] -pub(crate) fn new_from_iter( - py: Python<'_>, - elements: impl IntoIterator, -) -> PyResult> { - 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, T>( py: Python<'py>, diff --git a/src/types/slice.rs b/src/types/slice.rs index b74a228aae3..97c38e0f36c 100644 --- a/src/types/slice.rs +++ b/src/types/slice.rs @@ -2,9 +2,7 @@ use crate::err::{PyErr, PyResult}; use crate::ffi; use crate::ffi_ptr_ext::FfiPtrExt; use crate::types::any::PyAnyMethods; -#[allow(deprecated)] -use crate::ToPyObject; -use crate::{Bound, IntoPyObject, PyAny, PyObject, Python}; +use crate::{Bound, IntoPyObject, PyAny, Python}; use std::convert::Infallible; /// Represents a Python `slice`. @@ -122,13 +120,6 @@ 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() - } -} - impl<'py> IntoPyObject<'py> for PySliceIndices { type Target = PySlice; type Output = Bound<'py, Self::Target>; diff --git a/src/types/string.rs b/src/types/string.rs index f7a724b9fce..a9df79c305d 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; -#[allow(deprecated)] -use crate::IntoPy; use crate::{ffi, Bound, Py, PyAny, PyResult, Python}; use std::borrow::Cow; use std::ffi::CString; @@ -422,27 +420,6 @@ 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) - } -} - /// 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`]. diff --git a/src/types/tuple.rs b/src/types/tuple.rs index c412a81a752..0a315a42e00 100644 --- a/src/types/tuple.rs +++ b/src/types/tuple.rs @@ -6,11 +6,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, IntoPyObject, IntoPyObjectExt, Py, PyAny, PyErr, PyObject, - PyResult, Python, + exceptions, Bound, FromPyObject, IntoPyObject, IntoPyObjectExt, PyAny, PyErr, PyResult, Python, }; -#[allow(deprecated)] -use crate::{IntoPy, ToPyObject}; use std::iter::FusedIterator; #[cfg(feature = "nightly")] use std::num::NonZero; @@ -582,20 +579,6 @@ 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() - } -} - #[cold] fn wrong_tuple_length(t: &Bound<'_, PyTuple>, expected_length: usize) -> PyErr { let msg = format!( @@ -607,20 +590,6 @@ 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_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_bound(py)),+]).into() - } - } - impl <'py, $($T),+> IntoPyObject<'py> for ($($T,)+) where $($T: IntoPyObject<'py>,)+ @@ -916,13 +885,6 @@ 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).into_bound(py)),+]).unbind() - } - } - impl<'py, $($T: FromPyObject<'py>),+> FromPyObject<'py> for ($($T,)+) { fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { diff --git a/tests/ui/invalid_property_args.stderr b/tests/ui/invalid_property_args.stderr index 5ef01fa210a..047e6709c21 100644 --- a/tests/ui/invalid_property_args.stderr +++ b/tests/ui/invalid_property_args.stderr @@ -65,11 +65,11 @@ error[E0277]: `PhantomData` cannot be converted to a Python object &'a (T0, T1, T2, T3, T4) and $N others = note: required for `PhantomData` to implement `for<'py> PyO3GetField<'py>` -note: required by a bound in `PyClassGetterGenerator::::generate` +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 ... | for<'py> FieldT: PyO3GetField<'py>, - | ^^^^^^^^^^^^^^^^^ required by this bound in `PyClassGetterGenerator::::generate` + | ^^^^^^^^^^^^^^^^^ required by this bound in `PyClassGetterGenerator::::generate` From 561c699876fd7c114b8c8a776cfe3c362ea82ae8 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Wed, 2 Apr 2025 10:01:06 +0200 Subject: [PATCH 628/936] fix `unused_imports` warning for `IsOption` probe (#5002) (#5030) --- newsfragments/5030.fixed.md | 1 + pyo3-macros-backend/src/params.rs | 5 ++++- pyo3-macros-backend/src/pymethod.rs | 2 ++ tests/ui/pyclass_probe.rs | 17 +++++++++++++++++ 4 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 newsfragments/5030.fixed.md diff --git a/newsfragments/5030.fixed.md b/newsfragments/5030.fixed.md new file mode 100644 index 00000000000..4cd23405a6c --- /dev/null +++ b/newsfragments/5030.fixed.md @@ -0,0 +1 @@ +Fixes accidentally emitting `unused_imports` lint in `macro_rules` context. \ No newline at end of file diff --git a/pyo3-macros-backend/src/params.rs b/pyo3-macros-backend/src/params.rs index ae7a6c916a8..6ff065e7458 100644 --- a/pyo3-macros-backend/src/params.rs +++ b/pyo3-macros-backend/src/params.rs @@ -238,7 +238,10 @@ 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 _;); + let use_probe = quote! { + #[allow(unused_imports)] + use #pyo3_path::impl_::pyclass::Probe as _; + }; macro_rules! quote_arg_span { ($($tokens:tt)*) => { quote_spanned!(arg.ty.span() => { #use_probe $($tokens)* }) } } diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index a6d692775ef..b4637b48012 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -701,6 +701,7 @@ pub fn impl_py_setter_def( let holder = holders.push_holder(span); let ty = field.ty.clone().elide_lifetimes(); quote! { + #[allow(unused_imports)] use #pyo3_path::impl_::pyclass::Probe as _; let _val = #pyo3_path::impl_::extract_argument::extract_argument::< _, @@ -1203,6 +1204,7 @@ fn extract_object( let holder = holders.push_holder(Span::call_site()); let ty = arg.ty().clone().elide_lifetimes(); quote! {{ + #[allow(unused_imports)] use #pyo3_path::impl_::pyclass::Probe as _; #pyo3_path::impl_::extract_argument::extract_argument::< _, diff --git a/tests/ui/pyclass_probe.rs b/tests/ui/pyclass_probe.rs index 6029ceca1f7..89b265cddf4 100644 --- a/tests/ui/pyclass_probe.rs +++ b/tests/ui/pyclass_probe.rs @@ -1,3 +1,4 @@ +#![deny(unused_imports)] use pyo3::prelude::*; #[pyclass] @@ -17,4 +18,20 @@ fn probe(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { Ok(()) } +#[pyclass] +struct Check5029(); + +macro_rules! impl_methods { + ($name:ident) => { + #[pymethods] + impl Check5029 { + fn $name(&self, _value: Option<&str>) -> PyResult<()> { + Ok(()) + } + } + }; +} + +impl_methods!(some_method); + fn main() {} From a44a623ee00eb02b09b1fd95cfa63f2478f73728 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 4 Apr 2025 10:27:49 +0100 Subject: [PATCH 629/936] update UI tests for Rust 1.86 (#5041) --- tests/ui/invalid_intern_arg.stderr | 5 +++-- tests/ui/static_ref.stderr | 18 ++++++++++-------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/tests/ui/invalid_intern_arg.stderr b/tests/ui/invalid_intern_arg.stderr index ab4268310ac..935d8406e37 100644 --- a/tests/ui/invalid_intern_arg.stderr +++ b/tests/ui/invalid_intern_arg.stderr @@ -7,8 +7,9 @@ error[E0435]: attempt to use a non-constant value in a constant help: consider using `let` instead of `static` --> src/sync.rs | - | let INTERNED: $crate::sync::Interned = $crate::sync::Interned::new($text); - | ~~~ + - static INTERNED: $crate::sync::Interned = $crate::sync::Interned::new($text); + + 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/static_ref.stderr b/tests/ui/static_ref.stderr index 9fe37355980..fdcc61bd20a 100644 --- a/tests/ui/static_ref.stderr +++ b/tests/ui/static_ref.stderr @@ -1,11 +1,13 @@ -error: lifetime may not live long enough +error[E0521]: borrowed data escapes outside of function --> tests/ui/static_ref.rs:4:1 | 4 | #[pyfunction] | ^^^^^^^^^^^^^ | | + | `py` is a reference that is only valid in the function body + | `py` escapes the function body here | lifetime `'py` defined here - | coercion requires that `'py` must outlive `'static` + | argument 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) @@ -29,19 +31,19 @@ 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 { - | ^^^^^^- - | | | - | | argument requires that `holder_0` is borrowed for `'static` - | borrowed value does not live long enough + | ^^^^^^^ borrowed value does not live long enough -error: lifetime may not live long enough +error[E0521]: borrowed data escapes outside of function --> tests/ui/static_ref.rs:9:1 | 9 | #[pyfunction] | ^^^^^^^^^^^^^ | | + | `py` is a reference that is only valid in the function body + | `py` escapes the function body here | lifetime `'py` defined here - | coercion requires that `'py` must outlive `'static` + | argument 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 66034efc998c7a1a799eccc4ef331b277815efc8 Mon Sep 17 00:00:00 2001 From: David Brochart Date: Fri, 4 Apr 2025 11:46:23 +0200 Subject: [PATCH 630/936] Add pycrdt to examples (#5040) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 55558594495..2bf2342b311 100644 --- a/README.md +++ b/README.md @@ -209,6 +209,7 @@ about this topic. - [orjson](https://github.com/ijl/orjson) _Fast Python JSON library._ - [ormsgpack](https://github.com/aviramha/ormsgpack) _Fast Python msgpack library._ - [polars](https://github.com/pola-rs/polars) _Fast multi-threaded DataFrame library in Rust | Python | Node.js._ +- [pycrdt](https://github.com/jupyter-server/pycrdt) _Python bindings for the Rust CRDT implementation [Yrs](https://github.com/y-crdt/y-crdt)._ - [pydantic-core](https://github.com/pydantic/pydantic-core) _Core validation logic for pydantic 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._ From 90d5f5694ebf91466addc3c1c83559f36c9bd3d9 Mon Sep 17 00:00:00 2001 From: Thomas Tanon Date: Fri, 4 Apr 2025 11:46:33 +0200 Subject: [PATCH 631/936] Makes Clippy beta happy (#5032) * Use std::ptr::eq where relevant * allow(clippy::ptr_eq) in pyo3-ffi * Add ref for allow(clippy::large_enum_variant) --- pyo3-ffi/src/lib.rs | 3 ++- pyo3-macros-backend/src/method.rs | 1 + src/err/mod.rs | 6 +++++- src/impl_/pyclass.rs | 4 ++-- src/impl_/pyclass_init.rs | 3 ++- src/instance.rs | 5 +++-- src/internal_tricks.rs | 2 ++ src/pycell/impl_.rs | 2 +- src/type_object.rs | 8 ++++++- src/types/any.rs | 5 +++-- src/types/boolobject.rs | 3 ++- src/types/sequence.rs | 3 ++- src/types/weakref/anyref.rs | 18 ++++++++++------ src/types/weakref/proxy.rs | 36 ++++++++++++++++++++----------- src/types/weakref/reference.rs | 18 ++++++++++------ tests/test_exceptions.rs | 3 ++- 16 files changed, 82 insertions(+), 38 deletions(-) diff --git a/pyo3-ffi/src/lib.rs b/pyo3-ffi/src/lib.rs index 64c831c1043..aba8866b266 100644 --- a/pyo3-ffi/src/lib.rs +++ b/pyo3-ffi/src/lib.rs @@ -327,7 +327,8 @@ non_snake_case, non_upper_case_globals, clippy::upper_case_acronyms, - clippy::missing_safety_doc + clippy::missing_safety_doc, + clippy::ptr_eq )] #![warn(elided_lifetimes_in_paths, unused_lifetimes)] // This crate is a hand-maintained translation of CPython's headers, so requiring "unsafe" diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index a1d7a95df35..bc117874a5b 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -53,6 +53,7 @@ pub struct PyArg<'a> { pub ty: &'a syn::Type, } +#[allow(clippy::large_enum_variant)] // See #5039 #[derive(Clone, Debug)] pub enum FnArg<'a> { Regular(RegularArg<'a>), diff --git a/src/err/mod.rs b/src/err/mod.rs index ecdd4efe2eb..74b07d5c17a 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -20,6 +20,7 @@ mod impls; use crate::conversion::IntoPyObject; use err_state::{PyErrState, PyErrStateLazyFnOutput, PyErrStateNormalized}; use std::convert::Infallible; +use std::ptr; /// Represents a Python exception. /// @@ -324,7 +325,10 @@ impl PyErr { pub 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() { + if ptr::eq( + pvalue.get_type().as_ptr(), + PanicException::type_object_raw(py).cast(), + ) { let msg: String = pvalue .str() .map(|py_str| py_str.to_string_lossy().into()) diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index fccbf1610ac..c646b35ecfe 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -17,6 +17,7 @@ use std::{ ffi::{CStr, CString}, marker::PhantomData, os::raw::{c_int, c_void}, + ptr, ptr::NonNull, sync::Mutex, thread, @@ -948,7 +949,7 @@ pub unsafe extern "C" fn alloc_with_freelist( 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 // freelist - if nitems == 0 && subtype == self_type { + if nitems == 0 && ptr::eq(subtype, self_type) { let mut free_list = T::get_free_list(py).lock().unwrap(); if let Some(obj) = free_list.pop() { drop(free_list); @@ -1141,7 +1142,6 @@ 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, Py_LIMITED_API), diagnostic::on_unimplemented( diff --git a/src/impl_/pyclass_init.rs b/src/impl_/pyclass_init.rs index ff0e4e80e96..c6bea31a743 100644 --- a/src/impl_/pyclass_init.rs +++ b/src/impl_/pyclass_init.rs @@ -5,6 +5,7 @@ use crate::types::PyType; use crate::{ffi, Borrowed, PyErr, PyResult, Python}; use crate::{ffi::PyTypeObject, sealed::Sealed, type_object::PyTypeInfo}; use std::marker::PhantomData; +use std::ptr; /// Initializer for Python types. /// @@ -38,7 +39,7 @@ impl PyObjectInit for PyNativeTypeInitializer { 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 is_base_object = ptr::eq(type_object, ptr::addr_of!(ffi::PyBaseObject_Type)); let subtype_borrowed: Borrowed<'_, '_, PyType> = unsafe { subtype .cast::() diff --git a/src/instance.rs b/src/instance.rs index 89e950e0d91..530c11caa56 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -14,6 +14,7 @@ use crate::{gil, PyTypeCheck}; use std::marker::PhantomData; use std::mem::ManuallyDrop; use std::ops::Deref; +use std::ptr; use std::ptr::NonNull; /// Owned or borrowed gil-bound Python smart pointer @@ -1315,7 +1316,7 @@ impl Py { /// This is equivalent to the Python expression `self is other`. #[inline] pub fn is(&self, o: &U) -> bool { - self.as_ptr() == o.as_ptr() + ptr::eq(self.as_ptr(), o.as_ptr()) } /// Gets the reference count of the `ffi::PyObject` pointer. @@ -1387,7 +1388,7 @@ impl Py { /// /// This is equivalent to the Python expression `self is None`. pub fn is_none(&self, _py: Python<'_>) -> bool { - unsafe { ffi::Py_None() == self.as_ptr() } + unsafe { ptr::eq(ffi::Py_None(), self.as_ptr()) } } /// Returns whether the object is considered to be true. diff --git a/src/internal_tricks.rs b/src/internal_tricks.rs index 1c011253254..e6c6b755ece 100644 --- a/src/internal_tricks.rs +++ b/src/internal_tricks.rs @@ -34,6 +34,7 @@ pub(crate) fn ptr_from_mut(t: &mut T) -> *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)] + #[allow(clippy::incompatible_msrv)] { let Some(f) = f else { return false }; std::ptr::fn_addr_eq(f, g) @@ -48,6 +49,7 @@ pub(crate) fn clear_eq(f: Option, g: ffi::inquiry) -> bool { // 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)] + #[allow(clippy::incompatible_msrv)] { let Some(f) = f else { return false }; std::ptr::fn_addr_eq(f, g) diff --git a/src/pycell/impl_.rs b/src/pycell/impl_.rs index 6fe0545d529..b59314bc54a 100644 --- a/src/pycell/impl_.rs +++ b/src/pycell/impl_.rs @@ -241,7 +241,7 @@ where 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) { + if std::ptr::eq(type_ptr, std::ptr::addr_of!(ffi::PyBaseObject_Type)) { let tp_free = actual_type .get_slot(TP_FREE) .expect("PyBaseObject_Type should have tp_free"); diff --git a/src/type_object.rs b/src/type_object.rs index d181fff08b1..778fea7152c 100644 --- a/src/type_object.rs +++ b/src/type_object.rs @@ -4,6 +4,7 @@ use crate::ffi_ptr_ext::FfiPtrExt; use crate::types::any::PyAnyMethods; use crate::types::{PyAny, PyType}; use crate::{ffi, Bound, Python}; +use std::ptr; /// `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` @@ -71,7 +72,12 @@ pub unsafe trait PyTypeInfo: Sized { /// Checks if `object` is an instance of this type. #[inline] fn is_exact_type_of(object: &Bound<'_, PyAny>) -> bool { - unsafe { ffi::Py_TYPE(object.as_ptr()) == Self::type_object_raw(object.py()) } + unsafe { + ptr::eq( + ffi::Py_TYPE(object.as_ptr()), + Self::type_object_raw(object.py()), + ) + } } } diff --git a/src/types/any.rs b/src/types/any.rs index 00f5594ce5d..6308804c80a 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -16,6 +16,7 @@ use crate::{err, ffi, Borrowed, BoundObject, IntoPyObjectExt, Python}; use std::cell::UnsafeCell; use std::cmp::Ordering; use std::os::raw::c_int; +use std::ptr; /// Represents any Python object. /// @@ -951,7 +952,7 @@ macro_rules! implement_binop { impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { #[inline] fn is(&self, other: &T) -> bool { - self.as_ptr() == other.as_ptr() + ptr::eq(self.as_ptr(), other.as_ptr()) } fn hasattr(&self, attr_name: N) -> PyResult @@ -1355,7 +1356,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { #[inline] fn is_none(&self) -> bool { - unsafe { ffi::Py_None() == self.as_ptr() } + unsafe { ptr::eq(ffi::Py_None(), self.as_ptr()) } } fn is_empty(&self) -> PyResult { diff --git a/src/types/boolobject.rs b/src/types/boolobject.rs index 5091930a6cb..28806f8c4cf 100644 --- a/src/types/boolobject.rs +++ b/src/types/boolobject.rs @@ -8,6 +8,7 @@ use crate::{ use super::any::PyAnyMethods; use crate::conversion::IntoPyObject; use std::convert::Infallible; +use std::ptr; /// Represents a Python `bool`. /// @@ -51,7 +52,7 @@ pub trait PyBoolMethods<'py>: crate::sealed::Sealed { impl<'py> PyBoolMethods<'py> for Bound<'py, PyBool> { #[inline] fn is_true(&self) -> bool { - self.as_ptr() == unsafe { crate::ffi::Py_True() } + unsafe { ptr::eq(self.as_ptr(), ffi::Py_True()) } } } diff --git a/src/types/sequence.rs b/src/types/sequence.rs index bc2643dcf8e..3efd7ccff58 100644 --- a/src/types/sequence.rs +++ b/src/types/sequence.rs @@ -397,6 +397,7 @@ impl PyTypeCheck for PySequence { mod tests { use crate::types::{PyAnyMethods, PyList, PySequence, PySequenceMethods, PyTuple}; use crate::{ffi, IntoPyObject, PyObject, Python}; + use std::ptr; fn get_object() -> PyObject { // Convenience function for getting a single unique object @@ -548,7 +549,7 @@ mod tests { 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()); + assert!(ptr::eq(seq.get_item(1).unwrap().as_ptr(), obj.as_ptr())); }); Python::with_gil(move |py| { diff --git a/src/types/weakref/anyref.rs b/src/types/weakref/anyref.rs index cea930b7204..d496c175376 100644 --- a/src/types/weakref/anyref.rs +++ b/src/types/weakref/anyref.rs @@ -351,6 +351,7 @@ mod tests { use super::*; use crate::ffi; use crate::{py_result_ext::PyResultExt, types::PyType}; + use std::ptr; fn get_type(py: Python<'_>) -> PyResult> { py.run(ffi::c_str!("class A:\n pass\n"), None, None)?; @@ -379,8 +380,10 @@ mod tests { 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))); + assert!( + obj.map_or(false, |obj| ptr::eq(obj.as_ptr(), object.as_ptr()) + && obj.is_exact_instance(&class)) + ); } drop(object); @@ -421,8 +424,10 @@ mod tests { 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))); + assert!( + obj.map_or(false, |obj| ptr::eq(obj.as_ptr(), object.as_ptr()) + && obj.is_exact_instance(&class)) + ); } drop(object); @@ -481,6 +486,7 @@ mod tests { mod pyo3_pyclass { use super::*; use crate::{pyclass, Py}; + use std::ptr; #[pyclass(weakref, crate = "crate")] struct WeakrefablePyClass {} @@ -504,7 +510,7 @@ mod tests { let obj = obj.unwrap(); assert!(obj.is_some()); - assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr())); + assert!(obj.map_or(false, |obj| ptr::eq(obj.as_ptr(), object.as_ptr()))); } drop(object); @@ -542,7 +548,7 @@ mod tests { let obj = unsafe { reference.upgrade_as_unchecked::() }; assert!(obj.is_some()); - assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr())); + assert!(obj.map_or(false, |obj| ptr::eq(obj.as_ptr(), object.as_ptr()))); } drop(object); diff --git a/src/types/weakref/proxy.rs b/src/types/weakref/proxy.rs index 563c5b0982c..c16162361fb 100644 --- a/src/types/weakref/proxy.rs +++ b/src/types/weakref/proxy.rs @@ -243,6 +243,7 @@ mod tests { use super::*; use crate::ffi; use crate::{py_result_ext::PyResultExt, types::PyDict, types::PyType}; + use std::ptr; fn get_type(py: Python<'_>) -> PyResult> { let globals = PyDict::new(py); @@ -328,8 +329,10 @@ mod tests { 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))); + assert!( + obj.map_or(false, |obj| ptr::eq(obj.as_ptr(), object.as_ptr()) + && obj.is_exact_instance(&class)) + ); } drop(object); @@ -360,8 +363,10 @@ mod tests { 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))); + assert!( + obj.map_or(false, |obj| ptr::eq(obj.as_ptr(), object.as_ptr()) + && obj.is_exact_instance(&class)) + ); } drop(object); @@ -418,6 +423,7 @@ mod tests { mod pyo3_pyclass { use super::*; use crate::{pyclass, Py}; + use std::ptr; #[pyclass(weakref, crate = "crate")] struct WeakrefablePyClass {} @@ -499,7 +505,7 @@ mod tests { let obj = obj.unwrap(); assert!(obj.is_some()); - assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr())); + assert!(obj.map_or(false, |obj| ptr::eq(obj.as_ptr(), object.as_ptr()))); } drop(object); @@ -527,7 +533,7 @@ mod tests { let obj = unsafe { reference.upgrade_as_unchecked::() }; assert!(obj.is_some()); - assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr())); + assert!(obj.map_or(false, |obj| ptr::eq(obj.as_ptr(), object.as_ptr()))); } drop(object); @@ -573,6 +579,7 @@ mod tests { use super::*; use crate::ffi; use crate::{py_result_ext::PyResultExt, types::PyDict, types::PyType}; + use std::ptr; fn get_type(py: Python<'_>) -> PyResult> { let globals = PyDict::new(py); @@ -649,8 +656,10 @@ mod tests { 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))); + assert!( + obj.map_or(false, |obj| ptr::eq(obj.as_ptr(), object.as_ptr()) + && obj.is_exact_instance(&class)) + ); } drop(object); @@ -681,8 +690,10 @@ mod tests { 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))); + assert!( + obj.map_or(false, |obj| ptr::eq(obj.as_ptr(), object.as_ptr()) + && obj.is_exact_instance(&class)) + ); } drop(object); @@ -722,6 +733,7 @@ mod tests { mod pyo3_pyclass { use super::*; use crate::{pyclass, pymethods, Py}; + use std::ptr; #[pyclass(weakref, crate = "crate")] struct WeakrefablePyClass {} @@ -798,7 +810,7 @@ mod tests { let obj = obj.unwrap(); assert!(obj.is_some()); - assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr())); + assert!(obj.map_or(false, |obj| ptr::eq(obj.as_ptr(), object.as_ptr()))); } drop(object); @@ -826,7 +838,7 @@ mod tests { let obj = unsafe { reference.upgrade_as_unchecked::() }; assert!(obj.is_some()); - assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr())); + assert!(obj.map_or(false, |obj| ptr::eq(obj.as_ptr(), object.as_ptr()))); } drop(object); diff --git a/src/types/weakref/reference.rs b/src/types/weakref/reference.rs index 334f136aa33..fb63af4cc70 100644 --- a/src/types/weakref/reference.rs +++ b/src/types/weakref/reference.rs @@ -239,6 +239,7 @@ mod tests { use super::*; use crate::ffi; use crate::{py_result_ext::PyResultExt, types::PyType}; + use std::ptr; fn get_type(py: Python<'_>) -> PyResult> { py.run(ffi::c_str!("class A:\n pass\n"), None, None)?; @@ -303,8 +304,10 @@ mod tests { 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))); + assert!( + obj.map_or(false, |obj| ptr::eq(obj.as_ptr(), object.as_ptr()) + && obj.is_exact_instance(&class)) + ); } drop(object); @@ -335,8 +338,10 @@ mod tests { 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))); + assert!( + obj.map_or(false, |obj| ptr::eq(obj.as_ptr(), object.as_ptr()) + && obj.is_exact_instance(&class)) + ); } drop(object); @@ -378,6 +383,7 @@ mod tests { mod pyo3_pyclass { use super::*; use crate::{pyclass, Py}; + use std::ptr; #[pyclass(weakref, crate = "crate")] struct WeakrefablePyClass {} @@ -434,7 +440,7 @@ mod tests { let obj = obj.unwrap(); assert!(obj.is_some()); - assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr())); + assert!(obj.map_or(false, |obj| ptr::eq(obj.as_ptr(), object.as_ptr()))); } drop(object); @@ -462,7 +468,7 @@ mod tests { let obj = unsafe { reference.upgrade_as_unchecked::() }; assert!(obj.is_some()); - assert!(obj.map_or(false, |obj| obj.as_ptr() == object.as_ptr())); + assert!(obj.map_or(false, |obj| ptr::eq(obj.as_ptr(), object.as_ptr()))); } drop(object); diff --git a/tests/test_exceptions.rs b/tests/test_exceptions.rs index 777c2aa22d4..af21b2c33a8 100644 --- a/tests/test_exceptions.rs +++ b/tests/test_exceptions.rs @@ -103,6 +103,7 @@ fn test_exception_nosegfault() { fn test_write_unraisable() { use common::UnraisableCapture; use pyo3::{exceptions::PyRuntimeError, ffi, types::PyNotImplemented}; + use std::ptr; Python::with_gil(|py| { let capture = UnraisableCapture::install(py); @@ -121,7 +122,7 @@ fn test_write_unraisable() { let (err, object) = capture.borrow_mut(py).capture.take().unwrap(); assert_eq!(err.to_string(), "RuntimeError: bar"); - assert!(object.as_ptr() == unsafe { ffi::Py_NotImplemented() }); + assert!(unsafe { ptr::eq(object.as_ptr(), ffi::Py_NotImplemented()) }); capture.borrow_mut(py).uninstall(py); }); From 2f3c1a1ca6eb12c09ba6068c8090b4700fb0157d Mon Sep 17 00:00:00 2001 From: ddelange <14880945+ddelange@users.noreply.github.com> Date: Fri, 4 Apr 2025 12:46:45 +0300 Subject: [PATCH 632/936] docs: add blake3-py to Examples in README.md (#5028) --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 2bf2342b311..17a20382b74 100644 --- a/README.md +++ b/README.md @@ -187,6 +187,8 @@ about this topic. - [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 +- [blake3-py](https://github.com/oconnor663/blake3-py) _Python bindings for the [BLAKE3](https://github.com/BLAKE3-team/BLAKE3) cryptographic hash function._ + - Parallelized [builds](https://github.com/oconnor663/blake3-py/blob/master/.github/workflows/dists.yml) on GitHub Actions for MacOS, Linux, Windows, including free-threaded 3.13t wheels. - [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/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._ From ddcfc6975f1eac1657995f176136c4a2d5b28e8e Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers <7943856+bschoenmaeckers@users.noreply.github.com> Date: Fri, 4 Apr 2025 11:46:58 +0200 Subject: [PATCH 633/936] Make `datetime` rust wrappers compatible with `abi3` feature (#4970) * Make `PyDate`, `PyTime`, `PyDateTime` & `PyDelta` compatible with abi3 * refactor jiff * refactor chrono * update `std::time` conversions * Add changelog --- newsfragments/4970.changed.md | 1 + src/conversions/chrono.rs | 212 +++----------- src/conversions/jiff.rs | 243 +++------------- src/conversions/std/time.rs | 137 ++++----- src/types/datetime.rs | 505 +++++++++++++++++++++++++++------- src/types/datetime_abi3.rs | 51 ---- src/types/mod.rs | 9 +- 7 files changed, 537 insertions(+), 621 deletions(-) create mode 100644 newsfragments/4970.changed.md delete mode 100644 src/types/datetime_abi3.rs diff --git a/newsfragments/4970.changed.md b/newsfragments/4970.changed.md new file mode 100644 index 00000000000..7ea1b6ae0cd --- /dev/null +++ b/newsfragments/4970.changed.md @@ -0,0 +1 @@ +- Make `datetime` rust wrappers compatible with `abi3` feature diff --git a/src/conversions/chrono.rs b/src/conversions/chrono.rs index 6fd4465ca34..fb8890d696a 100644 --- a/src/conversions/chrono.rs +++ b/src/conversions/chrono.rs @@ -46,18 +46,13 @@ use crate::exceptions::{PyTypeError, PyUserWarning, PyValueError}; #[cfg(Py_LIMITED_API)] 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))] use crate::types::{ - timezone_utc, PyDate, PyDateAccess, PyDateTime, PyDelta, PyDeltaAccess, PyTime, PyTimeAccess, - PyTzInfo, PyTzInfoAccess, + datetime::timezone_from_offset, timezone_utc, PyDate, PyDateTime, PyDelta, PyTime, PyTzInfo, + PyTzInfoAccess, }; +#[cfg(not(Py_LIMITED_API))] +use crate::types::{PyDateAccess, PyDeltaAccess, PyTimeAccess}; use crate::{ffi, Bound, FromPyObject, IntoPyObjectExt, PyAny, PyErr, PyResult, Python}; use chrono::offset::{FixedOffset, Utc}; use chrono::{ @@ -66,9 +61,6 @@ use chrono::{ }; 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; @@ -85,35 +77,22 @@ impl<'py> IntoPyObject<'py> for Duration { // 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( - 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))) - } + // 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( + py, + days.try_into().unwrap_or(i32::MAX), + secs.try_into()?, + micros.try_into()?, + true, + ) } } 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; @@ -126,13 +105,13 @@ impl<'py> IntoPyObject<'py> for &Duration { impl FromPyObject<'_> for Duration { fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { + let delta = ob.downcast::()?; // Python size are much lower than rust size so we do not need bound checks. // 0 <= microseconds < 1000000 // 0 <= seconds < 3600*24 // -999999999 <= days <= 999999999 #[cfg(not(Py_LIMITED_API))] let (days, seconds, microseconds) = { - let delta = ob.downcast::()?; ( delta.get_days().into(), delta.get_seconds().into(), @@ -141,11 +120,11 @@ impl FromPyObject<'_> for Duration { }; #[cfg(Py_LIMITED_API)] let (days, seconds, microseconds) = { - check_type(ob, &DatetimeTypes::get(ob.py()).timedelta, "PyDelta")?; + let py = delta.py(); ( - ob.getattr(intern!(ob.py(), "days"))?.extract()?, - ob.getattr(intern!(ob.py(), "seconds"))?.extract()?, - ob.getattr(intern!(ob.py(), "microseconds"))?.extract()?, + delta.getattr(intern!(py, "days"))?.extract()?, + delta.getattr(intern!(py, "seconds"))?.extract()?, + delta.getattr(intern!(py, "microseconds"))?.extract()?, ) }; Ok( @@ -157,31 +136,17 @@ impl FromPyObject<'_> for Duration { } 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(py, year, month, day) - } - - #[cfg(Py_LIMITED_API)] - { - DatetimeTypes::try_get(py).and_then(|dt| dt.date.bind(py).call1((year, month, day))) - } + PyDate::new(py, year, month, day) } } 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; @@ -194,23 +159,12 @@ impl<'py> IntoPyObject<'py> for &NaiveDate { impl FromPyObject<'_> for NaiveDate { fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { - #[cfg(not(Py_LIMITED_API))] - { - let date = ob.downcast::()?; - py_date_to_naive_date(date) - } - #[cfg(Py_LIMITED_API)] - { - check_type(ob, &DatetimeTypes::get(ob.py()).date, "PyDate")?; - py_date_to_naive_date(ob) - } + let date = ob.downcast::()?; + py_date_to_naive_date(date) } } 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; @@ -224,13 +178,8 @@ impl<'py> IntoPyObject<'py> for NaiveTime { truncated_leap_second, } = (&self).into(); - #[cfg(not(Py_LIMITED_API))] let time = PyTime::new(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); } @@ -240,9 +189,6 @@ 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; @@ -255,23 +201,12 @@ impl<'py> IntoPyObject<'py> for &NaiveTime { impl FromPyObject<'_> for NaiveTime { fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { - #[cfg(not(Py_LIMITED_API))] - { - let time = ob.downcast::()?; - py_time_to_naive_time(time) - } - #[cfg(Py_LIMITED_API)] - { - check_type(ob, &DatetimeTypes::get(ob.py()).time, "PyTime")?; - py_time_to_naive_time(ob) - } + let time = ob.downcast::()?; + py_time_to_naive_time(time) } } 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; @@ -286,16 +221,8 @@ impl<'py> IntoPyObject<'py> for NaiveDateTime { truncated_leap_second, } = (&self.time()).into(); - #[cfg(not(Py_LIMITED_API))] 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| { - dt.datetime - .bind(py) - .call1((year, month, day, hour, min, sec, micro)) - })?; - if truncated_leap_second { warn_truncated_leap_second(&datetime); } @@ -305,9 +232,6 @@ 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; @@ -320,18 +244,12 @@ impl<'py> IntoPyObject<'py> for &NaiveDateTime { impl FromPyObject<'_> for NaiveDateTime { fn extract_bound(dt: &Bound<'_, PyAny>) -> PyResult { - #[cfg(not(Py_LIMITED_API))] let dt = dt.downcast::()?; - #[cfg(Py_LIMITED_API)] - check_type(dt, &DatetimeTypes::get(dt.py()).datetime, "PyDateTime")?; // If the user tries to convert a timezone aware datetime into a naive one, // we return a hard error. We could silently remove tzinfo, or assume local timezone // and do a conversion, but better leave this decision to the user of the library. - #[cfg(not(Py_LIMITED_API))] let has_tzinfo = dt.get_tzinfo().is_some(); - #[cfg(Py_LIMITED_API)] - let has_tzinfo = !dt.getattr(intern!(dt.py(), "tzinfo"))?.is_none(); if has_tzinfo { return Err(PyTypeError::new_err("expected a datetime without tzinfo")); } @@ -345,9 +263,6 @@ impl<'py, Tz: TimeZone> IntoPyObject<'py> for DateTime where Tz: IntoPyObject<'py>, { - #[cfg(Py_LIMITED_API)] - type Target = PyAny; - #[cfg(not(Py_LIMITED_API))] type Target = PyDateTime; type Output = Bound<'py, Self::Target>; type Error = PyErr; @@ -362,18 +277,12 @@ impl<'py, Tz: TimeZone> IntoPyObject<'py> for &DateTime where Tz: IntoPyObject<'py>, { - #[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.timezone().into_bound_py_any(py)?; - - #[cfg(not(Py_LIMITED_API))] - let tz = tz.downcast()?; + let tz = self.timezone().into_bound_py_any(py)?.downcast_into()?; let DateArgs { year, month, day } = (&self.naive_local().date()).into(); let TimeArgs { @@ -389,17 +298,18 @@ where LocalResult::Ambiguous(_, latest) if self.offset().fix() == latest.fix() ); - #[cfg(not(Py_LIMITED_API))] - 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).call( - (year, month, day, hour, min, sec, micro, tz), - Some(&[("fold", fold as u8)].into_py_dict(py)?), - ) - })?; + let datetime = PyDateTime::new_with_fold( + py, + year, + month, + day, + hour, + min, + sec, + micro, + Some(&tz), + fold, + )?; if truncated_leap_second { warn_truncated_leap_second(&datetime); @@ -411,15 +321,8 @@ where impl FromPyObject<'py>> FromPyObject<'_> for DateTime { fn extract_bound(dt: &Bound<'_, PyAny>) -> PyResult> { - #[cfg(not(Py_LIMITED_API))] let dt = dt.downcast::()?; - #[cfg(Py_LIMITED_API)] - check_type(dt, &DatetimeTypes::get(dt.py()).datetime, "PyDateTime")?; - - #[cfg(not(Py_LIMITED_API))] let tzinfo = dt.get_tzinfo(); - #[cfg(Py_LIMITED_API)] - let tzinfo: Option> = dt.getattr(intern!(dt.py(), "tzinfo"))?.extract()?; let tz = if let Some(tzinfo) = tzinfo { tzinfo.extract()? @@ -453,33 +356,18 @@ impl FromPyObject<'py>> FromPyObject<'_> for DateTime 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(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,))) - } + let td = PyDelta::new(py, 0, seconds_offset, 0, true)?; + timezone_from_offset(&td) } } 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; @@ -496,10 +384,7 @@ impl FromPyObject<'_> for FixedOffset { /// Note that the conversion will result in precision lost in microseconds as chrono offset /// does not supports microseconds. fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { - #[cfg(not(Py_LIMITED_API))] let ob = ob.downcast::()?; - #[cfg(Py_LIMITED_API)] - check_type(ob, &DatetimeTypes::get(ob.py()).tzinfo, "PyTzInfo")?; // Passing Python's None to the `utcoffset` function will only // work for timezones defined as fixed offsets in Python. @@ -522,29 +407,16 @@ impl FromPyObject<'_> for FixedOffset { } 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(py).into_any()) - } - #[cfg(not(Py_LIMITED_API))] - { - Ok(timezone_utc(py)) - } + Ok(timezone_utc(py)) } } 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; diff --git a/src/conversions/jiff.rs b/src/conversions/jiff.rs index 23ffddf99eb..2b90686844a 100644 --- a/src/conversions/jiff.rs +++ b/src/conversions/jiff.rs @@ -49,18 +49,13 @@ 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, + datetime::timezone_from_offset, timezone_utc, PyDate, PyDateTime, PyDelta, PyTime, PyTzInfo, + PyTzInfoAccess, }; use crate::types::{PyAnyMethods, PyNone, PyType}; +#[cfg(not(Py_LIMITED_API))] +use crate::types::{PyDateAccess, PyDeltaAccess, PyTimeAccess}; use crate::{intern, Bound, FromPyObject, IntoPyObject, Py, PyAny, PyErr, PyResult, Python}; use jiff::civil::{Date, DateTime, Time}; use jiff::tz::{Offset, TimeZone}; @@ -68,7 +63,6 @@ use jiff::{SignedDuration, Span, Timestamp, Zoned}; #[cfg(feature = "jiff-02")] use jiff_02 as jiff; -#[cfg(not(Py_LIMITED_API))] fn datetime_to_pydatetime<'py>( py: Python<'py>, datetime: &DateTime, @@ -92,28 +86,6 @@ fn datetime_to_pydatetime<'py>( ) } -#[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
+
Click to expand PyO3 0.23 is a significant rework of PyO3's internals for two major improvements: @@ -31,7 +37,7 @@ The sections below discuss the rationale and details of each change in more dept
### Free-threaded Python Support -
+
Click to expand PyO3 0.23 introduces initial support for the new free-threaded build of @@ -54,7 +60,7 @@ See [the guide section on free-threaded Python](free-threading.md) for more deta
### New `IntoPyObject` trait unifies to-Python conversions -
+
Click to expand PyO3 0.23 introduces a new `IntoPyObject` trait to convert Rust types into Python objects which replaces both `IntoPy` and `ToPyObject`. @@ -143,7 +149,7 @@ impl<'a, 'py> IntoPyObject<'py> for &'a MyPyObjectWrapper {
### To-Python conversions changed for byte collections (`Vec`, `[u8; N]` and `SmallVec<[u8; N]>`). -
+
Click to expand 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)]`. @@ -181,7 +187,7 @@ This is purely additional and should just extend the possible return types.
### `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. From 7ccfcdcd8f7f09cb97e759e4fc10a6671e2c22fc Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 9 May 2025 18:58:09 +0200 Subject: [PATCH 682/936] revert tz change for jiff test (#5130) --- src/conversions/jiff.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/conversions/jiff.rs b/src/conversions/jiff.rs index 3928fbe6605..cecc78ccd15 100644 --- a/src/conversions/jiff.rs +++ b/src/conversions/jiff.rs @@ -32,7 +32,7 @@ //! pyo3::prepare_freethreaded_python(); //! Python::with_gil(|py| { //! // Build some jiff values -//! let jiff_zoned = Zoned::now().in_tz("UTC")?; +//! let jiff_zoned = Zoned::now(); //! let jiff_span = 1.second(); //! // Convert them to Python //! let py_datetime = jiff_zoned.into_pyobject(py)?; From e613a79cad83b909624755c21b98dee4fe70ed39 Mon Sep 17 00:00:00 2001 From: superserious-dev Date: Fri, 9 May 2025 11:51:24 -0700 Subject: [PATCH 683/936] Implement optional feature `ordered-float` for NotNan/OrderedFloat <-> Python float conversions (#5114) * Implement OrderedFloat & NotNan conversions for f32 & f64 * Reduce test boilerplate with macros for the wrapper types * Make roundtripped zero test stricter on both Rust & Python side * Simplify conversion implementations using macros * Add documentation for feature * Add newsfragment * Remove unneeded closure * Fix typo * Remove ffi call * Use exact version in Cargo.toml instead of lower bound * Use `py_run!`, `py.eval`, and `py.import` to simplify code * Add type annotations for WASM due to `py_run!` * Change newsfragment filename to use Pull Request ID instead of Issue ID * Add missing #[test] annotation for wasm test --- Cargo.toml | 2 + guide/src/conversions/tables.md | 4 +- guide/src/features.md | 6 + newsfragments/5114.added.md | 1 + src/conversions/mod.rs | 1 + src/conversions/ordered_float.rs | 330 +++++++++++++++++++++++++++++++ src/lib.rs | 3 + 7 files changed, 346 insertions(+), 1 deletion(-) create mode 100644 newsfragments/5114.added.md create mode 100644 src/conversions/ordered_float.rs diff --git a/Cargo.toml b/Cargo.toml index f72dd508e34..ac23a3c474d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,7 @@ 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 } +ordered-float = { version = "5.0.0", default-features = false, optional = true } rust_decimal = { version = "1.15", default-features = false, optional = true } time = { version = "0.3.38", default-features = false, optional = true } serde = { version = "1.0", optional = true } @@ -135,6 +136,7 @@ full = [ "num-bigint", "num-complex", "num-rational", + "ordered-float", "py-clone", "rust_decimal", "serde", diff --git a/guide/src/conversions/tables.md b/guide/src/conversions/tables.md index 46119a4382f..23d6942b177 100644 --- a/guide/src/conversions/tables.md +++ b/guide/src/conversions/tables.md @@ -17,7 +17,7 @@ The table below contains the Python type and the corresponding function argument | `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` | -| `float` | `f32`, `f64` | `PyFloat` | +| `float` | `f32`, `f64`, `ordered_float::NotNan`[^10], `ordered_float::OrderedFloat`[^10] | `PyFloat` | | `complex` | `num_complex::Complex`[^2] | `PyComplex` | | `fractions.Fraction`| `num_rational::Ratio`[^8] | - | | `list[T]` | `Vec` | `PyList` | @@ -119,3 +119,5 @@ Finally, the following Rust types are also able to convert to Python as return v [^8]: Requires the `num-rational` optional feature. [^9]: Requires the `bigdecimal` optional feature. + +[^10]: Requires the `ordered-float` optional feature. diff --git a/guide/src/features.md b/guide/src/features.md index 971a60b0e25..04297b4dab2 100644 --- a/guide/src/features.md +++ b/guide/src/features.md @@ -180,6 +180,12 @@ Adds a dependency on [num-complex](https://docs.rs/num-complex) and enables conv 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. +### `ordered-float` + +Adds a dependency on [ordered-float](https://docs.rs/ordered-float) and enables conversions between [ordered-float](https://docs.rs/ordered-float)'s types and Python: +- [NotNan](https://docs.rs/ordered-float/latest/ordered_float/struct.NotNan.html) -> [`PyFloat`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyFloat.html) +- [OrderedFloat](https://docs.rs/ordered-float/latest/ordered_float/struct.OrderedFloat.html) -> [`PyFloat`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyFloat.html) + ### `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/5114.added.md b/newsfragments/5114.added.md new file mode 100644 index 00000000000..90d530e6747 --- /dev/null +++ b/newsfragments/5114.added.md @@ -0,0 +1 @@ +Added conversion support for `ordered_float::NotNan` & `ordered_float::OrderedFloat` to and from python native float type diff --git a/src/conversions/mod.rs b/src/conversions/mod.rs index 5403916ba8d..c623ca7b379 100644 --- a/src/conversions/mod.rs +++ b/src/conversions/mod.rs @@ -12,6 +12,7 @@ pub mod jiff; pub mod num_bigint; pub mod num_complex; pub mod num_rational; +pub mod ordered_float; pub mod rust_decimal; pub mod serde; pub mod smallvec; diff --git a/src/conversions/ordered_float.rs b/src/conversions/ordered_float.rs new file mode 100644 index 00000000000..79bce2c66c1 --- /dev/null +++ b/src/conversions/ordered_float.rs @@ -0,0 +1,330 @@ +#![cfg(feature = "ordered-float")] +//! Conversions to and from [ordered-float](https://docs.rs/ordered-float) types. +//! [`NotNan`]`<`[`f32`]`>` and [`NotNan`]`<`[`f64`]`>`. +//! [`OrderedFloat`]`<`[`f32`]`>` and [`OrderedFloat`]`<`[`f64`]`>`. +//! +//! This is useful for converting between Python's float into and from a native Rust type. +//! +//! Take care when comparing sorted collections of float types between Python and Rust. +//! They will likely differ due to the ambiguous sort order of NaNs in Python. +// +//! +//! To use this feature, add to your **`Cargo.toml`**: +//! +//! ```toml +//! [dependencies] +#![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"), "\", features = [\"ordered-float\"] }")] +//! ordered-float = "5.0.0" +//! ``` +//! +//! # Example +//! +//! Rust code to create functions that add ordered floats: +//! +//! ```rust,no_run +//! use ordered_float::{NotNan, OrderedFloat}; +//! use pyo3::prelude::*; +//! +//! #[pyfunction] +//! fn add_not_nans(a: NotNan, b: NotNan) -> NotNan { +//! a + b +//! } +//! +//! #[pyfunction] +//! fn add_ordered_floats(a: OrderedFloat, b: OrderedFloat) -> OrderedFloat { +//! a + b +//! } +//! +//! #[pymodule] +//! fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> { +//! m.add_function(wrap_pyfunction!(add_not_nans, m)?)?; +//! m.add_function(wrap_pyfunction!(add_ordered_floats, m)?)?; +//! Ok(()) +//! } +//! ``` +//! +//! Python code that validates the functionality: +//! ```python +//! from my_module import add_not_nans, add_ordered_floats +//! +//! assert add_not_nans(1.0,2.0) == 3.0 +//! assert add_ordered_floats(1.0,2.0) == 3.0 +//! ``` + +use crate::conversion::IntoPyObject; +use crate::exceptions::PyValueError; +use crate::types::{any::PyAnyMethods, PyFloat}; +use crate::{Bound, FromPyObject, PyAny, PyResult, Python}; +use ordered_float::{NotNan, OrderedFloat}; +use std::convert::Infallible; + +macro_rules! float_conversions { + ($wrapper:ident, $float_type:ty, $constructor:expr) => { + impl FromPyObject<'_> for $wrapper<$float_type> { + fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { + let val: $float_type = obj.extract()?; + $constructor(val) + } + } + + impl<'py> IntoPyObject<'py> for $wrapper<$float_type> { + type Target = PyFloat; + type Output = Bound<'py, Self::Target>; + type Error = Infallible; + + fn into_pyobject(self, py: Python<'py>) -> Result { + self.into_inner().into_pyobject(py) + } + } + + impl<'py> IntoPyObject<'py> for &$wrapper<$float_type> { + 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) + } + } + }; +} +float_conversions!(OrderedFloat, f32, |val| Ok(OrderedFloat(val))); +float_conversions!(OrderedFloat, f64, |val| Ok(OrderedFloat(val))); +float_conversions!(NotNan, f32, |val| NotNan::new(val) + .map_err(|e| PyValueError::new_err(e.to_string()))); +float_conversions!(NotNan, f64, |val| NotNan::new(val) + .map_err(|e| PyValueError::new_err(e.to_string()))); + +#[cfg(test)] +mod test_ordered_float { + use super::*; + use crate::ffi::c_str; + use crate::py_run; + + #[cfg(not(target_arch = "wasm32"))] + use proptest::prelude::*; + + macro_rules! float_roundtrip_tests { + ($wrapper:ident, $float_type:ty, $constructor:expr, $standard_test:ident, $wasm_test:ident, $infinity_test:ident, $zero_test:ident) => { + #[cfg(not(target_arch = "wasm32"))] + proptest! { + #[test] + fn $standard_test(inner_f: $float_type) { + let f = $constructor(inner_f); + + Python::with_gil(|py| { + let f_py: Bound<'_, PyFloat> = f.into_pyobject(py).unwrap(); + + py_run!( + py, + f_py, + &format!( + "import math\nassert math.isclose(f_py, {})", + inner_f as f64 // Always interpret the literal rs float value as f64 + // so that it's comparable with the python float + ) + ); + + let roundtripped_f: $wrapper<$float_type> = f_py.extract().unwrap(); + + assert_eq!(f, roundtripped_f); + }) + } + } + + #[cfg(target_arch = "wasm32")] + #[test] + fn $wasm_test() { + let inner_f = 10.0; + let f = $constructor(inner_f); + + Python::with_gil(|py| { + let f_py: Bound<'_, PyFloat> = f.into_pyobject(py).unwrap(); + + py_run!( + py, + f_py, + &format!( + "import math\nassert math.isclose(f_py, {})", + inner_f as f64 // Always interpret the literal rs float value as f64 + // so that it's comparable with the python float + ) + ); + + let roundtripped_f: $wrapper<$float_type> = f_py.extract().unwrap(); + + assert_eq!(f, roundtripped_f); + }) + } + + #[test] + fn $infinity_test() { + let inner_pinf = <$float_type>::INFINITY; + let pinf = $constructor(inner_pinf); + + let inner_ninf = <$float_type>::NEG_INFINITY; + let ninf = $constructor(inner_ninf); + + Python::with_gil(|py| { + let pinf_py: Bound<'_, PyFloat> = pinf.into_pyobject(py).unwrap(); + let ninf_py: Bound<'_, PyFloat> = ninf.into_pyobject(py).unwrap(); + + py_run!( + py, + pinf_py ninf_py, + "\ + assert pinf_py == float('inf')\n\ + assert ninf_py == float('-inf')" + ); + + let roundtripped_pinf: $wrapper<$float_type> = pinf_py.extract().unwrap(); + let roundtripped_ninf: $wrapper<$float_type> = ninf_py.extract().unwrap(); + + assert_eq!(pinf, roundtripped_pinf); + assert_eq!(ninf, roundtripped_ninf); + }) + } + + #[test] + fn $zero_test() { + let inner_pzero: $float_type = 0.0; + let pzero = $constructor(inner_pzero); + + let inner_nzero: $float_type = -0.0; + let nzero = $constructor(inner_nzero); + + Python::with_gil(|py| { + let pzero_py: Bound<'_, PyFloat> = pzero.into_pyobject(py).unwrap(); + let nzero_py: Bound<'_, PyFloat> = nzero.into_pyobject(py).unwrap(); + + // This python script verifies that the values are 0.0 in magnitude + // and that the signs are correct(+0.0 vs -0.0) + py_run!( + py, + pzero_py nzero_py, + "\ + import math\n\ + assert pzero_py == 0.0\n\ + assert math.copysign(1.0, pzero_py) > 0.0\n\ + assert nzero_py == 0.0\n\ + assert math.copysign(1.0, nzero_py) < 0.0" + ); + + let roundtripped_pzero: $wrapper<$float_type> = pzero_py.extract().unwrap(); + let roundtripped_nzero: $wrapper<$float_type> = nzero_py.extract().unwrap(); + + assert_eq!(pzero, roundtripped_pzero); + assert_eq!(roundtripped_pzero.signum(), 1.0); + assert_eq!(nzero, roundtripped_nzero); + assert_eq!(roundtripped_nzero.signum(), -1.0); + }) + } + }; + } + float_roundtrip_tests!( + OrderedFloat, + f32, + OrderedFloat, + ordered_float_f32_standard, + ordered_float_f32_wasm, + ordered_float_f32_infinity, + ordered_float_f32_zero + ); + float_roundtrip_tests!( + OrderedFloat, + f64, + OrderedFloat, + ordered_float_f64_standard, + ordered_float_f64_wasm, + ordered_float_f64_infinity, + ordered_float_f64_zero + ); + float_roundtrip_tests!( + NotNan, + f32, + |val| NotNan::new(val).unwrap(), + not_nan_f32_standard, + not_nan_f32_wasm, + not_nan_f32_infinity, + not_nan_f32_zero + ); + float_roundtrip_tests!( + NotNan, + f64, + |val| NotNan::new(val).unwrap(), + not_nan_f64_standard, + not_nan_f64_wasm, + not_nan_f64_infinity, + not_nan_f64_zero + ); + + macro_rules! ordered_float_pynan_tests { + ($test_name:ident, $float_type:ty) => { + #[test] + fn $test_name() { + let inner_nan: $float_type = <$float_type>::NAN; + let nan = OrderedFloat(inner_nan); + + Python::with_gil(|py| { + let nan_py: Bound<'_, PyFloat> = nan.into_pyobject(py).unwrap(); + + py_run!( + py, + nan_py, + "\ + import math\n\ + assert math.isnan(nan_py)" + ); + + let roundtripped_nan: OrderedFloat<$float_type> = nan_py.extract().unwrap(); + + assert_eq!(nan, roundtripped_nan); + }) + } + }; + } + ordered_float_pynan_tests!(test_ordered_float_pynan_f32, f32); + ordered_float_pynan_tests!(test_ordered_float_pynan_f64, f64); + + macro_rules! not_nan_pynan_tests { + ($test_name:ident, $float_type:ty) => { + #[test] + fn $test_name() { + Python::with_gil(|py| { + let nan_py = py.eval(c_str!("float('nan')"), None, None).unwrap(); + + let nan_rs: PyResult> = nan_py.extract(); + + assert!(nan_rs.is_err()); + }) + } + }; + } + not_nan_pynan_tests!(test_not_nan_pynan_f32, f32); + not_nan_pynan_tests!(test_not_nan_pynan_f64, f64); + + macro_rules! py64_rs32 { + ($test_name:ident, $wrapper:ident, $float_type:ty) => { + #[test] + fn $test_name() { + Python::with_gil(|py| { + let py_64 = py + .import("sys") + .unwrap() + .getattr("float_info") + .unwrap() + .getattr("max") + .unwrap(); + let rs_32 = py_64.extract::<$wrapper>().unwrap(); + // The python f64 is not representable in a rust f32 + assert!(rs_32.is_infinite()); + }) + } + }; + } + py64_rs32!(ordered_float_f32, OrderedFloat, f32); + py64_rs32!(ordered_float_f64, OrderedFloat, f64); + py64_rs32!(not_nan_f32, NotNan, f32); + py64_rs32!(not_nan_f64, NotNan, f64); +} diff --git a/src/lib.rs b/src/lib.rs index 5b893acb326..8d684b67c18 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -114,6 +114,7 @@ //! - [`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 +//! - [`ordered-float`]: Enables conversions between Python's float and [ordered-float]'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 @@ -311,6 +312,7 @@ //! [`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." +//! [`ordered-float`]: ./ordered_float/index.html "Documentation about the `ordered-float` 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." @@ -328,6 +330,7 @@ //! [num-bigint]: https://docs.rs/num-bigint //! [num-complex]: https://docs.rs/num-complex //! [num-rational]: https://docs.rs/num-rational +//! [ordered-float]: https://docs.rs/ordered-float //! [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 3a39844964abc08fcef50dccac4ac4964e008ffe Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers <7943856+bschoenmaeckers@users.noreply.github.com> Date: Fri, 9 May 2025 20:55:39 +0200 Subject: [PATCH 684/936] Add `PyRange` wrapper (#5117) * Add `PyRange` wrapper with conversions to/from `std::ops::Range` * `IntoPyObject` for `RangeFull` `RangeFrom` `RangeTo` `RangeToInclusive` * Remove conversions & address comments --- newsfragments/5117.added.md | 1 + src/sealed.rs | 5 +- src/types/mod.rs | 2 + src/types/range.rs | 92 +++++++++++++++++++++++++++++++++++++ src/types/slice.rs | 14 ++++++ 5 files changed, 112 insertions(+), 2 deletions(-) create mode 100644 newsfragments/5117.added.md create mode 100644 src/types/range.rs diff --git a/newsfragments/5117.added.md b/newsfragments/5117.added.md new file mode 100644 index 00000000000..46ff2f5d411 --- /dev/null +++ b/newsfragments/5117.added.md @@ -0,0 +1 @@ +Add `PyRange` wrapper. diff --git a/src/sealed.rs b/src/sealed.rs index 2c715468047..0367a6b10fc 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, PyMappingProxy, PyModule, PySequence, PySet, PySlice, PyString, PyTraceback, - PyTuple, PyType, PyWeakref, PyWeakrefProxy, PyWeakrefReference, + PyMapping, PyMappingProxy, PyModule, PyRange, PySequence, PySet, PySlice, PyString, + PyTraceback, PyTuple, PyType, PyWeakref, PyWeakrefProxy, PyWeakrefReference, }; use crate::{ffi, Bound, PyAny, PyResult}; @@ -35,6 +35,7 @@ impl Sealed for Bound<'_, PyList> {} impl Sealed for Bound<'_, PyMapping> {} impl Sealed for Bound<'_, PyMappingProxy> {} impl Sealed for Bound<'_, PyModule> {} +impl Sealed for Bound<'_, PyRange> {} impl Sealed for Bound<'_, PySequence> {} impl Sealed for Bound<'_, PySet> {} impl Sealed for Bound<'_, PySlice> {} diff --git a/src/types/mod.rs b/src/types/mod.rs index 9fff4cbadcb..a7101d65ced 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -38,6 +38,7 @@ pub use self::notimplemented::PyNotImplemented; pub use self::num::PyInt; #[cfg(not(any(PyPy, GraalPy)))] pub use self::pysuper::PySuper; +pub use self::range::{PyRange, PyRangeMethods}; pub use self::sequence::{PySequence, PySequenceMethods}; pub use self::set::{PySet, PySetMethods}; pub use self::slice::{PySlice, PySliceIndices, PySliceMethods}; @@ -246,6 +247,7 @@ mod notimplemented; mod num; #[cfg(not(any(PyPy, GraalPy)))] mod pysuper; +pub(crate) mod range; pub(crate) mod sequence; pub(crate) mod set; pub(crate) mod slice; diff --git a/src/types/range.rs b/src/types/range.rs new file mode 100644 index 00000000000..e499eac49f0 --- /dev/null +++ b/src/types/range.rs @@ -0,0 +1,92 @@ +use crate::sealed::Sealed; +use crate::types::PyAnyMethods; +use crate::{ffi, Bound, PyAny, PyResult, PyTypeInfo, Python}; + +/// Represents a Python `range`. +/// +/// Values of this type are accessed via PyO3's smart pointers, e.g. as +/// [`Py`][crate::Py] or [`Bound<'py, PyRange>`][Bound]. +/// +/// For APIs available on `range` objects, see the [`PyRangeMethods`] trait which is implemented for +/// [`Bound<'py, PyRange>`][Bound]. +#[repr(transparent)] +pub struct PyRange(PyAny); + +pyobject_native_type_core!(PyRange, pyobject_native_static_type_object!(ffi::PyRange_Type), #checkfunction=ffi::PyRange_Check); + +impl<'py> PyRange { + /// Creates a new Python `range` object with a default step of 1. + pub fn new(py: Python<'py>, start: isize, stop: isize) -> PyResult> { + Self::new_with_step(py, start, stop, 1) + } + + /// Creates a new Python `range` object with a specified step. + pub fn new_with_step( + py: Python<'py>, + start: isize, + stop: isize, + step: isize, + ) -> PyResult> { + unsafe { + Ok(Self::type_object(py) + .call1((start, stop, step))? + .downcast_into_unchecked()) + } + } +} + +/// Implementation of functionality for [`PyRange`]. +/// +/// These methods are defined for the `Bound<'py, PyRange>` 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 = "PyRange")] +pub trait PyRangeMethods<'py>: Sealed { + /// Returns the start of the range. + fn start(&self) -> PyResult; + + /// Returns the exclusive end of the range. + fn stop(&self) -> PyResult; + + /// Returns the step of the range. + fn step(&self) -> PyResult; +} + +impl<'py> PyRangeMethods<'py> for Bound<'py, PyRange> { + fn start(&self) -> PyResult { + self.getattr(intern!(self.py(), "start"))?.extract() + } + + fn stop(&self) -> PyResult { + self.getattr(intern!(self.py(), "stop"))?.extract() + } + + fn step(&self) -> PyResult { + self.getattr(intern!(self.py(), "step"))?.extract() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_py_range_new() { + Python::with_gil(|py| { + let range = PyRange::new(py, isize::MIN, isize::MAX).unwrap(); + assert_eq!(range.start().unwrap(), isize::MIN); + assert_eq!(range.stop().unwrap(), isize::MAX); + assert_eq!(range.step().unwrap(), 1); + }); + } + + #[test] + fn test_py_range_new_with_step() { + Python::with_gil(|py| { + let range = PyRange::new_with_step(py, 1, 10, 2).unwrap(); + assert_eq!(range.start().unwrap(), 1); + assert_eq!(range.stop().unwrap(), 10); + assert_eq!(range.step().unwrap(), 2); + }); + } +} diff --git a/src/types/slice.rs b/src/types/slice.rs index 97c38e0f36c..9b743156606 100644 --- a/src/types/slice.rs +++ b/src/types/slice.rs @@ -2,6 +2,7 @@ use crate::err::{PyErr, PyResult}; use crate::ffi; use crate::ffi_ptr_ext::FfiPtrExt; use crate::types::any::PyAnyMethods; +use crate::types::{PyRange, PyRangeMethods}; use crate::{Bound, IntoPyObject, PyAny, Python}; use std::convert::Infallible; @@ -140,6 +141,19 @@ impl<'py> IntoPyObject<'py> for &PySliceIndices { } } +impl<'py> TryFrom> for Bound<'py, PySlice> { + type Error = PyErr; + + fn try_from(range: Bound<'py, PyRange>) -> Result { + Ok(PySlice::new( + range.py(), + range.start()?, + range.stop()?, + range.step()?, + )) + } +} + #[cfg(test)] mod tests { use super::*; From 283ba3fbf0ce0634dbac480e6832d532279b08f0 Mon Sep 17 00:00:00 2001 From: Charlie Lin Date: Mon, 12 May 2025 06:05:10 -0400 Subject: [PATCH 685/936] Fix clippy errors (#5134) * Fix clippy errors * Add news file * Delete newsfragments/5134.fixed.md --- build.rs | 2 +- src/buffer.rs | 5 +- src/conversions/std/array.rs | 3 +- src/conversions/std/ipaddr.rs | 2 +- src/conversions/std/num.rs | 8 +-- src/err/impls.rs | 2 +- src/err/mod.rs | 6 +- src/exceptions.rs | 4 +- src/impl_/frompyobject.rs | 8 +-- src/impl_/pyclass.rs | 6 +- src/impl_/pyclass/lazy_type_object.rs | 4 +- src/instance.rs | 6 +- src/pyclass/create_type_object.rs | 6 +- src/types/num.rs | 6 +- src/types/string.rs | 4 +- src/types/weakref/proxy.rs | 12 ++-- tests/test_arithmetics.rs | 95 ++++++++++++------------- tests/test_class_formatting.rs | 6 +- tests/test_datetime.rs | 8 +-- tests/test_frompyobject.rs | 19 +++-- tests/test_methods.rs | 2 +- tests/test_pyerr_debug_unformattable.rs | 5 +- tests/test_pyfunction.rs | 9 +-- 23 files changed, 105 insertions(+), 123 deletions(-) diff --git a/build.rs b/build.rs index 68a658bf285..fd28b03b79d 100644 --- a/build.rs +++ b/build.rs @@ -38,7 +38,7 @@ fn configure_pyo3() -> Result<()> { ensure_auto_initialize_ok(interpreter_config)?; for cfg in interpreter_config.build_script_outputs() { - println!("{}", cfg) + println!("{cfg}") } // Emit cfgs like `invalid_from_utf8_lint` diff --git a/src/buffer.rs b/src/buffer.rs index c5e5de568eb..6f74b698de7 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -702,7 +702,7 @@ mod tests { buffer.0.suboffsets, buffer.0.internal ); - let debug_repr = format!("{:?}", buffer); + let debug_repr = format!("{buffer:?}"); assert_eq!(debug_repr, expected); }); } @@ -829,8 +829,7 @@ mod tests { assert_eq!( ElementType::from_format(cstr), expected, - "element from format &Cstr: {:?}", - cstr, + "element from format &Cstr: {cstr:?}", ); } } diff --git a/src/conversions/std/array.rs b/src/conversions/std/array.rs index 36db5ec640f..1d88965e711 100644 --- a/src/conversions/std/array.rs +++ b/src/conversions/std/array.rs @@ -110,8 +110,7 @@ where fn invalid_sequence_length(expected: usize, actual: usize) -> PyErr { exceptions::PyValueError::new_err(format!( - "expected a sequence of length {} (got {})", - expected, actual + "expected a sequence of length {expected} (got {actual})" )) } diff --git a/src/conversions/std/ipaddr.rs b/src/conversions/std/ipaddr.rs index 76f6a6927c2..0c479a2d836 100644 --- a/src/conversions/std/ipaddr.rs +++ b/src/conversions/std/ipaddr.rs @@ -123,7 +123,7 @@ mod test_ipaddr { 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)); + assert_eq!(repr, format!("{py_cls}('{ip}')")); let ip2: IpAddr = pyobj.extract().unwrap(); assert_eq!(ip, ip2); diff --git a/src/conversions/std/num.rs b/src/conversions/std/num.rs index 40073d8af69..ea5798cac99 100644 --- a/src/conversions/std/num.rs +++ b/src/conversions/std/num.rs @@ -606,7 +606,7 @@ mod test_128bit_integers { let x_py = x.into_pyobject(py).unwrap(); let locals = PyDict::new(py); locals.set_item("x_py", &x_py).unwrap(); - py.run(&CString::new(format!("assert x_py == {}", x)).unwrap(), None, Some(&locals)).unwrap(); + py.run(&CString::new(format!("assert x_py == {x}")).unwrap(), None, Some(&locals)).unwrap(); let roundtripped: i128 = x_py.extract().unwrap(); assert_eq!(x, roundtripped); }) @@ -622,7 +622,7 @@ mod test_128bit_integers { let x_py = x.into_pyobject(py).unwrap(); let locals = PyDict::new(py); locals.set_item("x_py", &x_py).unwrap(); - py.run(&CString::new(format!("assert x_py == {}", x)).unwrap(), None, Some(&locals)).unwrap(); + py.run(&CString::new(format!("assert x_py == {x}")).unwrap(), None, Some(&locals)).unwrap(); let roundtripped: NonZeroI128 = x_py.extract().unwrap(); assert_eq!(x, roundtripped); }) @@ -637,7 +637,7 @@ mod test_128bit_integers { let x_py = x.into_pyobject(py).unwrap(); let locals = PyDict::new(py); locals.set_item("x_py", &x_py).unwrap(); - py.run(&CString::new(format!("assert x_py == {}", x)).unwrap(), None, Some(&locals)).unwrap(); + py.run(&CString::new(format!("assert x_py == {x}")).unwrap(), None, Some(&locals)).unwrap(); let roundtripped: u128 = x_py.extract().unwrap(); assert_eq!(x, roundtripped); }) @@ -653,7 +653,7 @@ mod test_128bit_integers { let x_py = x.into_pyobject(py).unwrap(); let locals = PyDict::new(py); locals.set_item("x_py", &x_py).unwrap(); - py.run(&CString::new(format!("assert x_py == {}", x)).unwrap(), None, Some(&locals)).unwrap(); + py.run(&CString::new(format!("assert x_py == {x}")).unwrap(), None, Some(&locals)).unwrap(); let roundtripped: NonZeroU128 = x_py.extract().unwrap(); assert_eq!(x, roundtripped); }) diff --git a/src/err/impls.rs b/src/err/impls.rs index 1af45b7e628..814544a86f4 100644 --- a/src/err/impls.rs +++ b/src/err/impls.rs @@ -155,7 +155,7 @@ mod tests { let rust_err = io::Error::new(kind, "some error msg"); let py_err: PyErr = rust_err.into(); - let py_err_msg = format!("{}: some error msg", expected_ty); + let py_err_msg = format!("{expected_ty}: some error msg"); assert_eq!(py_err.to_string(), py_err_msg); let py_error_clone = py_err.clone_ref(py); diff --git a/src/err/mod.rs b/src/err/mod.rs index 74b07d5c17a..c4d5379cff6 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -671,7 +671,7 @@ impl std::fmt::Debug for PyErr { // 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) + format!("") } }), ) @@ -685,7 +685,7 @@ impl std::fmt::Display for PyErr { Python::with_gil(|py| { let value = self.value(py); let type_name = value.get_type().qualname().map_err(|_| std::fmt::Error)?; - write!(f, "{}", type_name)?; + write!(f, "{type_name}")?; if let Ok(s) = value.str() { write!(f, ": {}", &s.to_string_lossy()) } else { @@ -946,7 +946,7 @@ mod tests { .run(ffi::c_str!("raise Exception('banana')"), None, None) .expect_err("raising should have given us an error"); - let debug_str = format!("{:?}", err); + let debug_str = format!("{err:?}"); assert!(debug_str.starts_with("PyErr { ")); assert!(debug_str.ends_with(" }")); diff --git a/src/exceptions.rs b/src/exceptions.rs index dbbc8c714f1..5f39d474a72 100644 --- a/src/exceptions.rs +++ b/src/exceptions.rs @@ -996,7 +996,7 @@ mod tests { .into_value(py) .into_bound(py); assert_eq!( - format!("{:?}", exc), + format!("{exc:?}"), exc.repr().unwrap().extract::().unwrap() ); }); @@ -1025,7 +1025,7 @@ mod tests { Python::with_gil(|py| { let decode_err = PyUnicodeDecodeError::new_utf8(py, invalid_utf8, err).unwrap(); assert_eq!( - format!("{:?}", decode_err), + format!("{decode_err:?}"), "UnicodeDecodeError('utf-8', b'fo\\xd8o', 2, 3, 'invalid utf-8')" ); diff --git a/src/impl_/frompyobject.rs b/src/impl_/frompyobject.rs index 9f3f77340bc..729509c2231 100644 --- a/src/impl_/frompyobject.rs +++ b/src/impl_/frompyobject.rs @@ -36,7 +36,7 @@ fn extract_traceback(py: Python<'_>, mut error: PyErr) -> String { let mut error_msg = error.to_string(); while let Some(cause) = error.cause(py) { - write!(&mut error_msg, ", caused by {}", cause).unwrap(); + write!(&mut error_msg, ", caused by {cause}").unwrap(); error = cause } error_msg @@ -86,8 +86,7 @@ fn failed_to_extract_struct_field( field_name: &str, ) -> PyErr { let new_err = PyTypeError::new_err(format!( - "failed to extract field {}.{}", - struct_name, field_name + "failed to extract field {struct_name}.{field_name}" )); new_err.set_cause(py, ::std::option::Option::Some(inner_err)); new_err @@ -136,8 +135,7 @@ fn failed_to_extract_tuple_struct_field( struct_name: &str, index: usize, ) -> PyErr { - let new_err = - PyTypeError::new_err(format!("failed to extract field {}.{}", struct_name, index)); + let new_err = PyTypeError::new_err(format!("failed to extract field {struct_name}.{index}")); new_err.set_cause(py, ::std::option::Option::Some(inner_err)); new_err } diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index 4d0770d2edf..3191b8b902b 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -1106,8 +1106,7 @@ impl ThreadCheckerImpl { assert_eq!( thread::current().id(), self.0, - "{} is unsendable, but sent to another thread", - type_name + "{type_name} is unsendable, but sent to another thread" ); } @@ -1118,8 +1117,7 @@ impl ThreadCheckerImpl { fn can_drop(&self, py: Python<'_>, type_name: &'static str) -> bool { if thread::current().id() != self.0 { PyRuntimeError::new_err(format!( - "{} is unsendable, but is being dropped on another thread", - type_name + "{type_name} is unsendable, but is being dropped on another thread" )) .write_unraisable(py, None); return false; diff --git a/src/impl_/pyclass/lazy_type_object.rs b/src/impl_/pyclass/lazy_type_object.rs index fd141f2add8..ac4aa3fd3d9 100644 --- a/src/impl_/pyclass/lazy_type_object.rs +++ b/src/impl_/pyclass/lazy_type_object.rs @@ -92,7 +92,7 @@ impl LazyTypeObjectInner { wrap_in_runtime_error( py, err, - format!("An error occurred while initializing class {}", name), + format!("An error occurred while initializing class {name}"), ) }) } @@ -224,7 +224,7 @@ impl LazyTypeObjectInner { return Err(wrap_in_runtime_error( py, err.clone_ref(py), - format!("An error occurred while initializing `{}.__dict__`", name), + format!("An error occurred while initializing `{name}.__dict__`"), )); } diff --git a/src/instance.rs b/src/instance.rs index b2bfce05558..8dc6a2e73c1 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -497,7 +497,7 @@ fn python_format( } match any.get_type().name() { - Result::Ok(name) => std::write!(f, "", name), + Result::Ok(name) => std::write!(f, ""), Result::Err(_err) => f.write_str(""), } } @@ -2108,7 +2108,7 @@ a = A() fn test_debug_fmt() { Python::with_gil(|py| { let obj = "hello world".into_pyobject(py).unwrap(); - assert_eq!(format!("{:?}", obj), "'hello world'"); + assert_eq!(format!("{obj:?}"), "'hello world'"); }); } @@ -2116,7 +2116,7 @@ a = A() fn test_display_fmt() { Python::with_gil(|py| { let obj = "hello world".into_pyobject(py).unwrap(); - assert_eq!(format!("{}", obj), "hello world"); + assert_eq!(format!("{obj}"), "hello world"); }); } diff --git a/src/pyclass/create_type_object.rs b/src/pyclass/create_type_object.rs index a8e247ddff5..a507399a462 100644 --- a/src/pyclass/create_type_object.rs +++ b/src/pyclass/create_type_object.rs @@ -451,8 +451,7 @@ impl PyTypeBuilder { if self.has_clear && !self.has_traverse { return Err(PyTypeError::new_err(format!( - "`#[pyclass]` {} implements __clear__ without __traverse__", - name + "`#[pyclass]` {name} implements __clear__ without __traverse__" ))); } @@ -562,8 +561,7 @@ unsafe extern "C" fn no_constructor_defined( .name() .map_or_else(|_| "".into(), |name| name.to_string()); Err(crate::exceptions::PyTypeError::new_err(format!( - "No constructor defined for {}", - name + "No constructor defined for {name}" ))) }) } diff --git a/src/types/num.rs b/src/types/num.rs index 79cd800b75d..2ba92d0c742 100644 --- a/src/types/num.rs +++ b/src/types/num.rs @@ -139,13 +139,13 @@ mod tests { fn test_display_int() { Python::with_gil(|py| { let s = PyInt::new(py, 42u8); - assert_eq!(format!("{}", s), "42"); + assert_eq!(format!("{s}"), "42"); let s = PyInt::new(py, 43i32); - assert_eq!(format!("{}", s), "43"); + assert_eq!(format!("{s}"), "43"); let s = PyInt::new(py, 44usize); - assert_eq!(format!("{}", s), "44"); + assert_eq!(format!("{s}"), "44"); }) } } diff --git a/src/types/string.rs b/src/types/string.rs index a0daa0ba3d0..774624df108 100644 --- a/src/types/string.rs +++ b/src/types/string.rs @@ -609,7 +609,7 @@ mod tests { fn test_debug_string() { Python::with_gil(|py| { let s = "Hello\n".into_pyobject(py).unwrap(); - assert_eq!(format!("{:?}", s), "'Hello\\n'"); + assert_eq!(format!("{s:?}"), "'Hello\\n'"); }) } @@ -617,7 +617,7 @@ mod tests { fn test_display_string() { Python::with_gil(|py| { let s = "Hello\n".into_pyobject(py).unwrap(); - assert_eq!(format!("{}", s), "Hello\n"); + assert_eq!(format!("{s}"), "Hello\n"); }) } diff --git a/src/types/weakref/proxy.rs b/src/types/weakref/proxy.rs index c16162361fb..f60321fdd3d 100644 --- a/src/types/weakref/proxy.rs +++ b/src/types/weakref/proxy.rs @@ -265,7 +265,7 @@ mod tests { #[cfg(not(Py_LIMITED_API))] assert_eq!( reference.get_type().to_string(), - format!("", CLASS_NAME) + format!("") ); assert_eq!(reference.getattr("__class__")?.to_string(), ""); @@ -282,7 +282,7 @@ mod tests { #[cfg(not(Py_LIMITED_API))] let result = result & (err.value(py).to_string() - == format!("{} object is not callable", CLASS_NAME)); + == format!("{CLASS_NAME} object is not callable")); result })); @@ -306,7 +306,7 @@ mod tests { #[cfg(not(Py_LIMITED_API))] let result = result & (err.value(py).to_string() - == format!("{} object is not callable", CLASS_NAME)); + == format!("{CLASS_NAME} object is not callable")); result })); @@ -440,7 +440,7 @@ mod tests { #[cfg(not(Py_LIMITED_API))] assert_eq!( reference.get_type().to_string(), - format!("", CLASS_NAME) + format!("") ); assert_eq!( @@ -460,7 +460,7 @@ mod tests { #[cfg(not(Py_LIMITED_API))] let result = result & (err.value(py).to_string() - == format!("{} object is not callable", CLASS_NAME)); + == format!("{CLASS_NAME} object is not callable")); result })); @@ -484,7 +484,7 @@ mod tests { #[cfg(not(Py_LIMITED_API))] let result = result & (err.value(py).to_string() - == format!("{} object is not callable", CLASS_NAME)); + == format!("{CLASS_NAME} object is not callable")); result })); diff --git a/tests/test_arithmetics.rs b/tests/test_arithmetics.rs index 21e32d4c13c..2e5220eadd8 100644 --- a/tests/test_arithmetics.rs +++ b/tests/test_arithmetics.rs @@ -182,59 +182,59 @@ impl BinaryArithmetic { } fn __add__(&self, rhs: &Bound<'_, PyAny>) -> String { - format!("BA + {:?}", rhs) + format!("BA + {rhs:?}") } fn __sub__(&self, rhs: &Bound<'_, PyAny>) -> String { - format!("BA - {:?}", rhs) + format!("BA - {rhs:?}") } fn __mul__(&self, rhs: &Bound<'_, PyAny>) -> String { - format!("BA * {:?}", rhs) + format!("BA * {rhs:?}") } fn __matmul__(&self, rhs: &Bound<'_, PyAny>) -> String { - format!("BA @ {:?}", rhs) + format!("BA @ {rhs:?}") } fn __truediv__(&self, rhs: &Bound<'_, PyAny>) -> String { - format!("BA / {:?}", rhs) + format!("BA / {rhs:?}") } fn __floordiv__(&self, rhs: &Bound<'_, PyAny>) -> String { - format!("BA // {:?}", rhs) + format!("BA // {rhs:?}") } fn __mod__(&self, rhs: &Bound<'_, PyAny>) -> String { - format!("BA % {:?}", rhs) + format!("BA % {rhs:?}") } fn __divmod__(&self, rhs: &Bound<'_, PyAny>) -> String { - format!("divmod(BA, {:?})", rhs) + format!("divmod(BA, {rhs:?})") } fn __lshift__(&self, rhs: &Bound<'_, PyAny>) -> String { - format!("BA << {:?}", rhs) + format!("BA << {rhs:?}") } fn __rshift__(&self, rhs: &Bound<'_, PyAny>) -> String { - format!("BA >> {:?}", rhs) + format!("BA >> {rhs:?}") } fn __and__(&self, rhs: &Bound<'_, PyAny>) -> String { - format!("BA & {:?}", rhs) + format!("BA & {rhs:?}") } fn __xor__(&self, rhs: &Bound<'_, PyAny>) -> String { - format!("BA ^ {:?}", rhs) + format!("BA ^ {rhs:?}") } fn __or__(&self, rhs: &Bound<'_, PyAny>) -> String { - format!("BA | {:?}", rhs) + format!("BA | {rhs:?}") } fn __pow__(&self, rhs: &Bound<'_, PyAny>, mod_: Option) -> String { - format!("BA ** {:?} (mod: {:?})", rhs, mod_) + format!("BA ** {rhs:?} (mod: {mod_:?})") } } @@ -303,39 +303,39 @@ struct RhsArithmetic {} #[pymethods] impl RhsArithmetic { fn __radd__(&self, other: &Bound<'_, PyAny>) -> String { - format!("{:?} + RA", other) + format!("{other:?} + RA") } fn __rsub__(&self, other: &Bound<'_, PyAny>) -> String { - format!("{:?} - RA", other) + format!("{other:?} - RA") } fn __rmul__(&self, other: &Bound<'_, PyAny>) -> String { - format!("{:?} * RA", other) + format!("{other:?} * RA") } fn __rlshift__(&self, other: &Bound<'_, PyAny>) -> String { - format!("{:?} << RA", other) + format!("{other:?} << RA") } fn __rrshift__(&self, other: &Bound<'_, PyAny>) -> String { - format!("{:?} >> RA", other) + format!("{other:?} >> RA") } fn __rand__(&self, other: &Bound<'_, PyAny>) -> String { - format!("{:?} & RA", other) + format!("{other:?} & RA") } fn __rxor__(&self, other: &Bound<'_, PyAny>) -> String { - format!("{:?} ^ RA", other) + format!("{other:?} ^ RA") } fn __ror__(&self, other: &Bound<'_, PyAny>) -> String { - format!("{:?} | RA", other) + format!("{other:?} | RA") } fn __rpow__(&self, other: &Bound<'_, PyAny>, _mod: Option<&Bound<'_, PyAny>>) -> String { - format!("{:?} ** RA", other) + format!("{other:?} ** RA") } } @@ -380,91 +380,91 @@ impl LhsAndRhs { // } fn __add__(lhs: PyRef<'_, Self>, rhs: &Bound<'_, PyAny>) -> String { - format!("{:?} + {:?}", lhs, rhs) + format!("{lhs:?} + {rhs:?}") } fn __sub__(lhs: PyRef<'_, Self>, rhs: &Bound<'_, PyAny>) -> String { - format!("{:?} - {:?}", lhs, rhs) + format!("{lhs:?} - {rhs:?}") } fn __mul__(lhs: PyRef<'_, Self>, rhs: &Bound<'_, PyAny>) -> String { - format!("{:?} * {:?}", lhs, rhs) + format!("{lhs:?} * {rhs:?}") } fn __lshift__(lhs: PyRef<'_, Self>, rhs: &Bound<'_, PyAny>) -> String { - format!("{:?} << {:?}", lhs, rhs) + format!("{lhs:?} << {rhs:?}") } fn __rshift__(lhs: PyRef<'_, Self>, rhs: &Bound<'_, PyAny>) -> String { - format!("{:?} >> {:?}", lhs, rhs) + format!("{lhs:?} >> {rhs:?}") } fn __and__(lhs: PyRef<'_, Self>, rhs: &Bound<'_, PyAny>) -> String { - format!("{:?} & {:?}", lhs, rhs) + format!("{lhs:?} & {rhs:?}") } fn __xor__(lhs: PyRef<'_, Self>, rhs: &Bound<'_, PyAny>) -> String { - format!("{:?} ^ {:?}", lhs, rhs) + format!("{lhs:?} ^ {rhs:?}") } fn __or__(lhs: PyRef<'_, Self>, rhs: &Bound<'_, PyAny>) -> String { - format!("{:?} | {:?}", lhs, rhs) + format!("{lhs:?} | {rhs:?}") } fn __pow__(lhs: PyRef<'_, Self>, rhs: &Bound<'_, PyAny>, _mod: Option) -> String { - format!("{:?} ** {:?}", lhs, rhs) + format!("{lhs:?} ** {rhs:?}") } fn __matmul__(lhs: PyRef<'_, Self>, rhs: &Bound<'_, PyAny>) -> String { - format!("{:?} @ {:?}", lhs, rhs) + format!("{lhs:?} @ {rhs:?}") } fn __radd__(&self, other: &Bound<'_, PyAny>) -> String { - format!("{:?} + RA", other) + format!("{other:?} + RA") } fn __rsub__(&self, other: &Bound<'_, PyAny>) -> String { - format!("{:?} - RA", other) + format!("{other:?} - RA") } fn __rmul__(&self, other: &Bound<'_, PyAny>) -> String { - format!("{:?} * RA", other) + format!("{other:?} * RA") } fn __rlshift__(&self, other: &Bound<'_, PyAny>) -> String { - format!("{:?} << RA", other) + format!("{other:?} << RA") } fn __rrshift__(&self, other: &Bound<'_, PyAny>) -> String { - format!("{:?} >> RA", other) + format!("{other:?} >> RA") } fn __rand__(&self, other: &Bound<'_, PyAny>) -> String { - format!("{:?} & RA", other) + format!("{other:?} & RA") } fn __rxor__(&self, other: &Bound<'_, PyAny>) -> String { - format!("{:?} ^ RA", other) + format!("{other:?} ^ RA") } fn __ror__(&self, other: &Bound<'_, PyAny>) -> String { - format!("{:?} | RA", other) + format!("{other:?} | RA") } fn __rpow__(&self, other: &Bound<'_, PyAny>, _mod: Option<&Bound<'_, PyAny>>) -> String { - format!("{:?} ** RA", other) + format!("{other:?} ** RA") } fn __rmatmul__(&self, other: &Bound<'_, PyAny>) -> String { - format!("{:?} @ RA", other) + format!("{other:?} @ RA") } fn __rtruediv__(&self, other: &Bound<'_, PyAny>) -> String { - format!("{:?} / RA", other) + format!("{other:?} / RA") } fn __rfloordiv__(&self, other: &Bound<'_, PyAny>) -> String { - format!("{:?} // RA", other) + format!("{other:?} // RA") } } @@ -677,10 +677,7 @@ mod return_not_implemented { py_run!( py, c2, - &format!( - "class Other: pass\nassert c2.__{}__(Other()) is NotImplemented", - dunder - ) + &format!("class Other: pass\nassert c2.__{dunder}__(Other()) is NotImplemented") ); }); } diff --git a/tests/test_class_formatting.rs b/tests/test_class_formatting.rs index 10f760b9c4a..353f2abf7ed 100644 --- a/tests/test_class_formatting.rs +++ b/tests/test_class_formatting.rs @@ -16,7 +16,7 @@ pub enum MyEnum2 { impl Display for MyEnum2 { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", self) + write!(f, "{self:?}") } } @@ -34,7 +34,7 @@ impl Display for MyEnum3 { MyEnum3::Variant => "AwesomeVariant", MyEnum3::OtherVariant => "OtherVariant", }; - write!(f, "MyEnum.{}", variant) + write!(f, "MyEnum.{variant}") } } @@ -97,7 +97,7 @@ enum ComplexEnumWithStr { impl Display for ComplexEnumWithStr { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", self) + write!(f, "{self:?}") } } diff --git a/tests/test_datetime.rs b/tests/test_datetime.rs index 91c94324a1a..55b4d1bfdfb 100644 --- a/tests/test_datetime.rs +++ b/tests/test_datetime.rs @@ -17,7 +17,7 @@ fn _get_subclasses<'py>( .into_py_dict(py) .unwrap(); - let make_subclass_py = CString::new(format!("class Subklass({}):\n pass", py_type))?; + let make_subclass_py = CString::new(format!("class Subklass({py_type}):\n pass"))?; let make_sub_subclass_py = ffi::c_str!("class SubSubklass(Subklass):\n pass"); @@ -26,21 +26,21 @@ fn _get_subclasses<'py>( // Construct an instance of the base class let obj = py.eval( - &CString::new(format!("{}({})", py_type, args))?, + &CString::new(format!("{py_type}({args})"))?, None, Some(&locals), )?; // Construct an instance of the subclass let sub_obj = py.eval( - &CString::new(format!("Subklass({})", args))?, + &CString::new(format!("Subklass({args})"))?, None, Some(&locals), )?; // Construct an instance of the sub-subclass let sub_sub_obj = py.eval( - &CString::new(format!("SubSubklass({})", args))?, + &CString::new(format!("SubSubklass({args})"))?, None, Some(&locals), )?; diff --git a/tests/test_frompyobject.rs b/tests/test_frompyobject.rs index f1a68c18f01..7e5110d7e5a 100644 --- a/tests/test_frompyobject.rs +++ b/tests/test_frompyobject.rs @@ -467,7 +467,7 @@ fn test_enum() { assert_eq!(test, 1); assert_eq!(test2, "test"); } - _ => panic!("Expected extracting Foo::TupleVar, got {:?}", f), + _ => panic!("Expected extracting Foo::TupleVar, got {f:?}"), } let pye = PyE { @@ -481,7 +481,7 @@ fn test_enum() { .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), + _ => panic!("Expected extracting Foo::StructVar, got {f:?}"), } let int = 1i32.into_pyobject(py).unwrap(); @@ -490,7 +490,7 @@ fn test_enum() { .expect("Failed to extract Foo from int"); match f { Foo::TransparentTuple(test) => assert_eq!(test, 1), - _ => panic!("Expected extracting Foo::TransparentTuple, got {:?}", f), + _ => panic!("Expected extracting Foo::TransparentTuple, got {f:?}"), } let none = py.None(); let f = none @@ -498,7 +498,7 @@ fn test_enum() { .expect("Failed to extract Foo from int"); match f { Foo::TransparentStructVar { a } => assert!(a.is_none()), - _ => panic!("Expected extracting Foo::TransparentStructVar, got {:?}", f), + _ => panic!("Expected extracting Foo::TransparentStructVar, got {f:?}"), } let pybool = PyBool { bla: true }.into_pyobject(py).unwrap(); @@ -507,7 +507,7 @@ fn test_enum() { .expect("Failed to extract Foo from PyBool"); match f { Foo::StructVarGetAttrArg { a } => assert!(a), - _ => panic!("Expected extracting Foo::StructVarGetAttrArg, got {:?}", f), + _ => panic!("Expected extracting Foo::StructVarGetAttrArg, got {f:?}"), } let dict = PyDict::new(py); @@ -517,7 +517,7 @@ fn test_enum() { .expect("Failed to extract Foo from dict"); match f { Foo::StructWithGetItem { a } => assert_eq!(a, "test"), - _ => panic!("Expected extracting Foo::StructWithGetItem, got {:?}", f), + _ => panic!("Expected extracting Foo::StructWithGetItem, got {f:?}"), } let dict = PyDict::new(py); @@ -527,7 +527,7 @@ fn test_enum() { .expect("Failed to extract Foo from dict"); match f { Foo::StructWithGetItemArg { a } => assert_eq!(a, "test"), - _ => panic!("Expected extracting Foo::StructWithGetItemArg, got {:?}", f), + _ => panic!("Expected extracting Foo::StructWithGetItemArg, got {f:?}"), } }); } @@ -588,10 +588,7 @@ fn test_enum_catch_all() { let d = any.extract::>().expect("Expected pydict"); assert!(d.is_empty()); } - _ => panic!( - "Expected extracting EnumWithCatchAll::CatchAll, got {:?}", - f - ), + _ => panic!("Expected extracting EnumWithCatchAll::CatchAll, got {f:?}"), } }); } diff --git a/tests/test_methods.rs b/tests/test_methods.rs index bed81f02bc6..d6ce5e30aae 100644 --- a/tests/test_methods.rs +++ b/tests/test_methods.rs @@ -166,7 +166,7 @@ struct StaticMethodWithArgs {} impl StaticMethodWithArgs { #[staticmethod] fn method(_py: Python<'_>, input: i32) -> String { - format!("0x{:x}", input) + format!("0x{input:x}") } } diff --git a/tests/test_pyerr_debug_unformattable.rs b/tests/test_pyerr_debug_unformattable.rs index 615868207b4..6c4755087c5 100644 --- a/tests/test_pyerr_debug_unformattable.rs +++ b/tests/test_pyerr_debug_unformattable.rs @@ -32,7 +32,7 @@ raise Exception('banana')"# ) .expect_err("raising should have given us an error"); - let debug_str = format!("{:?}", err); + let debug_str = format!("{err:?}"); assert!(debug_str.starts_with("PyErr { ")); assert!(debug_str.ends_with(" }")); @@ -46,8 +46,7 @@ raise Exception('banana')"# let traceback = fields.next().unwrap(); assert!( traceback.starts_with("traceback: Some(\") -> String { - format!("{:?}", arg) + format!("{arg:?}") } #[test] @@ -278,10 +278,7 @@ fn conversion_error( option_arg: Option, struct_arg: Option, ) { - println!( - "{:?} {:?} {:?} {:?} {:?}", - str_arg, int_arg, tuple_arg, option_arg, struct_arg - ); + println!("{str_arg:?} {int_arg:?} {tuple_arg:?} {option_arg:?} {struct_arg:?}"); } #[test] @@ -488,7 +485,7 @@ fn test_closure() { s.push_str("-py"); Ok(s.into_pyobject(py)?.into_any().unbind()) } else { - panic!("unexpected argument type for {:?}", elem) + panic!("unexpected argument type for {elem:?}") } }) .collect(); From 5807d8056561ffebdd8d6f9833723ccb44bb3bcb Mon Sep 17 00:00:00 2001 From: Ivan Carvalho <8753214+IvanIsCoding@users.noreply.github.com> Date: Mon, 12 May 2025 09:12:31 -0400 Subject: [PATCH 686/936] Add `#[pyclass(generic)]` (#4926) * First attempt at supporting pyclass(generic) * Include class_geitem in macro * Minor formatting * Pass py argument to new() * Add tests covering pyclass(generic) * Add changelog entry * Add test covering __getitem__ * Fix compilation error tests * Fix Alias -> TypingAlias * Make test case slightly stronger * Add compilation error test * Do not allow pyclass(generic) for enums * Remove unused import * Add import only to 3.9+ * Add documentation * Ignore Python typing hints newly added Rust code * Fix clippy warnings for the test * Add newline * Update TRYBUILD tests * Move `generic` in table * TypingAlias is only available in 3.10+ * Update trybuild test --- guide/pyclass-parameters.md | 1 + guide/src/python-typing-hints.md | 78 +++++++++++++- newsfragments/4926.added.md | 4 + pyo3-macros-backend/src/attributes.rs | 1 + pyo3-macros-backend/src/pyclass.rs | 83 ++++++++++++--- tests/test_class_basics.rs | 31 ++++++ tests/test_compile_error.rs | 4 + tests/test_sequence.rs | 67 +++++++++++- tests/ui/invalid_pyclass_args.stderr | 4 +- tests/ui/invalid_pyclass_generic.rs | 24 +++++ tests/ui/invalid_pyclass_generic.stderr | 136 ++++++++++++++++++++++++ tests/ui/pyclass_generic_enum.rs | 15 +++ tests/ui/pyclass_generic_enum.stderr | 11 ++ 13 files changed, 436 insertions(+), 23 deletions(-) create mode 100644 newsfragments/4926.added.md create mode 100644 tests/ui/invalid_pyclass_generic.rs create mode 100644 tests/ui/invalid_pyclass_generic.stderr create mode 100644 tests/ui/pyclass_generic_enum.rs create mode 100644 tests/ui/pyclass_generic_enum.stderr diff --git a/guide/pyclass-parameters.md b/guide/pyclass-parameters.md index e2eb9595d4d..27b15118dd4 100644 --- a/guide/pyclass-parameters.md +++ b/guide/pyclass-parameters.md @@ -10,6 +10,7 @@ | `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. | +| `generic` | Implements runtime parametrization for the class following [PEP 560](https://peps.python.org/pep-0560/). | | `get_all` | Generates getters for all fields of the pyclass. | | `hash` | Implements `__hash__` using the `Hash` implementation of the underlying Rust datatype. | | `immutable_type` | Makes the type object immutable. Supported on 3.14+ with the `abi3` feature active, or 3.10+ otherwise. | diff --git a/guide/src/python-typing-hints.md b/guide/src/python-typing-hints.md index 43a15a21a6e..eb39bb35fb0 100644 --- a/guide/src/python-typing-hints.md +++ b/guide/src/python-typing-hints.md @@ -41,7 +41,7 @@ As we can see, those are not full definitions containing implementation, but jus ### What do the PEPs say? -At the time of writing this documentation, the `pyi` files are referenced in three PEPs. +At the time of writing this documentation, the `pyi` files are referenced in four PEPs. [PEP8 - Style Guide for Python Code - #Function Annotations](https://www.python.org/dev/peps/pep-0008/#function-annotations) (last point) recommends all third party library creators to provide stub files as the source of knowledge about the package for type checker tools. @@ -55,6 +55,8 @@ It contains a specification for them (highly recommended reading, since it conta [PEP561 - Distributing and Packaging Type Information](https://www.python.org/dev/peps/pep-0561/) describes in detail how to build packages that will enable type checking. In particular it contains information about how the stub files must be distributed in order for type checkers to use them. +[PEP560 - Core support for typing module and generic types](https://www.python.org/dev/peps/pep-0560/) describes the details on how Python's type system internally supports generics, including both runtime behavior and integration with static type checkers. + ## How to do it? [PEP561](https://www.python.org/dev/peps/pep-0561/) recognizes three ways of distributing type information: @@ -165,3 +167,77 @@ class Car: :return: the name of the color our great algorithm thinks is the best for this car """ ``` + +### Supporting Generics + +Type annotations can also be made generic in Python. They are useful for working +with different types while maintaining type safety. Usually, generic classes +inherit from the `typing.Generic` metaclass. + +Take for example the following `.pyi` file that specifies a `Car` that can +accept multiple types of wheels: + +```python +from typing import Generic, TypeVar + +W = TypeVar('W') + +class Car(Generic[W]): + def __init__(self, wheels: list[W]) -> None: ... + + def get_wheels(self) -> list[W]: ... + + def change_wheel(self, wheel_number: int, wheel: W) -> None: ... +``` + +This way, the end-user can specify the type with variables such as `truck: Car[SteelWheel] = ...` +and `f1_car: Car[AlloyWheel] = ...`. + +There is also a special syntax for specifying generic types in Python 3.12+: + +```python +class Car[W]: + def __init__(self, wheels: list[W]) -> None: ... + + def get_wheels(self) -> list[W]: ... +``` + +#### Runtime Behaviour + +Stub files (`pyi`) are only useful for static type checkers and ignored at runtime. Therefore, +PyO3 classes do not inherit from `typing.Generic` even if specified in the stub files. + +This can cause some runtime issues, as annotating a variable like `f1_car: Car[AlloyWheel] = ...` +can make Python call magic methods that are not defined. + +To overcome this limitation, implementers can pass the `generic` parameter to `pyclass` in Rust: + +```rust ignore +#[pyclass(generic)] +``` + +#### Advanced Users + +`#[pyclass(generic)]` implements a very simple runtime behavior that accepts +any generic argument. Advanced users can opt to manually implement +[`__class_geitem__`](https://docs.python.org/3/reference/datamodel.html#emulating-generic-types) +for the generic class to have more control. + +```rust ignore +impl MyClass { + #[classmethod] + #[pyo3(signature = (key, /))] + pub fn __class_getitem__( + cls: &Bound<'_, PyType>, + key: &Bound<'_, PyAny>, + ) -> PyResult { + /* implementation details */ + } +} +``` + +Note that [`pyo3::types::PyGenericAlias`][pygenericalias] can be helfpul when implementing +`__class_geitem__` as it can create [`types.GenericAlias`][genericalias] objects from Rust. + +[pygenericalias]: {{#PYO3_DOCS_URL}}/pyo3/types/struct.pygenericalias +[genericalias]: https://docs.python.org/3/library/types.html#types.GenericAlias \ No newline at end of file diff --git a/newsfragments/4926.added.md b/newsfragments/4926.added.md new file mode 100644 index 00000000000..a81a4155358 --- /dev/null +++ b/newsfragments/4926.added.md @@ -0,0 +1,4 @@ +Introduced a new optional parameter with `#[pyclass(generic)]`. This new +parameter makes classes support generic typing at the runtime following +[PEP 560](https://peps.python.org/pep-0560/). It is an alternative +to inheriting from [typing.Generic](https://docs.python.org/3/library/typing.html#typing.Generic). \ No newline at end of file diff --git a/pyo3-macros-backend/src/attributes.rs b/pyo3-macros-backend/src/attributes.rs index 74e953a26b3..ac3894d6419 100644 --- a/pyo3-macros-backend/src/attributes.rs +++ b/pyo3-macros-backend/src/attributes.rs @@ -46,6 +46,7 @@ pub mod kw { syn::custom_keyword!(transparent); syn::custom_keyword!(unsendable); syn::custom_keyword!(weakref); + syn::custom_keyword!(generic); syn::custom_keyword!(gil_used); } diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 3e0d8ed2374..19090d900b3 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -83,6 +83,7 @@ pub struct PyClassPyO3Options { pub subclass: Option, pub unsendable: Option, pub weakref: Option, + pub generic: Option, } pub enum PyClassPyO3Option { @@ -107,6 +108,7 @@ pub enum PyClassPyO3Option { Subclass(kw::subclass), Unsendable(kw::unsendable), Weakref(kw::weakref), + Generic(kw::generic), } impl Parse for PyClassPyO3Option { @@ -154,6 +156,8 @@ impl Parse for PyClassPyO3Option { input.parse().map(PyClassPyO3Option::Unsendable) } else if lookahead.peek(attributes::kw::weakref) { input.parse().map(PyClassPyO3Option::Weakref) + } else if lookahead.peek(attributes::kw::generic) { + input.parse().map(PyClassPyO3Option::Generic) } else { Err(lookahead.error()) } @@ -232,6 +236,7 @@ impl PyClassPyO3Options { ); set_option!(weakref); } + PyClassPyO3Option::Generic(generic) => set_option!(generic), } Ok(()) } @@ -429,6 +434,21 @@ fn impl_class( } } + let mut default_methods = descriptors_to_items( + cls, + args.options.rename_all.as_ref(), + args.options.frozen, + field_options, + ctx, + )?; + + let (default_class_geitem, default_class_geitem_method) = + pyclass_class_geitem(&args.options, &syn::parse_quote!(#cls), ctx)?; + + if let Some(default_class_geitem_method) = default_class_geitem_method { + default_methods.push(default_class_geitem_method); + } + let (default_str, default_str_slot) = implement_pyclass_str(&args.options, &syn::parse_quote!(#cls), ctx); @@ -443,21 +463,9 @@ fn impl_class( slots.extend(default_hash_slot); slots.extend(default_str_slot); - let py_class_impl = PyClassImplsBuilder::new( - cls, - args, - methods_type, - descriptors_to_items( - cls, - args.options.rename_all.as_ref(), - args.options.frozen, - field_options, - ctx, - )?, - slots, - ) - .doc(doc) - .impl_all(ctx)?; + let py_class_impl = PyClassImplsBuilder::new(cls, args, methods_type, default_methods, slots) + .doc(doc) + .impl_all(ctx)?; Ok(quote! { impl #pyo3_path::types::DerefToPyAny for #cls {} @@ -472,6 +480,7 @@ fn impl_class( #default_richcmp #default_hash #default_str + #default_class_geitem } }) } @@ -514,6 +523,10 @@ pub fn build_py_enum( bail_spanned!(enum_.brace_token.span.join() => "#[pyclass] can't be used on enums without any variants"); } + if let Some(generic) = &args.options.generic { + bail_spanned!(generic.span() => "enums do not support #[pyclass(generic)]"); + } + let doc = utils::get_doc(&enum_.attrs, None, ctx); let enum_ = PyClassEnum::new(enum_)?; impl_enum(enum_, &args, doc, method_type, ctx) @@ -2008,6 +2021,46 @@ fn pyclass_hash( } } +fn pyclass_class_geitem( + options: &PyClassPyO3Options, + cls: &syn::Type, + ctx: &Ctx, +) -> Result<(Option, Option)> { + let Ctx { pyo3_path, .. } = ctx; + match options.generic { + Some(_) => { + let ident = format_ident!("__class_getitem__"); + let mut class_geitem_impl: syn::ImplItemFn = { + parse_quote! { + #[classmethod] + fn #ident<'py>( + cls: &#pyo3_path::Bound<'py, #pyo3_path::types::PyType>, + key: &#pyo3_path::Bound<'py, #pyo3_path::types::PyAny> + ) -> #pyo3_path::PyResult<#pyo3_path::Bound<'py, #pyo3_path::types::PyGenericAlias>> { + #pyo3_path::types::PyGenericAlias::new(cls.py(), cls.as_any(), key) + } + } + }; + + let spec = FnSpec::parse( + &mut class_geitem_impl.sig, + &mut class_geitem_impl.attrs, + Default::default(), + )?; + + let class_geitem_method = crate::pymethod::impl_py_method_def( + cls, + &spec, + &spec.get_doc(&class_geitem_impl.attrs, ctx), + Some(quote!(#pyo3_path::ffi::METH_CLASS)), + ctx, + )?; + Ok((Some(class_geitem_impl), Some(class_geitem_method))) + } + None => Ok((None, None)), + } +} + /// Implements most traits used by `#[pyclass]`. /// /// Specifically, it implements traits that only depend on class name, diff --git a/tests/test_class_basics.rs b/tests/test_class_basics.rs index 3cca37c3bd9..1c579b7307a 100644 --- a/tests/test_class_basics.rs +++ b/tests/test_class_basics.rs @@ -714,3 +714,34 @@ fn test_unsendable_dict_with_weakref() { ); }); } + +#[cfg(Py_3_9)] +#[pyclass(generic)] +struct ClassWithRuntimeParametrization { + #[pyo3(get, set)] + value: PyObject, +} + +#[cfg(Py_3_9)] +#[pymethods] +impl ClassWithRuntimeParametrization { + #[new] + fn new(value: PyObject) -> ClassWithRuntimeParametrization { + Self { value } + } +} + +#[test] +#[cfg(Py_3_9)] +fn test_runtime_parametrization() { + Python::with_gil(|py| { + let ty = py.get_type::(); + py_assert!(py, ty, "ty[int] == ty.__class_getitem__((int,))"); + py_run!( + py, + ty, + "import types; + assert ty.__class_getitem__((int,)) == types.GenericAlias(ty, (int,))" + ); + }); +} diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index 0a4a630d4cf..4610bcee850 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -10,6 +10,10 @@ fn test_compile_errors() { t.compile_fail("tests/ui/invalid_pyclass_args.rs"); t.compile_fail("tests/ui/invalid_pyclass_enum.rs"); t.compile_fail("tests/ui/invalid_pyclass_item.rs"); + #[cfg(Py_3_9)] + t.compile_fail("tests/ui/invalid_pyclass_generic.rs"); + #[cfg(Py_3_9)] + t.compile_fail("tests/ui/pyclass_generic_enum.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))] diff --git a/tests/test_sequence.rs b/tests/test_sequence.rs index e8f61e80e42..9405aa08487 100644 --- a/tests/test_sequence.rs +++ b/tests/test_sequence.rs @@ -255,15 +255,15 @@ fn test_inplace_repeat() { // Check that #[pyo3(get, set)] works correctly for Vec #[pyclass] -struct GenericList { +struct AnyObjectList { #[pyo3(get, set)] items: Vec, } #[test] -fn test_generic_list_get() { +fn test_any_object_list_get() { Python::with_gil(|py| { - let list = GenericList { + let list = AnyObjectList { items: [1i32, 2, 3] .iter() .map(|i| i.into_pyobject(py).unwrap().into_any().unbind()) @@ -277,9 +277,9 @@ fn test_generic_list_get() { } #[test] -fn test_generic_list_set() { +fn test_any_object_list_set() { Python::with_gil(|py| { - let list = Bound::new(py, GenericList { items: vec![] }).unwrap(); + let list = Bound::new(py, AnyObjectList { items: vec![] }).unwrap(); py_run!(py, list, "list.items = [1, 2, 3]"); assert!(list @@ -367,3 +367,60 @@ fn sequence_length() { unsafe { ffi::PyErr_Clear() }; }) } + +#[cfg(Py_3_10)] +#[pyclass(generic, sequence)] +struct GenericList { + #[pyo3(get, set)] + items: Vec, +} + +#[cfg(Py_3_10)] +#[pymethods] +impl GenericList { + fn __len__(&self) -> usize { + self.items.len() + } + + fn __getitem__(&self, idx: isize) -> PyResult { + match self.items.get(idx as usize) { + Some(x) => pyo3::Python::with_gil(|py| Ok(x.clone_ref(py))), + None => Err(PyIndexError::new_err("Index out of bounds")), + } + } +} + +#[cfg(Py_3_10)] +#[test] +fn test_generic_both_subscriptions_types() { + use pyo3::types::PyInt; + use std::convert::Infallible; + + Python::with_gil(|py| { + let l = Bound::new( + py, + GenericList { + items: [1, 2, 3] + .iter() + .map(|x| -> PyObject { + let x: Result, Infallible> = x.into_pyobject(py); + x.unwrap().into_any().unbind() + }) + .chain([py.None()]) + .collect(), + }, + ) + .unwrap(); + let ty = py.get_type::(); + py_assert!(py, l, "l[0] == 1"); + py_run!( + py, + ty, + "import types; + import typing; + IntOrNone: typing.TypeAlias = typing.Union[int, None]; + assert ty[IntOrNone] == types.GenericAlias(ty, (IntOrNone,))" + ); + py_assert!(py, l, "list(reversed(l)) == [None, 3, 2, 1]"); + }); +} diff --git a/tests/ui/invalid_pyclass_args.stderr b/tests/ui/invalid_pyclass_args.stderr index 6b19e754f05..f75355b080e 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`, `immutable_type`, `mapping`, `module`, `name`, `ord`, `rename_all`, `sequence`, `set_all`, `str`, `subclass`, `unsendable`, `weakref` +error: expected one of: `crate`, `dict`, `eq`, `eq_int`, `extends`, `freelist`, `frozen`, `get_all`, `hash`, `immutable_type`, `mapping`, `module`, `name`, `ord`, `rename_all`, `sequence`, `set_all`, `str`, `subclass`, `unsendable`, `weakref`, `generic` --> tests/ui/invalid_pyclass_args.rs:4:11 | 4 | #[pyclass(extend=pyo3::types::PyDict)] @@ -46,7 +46,7 @@ error: expected string literal 25 | #[pyclass(module = my_module)] | ^^^^^^^^^ -error: expected one of: `crate`, `dict`, `eq`, `eq_int`, `extends`, `freelist`, `frozen`, `get_all`, `hash`, `immutable_type`, `mapping`, `module`, `name`, `ord`, `rename_all`, `sequence`, `set_all`, `str`, `subclass`, `unsendable`, `weakref` +error: expected one of: `crate`, `dict`, `eq`, `eq_int`, `extends`, `freelist`, `frozen`, `get_all`, `hash`, `immutable_type`, `mapping`, `module`, `name`, `ord`, `rename_all`, `sequence`, `set_all`, `str`, `subclass`, `unsendable`, `weakref`, `generic` --> tests/ui/invalid_pyclass_args.rs:28:11 | 28 | #[pyclass(weakrev)] diff --git a/tests/ui/invalid_pyclass_generic.rs b/tests/ui/invalid_pyclass_generic.rs new file mode 100644 index 00000000000..45cd5fec9b0 --- /dev/null +++ b/tests/ui/invalid_pyclass_generic.rs @@ -0,0 +1,24 @@ +use pyo3::prelude::*; +use pyo3::types::PyType; + +#[pyclass(generic)] +struct ClassRedefinesClassGetItem { +} + +#[pymethods] +impl ClassRedefinesClassGetItem { + #[new] + fn new() -> ClassRedefinesClassGetItem { + Self {} + } + + #[classmethod] + pub fn __class_getitem__( + cls: &Bound<'_, PyType>, + key: &Bound<'_, PyAny>, + ) -> PyResult { + pyo3::types::PyGenericAlias::new(cls.py(), cls.as_any(), key) + } +} + +fn main() {} diff --git a/tests/ui/invalid_pyclass_generic.stderr b/tests/ui/invalid_pyclass_generic.stderr new file mode 100644 index 00000000000..1381c31e1e1 --- /dev/null +++ b/tests/ui/invalid_pyclass_generic.stderr @@ -0,0 +1,136 @@ +error[E0592]: duplicate definitions with name `__pymethod___class_getitem____` + --> tests/ui/invalid_pyclass_generic.rs:4:1 + | +4 | #[pyclass(generic)] + | ^^^^^^^^^^^^^^^^^^^ duplicate definitions for `__pymethod___class_getitem____` +... +8 | #[pymethods] + | ------------ other definition for `__pymethod___class_getitem____` + | + = 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 `__class_getitem__` + --> tests/ui/invalid_pyclass_generic.rs:4:1 + | +4 | #[pyclass(generic)] + | ^^^^^^^^^^^^^^^^^^^ duplicate definitions for `__class_getitem__` +... +16 | / pub fn __class_getitem__( +17 | | cls: &Bound<'_, PyType>, +18 | | key: &Bound<'_, PyAny>, +19 | | ) -> PyResult { + | |___________________________- other definition for `__class_getitem__` + | + = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0034]: multiple applicable items in scope + --> tests/ui/invalid_pyclass_generic.rs:4:1 + | +4 | #[pyclass(generic)] + | ^^^^^^^^^^^^^^^^^^^ multiple `__pymethod___class_getitem____` found + | +note: candidate #1 is defined in an impl for the type `ClassRedefinesClassGetItem` + --> tests/ui/invalid_pyclass_generic.rs:4:1 + | +4 | #[pyclass(generic)] + | ^^^^^^^^^^^^^^^^^^^ +note: candidate #2 is defined in an impl for the type `ClassRedefinesClassGetItem` + --> tests/ui/invalid_pyclass_generic.rs:8:1 + | +8 | #[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_generic.rs:4:1 + | +4 | #[pyclass(generic)] + | ^^^^^^^^^^^^^^^^^^^ multiple `__class_getitem__` found + | +note: candidate #1 is defined in an impl for the type `ClassRedefinesClassGetItem` + --> tests/ui/invalid_pyclass_generic.rs:4:1 + | +4 | #[pyclass(generic)] + | ^^^^^^^^^^^^^^^^^^^ +note: candidate #2 is defined in an impl for the type `ClassRedefinesClassGetItem` + --> tests/ui/invalid_pyclass_generic.rs:16:5 + | +16 | / pub fn __class_getitem__( +17 | | cls: &Bound<'_, PyType>, +18 | | key: &Bound<'_, PyAny>, +19 | | ) -> PyResult { + | |___________________________^ + = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0034]: multiple applicable items in scope + --> tests/ui/invalid_pyclass_generic.rs:4:1 + | +4 | #[pyclass(generic)] + | ^^^^^^^^^^^^^^^^^^^ multiple `wrap` found + | + = note: candidate #1 is defined in an impl for the type `IntoPyObjectConverter>` + = note: candidate #2 is defined in an impl for the type `IntoPyObjectConverter` + = note: candidate #3 is defined in an impl for the type `UnknownReturnResultType>` + = note: candidate #4 is defined in an impl for the type `UnknownReturnType` + = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0308]: mismatched types + --> tests/ui/invalid_pyclass_generic.rs:20:9 + | +19 | ) -> PyResult { + | ------------------ expected `Result, PyErr>` because of return type +20 | pyo3::types::PyGenericAlias::new(cls.py(), cls.as_any(), key) + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `Result, PyErr>`, found `Result, PyErr>` + | + = note: expected enum `Result, PyErr>` + found enum `Result, PyErr>` + +error[E0034]: multiple applicable items in scope + --> tests/ui/invalid_pyclass_generic.rs:16:12 + | +16 | pub fn __class_getitem__( + | ^^^^^^^^^^^^^^^^^ multiple `__pymethod___class_getitem____` found + | +note: candidate #1 is defined in an impl for the type `ClassRedefinesClassGetItem` + --> tests/ui/invalid_pyclass_generic.rs:4:1 + | +4 | #[pyclass(generic)] + | ^^^^^^^^^^^^^^^^^^^ +note: candidate #2 is defined in an impl for the type `ClassRedefinesClassGetItem` + --> tests/ui/invalid_pyclass_generic.rs:8:1 + | +8 | #[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_generic.rs:16:12 + | +16 | pub fn __class_getitem__( + | ^^^^^^^^^^^^^^^^^ multiple `__class_getitem__` found + | +note: candidate #1 is defined in an impl for the type `ClassRedefinesClassGetItem` + --> tests/ui/invalid_pyclass_generic.rs:4:1 + | +4 | #[pyclass(generic)] + | ^^^^^^^^^^^^^^^^^^^ +note: candidate #2 is defined in an impl for the type `ClassRedefinesClassGetItem` + --> tests/ui/invalid_pyclass_generic.rs:16:5 + | +16 | / pub fn __class_getitem__( +17 | | cls: &Bound<'_, PyType>, +18 | | key: &Bound<'_, PyAny>, +19 | | ) -> PyResult { + | |___________________________^ + = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0034]: multiple applicable items in scope + --> tests/ui/invalid_pyclass_generic.rs:19:10 + | +19 | ) -> PyResult { + | ^^^^^^^^ multiple `wrap` found + | + = note: candidate #1 is defined in an impl for the type `IntoPyObjectConverter>` + = note: candidate #2 is defined in an impl for the type `IntoPyObjectConverter` + = note: candidate #3 is defined in an impl for the type `UnknownReturnResultType>` + = note: candidate #4 is defined in an impl for the type `UnknownReturnType` diff --git a/tests/ui/pyclass_generic_enum.rs b/tests/ui/pyclass_generic_enum.rs new file mode 100644 index 00000000000..94fd779b2d4 --- /dev/null +++ b/tests/ui/pyclass_generic_enum.rs @@ -0,0 +1,15 @@ +use pyo3::prelude::*; + +#[pyclass(generic)] +enum NotGenericForEnum { + A, + B, +} + +#[pyclass(generic)] +enum NoGenericForComplexEnum { + A { x: f64 }, + B { y: f64, z: f64 }, +} + +fn main() {} diff --git a/tests/ui/pyclass_generic_enum.stderr b/tests/ui/pyclass_generic_enum.stderr new file mode 100644 index 00000000000..cfc1e178af3 --- /dev/null +++ b/tests/ui/pyclass_generic_enum.stderr @@ -0,0 +1,11 @@ +error: enums do not support #[pyclass(generic)] + --> tests/ui/pyclass_generic_enum.rs:3:11 + | +3 | #[pyclass(generic)] + | ^^^^^^^ + +error: enums do not support #[pyclass(generic)] + --> tests/ui/pyclass_generic_enum.rs:9:11 + | +9 | #[pyclass(generic)] + | ^^^^^^^ From b36849a1f5d5b8482c74565366ffb117a9edc0f1 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Mon, 12 May 2025 15:21:42 +0100 Subject: [PATCH 687/936] more format args inlining (#5135) * more format args inlining * cargo fmt * even more inlining * also fix time feature, more coverage --- examples/decorator/src/lib.rs | 2 +- pyo3-build-config/src/errors.rs | 2 +- pyo3-build-config/src/impl_.rs | 30 +++++++++++----------- pyo3-build-config/src/lib.rs | 13 +++------- pyo3-ffi/build.rs | 6 ++--- pyo3-macros-backend/src/method.rs | 6 ++--- pyo3-macros-backend/src/module.rs | 2 +- pyo3-macros-backend/src/pyclass.rs | 2 +- pyo3-macros-backend/src/pymethod.rs | 4 +-- pytests/src/pyclasses.rs | 3 +-- src/conversions/anyhow.rs | 6 ++--- src/conversions/bigdecimal.rs | 3 +-- src/conversions/chrono.rs | 40 ++++++++--------------------- src/conversions/chrono_tz.rs | 2 +- src/conversions/eyre.rs | 6 ++--- src/conversions/jiff.rs | 32 ++++++----------------- src/conversions/rust_decimal.rs | 3 +-- src/conversions/time.rs | 3 +-- src/inspect/types.rs | 14 +++++----- src/types/typeobject.rs | 2 +- tests/test_compile_error.rs | 1 - 21 files changed, 68 insertions(+), 114 deletions(-) diff --git a/examples/decorator/src/lib.rs b/examples/decorator/src/lib.rs index 4c5471c9945..69915ff8256 100644 --- a/examples/decorator/src/lib.rs +++ b/examples/decorator/src/lib.rs @@ -46,7 +46,7 @@ impl PyCounter { 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); + println!("{name} has been called {new_count} time(s)."); // After doing something, we finally forward the call to the wrapped function let ret = self.wraps.call(py, args, kwargs)?; diff --git a/pyo3-build-config/src/errors.rs b/pyo3-build-config/src/errors.rs index 87c59a998b4..b11d02fd581 100644 --- a/pyo3-build-config/src/errors.rs +++ b/pyo3-build-config/src/errors.rs @@ -68,7 +68,7 @@ impl std::fmt::Display for ErrorReport<'_> { writeln!(f, "\ncaused by:")?; let mut index = 0; while let Some(some_source) = source { - writeln!(f, " - {}: {}", index, some_source)?; + writeln!(f, " - {index}: {some_source}")?; source = some_source.source(); index += 1; } diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 96eb67657ea..9485b8a0394 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -58,7 +58,7 @@ pub fn cargo_env_var(var: &str) -> Option { /// the variable changes. pub fn env_var(var: &str) -> Option { if cfg!(feature = "resolve-config") { - println!("cargo:rerun-if-env-changed={}", var); + println!("cargo:rerun-if-env-changed={var}"); } #[cfg(test)] { @@ -180,7 +180,7 @@ impl InterpreterConfig { let mut out = vec![]; for i in MINIMUM_SUPPORTED_VERSION.minor..=self.version.minor { - out.push(format!("cargo:rustc-cfg=Py_3_{}", i)); + out.push(format!("cargo:rustc-cfg=Py_3_{i}")); } match self.implementation { @@ -199,7 +199,7 @@ impl InterpreterConfig { 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)), + flag => out.push(format!("cargo:rustc-cfg=py_sys_config=\"{flag}\"")), } } @@ -338,7 +338,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) let lib_dir = if cfg!(windows) { map.get("base_prefix") - .map(|base_prefix| format!("{}\\libs", base_prefix)) + .map(|base_prefix| format!("{base_prefix}\\libs")) } else { map.get("libdir").cloned() }; @@ -666,7 +666,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) 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) + writeln!(writer, "extra_build_script_line={line}") .context("failed to write extra_build_script_line")?; } Ok(()) @@ -853,7 +853,7 @@ fn is_abi3() -> bool { /// Must be called from a PyO3 crate build script. pub fn get_abi3_version() -> Option { let minor_version = (MINIMUM_SUPPORTED_VERSION.minor..=ABI3_MAX_MINOR) - .find(|i| cargo_env_var(&format!("CARGO_FEATURE_ABI3_PY3{}", i)).is_some()); + .find(|i| cargo_env_var(&format!("CARGO_FEATURE_ABI3_PY3{i}")).is_some()); minor_version.map(|minor| PythonVersion { major: 3, minor }) } @@ -1121,8 +1121,8 @@ pub enum BuildFlag { impl Display for BuildFlag { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - BuildFlag::Other(flag) => write!(f, "{}", flag), - _ => write!(f, "{:?}", self), + BuildFlag::Other(flag) => write!(f, "{flag}"), + _ => write!(f, "{self:?}"), } } } @@ -1202,7 +1202,7 @@ impl BuildFlags { for k in &BuildFlags::ALL { use std::fmt::Write; - writeln!(&mut script, "print(config.get('{}', '0'))", k).unwrap(); + writeln!(&mut script, "print(config.get('{k}', '0'))").unwrap(); } let stdout = run_python_script(interpreter.as_ref(), &script)?; @@ -1240,7 +1240,7 @@ impl Display for BuildFlags { } else { write!(f, ",")?; } - write!(f, "{}", flag)?; + write!(f, "{flag}")?; } Ok(()) } @@ -1427,7 +1427,7 @@ pub fn find_all_sysconfigdata(cross: &CrossCompileConfig) -> Result fn is_pypy_lib_dir(path: &str, v: &Option) -> bool { let pypy_version_pat = if let Some(v) = v { - format!("pypy{}", v) + format!("pypy{v}") } else { "pypy3.".into() }; @@ -1436,7 +1436,7 @@ fn is_pypy_lib_dir(path: &str, v: &Option) -> bool { fn is_graalpy_lib_dir(path: &str, v: &Option) -> bool { let graalpy_version_pat = if let Some(v) = v { - format!("graalpy{}", v) + format!("graalpy{v}") } else { "graalpy2".into() }; @@ -1445,7 +1445,7 @@ fn is_graalpy_lib_dir(path: &str, v: &Option) -> bool { fn is_cpython_lib_dir(path: &str, v: &Option) -> bool { let cpython_version_pat = if let Some(v) = v { - format!("python{}", v) + format!("python{v}") } else { "python3.".into() }; @@ -1759,7 +1759,7 @@ fn default_lib_name_unix( ) -> Result { match implementation { PythonImplementation::CPython => match ld_version { - Some(ld_version) => Ok(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 @@ -1776,7 +1776,7 @@ fn default_lib_name_unix( } }, PythonImplementation::PyPy => match ld_version { - Some(ld_version) => Ok(format!("pypy{}-c", ld_version)), + Some(ld_version) => Ok(format!("pypy{ld_version}-c")), None => Ok(format!("pypy{}.{}-c", version.major, version.minor)), }, diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index 30d5f818cd5..f47c16f425d 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -46,7 +46,7 @@ use target_lexicon::OperatingSystem; pub fn use_pyo3_cfgs() { print_expected_cfgs(); for cargo_command in get().build_script_outputs() { - println!("{}", cargo_command) + println!("{cargo_command}") } } @@ -102,12 +102,7 @@ fn _add_python_framework_link_args( ) { 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(); + writeln!(writer, "cargo:rustc-link-arg=-Wl,-rpath,{framework_prefix}").unwrap(); } } } @@ -173,12 +168,12 @@ 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); + 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); + println!("cargo:rustc-check-cfg=cfg({cfg})"); } } diff --git a/pyo3-ffi/build.rs b/pyo3-ffi/build.rs index da5a81381c3..6776cd80476 100644 --- a/pyo3-ffi/build.rs +++ b/pyo3-ffi/build.rs @@ -176,7 +176,7 @@ fn emit_link_config(interpreter_config: &InterpreterConfig) -> Result<()> { ); if let Some(lib_dir) = &interpreter_config.lib_dir { - println!("cargo:rustc-link-search=native={}", lib_dir); + println!("cargo:rustc-link-search=native={lib_dir}"); } Ok(()) @@ -207,12 +207,12 @@ fn configure_pyo3() -> Result<()> { } for cfg in interpreter_config.build_script_outputs() { - println!("{}", cfg) + println!("{cfg}") } // Extra lines come last, to support last write wins. for line in &interpreter_config.extra_build_script_lines { - println!("{}", line); + println!("{line}"); } // Emit cfgs like `invalid_from_utf8_lint` diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index dfcd5dd2203..0ab5135e93a 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -647,10 +647,10 @@ impl<'a> FnSpec<'a> { .fold(first.span(), |s, next| s.join(next.span()).unwrap_or(s)); let span = span.join(last.span()).unwrap_or(span); // List all the attributes in the error message - let mut msg = format!("`{}` may not be combined with", first); + let mut msg = format!("`{first}` may not be combined with"); let mut is_first = true; for attr in &*rest { - msg.push_str(&format!(" `{}`", attr)); + msg.push_str(&format!(" `{attr}`")); if is_first { is_first = false; } else { @@ -660,7 +660,7 @@ impl<'a> FnSpec<'a> { if !rest.is_empty() { msg.push_str(" and"); } - msg.push_str(&format!(" `{}`", last)); + msg.push_str(&format!(" `{last}`")); bail_spanned!(span => msg) } }; diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index 41e7777fb64..860a3b6d857 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -488,7 +488,7 @@ fn module_initialization( gil_used: bool, ) -> TokenStream { let Ctx { pyo3_path, .. } = ctx; - let pyinit_symbol = format!("PyInit_{}", name); + let pyinit_symbol = format!("PyInit_{name}"); let name = name.to_string(); let pyo3_name = LitCStr::new(CString::new(name).unwrap(), Span::call_site(), ctx); diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 19090d900b3..ab86138338b 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -1493,7 +1493,7 @@ fn generate_default_protocol_slot( slot.generate_type_slot( &syn::parse_quote!(#cls), &spec, - &format!("__default_{}__", name), + &format!("__default_{name}__"), ctx, ) } diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index dd821c57ab6..15cce6f365f 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -278,7 +278,7 @@ pub fn gen_py_method( } pub fn check_generic(sig: &syn::Signature) -> syn::Result<()> { - let err_msg = |typ| format!("Python functions cannot have generic {} parameters", typ); + let err_msg = |typ| format!("Python functions cannot have generic {typ} parameters"); for param in &sig.generics.params { match param { syn::GenericParam::Lifetime(_) => {} @@ -1601,7 +1601,7 @@ fn extract_proto_arguments( if let FnArg::Py(..) = arg { args.push(quote! { py }); } else { - let ident = syn::Ident::new(&format!("arg{}", non_python_args), Span::call_site()); + let ident = syn::Ident::new(&format!("arg{non_python_args}"), Span::call_site()); let conversions = proto_args.get(non_python_args) .ok_or_else(|| err_spanned!(arg.ty().span() => format!("Expected at most {} non-python arguments", proto_args.len())))? .extract(&ident, arg, extract_error_mode, holders, ctx); diff --git a/pytests/src/pyclasses.rs b/pytests/src/pyclasses.rs index 1091e6c16b3..4c3398b6627 100644 --- a/pytests/src/pyclasses.rs +++ b/pytests/src/pyclasses.rs @@ -80,8 +80,7 @@ impl AssertingBaseClass { fn new(cls: &Bound<'_, PyType>, expected_type: Bound<'_, PyType>) -> PyResult { if !cls.is(&expected_type) { return Err(PyValueError::new_err(format!( - "{:?} != {:?}", - cls, expected_type + "{cls:?} != {expected_type:?}" ))); } Ok(Self) diff --git a/src/conversions/anyhow.rs b/src/conversions/anyhow.rs index d2cb3f3eb60..0bf346d835a 100644 --- a/src/conversions/anyhow.rs +++ b/src/conversions/anyhow.rs @@ -113,7 +113,7 @@ impl From for PyErr { Err(error) => error, }; } - PyRuntimeError::new_err(format!("{:?}", error)) + PyRuntimeError::new_err(format!("{error:?}")) } } @@ -141,7 +141,7 @@ mod test_anyhow { #[test] fn test_pyo3_exception_contents() { let err = h().unwrap_err(); - let expected_contents = format!("{:?}", err); + let expected_contents = format!("{err:?}"); let pyerr = PyErr::from(err); Python::with_gil(|py| { @@ -160,7 +160,7 @@ mod test_anyhow { #[test] fn test_pyo3_exception_contents2() { let err = k().unwrap_err(); - let expected_contents = format!("{:?}", err); + let expected_contents = format!("{err:?}"); let pyerr = PyErr::from(err); Python::with_gil(|py| { diff --git a/src/conversions/bigdecimal.rs b/src/conversions/bigdecimal.rs index 446e38bf6c5..129def772c8 100644 --- a/src/conversions/bigdecimal.rs +++ b/src/conversions/bigdecimal.rs @@ -161,8 +161,7 @@ mod test_bigdecimal { locals.set_item("rs_dec", &rs_dec).unwrap(); py.run( &CString::new(format!( - "import decimal\npy_dec = decimal.Decimal(\"{}\")\nassert py_dec == rs_dec", - num)).unwrap(), + "import decimal\npy_dec = decimal.Decimal(\"{num}\")\nassert py_dec == rs_dec")).unwrap(), None, Some(&locals)).unwrap(); let roundtripped: BigDecimal = rs_dec.extract().unwrap(); assert_eq!(num, roundtripped); diff --git a/src/conversions/chrono.rs b/src/conversions/chrono.rs index 165fc5b9c43..bf99951e459 100644 --- a/src/conversions/chrono.rs +++ b/src/conversions/chrono.rs @@ -345,8 +345,7 @@ impl FromPyObject<'py>> FromPyObject<'_> for DateTime Err(PyValueError::new_err(format!( - "The datetime {:?} contains an incompatible timezone", - dt + "The datetime {dt:?} contains an incompatible timezone" ))), } } @@ -391,8 +390,7 @@ impl FromPyObject<'_> for FixedOffset { 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", - ob + "{ob:?} is not a fixed offset timezone" ))); } let total_seconds: Duration = py_timedelta.extract()?; @@ -659,10 +657,7 @@ mod tests { let py_delta = new_py_datetime_ob(py, "timedelta", (py_days, py_seconds, py_ms)); assert!( delta.eq(&py_delta).unwrap(), - "{}: {} != {}", - name, - delta, - py_delta + "{name}: {delta} != {py_delta}" ); }); }; @@ -698,7 +693,7 @@ mod tests { Python::with_gil(|py| { let py_delta = new_py_datetime_ob(py, "timedelta", (py_days, py_seconds, py_ms)); let py_delta: Duration = py_delta.extract().unwrap(); - assert_eq!(py_delta, delta, "{}: {} != {}", name, py_delta, delta); + assert_eq!(py_delta, delta, "{name}: {py_delta} != {delta}"); }) }; @@ -761,10 +756,7 @@ mod tests { assert_eq!( date.compare(&py_date).unwrap(), Ordering::Equal, - "{}: {} != {}", - name, - date, - py_date + "{name}: {date} != {py_date}" ); }) }; @@ -782,7 +774,7 @@ mod tests { let py_date = new_py_datetime_ob(py, "date", (year, month, day)); let py_date: NaiveDate = py_date.extract().unwrap(); let date = NaiveDate::from_ymd_opt(year, month, day).unwrap(); - assert_eq!(py_date, date, "{}: {} != {}", name, date, py_date); + assert_eq!(py_date, date, "{name}: {date} != {py_date}"); }) }; @@ -820,10 +812,7 @@ mod tests { assert_eq!( datetime.compare(&py_datetime).unwrap(), Ordering::Equal, - "{}: {} != {}", - name, - datetime, - py_datetime + "{name}: {datetime} != {py_datetime}" ); }; @@ -863,10 +852,7 @@ mod tests { assert_eq!( datetime.compare(&py_datetime).unwrap(), Ordering::Equal, - "{}: {} != {}", - name, - datetime, - py_datetime + "{name}: {datetime} != {py_datetime}" ); }; @@ -1053,13 +1039,7 @@ mod tests { .into_pyobject(py) .unwrap(); let py_time = new_py_datetime_ob(py, "time", (hour, minute, second, py_ms)); - assert!( - time.eq(&py_time).unwrap(), - "{}: {} != {}", - name, - time, - py_time - ); + assert!(time.eq(&py_time).unwrap(), "{name}: {time} != {py_time}"); }; check_time("regular", 3, 5, 7, 999_999, 999_999); @@ -1142,7 +1122,7 @@ mod tests { Python::with_gil(|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 code = format!("datetime.datetime.fromtimestamp({timestamp}).replace(tzinfo=datetime.timezone(datetime.timedelta(seconds={timedelta})))"); let t = py.eval(&CString::new(code).unwrap(), Some(&globals), None).unwrap(); // Get ISO 8601 string from python diff --git a/src/conversions/chrono_tz.rs b/src/conversions/chrono_tz.rs index ee7bd504888..e8e5dc60e7a 100644 --- a/src/conversions/chrono_tz.rs +++ b/src/conversions/chrono_tz.rs @@ -150,7 +150,7 @@ mod tests { fn test_into_pyobject() { Python::with_gil(|py| { let assert_eq = |l: Bound<'_, PyTzInfo>, r: Bound<'_, PyTzInfo>| { - assert!(l.eq(&r).unwrap(), "{:?} != {:?}", l, r); + assert!(l.eq(&r).unwrap(), "{l:?} != {r:?}"); }; assert_eq( diff --git a/src/conversions/eyre.rs b/src/conversions/eyre.rs index 42d7a12c872..4a501de9c69 100644 --- a/src/conversions/eyre.rs +++ b/src/conversions/eyre.rs @@ -119,7 +119,7 @@ impl From for PyErr { Err(error) => error, }; } - PyRuntimeError::new_err(format!("{:?}", error)) + PyRuntimeError::new_err(format!("{error:?}")) } } @@ -147,7 +147,7 @@ mod tests { #[test] fn test_pyo3_exception_contents() { let err = h().unwrap_err(); - let expected_contents = format!("{:?}", err); + let expected_contents = format!("{err:?}"); let pyerr = PyErr::from(err); Python::with_gil(|py| { @@ -166,7 +166,7 @@ mod tests { #[test] fn test_pyo3_exception_contents2() { let err = k().unwrap_err(); - let expected_contents = format!("{:?}", err); + let expected_contents = format!("{err:?}"); let pyerr = PyErr::from(err); Python::with_gil(|py| { diff --git a/src/conversions/jiff.rs b/src/conversions/jiff.rs index cecc78ccd15..814040e5fb4 100644 --- a/src/conversions/jiff.rs +++ b/src/conversions/jiff.rs @@ -386,8 +386,7 @@ impl<'py> FromPyObject<'py> for Offset { let py_timedelta = ob.call_method1(intern!(py, "utcoffset"), (PyNone::get(py),))?; if py_timedelta.is_none() { return Err(PyTypeError::new_err(format!( - "{:?} is not a fixed offset timezone", - ob + "{ob:?} is not a fixed offset timezone" ))); } @@ -576,10 +575,7 @@ mod tests { assert_eq!( date.compare(&py_date).unwrap(), Ordering::Equal, - "{}: {} != {}", - name, - date, - py_date + "{name}: {date} != {py_date}" ); }) }; @@ -597,7 +593,7 @@ mod tests { let py_date = new_py_datetime_ob(py, "date", (year, month, day)); let py_date: Date = py_date.extract().unwrap(); let date = Date::new(year, month, day).unwrap(); - assert_eq!(py_date, date, "{}: {} != {}", name, date, py_date); + assert_eq!(py_date, date, "{name}: {date} != {py_date}"); }) }; @@ -634,10 +630,7 @@ mod tests { assert_eq!( datetime.compare(&py_datetime).unwrap(), Ordering::Equal, - "{}: {} != {}", - name, - datetime, - py_datetime + "{name}: {datetime} != {py_datetime}" ); }; @@ -653,7 +646,7 @@ mod tests { let offset = Offset::from_seconds(3600).unwrap(); let datetime = DateTime::new(year, month, day, hour, minute, second, ms * 1000) .map_err(|e| { - eprintln!("{}: {}", name, e); + eprintln!("{name}: {e}"); e }) .unwrap() @@ -669,10 +662,7 @@ mod tests { assert_eq!( datetime.compare(&py_datetime).unwrap(), Ordering::Equal, - "{}: {} != {}", - name, - datetime, - py_datetime + "{name}: {datetime} != {py_datetime}" ); }; @@ -883,13 +873,7 @@ mod tests { .into_pyobject(py) .unwrap(); let py_time = new_py_datetime_ob(py, "time", (hour, minute, second, py_ms)); - assert!( - time.eq(&py_time).unwrap(), - "{}: {} != {}", - name, - time, - py_time - ); + assert!(time.eq(&py_time).unwrap(), "{name}: {time} != {py_time}"); }; check_time("regular", 3, 5, 7, 999_999, 999_999); @@ -991,7 +975,7 @@ mod tests { Python::with_gil(|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 code = format!("datetime.datetime.fromtimestamp({timestamp}).replace(tzinfo=datetime.timezone(datetime.timedelta(seconds={timedelta})))"); let t = py.eval(&CString::new(code).unwrap(), Some(&globals), None).unwrap(); // Get ISO 8601 string from python diff --git a/src/conversions/rust_decimal.rs b/src/conversions/rust_decimal.rs index 392971a0b4b..8c5d870a2dd 100644 --- a/src/conversions/rust_decimal.rs +++ b/src/conversions/rust_decimal.rs @@ -169,8 +169,7 @@ mod test_rust_decimal { locals.set_item("rs_dec", &rs_dec).unwrap(); py.run( &CString::new(format!( - "import decimal\npy_dec = decimal.Decimal(\"{}\")\nassert py_dec == rs_dec", - num)).unwrap(), + "import decimal\npy_dec = decimal.Decimal(\"{num}\")\nassert py_dec == rs_dec")).unwrap(), None, Some(&locals)).unwrap(); let roundtripped: Decimal = rs_dec.extract().unwrap(); assert_eq!(num, roundtripped); diff --git a/src/conversions/time.rs b/src/conversions/time.rs index 351fbb637d2..f077b3be55d 100644 --- a/src/conversions/time.rs +++ b/src/conversions/time.rs @@ -369,8 +369,7 @@ impl FromPyObject<'_> for 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", - ob + "{ob:?} is not a fixed offset timezone" ))); } diff --git a/src/inspect/types.rs b/src/inspect/types.rs index 50674ce96f9..b0159f71af0 100644 --- a/src/inspect/types.rs +++ b/src/inspect/types.rs @@ -212,7 +212,7 @@ impl Display for TypeInfo { if comma { write!(f, ", ")?; } - write!(f, "{}", arg)?; + write!(f, "{arg}")?; comma = true; } write!(f, "]")?; @@ -220,7 +220,7 @@ impl Display for TypeInfo { write!(f, "...")?; } - write!(f, ", {}]", output) + write!(f, ", {output}]") } TypeInfo::Tuple(types) => { write!(f, "Tuple[")?; @@ -234,7 +234,7 @@ impl Display for TypeInfo { if comma { write!(f, ", ")?; } - write!(f, "{}", t)?; + write!(f, "{t}")?; comma = true; } } @@ -244,11 +244,11 @@ impl Display for TypeInfo { write!(f, "]") } - TypeInfo::UnsizedTypedTuple(t) => write!(f, "Tuple[{}, ...]", t), + TypeInfo::UnsizedTypedTuple(t) => write!(f, "Tuple[{t}, ...]"), TypeInfo::Class { name, type_vars, .. } => { - write!(f, "{}", name)?; + write!(f, "{name}")?; if !type_vars.is_empty() { write!(f, "[")?; @@ -258,7 +258,7 @@ impl Display for TypeInfo { if comma { write!(f, ", ")?; } - write!(f, "{}", var)?; + write!(f, "{var}")?; comma = true; } @@ -279,7 +279,7 @@ mod test { #[track_caller] pub fn assert_display(t: &TypeInfo, expected: &str) { - assert_eq!(format!("{}", t), expected) + assert_eq!(format!("{t}"), expected) } #[test] diff --git a/src/types/typeobject.rs b/src/types/typeobject.rs index 194755fc5a3..2efe26bff49 100644 --- a/src/types/typeobject.rs +++ b/src/types/typeobject.rs @@ -165,7 +165,7 @@ impl<'py> PyTypeMethods<'py> for Bound<'py, PyType> { if module_str == "builtins" || module_str == "__main__" { qualname.downcast_into()? } else { - PyString::new(self.py(), &format!("{}.{}", module, qualname)) + PyString::new(self.py(), &format!("{module}.{qualname}")) } }; diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index 4610bcee850..c6990430366 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -37,7 +37,6 @@ fn test_compile_errors() { t.compile_fail("tests/ui/wrong_aspyref_lifetimes.rs"); #[cfg(not(any(feature = "uuid")))] 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"))] From f87b286e5eb6ab5ea2aef2bdd4022d074c8af6c2 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 12 May 2025 10:15:18 -0600 Subject: [PATCH 688/936] Don't expose PyASCIIObjectState on Python3.14 and newer (#5133) * fix FFI PyUnicode bindings for 3.14.0b1 * Remove PyASCIIObjectState from pyo3-ffi for Py_3_14 * Update pyo3-ffi/src/cpython/unicodeobject.rs Co-authored-by: David Hewitt --------- Co-authored-by: David Hewitt --- newsfragments/5133.removed.md | 2 + pyo3-ffi/src/cpython/unicodeobject.rs | 319 ++++---------------------- src/ffi/tests.rs | 63 ++--- 3 files changed, 81 insertions(+), 303 deletions(-) create mode 100644 newsfragments/5133.removed.md diff --git a/newsfragments/5133.removed.md b/newsfragments/5133.removed.md new file mode 100644 index 00000000000..bfcb550e58f --- /dev/null +++ b/newsfragments/5133.removed.md @@ -0,0 +1,2 @@ +* Removed PyASCIIObjectState from the FFI bindings on Python 3.14 and newer. +* Removed PyUnicode_IS_ASCII, PyUnicode_IS_COMPACT, and PyUnicode_IS_COMPACT_ASCII from the FFI bindings on Python 3.14 and newer, since they relied on PyASCIIObjectState. \ No newline at end of file diff --git a/pyo3-ffi/src/cpython/unicodeobject.rs b/pyo3-ffi/src/cpython/unicodeobject.rs index 51317bf0bdd..452c82e4c4b 100644 --- a/pyo3-ffi/src/cpython/unicodeobject.rs +++ b/pyo3-ffi/src/cpython/unicodeobject.rs @@ -2,8 +2,6 @@ use crate::Py_hash_t; use crate::{PyObject, Py_UCS1, Py_UCS2, Py_UCS4, Py_ssize_t}; use libc::wchar_t; -#[cfg(Py_3_14)] -use std::os::raw::c_ushort; use std::os::raw::{c_char, c_int, c_uint, c_void}; // skipped Py_UNICODE_ISSPACE() @@ -33,11 +31,13 @@ use std::os::raw::{c_char, c_int, c_uint, c_void}; // skipped Py_UNICODE_LOW_SURROGATE // generated by bindgen v0.63.0 (with small adaptations) +#[cfg(not(Py_3_14))] #[repr(C)] struct BitfieldUnit { storage: Storage, } +#[cfg(not(Py_3_14))] impl BitfieldUnit { #[inline] pub const fn new(storage: Storage) -> Self { @@ -45,7 +45,7 @@ impl BitfieldUnit { } } -#[cfg(not(GraalPy))] +#[cfg(not(any(GraalPy, Py_3_14)))] impl BitfieldUnit where Storage: AsRef<[u8]> + AsMut<[u8]>, @@ -119,33 +119,31 @@ where } } -#[cfg(not(GraalPy))] +#[cfg(not(any(GraalPy, Py_3_14)))] const STATE_INTERNED_INDEX: usize = 0; -#[cfg(all(not(GraalPy), not(Py_3_14)))] +#[cfg(not(any(GraalPy, Py_3_14)))] const STATE_INTERNED_WIDTH: u8 = 2; -#[cfg(all(not(GraalPy), Py_3_14))] -const STATE_INTERNED_WIDTH: u8 = 16; -#[cfg(not(GraalPy))] +#[cfg(not(any(GraalPy, Py_3_14)))] const STATE_KIND_INDEX: usize = STATE_INTERNED_WIDTH as usize; -#[cfg(not(GraalPy))] +#[cfg(not(any(GraalPy, Py_3_14)))] const STATE_KIND_WIDTH: u8 = 3; -#[cfg(not(GraalPy))] +#[cfg(not(any(GraalPy, Py_3_14)))] const STATE_COMPACT_INDEX: usize = (STATE_INTERNED_WIDTH + STATE_KIND_WIDTH) as usize; -#[cfg(not(GraalPy))] +#[cfg(not(any(GraalPy, Py_3_14)))] const STATE_COMPACT_WIDTH: u8 = 1; -#[cfg(not(GraalPy))] +#[cfg(not(any(GraalPy, Py_3_14)))] const STATE_ASCII_INDEX: usize = (STATE_INTERNED_WIDTH + STATE_KIND_WIDTH + STATE_COMPACT_WIDTH) as usize; -#[cfg(not(GraalPy))] +#[cfg(not(any(GraalPy, Py_3_14)))] const STATE_ASCII_WIDTH: u8 = 1; -#[cfg(all(not(GraalPy), Py_3_12))] +#[cfg(all(not(any(GraalPy, Py_3_14)), Py_3_12))] const STATE_STATICALLY_ALLOCATED_INDEX: usize = (STATE_INTERNED_WIDTH + STATE_KIND_WIDTH + STATE_COMPACT_WIDTH + STATE_ASCII_WIDTH) as usize; -#[cfg(all(not(GraalPy), Py_3_12))] +#[cfg(all(not(any(GraalPy, Py_3_14)), Py_3_12))] const STATE_STATICALLY_ALLOCATED_WIDTH: u8 = 1; #[cfg(not(any(Py_3_12, GraalPy)))] @@ -163,19 +161,18 @@ const STATE_READY_WIDTH: u8 = 1; /// /// Memory layout of C bitfields is implementation defined, so these functions are still /// unsafe. Users must verify that they work as expected on the architectures they target. +#[cfg(not(Py_3_14))] #[repr(C)] -#[repr(align(4))] struct PyASCIIObjectState { bitfield_align: [u8; 0], bitfield: BitfieldUnit<[u8; 4usize]>, } // c_uint and u32 are not necessarily the same type on all targets / architectures -#[cfg(not(GraalPy))] +#[cfg(not(any(GraalPy, Py_3_14)))] #[allow(clippy::useless_transmute)] impl PyASCIIObjectState { #[inline] - #[cfg(not(Py_3_14))] unsafe fn interned(&self) -> c_uint { std::mem::transmute( self.bitfield @@ -184,7 +181,6 @@ impl PyASCIIObjectState { } #[inline] - #[cfg(not(Py_3_14))] unsafe fn set_interned(&mut self, val: c_uint) { let val: u32 = std::mem::transmute(val); self.bitfield @@ -192,36 +188,11 @@ impl PyASCIIObjectState { } #[inline] - #[cfg(Py_3_14)] - unsafe fn interned(&self) -> u16 { - std::mem::transmute( - self.bitfield - .get(STATE_INTERNED_INDEX, STATE_INTERNED_WIDTH) as u16, - ) - } - - #[inline] - #[cfg(Py_3_14)] - unsafe fn set_interned(&mut self, val: u16) { - let val: u16 = std::mem::transmute(val); - self.bitfield - .set(STATE_INTERNED_INDEX, STATE_INTERNED_WIDTH, val as u64) - } - - #[inline] - #[cfg(not(Py_3_14))] unsafe fn kind(&self) -> c_uint { std::mem::transmute(self.bitfield.get(STATE_KIND_INDEX, STATE_KIND_WIDTH) as u32) } #[inline] - #[cfg(Py_3_14)] - unsafe fn kind(&self) -> c_ushort { - std::mem::transmute(self.bitfield.get(STATE_KIND_INDEX, STATE_KIND_WIDTH) as c_ushort) - } - - #[inline] - #[cfg(not(Py_3_14))] unsafe fn set_kind(&mut self, val: c_uint) { let val: u32 = std::mem::transmute(val); self.bitfield @@ -229,27 +200,11 @@ impl PyASCIIObjectState { } #[inline] - #[cfg(Py_3_14)] - unsafe fn set_kind(&mut self, val: c_ushort) { - let val: c_ushort = std::mem::transmute(val); - self.bitfield - .set(STATE_KIND_INDEX, STATE_KIND_WIDTH, val as u64) - } - - #[inline] - #[cfg(not(Py_3_14))] unsafe fn compact(&self) -> c_uint { std::mem::transmute(self.bitfield.get(STATE_COMPACT_INDEX, STATE_COMPACT_WIDTH) as u32) } #[inline] - #[cfg(Py_3_14)] - unsafe fn compact(&self) -> c_ushort { - std::mem::transmute(self.bitfield.get(STATE_COMPACT_INDEX, STATE_COMPACT_WIDTH) as c_ushort) - } - - #[inline] - #[cfg(not(Py_3_14))] unsafe fn set_compact(&mut self, val: c_uint) { let val: u32 = std::mem::transmute(val); self.bitfield @@ -257,42 +212,18 @@ impl PyASCIIObjectState { } #[inline] - #[cfg(Py_3_14)] - unsafe fn set_compact(&mut self, val: c_ushort) { - let val: c_ushort = std::mem::transmute(val); - self.bitfield - .set(STATE_COMPACT_INDEX, STATE_COMPACT_WIDTH, val as u64) - } - - #[inline] - #[cfg(not(Py_3_14))] unsafe fn ascii(&self) -> c_uint { std::mem::transmute(self.bitfield.get(STATE_ASCII_INDEX, STATE_ASCII_WIDTH) as u32) } #[inline] - #[cfg(not(Py_3_14))] unsafe fn set_ascii(&mut self, val: c_uint) { let val: u32 = std::mem::transmute(val); self.bitfield .set(STATE_ASCII_INDEX, STATE_ASCII_WIDTH, val as u64) } - #[inline] - #[cfg(Py_3_14)] - unsafe fn ascii(&self) -> c_ushort { - std::mem::transmute(self.bitfield.get(STATE_ASCII_INDEX, STATE_ASCII_WIDTH) as c_ushort) - } - - #[inline] - #[cfg(Py_3_14)] - unsafe fn set_ascii(&mut self, val: c_ushort) { - let val: c_ushort = std::mem::transmute(val); - self.bitfield - .set(STATE_ASCII_INDEX, STATE_ASCII_WIDTH, val as u64) - } - - #[cfg(all(Py_3_12, not(Py_3_14)))] + #[cfg(Py_3_12)] #[inline] unsafe fn statically_allocated(&self) -> c_uint { std::mem::transmute(self.bitfield.get( @@ -301,7 +232,7 @@ impl PyASCIIObjectState { ) as u32) } - #[cfg(all(Py_3_12, not(Py_3_14)))] + #[cfg(Py_3_12)] #[inline] unsafe fn set_statically_allocated(&mut self, val: c_uint) { let val: u32 = std::mem::transmute(val); @@ -312,26 +243,6 @@ impl PyASCIIObjectState { ) } - #[inline] - #[cfg(Py_3_14)] - unsafe fn statically_allocated(&self) -> c_ushort { - std::mem::transmute(self.bitfield.get( - STATE_STATICALLY_ALLOCATED_INDEX, - STATE_STATICALLY_ALLOCATED_WIDTH, - ) as c_ushort) - } - - #[inline] - #[cfg(Py_3_14)] - unsafe fn set_statically_allocated(&mut self, val: c_ushort) { - let val: c_ushort = std::mem::transmute(val); - self.bitfield.set( - STATE_STATICALLY_ALLOCATED_INDEX, - STATE_STATICALLY_ALLOCATED_WIDTH, - val as u64, - ) - } - #[cfg(not(Py_3_12))] #[inline] unsafe fn ready(&self) -> c_uint { @@ -347,6 +258,7 @@ impl PyASCIIObjectState { } } +#[cfg(not(Py_3_14))] impl From for PyASCIIObjectState { #[inline] fn from(value: u32) -> Self { @@ -357,6 +269,7 @@ impl From for PyASCIIObjectState { } } +#[cfg(not(Py_3_14))] impl From for u32 { #[inline] fn from(value: PyASCIIObjectState) -> Self { @@ -383,28 +296,21 @@ pub struct PyASCIIObject { /// unsigned int ready:1; /// unsigned int :24; /// - /// 3.12 and 3.13: + /// 3.12, and 3.13 /// unsigned int interned:2; // SSTATE_* constants. /// unsigned int kind:3; // PyUnicode_*_KIND constants. /// unsigned int compact:1; /// unsigned int ascii:1; /// unsigned int statically_allocated:1; /// unsigned int :24; - /// - /// 3.14 and later: - /// uint16_t interned; // SSTATE_* constants. - /// unsigned short kind:3; // PyUnicode_*_KIND constants. - /// unsigned short compact:1; - /// unsigned short ascii:1; - /// unsigned int statically_allocated:1; - /// unsigned int :10; + /// on 3.14 and higher PyO3 doesn't access the internal state pub state: u32, #[cfg(not(Py_3_12))] pub wstr: *mut wchar_t, } /// Interacting with the bitfield is not actually well-defined, so we mark these APIs unsafe. -#[cfg(not(GraalPy))] +#[cfg(not(any(GraalPy, Py_3_14)))] impl PyASCIIObject { #[cfg_attr(not(Py_3_12), allow(rustdoc::broken_intra_doc_links))] // SSTATE_INTERNED_IMMORTAL_STATIC requires 3.12 /// Get the `interned` field of the [`PyASCIIObject`] state bitfield. @@ -412,7 +318,6 @@ impl PyASCIIObject { /// Returns one of: [`SSTATE_NOT_INTERNED`], [`SSTATE_INTERNED_MORTAL`], /// [`SSTATE_INTERNED_IMMORTAL`], or [`SSTATE_INTERNED_IMMORTAL_STATIC`]. #[inline] - #[cfg(not(Py_3_14))] pub unsafe fn interned(&self) -> c_uint { PyASCIIObjectState::from(self.state).interned() } @@ -424,131 +329,56 @@ impl PyASCIIObject { /// [`SSTATE_INTERNED_MORTAL`], [`SSTATE_INTERNED_IMMORTAL`], or /// [`SSTATE_INTERNED_IMMORTAL_STATIC`] is invalid. #[inline] - #[cfg(not(Py_3_14))] pub unsafe fn set_interned(&mut self, val: c_uint) { let mut state = PyASCIIObjectState::from(self.state); state.set_interned(val); self.state = u32::from(state); } - #[cfg_attr(not(Py_3_12), allow(rustdoc::broken_intra_doc_links))] // SSTATE_INTERNED_IMMORTAL_STATIC requires 3.12 - /// Get the `interned` field of the [`PyASCIIObject`] state bitfield. - /// - /// Returns one of: [`SSTATE_NOT_INTERNED`], [`SSTATE_INTERNED_MORTAL`], - /// [`SSTATE_INTERNED_IMMORTAL`], or [`SSTATE_INTERNED_IMMORTAL_STATIC`]. - #[inline] - #[cfg(Py_3_14)] - pub unsafe fn interned(&self) -> u16 { - PyASCIIObjectState::from(self.state).interned() - } - - #[cfg_attr(not(Py_3_12), allow(rustdoc::broken_intra_doc_links))] // SSTATE_INTERNED_IMMORTAL_STATIC requires 3.12 - /// Set the `interned` field of the [`PyASCIIObject`] state bitfield. - /// - /// Calling this function with an argument that is not [`SSTATE_NOT_INTERNED`], - /// [`SSTATE_INTERNED_MORTAL`], [`SSTATE_INTERNED_IMMORTAL`], or - /// [`SSTATE_INTERNED_IMMORTAL_STATIC`] is invalid. - #[inline] - #[cfg(Py_3_14)] - pub unsafe fn set_interned(&mut self, val: u16) { - let mut state = PyASCIIObjectState::from(self.state); - state.set_interned(val); - self.state = u32::from(state); - } - /// Get the `kind` field of the [`PyASCIIObject`] state bitfield. /// /// Returns one of: #[cfg_attr(not(Py_3_12), doc = "[`PyUnicode_WCHAR_KIND`], ")] /// [`PyUnicode_1BYTE_KIND`], [`PyUnicode_2BYTE_KIND`], or [`PyUnicode_4BYTE_KIND`]. #[inline] - #[cfg(not(Py_3_14))] pub unsafe fn kind(&self) -> c_uint { PyASCIIObjectState::from(self.state).kind() } - /// Get the `kind` field of the [`PyASCIIObject`] state bitfield. - /// - /// Returns one of: - #[cfg_attr(not(Py_3_12), doc = "[`PyUnicode_WCHAR_KIND`], ")] - /// [`PyUnicode_1BYTE_KIND`], [`PyUnicode_2BYTE_KIND`], or [`PyUnicode_4BYTE_KIND`]. - #[inline] - #[cfg(Py_3_14)] - pub unsafe fn kind(&self) -> c_ushort { - PyASCIIObjectState::from(self.state).kind() - } - /// Set the `kind` field of the [`PyASCIIObject`] state bitfield. /// /// Calling this function with an argument that is not #[cfg_attr(not(Py_3_12), doc = "[`PyUnicode_WCHAR_KIND`], ")] /// [`PyUnicode_1BYTE_KIND`], [`PyUnicode_2BYTE_KIND`], or [`PyUnicode_4BYTE_KIND`] is invalid. #[inline] - #[cfg(not(Py_3_14))] pub unsafe fn set_kind(&mut self, val: c_uint) { let mut state = PyASCIIObjectState::from(self.state); state.set_kind(val); self.state = u32::from(state); } - /// Set the `kind` field of the [`PyASCIIObject`] state bitfield. - /// - /// Calling this function with an argument that is not - #[cfg_attr(not(Py_3_12), doc = "[`PyUnicode_WCHAR_KIND`], ")] - /// [`PyUnicode_1BYTE_KIND`], [`PyUnicode_2BYTE_KIND`], or [`PyUnicode_4BYTE_KIND`] is invalid. - #[inline] - #[cfg(Py_3_14)] - pub unsafe fn set_kind(&mut self, val: c_ushort) { - let mut state = PyASCIIObjectState::from(self.state); - state.set_kind(val); - self.state = u32::from(state); - } - /// Get the `compact` field of the [`PyASCIIObject`] state bitfield. /// /// Returns either `0` or `1`. #[inline] - #[cfg(not(Py_3_14))] pub unsafe fn compact(&self) -> c_uint { PyASCIIObjectState::from(self.state).compact() } - /// Get the `compact` field of the [`PyASCIIObject`] state bitfield. - /// - /// Returns either `0` or `1`. - #[inline] - #[cfg(Py_3_14)] - pub unsafe fn compact(&self) -> c_ushort { - PyASCIIObjectState::from(self.state).compact() - } - /// Set the `compact` flag of the [`PyASCIIObject`] state bitfield. /// /// Calling this function with an argument that is neither `0` nor `1` is invalid. #[inline] - #[cfg(not(Py_3_14))] pub unsafe fn set_compact(&mut self, val: c_uint) { let mut state = PyASCIIObjectState::from(self.state); state.set_compact(val); self.state = u32::from(state); } - /// Set the `compact` flag of the [`PyASCIIObject`] state bitfield. - /// - /// Calling this function with an argument that is neither `0` nor `1` is invalid. - #[inline] - #[cfg(Py_3_14)] - pub unsafe fn set_compact(&mut self, val: c_ushort) { - let mut state = PyASCIIObjectState::from(self.state); - state.set_compact(val); - self.state = u32::from(state); - } - /// Get the `ascii` field of the [`PyASCIIObject`] state bitfield. /// /// Returns either `0` or `1`. #[inline] - #[cfg(not(Py_3_14))] pub unsafe fn ascii(&self) -> c_uint { PyASCIIObjectState::from(self.state).ascii() } @@ -557,33 +387,13 @@ impl PyASCIIObject { /// /// Calling this function with an argument that is neither `0` nor `1` is invalid. #[inline] - #[cfg(not(Py_3_14))] + #[cfg(not(all(Py_3_14, Py_GIL_DISABLED)))] pub unsafe fn set_ascii(&mut self, val: c_uint) { let mut state = PyASCIIObjectState::from(self.state); state.set_ascii(val); self.state = u32::from(state); } - /// Get the `ascii` field of the [`PyASCIIObject`] state bitfield. - /// - /// Returns either `0` or `1`. - #[inline] - #[cfg(Py_3_14)] - pub unsafe fn ascii(&self) -> c_ushort { - PyASCIIObjectState::from(self.state).ascii() - } - - /// Set the `ascii` flag of the [`PyASCIIObject`] state bitfield. - /// - /// Calling this function with an argument that is neither `0` nor `1` is invalid. - #[inline] - #[cfg(Py_3_14)] - pub unsafe fn set_ascii(&mut self, val: c_ushort) { - let mut state = PyASCIIObjectState::from(self.state); - state.set_ascii(val); - self.state = u32::from(state); - } - /// Get the `ready` field of the [`PyASCIIObject`] state bitfield. /// /// Returns either `0` or `1`. @@ -608,7 +418,7 @@ impl PyASCIIObject { /// /// Returns either `0` or `1`. #[inline] - #[cfg(all(Py_3_12, not(Py_3_14)))] + #[cfg(Py_3_12)] pub unsafe fn statically_allocated(&self) -> c_uint { PyASCIIObjectState::from(self.state).statically_allocated() } @@ -617,32 +427,12 @@ impl PyASCIIObject { /// /// Calling this function with an argument that is neither `0` nor `1` is invalid. #[inline] - #[cfg(all(Py_3_12, not(Py_3_14)))] + #[cfg(Py_3_12)] pub unsafe fn set_statically_allocated(&mut self, val: c_uint) { let mut state = PyASCIIObjectState::from(self.state); state.set_statically_allocated(val); self.state = u32::from(state); } - - /// Get the `statically_allocated` field of the [`PyASCIIObject`] state bitfield. - /// - /// Returns either `0` or `1`. - #[inline] - #[cfg(Py_3_14)] - pub unsafe fn statically_allocated(&self) -> c_ushort { - PyASCIIObjectState::from(self.state).statically_allocated() - } - - /// Set the `statically_allocated` flag of the [`PyASCIIObject`] state bitfield. - /// - /// Calling this function with an argument that is neither `0` nor `1` is invalid. - #[inline] - #[cfg(Py_3_14)] - pub unsafe fn set_statically_allocated(&mut self, val: c_ushort) { - let mut state = PyASCIIObjectState::from(self.state); - state.set_statically_allocated(val); - self.state = u32::from(state); - } } #[repr(C)] @@ -684,7 +474,7 @@ pub const SSTATE_INTERNED_IMMORTAL: c_uint = 2; #[cfg(Py_3_12)] pub const SSTATE_INTERNED_IMMORTAL_STATIC: c_uint = 3; -#[cfg(all(not(GraalPy), not(Py_3_14)))] +#[cfg(not(any(GraalPy, Py_3_14)))] #[inline] pub unsafe fn PyUnicode_IS_ASCII(op: *mut PyObject) -> c_uint { debug_assert!(crate::PyUnicode_Check(op) != 0); @@ -694,29 +484,13 @@ pub unsafe fn PyUnicode_IS_ASCII(op: *mut PyObject) -> c_uint { (*(op as *mut PyASCIIObject)).ascii() } -#[cfg(all(not(GraalPy), not(Py_3_14)))] +#[cfg(not(any(GraalPy, Py_3_14)))] #[inline] pub unsafe fn PyUnicode_IS_COMPACT(op: *mut PyObject) -> c_uint { (*(op as *mut PyASCIIObject)).compact() } -#[cfg(all(not(GraalPy), Py_3_14))] -#[inline] -pub unsafe fn PyUnicode_IS_ASCII(op: *mut PyObject) -> c_ushort { - debug_assert!(crate::PyUnicode_Check(op) != 0); - #[cfg(not(Py_3_12))] - debug_assert!(PyUnicode_IS_READY(op) != 0); - - (*(op as *mut PyASCIIObject)).ascii() -} - -#[cfg(all(not(GraalPy), Py_3_14))] -#[inline] -pub unsafe fn PyUnicode_IS_COMPACT(op: *mut PyObject) -> c_ushort { - (*(op as *mut PyASCIIObject)).compact() -} - -#[cfg(not(GraalPy))] +#[cfg(not(any(GraalPy, Py_3_14)))] #[inline] pub unsafe fn PyUnicode_IS_COMPACT_ASCII(op: *mut PyObject) -> c_uint { ((*(op as *mut PyASCIIObject)).ascii() != 0 && PyUnicode_IS_COMPACT(op) != 0).into() @@ -726,20 +500,10 @@ pub unsafe fn PyUnicode_IS_COMPACT_ASCII(op: *mut PyObject) -> c_uint { #[deprecated(note = "Removed in Python 3.12")] pub const PyUnicode_WCHAR_KIND: c_uint = 0; -#[cfg(not(Py_3_14))] pub const PyUnicode_1BYTE_KIND: c_uint = 1; -#[cfg(not(Py_3_14))] pub const PyUnicode_2BYTE_KIND: c_uint = 2; -#[cfg(not(Py_3_14))] pub const PyUnicode_4BYTE_KIND: c_uint = 4; -#[cfg(Py_3_14)] -pub const PyUnicode_1BYTE_KIND: c_ushort = 1; -#[cfg(Py_3_14)] -pub const PyUnicode_2BYTE_KIND: c_ushort = 2; -#[cfg(Py_3_14)] -pub const PyUnicode_4BYTE_KIND: c_ushort = 4; - #[cfg(not(any(GraalPy, PyPy)))] #[inline] pub unsafe fn PyUnicode_1BYTE_DATA(op: *mut PyObject) -> *mut Py_UCS1 { @@ -758,19 +522,15 @@ pub unsafe fn PyUnicode_4BYTE_DATA(op: *mut PyObject) -> *mut Py_UCS4 { PyUnicode_DATA(op) as *mut Py_UCS4 } -#[cfg(all(not(GraalPy), not(Py_3_14)))] -#[inline] -pub unsafe fn PyUnicode_KIND(op: *mut PyObject) -> c_uint { - debug_assert!(crate::PyUnicode_Check(op) != 0); - #[cfg(not(Py_3_12))] - debug_assert!(PyUnicode_IS_READY(op) != 0); - - (*(op as *mut PyASCIIObject)).kind() +#[cfg(all(not(GraalPy), Py_3_14))] +extern "C" { + #[cfg_attr(PyPy, link_name = "PyPyUnicode_KIND")] + pub fn PyUnicode_KIND(op: *mut PyObject) -> c_uint; } -#[cfg(all(not(GraalPy), Py_3_14))] +#[cfg(all(not(GraalPy), not(Py_3_14)))] #[inline] -pub unsafe fn PyUnicode_KIND(op: *mut PyObject) -> c_ushort { +pub unsafe fn PyUnicode_KIND(op: *mut PyObject) -> c_uint { debug_assert!(crate::PyUnicode_Check(op) != 0); #[cfg(not(Py_3_12))] debug_assert!(PyUnicode_IS_READY(op) != 0); @@ -778,7 +538,7 @@ pub unsafe fn PyUnicode_KIND(op: *mut PyObject) -> c_ushort { (*(op as *mut PyASCIIObject)).kind() } -#[cfg(not(GraalPy))] +#[cfg(not(any(GraalPy, Py_3_14)))] #[inline] pub unsafe fn _PyUnicode_COMPACT_DATA(op: *mut PyObject) -> *mut c_void { if PyUnicode_IS_ASCII(op) != 0 { @@ -796,7 +556,7 @@ pub unsafe fn _PyUnicode_NONCOMPACT_DATA(op: *mut PyObject) -> *mut c_void { (*(op as *mut PyUnicodeObject)).data.any } -#[cfg(not(any(GraalPy, PyPy)))] +#[cfg(not(any(GraalPy, PyPy, Py_3_14)))] #[inline] pub unsafe fn PyUnicode_DATA(op: *mut PyObject) -> *mut c_void { debug_assert!(crate::PyUnicode_Check(op) != 0); @@ -808,6 +568,13 @@ pub unsafe fn PyUnicode_DATA(op: *mut PyObject) -> *mut c_void { } } +#[cfg(Py_3_14)] +#[cfg(all(not(GraalPy), Py_3_14))] +extern "C" { + #[cfg_attr(PyPy, link_name = "PyPyUnicode_DATA")] + pub fn PyUnicode_DATA(op: *mut PyObject) -> *mut c_void; +} + // skipped PyUnicode_WRITE // skipped PyUnicode_READ // skipped PyUnicode_READ_CHAR diff --git a/src/ffi/tests.rs b/src/ffi/tests.rs index 1f934d670d2..7de4e3b1875 100644 --- a/src/ffi/tests.rs +++ b/src/ffi/tests.rs @@ -118,6 +118,7 @@ fn test_timezone_from_offset_and_name() { fn ascii_object_bitfield() { let ob_base: PyObject = unsafe { std::mem::zeroed() }; + #[cfg_attr(Py_3_14, allow(unused_mut, unused_variables))] let mut o = PyASCIIObject { ob_base, length: 0, @@ -128,6 +129,7 @@ fn ascii_object_bitfield() { wstr: std::ptr::null_mut() as *mut wchar_t, }; + #[cfg(not(Py_3_14))] unsafe { assert_eq!(o.interned(), 0); assert_eq!(o.kind(), 0); @@ -175,24 +177,28 @@ fn ascii() { let ptr = s.as_ptr(); unsafe { - let ascii_ptr = ptr as *mut PyASCIIObject; - let ascii = ascii_ptr.as_ref().unwrap(); - - assert_eq!(ascii.interned(), 0); - assert_eq!(ascii.kind(), PyUnicode_1BYTE_KIND); - assert_eq!(ascii.compact(), 1); - assert_eq!(ascii.ascii(), 1); - #[cfg(not(Py_3_12))] - assert_eq!(ascii.ready(), 1); - - assert_eq!(PyUnicode_IS_ASCII(ptr), 1); - assert_eq!(PyUnicode_IS_COMPACT(ptr), 1); - assert_eq!(PyUnicode_IS_COMPACT_ASCII(ptr), 1); + #[cfg(not(Py_3_14))] + { + let ascii_ptr = ptr as *mut PyASCIIObject; + let ascii = ascii_ptr.as_ref().unwrap(); + + assert_eq!(ascii.interned(), 0); + assert_eq!(ascii.kind(), PyUnicode_1BYTE_KIND); + assert_eq!(ascii.compact(), 1); + assert_eq!(ascii.ascii(), 1); + #[cfg(not(Py_3_12))] + assert_eq!(ascii.ready(), 1); + + assert_eq!(PyUnicode_IS_ASCII(ptr), 1); + assert_eq!(PyUnicode_IS_COMPACT(ptr), 1); + assert_eq!(PyUnicode_IS_COMPACT_ASCII(ptr), 1); + } assert!(!PyUnicode_1BYTE_DATA(ptr).is_null()); // 2 and 4 byte macros return nonsense for this string instance. assert_eq!(PyUnicode_KIND(ptr), PyUnicode_1BYTE_KIND); + #[cfg(not(Py_3_14))] assert!(!_PyUnicode_COMPACT_DATA(ptr).is_null()); // _PyUnicode_NONCOMPACT_DATA isn't valid for compact strings. assert!(!PyUnicode_DATA(ptr).is_null()); @@ -216,23 +222,26 @@ fn ucs4() { let ptr = py_string.as_ptr(); unsafe { - let ascii_ptr = ptr as *mut PyASCIIObject; - let ascii = ascii_ptr.as_ref().unwrap(); - - assert_eq!(ascii.interned(), 0); - assert_eq!(ascii.kind(), PyUnicode_4BYTE_KIND); - assert_eq!(ascii.compact(), 1); - assert_eq!(ascii.ascii(), 0); - #[cfg(not(Py_3_12))] - assert_eq!(ascii.ready(), 1); - - assert_eq!(PyUnicode_IS_ASCII(ptr), 0); - assert_eq!(PyUnicode_IS_COMPACT(ptr), 1); - assert_eq!(PyUnicode_IS_COMPACT_ASCII(ptr), 0); - + #[cfg(not(Py_3_14))] + { + let ascii_ptr = ptr as *mut PyASCIIObject; + let ascii = ascii_ptr.as_ref().unwrap(); + + assert_eq!(ascii.interned(), 0); + assert_eq!(ascii.kind(), PyUnicode_4BYTE_KIND); + assert_eq!(ascii.compact(), 1); + assert_eq!(ascii.ascii(), 0); + #[cfg(not(Py_3_12))] + assert_eq!(ascii.ready(), 1); + + assert_eq!(PyUnicode_IS_ASCII(ptr), 0); + assert_eq!(PyUnicode_IS_COMPACT(ptr), 1); + assert_eq!(PyUnicode_IS_COMPACT_ASCII(ptr), 0); + } assert!(!PyUnicode_4BYTE_DATA(ptr).is_null()); assert_eq!(PyUnicode_KIND(ptr), PyUnicode_4BYTE_KIND); + #[cfg(not(Py_3_14))] assert!(!_PyUnicode_COMPACT_DATA(ptr).is_null()); // _PyUnicode_NONCOMPACT_DATA isn't valid for compact strings. assert!(!PyUnicode_DATA(ptr).is_null()); From d74fadc9f400d6e9ba35002784b0c5a0dd89c967 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers <7943856+bschoenmaeckers@users.noreply.github.com> Date: Mon, 12 May 2025 19:08:34 +0200 Subject: [PATCH 689/936] Implement `OnceExt` & `MutexExt` for `parking_lot` & `lock_api` (#5044) * Implement `OnceExt` & `MutexExt` for `parking_lot` & `lock_api` * Implement `MutexExt` for `Arc` * Move `MutexExt` lock result into type parameter * Remove lifetime from associated type of `OnceExt` * Add docs * add changelog * Add new features to `full` * mention `parking_lot` in guide * Add tests * Update docs to mention all cargo features Co-authored-by: Nathan Goldbaum * Also test `parking_lot::Mutex::lock_py_attached` on `lock_api` feature * Update newsfragments/5044.added.md Co-authored-by: Nathan Goldbaum * Add additional case to `once_ext` test * enable `arc_lock` feature on `parking_lot` during testing --------- Co-authored-by: Nathan Goldbaum --- Cargo.toml | 9 ++ guide/src/class/thread-safety.md | 2 +- guide/src/features.md | 12 ++ newsfragments/5044.added.md | 1 + src/sealed.rs | 6 + src/sync.rs | 204 +++++++++++++++++++++++++------ 6 files changed, 199 insertions(+), 35 deletions(-) create mode 100644 newsfragments/5044.added.md diff --git a/Cargo.toml b/Cargo.toml index ac23a3c474d..c17c9bd4dbb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,8 @@ time = { version = "0.3.38", default-features = false, optional = true } serde = { version = "1.0", optional = true } smallvec = { version = "1.0", optional = true } uuid = { version = "1.11.0", optional = true } +lock_api = { version = "0.4", optional = true } +parking_lot = { version = "0.12", optional = true} [target.'cfg(not(target_has_atomic = "64"))'.dependencies] portable-atomic = "1.0" @@ -68,6 +70,7 @@ futures = "0.3.28" tempfile = "3.12.0" static_assertions = "1.1.0" uuid = { version = "1.10.0", features = ["v4"] } +parking_lot = { version = "0.12.3", features = ["arc_lock"]} [build-dependencies] pyo3-build-config = { path = "pyo3-build-config", version = "=0.25.0-dev", features = ["resolve-config"] } @@ -115,6 +118,9 @@ auto-initialize = [] # Enables `Clone`ing references to Python objects `Py` which panics if the GIL is not held. py-clone = [] +parking_lot = ["dep:parking_lot", "lock_api"] +arc_lock = ["lock_api", "lock_api/arc_lock", "parking_lot?/arc_lock"] + # Optimizes PyObject to Vec conversion and so on. nightly = [] @@ -124,6 +130,7 @@ full = [ "macros", # "multiple-pymethods", # Not supported by wasm "anyhow", + "arc_lock", "bigdecimal", "chrono", "chrono-tz", @@ -133,10 +140,12 @@ full = [ "eyre", "hashbrown", "indexmap", + "lock_api", "num-bigint", "num-complex", "num-rational", "ordered-float", + "parking_lot", "py-clone", "rust_decimal", "serde", diff --git a/guide/src/class/thread-safety.md b/guide/src/class/thread-safety.md index 75c0b1a7423..d0cf9e10a7f 100644 --- a/guide/src/class/thread-safety.md +++ b/guide/src/class/thread-safety.md @@ -101,7 +101,7 @@ 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. +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. Additionally, support for the `parking_lot` and `lock_api` synchronization libraries is gated behind the `parking_lot` and `lock_api` features. You can also enable the `arc_lock` feature if you need the `arc_lock` features of either library. ### Wrapping unsynchronized data diff --git a/guide/src/features.md b/guide/src/features.md index 04297b4dab2..450c1b4ea04 100644 --- a/guide/src/features.md +++ b/guide/src/features.md @@ -121,6 +121,10 @@ These features enable conversions between Python types and types from other Rust Adds a dependency on [anyhow](https://docs.rs/anyhow). Enables a conversion from [anyhow](https://docs.rs/anyhow)’s [`Error`](https://docs.rs/anyhow/latest/anyhow/struct.Error.html) type to [`PyErr`]({{#PYO3_DOCS_URL}}/pyo3/struct.PyErr.html), for easy error handling. +### `arc_lock` + +Enables Pyo3's `MutexExt` trait for all Mutexes that extend on [`lock_api::Mutex`](https://docs.rs/lock_api/latest/lock_api/struct.Mutex.html) and are wrapped in an [`Arc`](https://doc.rust-lang.org/std/sync/struct.Arc.html) type. Like [`Arc`](https://docs.rs/parking_lot/latest/parking_lot/type.Mutex.html#method.lock_arc) + ### `bigdecimal` Adds a dependency on [bigdecimal](https://docs.rs/bigdecimal) and enables conversions into its [`BigDecimal`](https://docs.rs/bigdecimal/latest/bigdecimal/struct.BigDecimal.html) type. @@ -168,6 +172,10 @@ Adds a dependency on [jiff@0.2](https://docs.rs/jiff/0.2) and requires MSRV 1.70 - [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) +### `lock_api` + +Adds a dependency on [lock_api](https://docs.rs/lock_api) and enables Pyo3's `MutexExt` trait for all mutexes that extend on [`lock_api::Mutex`](https://docs.rs/lock_api/latest/lock_api/struct.Mutex.html) (like `parking_lot` or `spin`). + ### `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. @@ -186,6 +194,10 @@ Adds a dependency on [ordered-float](https://docs.rs/ordered-float) and enables - [NotNan](https://docs.rs/ordered-float/latest/ordered_float/struct.NotNan.html) -> [`PyFloat`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyFloat.html) - [OrderedFloat](https://docs.rs/ordered-float/latest/ordered_float/struct.OrderedFloat.html) -> [`PyFloat`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyFloat.html) +### `parking-lot` + +Adds a dependency on [parking_lot](https://docs.rs/parking_lot) and enables Pyo3's `OnceExt` & `MutexExt` traits for [`parking_lot::Once`](https://docs.rs/parking_lot/latest/parking_lot/struct.Once.html) and [`parking_lot::Mutex`](https://docs.rs/parking_lot/latest/parking_lot/type.Mutex.html) types. + ### `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/5044.added.md b/newsfragments/5044.added.md new file mode 100644 index 00000000000..c6898f277f4 --- /dev/null +++ b/newsfragments/5044.added.md @@ -0,0 +1 @@ +Implement `OnceExt` & `MutexExt` for `parking_lot` & `lock_api`. Use the new extension traits by enabling the `arc_lock`, `lock_api`, or `parking_lot` cargo features. diff --git a/src/sealed.rs b/src/sealed.rs index 0367a6b10fc..29ffa951f4e 100644 --- a/src/sealed.rs +++ b/src/sealed.rs @@ -57,3 +57,9 @@ impl Sealed for PyClassInitializer {} impl Sealed for std::sync::Once {} impl Sealed for std::sync::Mutex {} +#[cfg(feature = "lock_api")] +impl Sealed for lock_api::Mutex {} +#[cfg(feature = "parking_lot")] +impl Sealed for parking_lot::Once {} +#[cfg(feature = "arc_lock")] +impl Sealed for std::sync::Arc> {} diff --git a/src/sync.rs b/src/sync.rs index 193cc79b105..e1a74eae7bd 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -536,13 +536,16 @@ mod once_lock_ext_sealed { /// Helper trait for `Once` to help avoid deadlocking when using a `Once` when attached to a /// Python thread. pub trait OnceExt: Sealed { + ///The state of `Once` + type OnceState; + /// 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)); + fn call_once_force_py_attached(&self, py: Python<'_>, f: impl FnOnce(&Self::OnceState)); } /// Extension trait for [`std::sync::OnceLock`] which helps avoid deadlocks between the Python @@ -565,7 +568,10 @@ pub trait OnceLockExt: once_lock_ext_sealed::Sealed { /// Extension trait for [`std::sync::Mutex`] which helps avoid deadlocks between /// the Python interpreter and acquiring the `Mutex`. -pub trait MutexExt: Sealed { +pub trait MutexExt<'a, T, R>: Sealed +where + Self: 'a, +{ /// Lock this `Mutex` in a manner that cannot deadlock with the Python interpreter. /// /// Before attempting to lock the mutex, this function detaches from the @@ -573,13 +579,12 @@ pub trait MutexExt: Sealed { /// 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>; + fn lock_py_attached(&'a self, py: Python<'_>) -> R; } impl OnceExt for Once { + type OnceState = OnceState; + fn call_once_py_attached(&self, py: Python<'_>, f: impl FnOnce()) { if self.is_completed() { return; @@ -597,6 +602,41 @@ impl OnceExt for Once { } } +#[cfg(feature = "parking_lot")] +impl OnceExt for parking_lot::Once { + type OnceState = parking_lot::OnceState; + + fn call_once_py_attached(&self, _py: Python<'_>, f: impl FnOnce()) { + if self.state().done() { + return; + } + + let ts_guard = unsafe { SuspendGIL::new() }; + + self.call_once(move || { + drop(ts_guard); + f(); + }); + } + + fn call_once_force_py_attached( + &self, + _py: Python<'_>, + f: impl FnOnce(&parking_lot::OnceState), + ) { + if self.state().done() { + return; + } + + let ts_guard = unsafe { SuspendGIL::new() }; + + self.call_once_force(move |state| { + drop(ts_guard); + f(&state); + }); + } +} + #[cfg(rustc_has_once_lock)] impl OnceLockExt for std::sync::OnceLock { fn get_or_init_py_attached(&self, py: Python<'_>, f: F) -> &T @@ -612,11 +652,13 @@ impl OnceLockExt for std::sync::OnceLock { } } -impl MutexExt for std::sync::Mutex { +impl<'a, T> MutexExt<'a, T, std::sync::LockResult>> + for std::sync::Mutex +{ fn lock_py_attached( - &self, + &'a self, _py: Python<'_>, - ) -> std::sync::LockResult> { + ) -> 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 @@ -638,6 +680,41 @@ impl MutexExt for std::sync::Mutex { } } +#[cfg(feature = "lock_api")] +impl<'a, R: lock_api::RawMutex, T> MutexExt<'a, T, lock_api::MutexGuard<'a, R, T>> + for lock_api::Mutex +{ + fn lock_py_attached(&'a self, _py: Python<'_>) -> lock_api::MutexGuard<'a, R, T> { + if let Some(guard) = self.try_lock() { + return guard; + } + + let ts_guard = unsafe { SuspendGIL::new() }; + let res = self.lock(); + drop(ts_guard); + res + } +} + +#[cfg(feature = "arc_lock")] +impl<'a, R, T> MutexExt<'a, T, lock_api::ArcMutexGuard> + for std::sync::Arc> +where + R: lock_api::RawMutex, + Self: 'a, +{ + fn lock_py_attached(&self, _py: Python<'_>) -> lock_api::ArcMutexGuard { + if let Some(guard) = self.try_lock_arc() { + return guard; + } + + let ts_guard = unsafe { SuspendGIL::new() }; + let res = self.lock_arc(); + drop(ts_guard); + res + } +} + #[cold] fn init_once_py_attached(once: &Once, _py: Python<'_>, f: F) where @@ -966,36 +1043,47 @@ 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()); + macro_rules! test_once { + ($once:expr, $is_poisoned:expr) => {{ + // adapted from the example in the docs for Once::try_once_force + let init = $once; + 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, || {}); - }); - }); + // poisoning propagates + let handle = s.spawn(|| { + Python::with_gil(|py| { + init.call_once_py_attached(py, || {}); + }); + }); + + assert!(handle.join().is_err()); - 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!($is_poisoned(state.clone())); + }); - // 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, || {}); + }); + + // calling call_once_force should return immediately without calling the closure + Python::with_gil(|py| init.call_once_force_py_attached(py, |_| panic!())); }); + }}; + } - // once any success happens, we stop propagating the poison - init.call_once_py_attached(py, || {}); - }); - }); + test_once!(Once::new(), OnceState::is_poisoned); + #[cfg(feature = "parking_lot")] + test_once!(parking_lot::Once::new(), parking_lot::OnceState::poisoned); } #[cfg(rustc_has_once_lock)] @@ -1047,6 +1135,54 @@ mod tests { }); } + #[cfg(feature = "macros")] + #[cfg(all( + any(feature = "parking_lot", feature = "lock_api"), + not(target_arch = "wasm32") // We are building wasm Python with pthreads disabled + ))] + #[test] + fn test_parking_lot_mutex_ext() { + macro_rules! test_mutex { + ($guard:ty ,$mutex:stmt) => {{ + let barrier = Barrier::new(2); + + let mutex = Python::with_gil({ $mutex }); + + std::thread::scope(|s| { + s.spawn(|| { + Python::with_gil(|py| { + let b: $guard = mutex.lock_py_attached(py); + 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: $guard = mutex.lock_py_attached(py); + assert!((*b).bind(py).borrow().0.load(Ordering::Acquire)); + }); + }); + }); + }}; + } + + test_mutex!(parking_lot::MutexGuard<'_, _>, |py| { + parking_lot::Mutex::new(Py::new(py, BoolWrapper(AtomicBool::new(false))).unwrap()) + }); + + #[cfg(feature = "arc_lock")] + test_mutex!(parking_lot::ArcMutexGuard<_, _>, |py| { + let mutex = + parking_lot::Mutex::new(Py::new(py, BoolWrapper(AtomicBool::new(false))).unwrap()); + std::sync::Arc::new(mutex) + }); + } + #[cfg(not(target_arch = "wasm32"))] // We are building wasm Python with pthreads disabled #[test] fn test_mutex_ext_poison() { From b61dc7fb0f7b6eef5d61289f77990cbd42649d02 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Mon, 12 May 2025 21:33:28 +0200 Subject: [PATCH 690/936] add `rename_all` support for `#[derive(IntoPyObject)]` (#5112) --- newsfragments/5112.added.md | 1 + pyo3-macros-backend/src/intopyobject.rs | 22 +++++++++++++--- tests/test_frompy_intopy_roundtrip.rs | 15 +++++++++++ tests/test_intopyobject.rs | 35 +++++++++++++++++++++++++ tests/ui/invalid_intopy_derive.rs | 29 ++++++++++++++++++++ tests/ui/invalid_intopy_derive.stderr | 30 +++++++++++++++++++++ 6 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 newsfragments/5112.added.md diff --git a/newsfragments/5112.added.md b/newsfragments/5112.added.md new file mode 100644 index 00000000000..5320d26bb6f --- /dev/null +++ b/newsfragments/5112.added.md @@ -0,0 +1 @@ +add `rename_all` support for `#[derive(IntoPyObject)]` \ No newline at end of file diff --git a/pyo3-macros-backend/src/intopyobject.rs b/pyo3-macros-backend/src/intopyobject.rs index 111a40089f3..2532812f6e1 100644 --- a/pyo3-macros-backend/src/intopyobject.rs +++ b/pyo3-macros-backend/src/intopyobject.rs @@ -1,6 +1,6 @@ -use crate::attributes::IntoPyWithAttribute; +use crate::attributes::{IntoPyWithAttribute, RenamingRule}; use crate::derive_attributes::{ContainerAttributes, FieldAttributes}; -use crate::utils::Ctx; +use crate::utils::{self, Ctx}; use proc_macro2::{Span, TokenStream}; use quote::{format_ident, quote, quote_spanned, ToTokens}; use syn::ext::IdentExt; @@ -65,6 +65,7 @@ struct Container<'a, const REF: bool> { path: syn::Path, receiver: Option, ty: ContainerType<'a>, + rename_rule: Option, } /// Construct a container based on fields, identifier and attributes. @@ -79,6 +80,10 @@ impl<'a, const REF: bool> Container<'a, REF> { ) -> 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() @@ -127,6 +132,10 @@ impl<'a, const REF: bool> Container<'a, REF> { attrs.getter.is_none(), attrs.getter.unwrap().span() => "`transparent` structs may not have `item` nor `attribute` for the inner field" ); + ensure_spanned!( + options.rename_all.is_none(), + options.rename_all.span() => "`rename_all` is not permitted on `transparent` structs and variants" + ); ensure_spanned!( attrs.into_py_with.is_none(), attrs.into_py_with.span() => "`into_py_with` is not permitted on `transparent` structs or variants" @@ -169,6 +178,7 @@ impl<'a, const REF: bool> Container<'a, REF> { path, receiver, ty: style, + rename_rule: options.rename_all.map(|v| v.value.rule), }; Ok(v) } @@ -254,7 +264,10 @@ impl<'a, const REF: bool> Container<'a, REF> { .as_ref() .and_then(|item| item.0.as_ref()) .map(|item| item.into_token_stream()) - .unwrap_or_else(|| f.ident.unraw().to_string().into_token_stream()); + .unwrap_or_else(|| { + let name = f.ident.unraw().to_string(); + self.rename_rule.map(|rule| utils::apply_renaming_rule(rule, &name)).unwrap_or(name).into_token_stream() + }); let value = Ident::new(&format!("arg{i}"), f.field.ty.span()); if let Some(expr_path) = f.into_py_with.as_ref().map(|i|&i.value) { @@ -459,6 +472,9 @@ 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"); } + if let Some(rename_all) = options.rename_all { + bail_spanned!(rename_all.span() => "`rename_all` is not supported at top level for enums"); + } let en = Enum::::new(en, &tokens.ident)?; en.build(ctx) } diff --git a/tests/test_frompy_intopy_roundtrip.rs b/tests/test_frompy_intopy_roundtrip.rs index dad1dffdc62..58d4053e472 100644 --- a/tests/test_frompy_intopy_roundtrip.rs +++ b/tests/test_frompy_intopy_roundtrip.rs @@ -225,6 +225,11 @@ pub enum Foo { TransparentStructVar { a: Option, }, + #[pyo3(rename_all = "camelCase", from_item_all)] + RenameAll { + long_field_name: [u16; 2], + other_field: Option, + }, } #[test] @@ -270,5 +275,15 @@ fn test_enum() { let foo = transparent_struct_var.clone().into_pyobject(py).unwrap(); assert_eq!(transparent_struct_var, foo.extract::().unwrap()); + + let rename_all_struct_var = Foo::RenameAll { + long_field_name: [1, 2], + other_field: None, + }; + let foo = (&rename_all_struct_var).into_pyobject(py).unwrap(); + assert_eq!(rename_all_struct_var, foo.extract::().unwrap()); + + let foo = rename_all_struct_var.clone().into_pyobject(py).unwrap(); + assert_eq!(rename_all_struct_var, foo.extract::().unwrap()); }); } diff --git a/tests/test_intopyobject.rs b/tests/test_intopyobject.rs index 8e758c636cd..2fbddddb8c1 100644 --- a/tests/test_intopyobject.rs +++ b/tests/test_intopyobject.rs @@ -252,3 +252,38 @@ fn test_into_py_with() { ); }); } + +#[test] +fn test_struct_into_py_rename_all() { + #[derive(IntoPyObject, IntoPyObjectRef)] + #[pyo3(rename_all = "camelCase")] + struct Foo { + foo_bar: String, + #[pyo3(item("BAZ"))] + baz: usize, + #[pyo3(item)] + long_field_name: f32, + } + + let foo = Foo { + foo_bar: "foobar".into(), + baz: 42, + long_field_name: 0.0, + }; + + Python::with_gil(|py| { + let py_foo_ref = (&foo).into_pyobject(py).unwrap(); + let py_foo = foo.into_pyobject(py).unwrap(); + + py_run!( + py, + py_foo_ref, + "assert py_foo_ref == {'fooBar': 'foobar', 'BAZ': 42, 'longFieldName': 0},f'{py_foo_ref}'" + ); + py_run!( + py, + py_foo, + "assert py_foo == {'fooBar': 'foobar', 'BAZ': 42, 'longFieldName': 0},f'{py_foo}'" + ); + }); +} diff --git a/tests/ui/invalid_intopy_derive.rs b/tests/ui/invalid_intopy_derive.rs index b609a740e56..5d65e36bfcb 100644 --- a/tests/ui/invalid_intopy_derive.rs +++ b/tests/ui/invalid_intopy_derive.rs @@ -138,4 +138,33 @@ enum EnumStructIntoPyWith { }, } +#[derive(IntoPyObject, IntoPyObjectRef)] +#[pyo3(transparent, rename_all = "camelCase")] +struct StructTransparentRenameAll { + foo_bar: String, +} + +#[derive(IntoPyObject, IntoPyObjectRef)] +#[pyo3(rename_all = "camelCase")] +struct StructTupleRenameAll(String, usize); + +#[derive(IntoPyObject, IntoPyObjectRef)] +enum EnumTransparentVariantRenameAll { + #[pyo3(rename_all = "camelCase")] + #[pyo3(transparent)] + Variant { foo: String }, +} + +#[derive(IntoPyObject, IntoPyObjectRef)] +enum EnumTupleVariantRenameAll { + #[pyo3(rename_all = "camelCase")] + Variant(String, usize), +} + +#[derive(IntoPyObject, IntoPyObjectRef)] +#[pyo3(rename_all = "camelCase")] +enum EnumTopRenameAll { + Variant { foo: String }, +} + fn main() {} diff --git a/tests/ui/invalid_intopy_derive.stderr b/tests/ui/invalid_intopy_derive.stderr index e429defc52d..518066f3460 100644 --- a/tests/ui/invalid_intopy_derive.stderr +++ b/tests/ui/invalid_intopy_derive.stderr @@ -149,3 +149,33 @@ error: `into_py_with` is not permitted on `transparent` structs or variants | 136 | #[pyo3(into_py_with = into)] | ^^^^^^^^^^^^ + +error: `rename_all` is not permitted on `transparent` structs and variants + --> tests/ui/invalid_intopy_derive.rs:142:21 + | +142 | #[pyo3(transparent, rename_all = "camelCase")] + | ^^^^^^^^^^ + +error: `rename_all` is useless on tuple structs and variants. + --> tests/ui/invalid_intopy_derive.rs:148:8 + | +148 | #[pyo3(rename_all = "camelCase")] + | ^^^^^^^^^^ + +error: `rename_all` is not permitted on `transparent` structs and variants + --> tests/ui/invalid_intopy_derive.rs:153:12 + | +153 | #[pyo3(rename_all = "camelCase")] + | ^^^^^^^^^^ + +error: `rename_all` is useless on tuple structs and variants. + --> tests/ui/invalid_intopy_derive.rs:160:12 + | +160 | #[pyo3(rename_all = "camelCase")] + | ^^^^^^^^^^ + +error: `rename_all` is not supported at top level for enums + --> tests/ui/invalid_intopy_derive.rs:165:8 + | +165 | #[pyo3(rename_all = "camelCase")] + | ^^^^^^^^^^ From c4efb16f434f6a3ceb23304d8c2fac84822605c0 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Tue, 13 May 2025 05:51:52 +0100 Subject: [PATCH 691/936] ci: reduce verbosity & install zoneinfo backport on <3.9 (#5136) * ci: fixup verbosity & install zoneinfo backport on <3.9 * correct typo * try different syntax for `verbose` --- .github/workflows/build.yml | 13 ++++++++++--- .github/workflows/ci.yml | 3 +++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4c15b5b9f46..60744cadbf7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,6 +19,9 @@ on: MSRV: required: true type: string + verbose: + type: boolean + default: false jobs: build: @@ -66,6 +69,10 @@ jobs: run: | echo "CARGO_BUILD_TARGET=i686-pc-windows-msvc" >> $GITHUB_ENV + - name: Install zoneinfo backport for Python 3.7 / 3.8 + if: contains(fromJSON('["3.7", "3.8"]'), inputs.python-version) + run: python -m pip install backports.zoneinfo + - uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'CI-save-pr-cache') }} @@ -86,8 +93,8 @@ jobs: 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 }} - ref: ${{ github.event.pull_request.head.ref || github.event.merge_group.head_ref }} + base: ${{ github.event.merge_group.base_ref }} + ref: ${{ github.event.merge_group.head_ref }} filters: | changed: - 'pyo3-ffi/**' @@ -142,7 +149,7 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} env: - CARGO_TERM_VERBOSE: true + CARGO_TERM_VERBOSE: ${{ inputs.verbose }} RUST_BACKTRACE: 1 RUSTFLAGS: "-D warnings" RUSTDOCFLAGS: "-D warnings" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f4a9d8be9b6..428b78e5c9d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,6 +37,7 @@ jobs: runs-on: ubuntu-latest outputs: MSRV: ${{ steps.resolve-msrv.outputs.MSRV }} + verbose: ${{ runner.debug == '1' }} steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 @@ -175,6 +176,7 @@ jobs: rust: ${{ matrix.rust }} rust-target: ${{ matrix.platform.rust-target }} MSRV: ${{ needs.resolve.outputs.MSRV }} + verbose: ${{ needs.resolve.outputs.verbose == 'true' }} secrets: inherit strategy: # If one platform fails, allow the rest to keep testing if `CI-no-fail-fast` label is present @@ -248,6 +250,7 @@ jobs: rust: ${{ matrix.rust }} rust-target: ${{ matrix.platform.rust-target }} MSRV: ${{ needs.resolve.outputs.MSRV }} + verbose: ${{ needs.resolve.outputs.verbose == 'true' }} secrets: inherit strategy: # If one platform fails, allow the rest to keep testing if `CI-no-fail-fast` label is present From ec1a350afee0d9cb5a1bacd963e207ed9c84cb0f Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Wed, 14 May 2025 04:49:31 -0400 Subject: [PATCH 692/936] release: 0.25.0 (#5128) --- CHANGELOG.md | 62 ++++++++++++++++++- 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/3977.added.md | 1 - newsfragments/4811.fixed.md | 1 - newsfragments/4926.added.md | 4 -- newsfragments/4970.changed.md | 1 - newsfragments/4982.removed.md | 1 - newsfragments/5010.removed.md | 1 - newsfragments/5011.added.md | 1 - newsfragments/5037.fixed.md | 1 - newsfragments/5044.added.md | 1 - newsfragments/5054.added.md | 1 - newsfragments/5055.added.md | 1 - newsfragments/5055.changed.md | 1 - newsfragments/5057.added.md | 1 - newsfragments/5064.added.md | 1 - newsfragments/5064.changed.md | 3 - newsfragments/5064.removed.md | 2 - newsfragments/5070.fixed.md | 1 - newsfragments/5071.added.md | 3 - newsfragments/5071.changed.md | 3 - newsfragments/5071.removed.md | 1 - newsfragments/5079.changed.md | 1 - newsfragments/5085.added.md | 1 - newsfragments/5086.added.md | 3 - newsfragments/5091.fixed.md | 1 - newsfragments/5096.added.md | 1 - newsfragments/5097.removed.md | 1 - newsfragments/5101.added.md | 1 - newsfragments/5105.removed.md | 1 - newsfragments/5109.changed.md | 1 - newsfragments/5110.changed.md | 1 - newsfragments/5112.added.md | 1 - newsfragments/5114.added.md | 1 - newsfragments/5115.changed.md | 1 - newsfragments/5116.packaging.md | 1 - newsfragments/5117.added.md | 1 - newsfragments/5120.fixed.md | 1 - newsfragments/5123.fixed.md | 1 - newsfragments/5133.removed.md | 2 - pyo3-build-config/Cargo.toml | 2 +- pyo3-ffi/Cargo.toml | 4 +- pyo3-ffi/README.md | 4 +- pyo3-introspection/Cargo.toml | 2 +- pyo3-macros-backend/Cargo.toml | 6 +- pyo3-macros/Cargo.toml | 4 +- pyproject.toml | 2 +- tests/ui/reject_generics.stderr | 4 +- 54 files changed, 86 insertions(+), 77 deletions(-) delete mode 100644 newsfragments/3977.added.md delete mode 100644 newsfragments/4811.fixed.md delete mode 100644 newsfragments/4926.added.md delete mode 100644 newsfragments/4970.changed.md delete mode 100644 newsfragments/4982.removed.md delete mode 100644 newsfragments/5010.removed.md delete mode 100644 newsfragments/5011.added.md delete mode 100644 newsfragments/5037.fixed.md delete mode 100644 newsfragments/5044.added.md delete mode 100644 newsfragments/5054.added.md delete mode 100644 newsfragments/5055.added.md delete mode 100644 newsfragments/5055.changed.md delete mode 100644 newsfragments/5057.added.md delete mode 100644 newsfragments/5064.added.md delete mode 100644 newsfragments/5064.changed.md delete mode 100644 newsfragments/5064.removed.md delete mode 100644 newsfragments/5070.fixed.md delete mode 100644 newsfragments/5071.added.md delete mode 100644 newsfragments/5071.changed.md delete mode 100644 newsfragments/5071.removed.md delete mode 100644 newsfragments/5079.changed.md delete mode 100644 newsfragments/5085.added.md delete mode 100644 newsfragments/5086.added.md delete mode 100644 newsfragments/5091.fixed.md delete mode 100644 newsfragments/5096.added.md delete mode 100644 newsfragments/5097.removed.md delete mode 100644 newsfragments/5101.added.md delete mode 100644 newsfragments/5105.removed.md delete mode 100644 newsfragments/5109.changed.md delete mode 100644 newsfragments/5110.changed.md delete mode 100644 newsfragments/5112.added.md delete mode 100644 newsfragments/5114.added.md delete mode 100644 newsfragments/5115.changed.md delete mode 100644 newsfragments/5116.packaging.md delete mode 100644 newsfragments/5117.added.md delete mode 100644 newsfragments/5120.fixed.md delete mode 100644 newsfragments/5123.fixed.md delete mode 100644 newsfragments/5133.removed.md diff --git a/CHANGELOG.md b/CHANGELOG.md index fa9c80f7f2b..b13c92caef9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,65 @@ To see unreleased changes, please see the [CHANGELOG on the main branch guide](h +## [0.25.0] - 2025-05-14 + +### Packaging + +- Support Python 3.14.0b1. [#4811](https://github.com/PyO3/pyo3/pull/4811) +- Bump supported GraalPy version to 24.2. [#5116](https://github.com/PyO3/pyo3/pull/5116) +- Add optional `bigdecimal` dependency to add conversions for `bigdecimal::BigDecimal`. [#5011](https://github.com/PyO3/pyo3/pull/5011) +- Add optional `time` dependency to add conversions for `time` types. [#5057](https://github.com/PyO3/pyo3/pull/5057) +- Remove `cfg-if` dependency. [#5110](https://github.com/PyO3/pyo3/pull/5110) +- Add optional `ordered_float` dependency to add conversions for `ordered_float::NotNan` and `ordered_float::OrderedFloat`. [#5114](https://github.com/PyO3/pyo3/pull/5114) + +### Added + +- Add initial type stub generation to the `experimental-inspect` feature. [#3977](https://github.com/PyO3/pyo3/pull/3977) +- Add `#[pyclass(generic)]` option to support runtime generic typing. [#4926](https://github.com/PyO3/pyo3/pull/4926) +- Implement `OnceExt` & `MutexExt` for `parking_lot` & `lock_api`. Use the new extension traits by enabling the `arc_lock`, `lock_api`, or `parking_lot` cargo features. [#5044](https://github.com/PyO3/pyo3/pull/5044) +- Implement `From`/`Into` for `Borrowed` -> `Py`. [#5054](https://github.com/PyO3/pyo3/pull/5054) +- Add `PyTzInfo` constructors. [#5055](https://github.com/PyO3/pyo3/pull/5055) +- Add FFI definition `PY_INVALID_STACK_EFFECT`. [#5064](https://github.com/PyO3/pyo3/pull/5064) +- Implement `AsRef>` for `Py`, `Bound` and `Borrowed`. [#5071](https://github.com/PyO3/pyo3/pull/5071) +- Add FFI definition `PyModule_Add` and `compat::PyModule_Add`. [#5085](https://github.com/PyO3/pyo3/pull/5085) +- Add FFI definitions `Py_HashBuffer`, `Py_HashPointer`, and `PyObject_GenericHash`. [#5086](https://github.com/PyO3/pyo3/pull/5086) +- Support `#[pymodule_export]` on `const` items in declarative modules. [#5096](https://github.com/PyO3/pyo3/pull/5096) +- Add `#[pyclass(immutable_type)]` option (on Python 3.14+ with `abi3`, or 3.10+ otherwise) for immutable type objects. [#5101](https://github.com/PyO3/pyo3/pull/5101) +- Support `#[pyo3(rename_all)]` support on `#[derive(IntoPyObject)]`. [#5112](https://github.com/PyO3/pyo3/pull/5112) +- Add `PyRange` wrapper. [#5117](https://github.com/PyO3/pyo3/pull/5117) + +### Changed + +- Enable use of `datetime` types with `abi3` feature enabled. [#4970](https://github.com/PyO3/pyo3/pull/4970) +- Deprecate `timezone_utc` in favor of `PyTzInfo::utc`. [#5055](https://github.com/PyO3/pyo3/pull/5055) +- Reduce visibility of some CPython implementation details: [#5064](https://github.com/PyO3/pyo3/pull/5064) + - The FFI definition `PyCodeObject` is now an opaque struct on all Python versions. + - The FFI definition `PyFutureFeatures` is now only defined up until Python 3.10 (it was present in CPython headers but unused in 3.11 and 3.12). +- Change `PyAnyMethods::is` to take `other: &Bound`. [#5071](https://github.com/PyO3/pyo3/pull/5071) +- Change `Py::is` to take `other: &Py`. [#5071](https://github.com/PyO3/pyo3/pull/5071) +- Change `PyVisit::call` to take `T: Into>>`. [#5071](https://github.com/PyO3/pyo3/pull/5071) +- Expose `PyDateTime_DATE_GET_TZINFO` and `PyDateTime_TIME_GET_TZINFO` on PyPy 3.10 and later. [#5079](https://github.com/PyO3/pyo3/pull/5079) +- Add `#[track_caller]` to `with_gil` and `with_gil_unchecked`. [#5109](https://github.com/PyO3/pyo3/pull/5109) +- Use `std::thread::park()` instead of `libc::pause()` or `sleep(9999999)`. [#5115](https://github.com/PyO3/pyo3/pull/5115) + +### Removed + +- Remove all functionality deprecated in PyO3 0.23. [#4982](https://github.com/PyO3/pyo3/pull/4982) +- Remove deprecated `IntoPy` and `ToPyObject` traits. [#5010](https://github.com/PyO3/pyo3/pull/5010) +- Remove private types from `pyo3-ffi` (i.e. starting with `_Py`) which are not referenced by public APIs: `_PyLocalMonitors`, `_Py_GlobalMonitors`, `_PyCoCached`, `_PyCoLineInstrumentationData`, `_PyCoMonitoringData`, `_PyCompilerSrcLocation`, `_PyErr_StackItem`. [#5064](https://github.com/PyO3/pyo3/pull/5064) +- Remove FFI definition `PyCode_GetNumFree` (PyO3 cannot support it due to knowledge of the code object). [#5064](https://github.com/PyO3/pyo3/pull/5064) +- Remove `AsPyPointer` trait. [#5071](https://github.com/PyO3/pyo3/pull/5071) +- Remove support for the deprecated string form of `from_py_with`. [#5097](https://github.com/PyO3/pyo3/pull/5097) +- Remove FFI definitions of private static variables: `_PyMethodWrapper_Type`, `_PyCoroWrapper_Type`, `_PyImport_FrozenBootstrap`, `_PyImport_FrozenStdlib`, `_PyImport_FrozenTest`, `_PyManagedBuffer_Type`, `_PySet_Dummy`, `_PyWeakref_ProxyType`, and `_PyWeakref_CallableProxyType`. [#5105](https://github.com/PyO3/pyo3/pull/5105) +- Remove FFI definitions `PyASCIIObjectState`, `PyUnicode_IS_ASCII`, `PyUnicode_IS_COMPACT`, and `PyUnicode_IS_COMPACT_ASCII` on Python 3.14 and newer. [#5133](https://github.com/PyO3/pyo3/pull/5133) + +### Fixed + +- Correctly pick up the shared state for conda-based Python installation when reading information from sysconfigdata. [#5037](https://github.com/PyO3/pyo3/pull/5037) +- Fix compile failure with `#[derive(IntoPyObject, FromPyObject)]` when using `#[pyo3()]` options recognised by only one of the two derives. [#5070](https://github.com/PyO3/pyo3/pull/5070) +- Fix various compile errors from missing FFI definitions using certain feature combinations on PyPy and GraalPy. [#5091](https://github.com/PyO3/pyo3/pull/5091) +- Fallback on `backports.zoneinfo` for python <3.9 when converting timezones into python. [#5120](https://github.com/PyO3/pyo3/pull/5120) + ## [0.24.2] - 2025-04-21 ### Fixed @@ -2139,7 +2198,8 @@ Yanked - Initial release -[Unreleased]: https://github.com/pyo3/pyo3/compare/v0.24.2...HEAD +[Unreleased]: https://github.com/pyo3/pyo3/compare/v0.25.0...HEAD +[0.25.0]: https://github.com/pyo3/pyo3/compare/v0.24.2...v0.25.0 [0.24.2]: https://github.com/pyo3/pyo3/compare/v0.24.1...v0.24.2 [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 diff --git a/Cargo.toml b/Cargo.toml index c17c9bd4dbb..55e4a29170c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3" -version = "0.25.0-dev" +version = "0.25.0" description = "Bindings to Python interpreter" authors = ["PyO3 Project and Contributors "] readme = "README.md" @@ -20,10 +20,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.25.0-dev" } +pyo3-ffi = { path = "pyo3-ffi", version = "=0.25.0" } # support crates for macros feature -pyo3-macros = { path = "pyo3-macros", version = "=0.25.0-dev", optional = true } +pyo3-macros = { path = "pyo3-macros", version = "=0.25.0", optional = true } indoc = { version = "2.0.1", optional = true } unindent = { version = "0.2.1", optional = true } @@ -73,7 +73,7 @@ uuid = { version = "1.10.0", features = ["v4"] } parking_lot = { version = "0.12.3", features = ["arc_lock"]} [build-dependencies] -pyo3-build-config = { path = "pyo3-build-config", version = "=0.25.0-dev", features = ["resolve-config"] } +pyo3-build-config = { path = "pyo3-build-config", version = "=0.25.0", features = ["resolve-config"] } [features] default = ["macros"] diff --git a/README.md b/README.md index 8b5c5dd085b..47a74ec84a3 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ name = "string_sum" crate-type = ["cdylib"] [dependencies] -pyo3 = { version = "0.24.2", features = ["extension-module"] } +pyo3 = { version = "0.25.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.24.2" +version = "0.25.0" features = ["auto-initialize"] ``` diff --git a/examples/decorator/.template/pre-script.rhai b/examples/decorator/.template/pre-script.rhai index eb613152e8f..80ebe3ce006 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.2"); +variable::set("PYO3_VERSION", "0.25.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 eb613152e8f..80ebe3ce006 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.2"); +variable::set("PYO3_VERSION", "0.25.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 b65bf19a654..2a21f6b2e53 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.2"); +variable::set("PYO3_VERSION", "0.25.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 854b4662940..92eeabb3ea8 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.2"); +variable::set("PYO3_VERSION", "0.25.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 eb613152e8f..80ebe3ce006 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.2"); +variable::set("PYO3_VERSION", "0.25.0"); file::rename(".template/Cargo.toml", "Cargo.toml"); file::rename(".template/pyproject.toml", "pyproject.toml"); file::delete(".template"); diff --git a/newsfragments/3977.added.md b/newsfragments/3977.added.md deleted file mode 100644 index 58f116cd837..00000000000 --- a/newsfragments/3977.added.md +++ /dev/null @@ -1 +0,0 @@ -Basic introspection and stub generation based on metadata embedded in produced cdylib. \ No newline at end of file diff --git a/newsfragments/4811.fixed.md b/newsfragments/4811.fixed.md deleted file mode 100644 index cad2a26146b..00000000000 --- a/newsfragments/4811.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Bump supported cpython version to 3.14 for testing diff --git a/newsfragments/4926.added.md b/newsfragments/4926.added.md deleted file mode 100644 index a81a4155358..00000000000 --- a/newsfragments/4926.added.md +++ /dev/null @@ -1,4 +0,0 @@ -Introduced a new optional parameter with `#[pyclass(generic)]`. This new -parameter makes classes support generic typing at the runtime following -[PEP 560](https://peps.python.org/pep-0560/). It is an alternative -to inheriting from [typing.Generic](https://docs.python.org/3/library/typing.html#typing.Generic). \ No newline at end of file diff --git a/newsfragments/4970.changed.md b/newsfragments/4970.changed.md deleted file mode 100644 index 7ea1b6ae0cd..00000000000 --- a/newsfragments/4970.changed.md +++ /dev/null @@ -1 +0,0 @@ -- Make `datetime` rust wrappers compatible with `abi3` feature diff --git a/newsfragments/4982.removed.md b/newsfragments/4982.removed.md deleted file mode 100644 index 61f9dedbc48..00000000000 --- a/newsfragments/4982.removed.md +++ /dev/null @@ -1 +0,0 @@ -Remove all functionality deprecated in PyO3 0.23. diff --git a/newsfragments/5010.removed.md b/newsfragments/5010.removed.md deleted file mode 100644 index 9b42e682154..00000000000 --- a/newsfragments/5010.removed.md +++ /dev/null @@ -1 +0,0 @@ -Remove deprecated `IntoPy` and `ToPyObject` traits \ No newline at end of file diff --git a/newsfragments/5011.added.md b/newsfragments/5011.added.md deleted file mode 100644 index 741a25b3707..00000000000 --- a/newsfragments/5011.added.md +++ /dev/null @@ -1 +0,0 @@ -Added conversion support for `bigdecimal::BigDecimal` to and from python `Decimal` type \ No newline at end of file diff --git a/newsfragments/5037.fixed.md b/newsfragments/5037.fixed.md deleted file mode 100644 index 770a136f4f1..00000000000 --- a/newsfragments/5037.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Ensure we correctly pick up the shared state for conda-based Python installation when reading information from sysconfigdata. diff --git a/newsfragments/5044.added.md b/newsfragments/5044.added.md deleted file mode 100644 index c6898f277f4..00000000000 --- a/newsfragments/5044.added.md +++ /dev/null @@ -1 +0,0 @@ -Implement `OnceExt` & `MutexExt` for `parking_lot` & `lock_api`. Use the new extension traits by enabling the `arc_lock`, `lock_api`, or `parking_lot` cargo features. diff --git a/newsfragments/5054.added.md b/newsfragments/5054.added.md deleted file mode 100644 index 1400624a577..00000000000 --- a/newsfragments/5054.added.md +++ /dev/null @@ -1 +0,0 @@ -Implement `From`/`Into` for `Borrowed` -> `Py` diff --git a/newsfragments/5055.added.md b/newsfragments/5055.added.md deleted file mode 100644 index 75f013f2bb7..00000000000 --- a/newsfragments/5055.added.md +++ /dev/null @@ -1 +0,0 @@ -Add `PyTzInfo` constructors diff --git a/newsfragments/5055.changed.md b/newsfragments/5055.changed.md deleted file mode 100644 index 1088f0c92ec..00000000000 --- a/newsfragments/5055.changed.md +++ /dev/null @@ -1 +0,0 @@ -Deprecated `timezone_utc` in favor of `PyTzInfo::utc` diff --git a/newsfragments/5057.added.md b/newsfragments/5057.added.md deleted file mode 100644 index 0bfcfaa9d10..00000000000 --- a/newsfragments/5057.added.md +++ /dev/null @@ -1 +0,0 @@ -Integrate `time` crate into PyO3 \ No newline at end of file diff --git a/newsfragments/5064.added.md b/newsfragments/5064.added.md deleted file mode 100644 index 21e2cce6430..00000000000 --- a/newsfragments/5064.added.md +++ /dev/null @@ -1 +0,0 @@ -Add FFI definition `PY_INVALID_STACK_EFFECT`. diff --git a/newsfragments/5064.changed.md b/newsfragments/5064.changed.md deleted file mode 100644 index 0ca3ad7d542..00000000000 --- a/newsfragments/5064.changed.md +++ /dev/null @@ -1,3 +0,0 @@ -Reduce visibility of some CPython implementation details: -- The FFI definition `PyCodeObject` is now an opaque struct on all Python versions. -- The FFI definition `PyFutureFeatures` is now only defined up until Python 3.10 (it was present in CPython headers but unused in 3.11 and 3.12). diff --git a/newsfragments/5064.removed.md b/newsfragments/5064.removed.md deleted file mode 100644 index f1a04a78aa7..00000000000 --- a/newsfragments/5064.removed.md +++ /dev/null @@ -1,2 +0,0 @@ -Remove private types from `pyo3-ffi` (i.e. starting with `_Py`) which are not referenced by public APIs: `_PyLocalMonitors`, `_Py_GlobalMonitors`, `_PyCoCached`, `_PyCoLineInstrumentationData`, `_PyCoMonitoringData`, `_PyCompilerSrcLocation`, `_PyErr_StackItem`. -Remove FFI definition `PyCode_GetNumFree` (PyO3 cannot support it due to knowledge of the code object). diff --git a/newsfragments/5070.fixed.md b/newsfragments/5070.fixed.md deleted file mode 100644 index 813bd7e0c4c..00000000000 --- a/newsfragments/5070.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fix compile failure with `#[derive(IntoPyObject, FromPyObject)]` when using `#[pyo3()]` options recognised by only one of the two derives. \ No newline at end of file diff --git a/newsfragments/5071.added.md b/newsfragments/5071.added.md deleted file mode 100644 index 96ae6761c4a..00000000000 --- a/newsfragments/5071.added.md +++ /dev/null @@ -1,3 +0,0 @@ -- `AsRef> for Py` -- `AsRef> for Bound` -- `AsRef> for Borrowed` \ No newline at end of file diff --git a/newsfragments/5071.changed.md b/newsfragments/5071.changed.md deleted file mode 100644 index 0cd2f175b83..00000000000 --- a/newsfragments/5071.changed.md +++ /dev/null @@ -1,3 +0,0 @@ -- `PyAnyMethods::is` now takes `other: &Bound` -- `Py::is` now takes `other: &Py` -- `PyVisit::call` now takes `T: Into>>` \ No newline at end of file diff --git a/newsfragments/5071.removed.md b/newsfragments/5071.removed.md deleted file mode 100644 index 2d037b74274..00000000000 --- a/newsfragments/5071.removed.md +++ /dev/null @@ -1 +0,0 @@ -removed `AsPyPointer` trait \ No newline at end of file diff --git a/newsfragments/5079.changed.md b/newsfragments/5079.changed.md deleted file mode 100644 index c5321466994..00000000000 --- a/newsfragments/5079.changed.md +++ /dev/null @@ -1 +0,0 @@ -Expose `PyDateTime_DATE_GET_TZINFO` and `PyDateTime_TIME_GET_TZINFO` on PyPy 3.10 and later diff --git a/newsfragments/5085.added.md b/newsfragments/5085.added.md deleted file mode 100644 index b11570327cf..00000000000 --- a/newsfragments/5085.added.md +++ /dev/null @@ -1 +0,0 @@ -add `PyModule_Add` to `pyo3-ffi` diff --git a/newsfragments/5086.added.md b/newsfragments/5086.added.md deleted file mode 100644 index b79e62d77fc..00000000000 --- a/newsfragments/5086.added.md +++ /dev/null @@ -1,3 +0,0 @@ -- add `Py_HashBuffer` to `pyo3-ffi` -- add `Py_HashPointer` to `pyo3-ffi` -- add `PyObject_GenericHash` to `pyo3-ffi` diff --git a/newsfragments/5091.fixed.md b/newsfragments/5091.fixed.md deleted file mode 100644 index 2f061195027..00000000000 --- a/newsfragments/5091.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fix various compile errors from missing FFI definitions using certain feature combinations on PyPy and GraalPy. diff --git a/newsfragments/5096.added.md b/newsfragments/5096.added.md deleted file mode 100644 index 9d5c12a99db..00000000000 --- a/newsfragments/5096.added.md +++ /dev/null @@ -1 +0,0 @@ -declarative modules can now `#[pymodule_export]` `const` items \ No newline at end of file diff --git a/newsfragments/5097.removed.md b/newsfragments/5097.removed.md deleted file mode 100644 index a2fb6998959..00000000000 --- a/newsfragments/5097.removed.md +++ /dev/null @@ -1 +0,0 @@ -removed support for the deprecated string from of `from_py_with` \ No newline at end of file diff --git a/newsfragments/5101.added.md b/newsfragments/5101.added.md deleted file mode 100644 index 8481dcc7f82..00000000000 --- a/newsfragments/5101.added.md +++ /dev/null @@ -1 +0,0 @@ -added `immutable_type` pyclass option (on Python 3.14+ with `abi3`, or 3.10+ otherwise) for immutable type objects \ No newline at end of file diff --git a/newsfragments/5105.removed.md b/newsfragments/5105.removed.md deleted file mode 100644 index d1a0f916bd9..00000000000 --- a/newsfragments/5105.removed.md +++ /dev/null @@ -1 +0,0 @@ -Remove FFI definitions of private static variables: `_PyMethodWrapper_Type`, `_PyCoroWrapper_Type`, `_PyImport_FrozenBootstrap`, `_PyImport_FrozenStdlib`, `_PyImport_FrozenTest`, `_PyManagedBuffer_Type`, `_PySet_Dummy`, `_PyWeakref_ProxyType`, and `_PyWeakref_CallableProxyType`. diff --git a/newsfragments/5109.changed.md b/newsfragments/5109.changed.md deleted file mode 100644 index 0f46eb936e6..00000000000 --- a/newsfragments/5109.changed.md +++ /dev/null @@ -1 +0,0 @@ -Add `#[track_caller]` to `with_gil` and `with_gil_unchecked`. diff --git a/newsfragments/5110.changed.md b/newsfragments/5110.changed.md deleted file mode 100644 index 262d1b36617..00000000000 --- a/newsfragments/5110.changed.md +++ /dev/null @@ -1 +0,0 @@ -Remove `cfg-if` dependency. diff --git a/newsfragments/5112.added.md b/newsfragments/5112.added.md deleted file mode 100644 index 5320d26bb6f..00000000000 --- a/newsfragments/5112.added.md +++ /dev/null @@ -1 +0,0 @@ -add `rename_all` support for `#[derive(IntoPyObject)]` \ No newline at end of file diff --git a/newsfragments/5114.added.md b/newsfragments/5114.added.md deleted file mode 100644 index 90d530e6747..00000000000 --- a/newsfragments/5114.added.md +++ /dev/null @@ -1 +0,0 @@ -Added conversion support for `ordered_float::NotNan` & `ordered_float::OrderedFloat` to and from python native float type diff --git a/newsfragments/5115.changed.md b/newsfragments/5115.changed.md deleted file mode 100644 index df91b7c36a8..00000000000 --- a/newsfragments/5115.changed.md +++ /dev/null @@ -1 +0,0 @@ -Use `std::thread::park()` instead of `libc::pause()` or `sleep(9999999)`. diff --git a/newsfragments/5116.packaging.md b/newsfragments/5116.packaging.md deleted file mode 100644 index a0675b626a4..00000000000 --- a/newsfragments/5116.packaging.md +++ /dev/null @@ -1 +0,0 @@ -Bump supported GraalPy version to 24.2. diff --git a/newsfragments/5117.added.md b/newsfragments/5117.added.md deleted file mode 100644 index 46ff2f5d411..00000000000 --- a/newsfragments/5117.added.md +++ /dev/null @@ -1 +0,0 @@ -Add `PyRange` wrapper. diff --git a/newsfragments/5120.fixed.md b/newsfragments/5120.fixed.md deleted file mode 100644 index 65db11b3f95..00000000000 --- a/newsfragments/5120.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fallback on `backports.zoneinfo` for python <3.9 when converting timezones into python. diff --git a/newsfragments/5123.fixed.md b/newsfragments/5123.fixed.md deleted file mode 100644 index 1033fcd559a..00000000000 --- a/newsfragments/5123.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fix FFI definitions for 3.14 beta 1 diff --git a/newsfragments/5133.removed.md b/newsfragments/5133.removed.md deleted file mode 100644 index bfcb550e58f..00000000000 --- a/newsfragments/5133.removed.md +++ /dev/null @@ -1,2 +0,0 @@ -* Removed PyASCIIObjectState from the FFI bindings on Python 3.14 and newer. -* Removed PyUnicode_IS_ASCII, PyUnicode_IS_COMPACT, and PyUnicode_IS_COMPACT_ASCII from the FFI bindings on Python 3.14 and newer, since they relied on PyASCIIObjectState. \ No newline at end of file diff --git a/pyo3-build-config/Cargo.toml b/pyo3-build-config/Cargo.toml index b2bdc760687..8070860a1e9 100644 --- a/pyo3-build-config/Cargo.toml +++ b/pyo3-build-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-build-config" -version = "0.25.0-dev" +version = "0.25.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 e4f4498780d..647b76fbdcd 100644 --- a/pyo3-ffi/Cargo.toml +++ b/pyo3-ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-ffi" -version = "0.25.0-dev" +version = "0.25.0" description = "Python-API bindings for the PyO3 ecosystem" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -44,7 +44,7 @@ generate-import-lib = ["pyo3-build-config/python3-dll-a"] paste = "1" [build-dependencies] -pyo3-build-config = { path = "../pyo3-build-config", version = "=0.25.0-dev", features = ["resolve-config"] } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.25.0", features = ["resolve-config"] } [lints] workspace = true diff --git a/pyo3-ffi/README.md b/pyo3-ffi/README.md index 9b96da15b78..09be0a06f66 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.2" +version = "0.25.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.24.2" +pyo3_build_config = "0.25.0" ``` If you need to use conditional compilation based on Python version or how diff --git a/pyo3-introspection/Cargo.toml b/pyo3-introspection/Cargo.toml index d861ec7b6d5..98dfdebbd10 100644 --- a/pyo3-introspection/Cargo.toml +++ b/pyo3-introspection/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-introspection" -version = "0.25.0-dev" +version = "0.25.0" description = "Introspect dynamic libraries built with PyO3 to get metadata about the exported Python types" authors = ["PyO3 Project and Contributors "] homepage = "https://github.com/pyo3/pyo3" diff --git a/pyo3-macros-backend/Cargo.toml b/pyo3-macros-backend/Cargo.toml index 5730df1ae1d..d626fa1ebf7 100644 --- a/pyo3-macros-backend/Cargo.toml +++ b/pyo3-macros-backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-macros-backend" -version = "0.25.0-dev" +version = "0.25.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.25.0-dev", features = ["resolve-config"] } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.25.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.25.0-dev" } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.25.0" } [lints] workspace = true diff --git a/pyo3-macros/Cargo.toml b/pyo3-macros/Cargo.toml index c7bd43fad69..7df15c23f76 100644 --- a/pyo3-macros/Cargo.toml +++ b/pyo3-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-macros" -version = "0.25.0-dev" +version = "0.25.0" description = "Proc macros for PyO3 package" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -23,7 +23,7 @@ experimental-inspect = ["pyo3-macros-backend/experimental-inspect"] 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.25.0-dev" } +pyo3-macros-backend = { path = "../pyo3-macros-backend", version = "=0.25.0" } [lints] workspace = true diff --git a/pyproject.toml b/pyproject.toml index 1ff0f0332de..adfe3a27348 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ [tool.towncrier] filename = "CHANGELOG.md" -version = "0.25.0-dev" +version = "0.25.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 fed5d48937e..9b16f1d68e7 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.25.0-dev/class.html#no-generic-parameters +error: #[pyclass] cannot have generic parameters. For an explanation, see https://pyo3.rs/v0.25.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.25.0-dev/class.html#no-lifetime-parameters +error: #[pyclass] cannot have lifetime parameters. For an explanation, see https://pyo3.rs/v0.25.0/class.html#no-lifetime-parameters --> tests/ui/reject_generics.rs:9:27 | 9 | struct ClassWithLifetimes<'a> { From 0e298e5d73bec3f45245107d3a5c80f7cab5d2c9 Mon Sep 17 00:00:00 2001 From: Thomas Tanon Date: Wed, 14 May 2025 10:42:06 +0200 Subject: [PATCH 693/936] Implements basic method introspection (#5087) * Implements basic method introspection * Code review feedback * Fixes CI --- pyo3-introspection/src/introspection.rs | 189 ++++++++++++++++------- pyo3-introspection/src/model.rs | 3 + pyo3-introspection/src/stubs.rs | 48 +++++- pyo3-introspection/tests/test.rs | 5 +- pyo3-macros-backend/src/introspection.rs | 77 +++++++-- pyo3-macros-backend/src/pyfunction.rs | 10 +- pyo3-macros-backend/src/pyimpl.rs | 71 ++++++++- pyo3-macros-backend/src/pymethod.rs | 14 +- pytests/src/pyclasses.rs | 42 ++++- pytests/stubs/pyclasses.pyi | 34 +++- pytests/tests/test_pyclasses.py | 29 ++++ 11 files changed, 421 insertions(+), 101 deletions(-) diff --git a/pyo3-introspection/src/introspection.rs b/pyo3-introspection/src/introspection.rs index e4f49d5e0e3..665109d0a66 100644 --- a/pyo3-introspection/src/introspection.rs +++ b/pyo3-introspection/src/introspection.rs @@ -7,6 +7,7 @@ use goblin::mach::{Mach, MachO, SingleArch}; use goblin::pe::PE; use goblin::Object; use serde::Deserialize; +use std::cmp::Ordering; use std::collections::HashMap; use std::fs; use std::path::Path; @@ -21,19 +22,23 @@ pub fn introspect_cdylib(library_path: impl AsRef, main_module_name: &str) /// Parses the introspection chunks found in the binary fn parse_chunks(chunks: &[Chunk], main_module_name: &str) -> Result { - let chunks_by_id = chunks - .iter() - .map(|c| { - ( - match c { - Chunk::Module { id, .. } => id, - Chunk::Class { id, .. } => id, - Chunk::Function { id, .. } => id, - }, - c, - ) - }) - .collect::>(); + let mut chunks_by_id = HashMap::<&str, &Chunk>::new(); + let mut chunks_by_parent = HashMap::<&str, Vec<&Chunk>>::new(); + for chunk in chunks { + if let Some(id) = match chunk { + Chunk::Module { id, .. } => Some(id), + Chunk::Class { id, .. } => Some(id), + Chunk::Function { id, .. } => id.as_ref(), + } { + chunks_by_id.insert(id, chunk); + } + if let Some(parent) = match chunk { + Chunk::Module { .. } | Chunk::Class { .. } => None, + Chunk::Function { parent, .. } => parent.as_ref(), + } { + chunks_by_parent.entry(parent).or_default().push(chunk); + } + } // We look for the root chunk for chunk in chunks { if let Chunk::Module { @@ -43,7 +48,7 @@ fn parse_chunks(chunks: &[Chunk], main_module_name: &str) -> Result { } = chunk { if name == main_module_name { - return convert_module(name, members, &chunks_by_id); + return convert_module(name, members, &chunks_by_id, &chunks_by_parent); } } } @@ -53,61 +58,126 @@ fn parse_chunks(chunks: &[Chunk], main_module_name: &str) -> Result { fn convert_module( name: &str, members: &[String], - chunks_by_id: &HashMap<&String, &Chunk>, + chunks_by_id: &HashMap<&str, &Chunk>, + chunks_by_parent: &HashMap<&str, Vec<&Chunk>>, ) -> Result { + let (modules, classes, functions) = convert_members( + &members + .iter() + .filter_map(|id| chunks_by_id.get(id.as_str()).copied()) + .collect::>(), + chunks_by_id, + chunks_by_parent, + )?; + Ok(Module { + name: name.into(), + modules, + classes, + functions, + }) +} + +/// Convert a list of members of a module or a class +fn convert_members( + chunks: &[&Chunk], + chunks_by_id: &HashMap<&str, &Chunk>, + chunks_by_parent: &HashMap<&str, Vec<&Chunk>>, +) -> Result<(Vec, Vec, Vec)> { let mut modules = Vec::new(); let mut classes = Vec::new(); let mut functions = Vec::new(); - for member in members { - if let Some(chunk) = chunks_by_id.get(member) { - match chunk { - Chunk::Module { + for chunk in chunks { + match chunk { + Chunk::Module { + name, + members, + id: _, + } => { + modules.push(convert_module( name, members, - id: _, - } => { - modules.push(convert_module(name, members, chunks_by_id)?); - } - Chunk::Class { name, id: _ } => classes.push(Class { name: name.into() }), - Chunk::Function { - name, - id: _, - arguments, - } => functions.push(Function { - name: name.into(), - arguments: Arguments { - positional_only_arguments: arguments - .posonlyargs - .iter() - .map(convert_argument) - .collect(), - arguments: arguments.args.iter().map(convert_argument).collect(), - vararg: arguments - .vararg - .as_ref() - .map(convert_variable_length_argument), - keyword_only_arguments: arguments - .kwonlyargs - .iter() - .map(convert_argument) - .collect(), - kwarg: arguments - .kwarg - .as_ref() - .map(convert_variable_length_argument), - }, - }), + chunks_by_id, + chunks_by_parent, + )?); } + Chunk::Class { name, id } => { + classes.push(convert_class(id, name, chunks_by_id, chunks_by_parent)?) + } + Chunk::Function { + name, + id: _, + arguments, + parent: _, + decorators, + } => functions.push(convert_function(name, arguments, decorators)), } } - Ok(Module { + Ok((modules, classes, functions)) +} + +fn convert_class( + id: &str, + name: &str, + chunks_by_id: &HashMap<&str, &Chunk>, + chunks_by_parent: &HashMap<&str, Vec<&Chunk>>, +) -> Result { + let (nested_modules, nested_classes, mut methods) = convert_members( + chunks_by_parent + .get(&id) + .map(Vec::as_slice) + .unwrap_or_default(), + chunks_by_id, + chunks_by_parent, + )?; + ensure!( + nested_modules.is_empty(), + "Classes cannot contain nested modules" + ); + ensure!( + nested_classes.is_empty(), + "Nested classes are not supported yet" + ); + // We sort methods to get a stable output + methods.sort_by(|l, r| match l.name.cmp(&r.name) { + Ordering::Equal => { + // We put the getter before the setter + if l.decorators.iter().any(|d| d == "property") { + Ordering::Less + } else if r.decorators.iter().any(|d| d == "property") { + Ordering::Greater + } else { + // We pick an ordering based on decorators + l.decorators.cmp(&r.decorators) + } + } + o => o, + }); + Ok(Class { name: name.into(), - modules, - classes, - functions, + methods, }) } +fn convert_function(name: &str, arguments: &ChunkArguments, decorators: &[String]) -> Function { + Function { + name: name.into(), + decorators: decorators.to_vec(), + arguments: Arguments { + positional_only_arguments: arguments.posonlyargs.iter().map(convert_argument).collect(), + arguments: arguments.args.iter().map(convert_argument).collect(), + vararg: arguments + .vararg + .as_ref() + .map(convert_variable_length_argument), + keyword_only_arguments: arguments.kwonlyargs.iter().map(convert_argument).collect(), + kwarg: arguments + .kwarg + .as_ref() + .map(convert_variable_length_argument), + }, + } +} + fn convert_argument(arg: &ChunkArgument) -> Argument { Argument { name: arg.name.clone(), @@ -290,9 +360,14 @@ enum Chunk { name: String, }, Function { - id: String, + #[serde(default)] + id: Option, name: String, arguments: ChunkArguments, + #[serde(default)] + parent: Option, + #[serde(default)] + decorators: Vec, }, } diff --git a/pyo3-introspection/src/model.rs b/pyo3-introspection/src/model.rs index 7705a0006a4..021475b9392 100644 --- a/pyo3-introspection/src/model.rs +++ b/pyo3-introspection/src/model.rs @@ -9,11 +9,14 @@ pub struct Module { #[derive(Debug, Eq, PartialEq, Clone, Hash)] pub struct Class { pub name: String, + pub methods: Vec, } #[derive(Debug, Eq, PartialEq, Clone, Hash)] pub struct Function { pub name: String, + /// decorator like 'property' or 'staticmethod' + pub decorators: Vec, pub arguments: Arguments, } diff --git a/pyo3-introspection/src/stubs.rs b/pyo3-introspection/src/stubs.rs index 2312d7d37ac..cc0a11ebd38 100644 --- a/pyo3-introspection/src/stubs.rs +++ b/pyo3-introspection/src/stubs.rs @@ -39,12 +39,39 @@ fn module_stubs(module: &Module) -> String { for function in &module.functions { elements.push(function_stubs(function)); } - elements.push(String::new()); // last line jump - elements.join("\n") + + // We insert two line jumps (i.e. empty strings) only above and below multiple line elements (classes with methods, functions with decorators) + let mut output = String::new(); + for element in elements { + let is_multiline = element.contains('\n'); + if is_multiline && !output.is_empty() && !output.ends_with("\n\n") { + output.push('\n'); + } + output.push_str(&element); + output.push('\n'); + if is_multiline { + output.push('\n'); + } + } + // We remove a line jump at the end if they are two + if output.ends_with("\n\n") { + output.pop(); + } + output } fn class_stubs(class: &Class) -> String { - format!("class {}: ...", class.name) + let mut buffer = format!("class {}:", class.name); + if class.methods.is_empty() { + buffer.push_str(" ..."); + return buffer; + } + for method in &class.methods { + // We do the indentation + buffer.push_str("\n "); + buffer.push_str(&function_stubs(method).replace('\n', "\n ")); + } + buffer } fn function_stubs(function: &Function) -> String { @@ -70,7 +97,18 @@ fn function_stubs(function: &Function) -> String { if let Some(argument) = &function.arguments.kwarg { parameters.push(format!("**{}", variable_length_argument_stub(argument))); } - format!("def {}({}): ...", function.name, parameters.join(", ")) + let output = format!("def {}({}): ...", function.name, parameters.join(", ")); + if function.decorators.is_empty() { + return output; + } + let mut buffer = String::new(); + for decorator in &function.decorators { + buffer.push('@'); + buffer.push_str(decorator); + buffer.push('\n'); + } + buffer.push_str(&output); + buffer } fn argument_stub(argument: &Argument) -> String { @@ -95,6 +133,7 @@ mod tests { fn function_stubs_with_variable_length() { let function = Function { name: "func".into(), + decorators: Vec::new(), arguments: Arguments { positional_only_arguments: vec![Argument { name: "posonly".into(), @@ -126,6 +165,7 @@ mod tests { fn function_stubs_without_variable_length() { let function = Function { name: "afunc".into(), + decorators: Vec::new(), arguments: Arguments { positional_only_arguments: vec![Argument { name: "posonly".into(), diff --git a/pyo3-introspection/tests/test.rs b/pyo3-introspection/tests/test.rs index 37070a53a13..302efd1ea47 100644 --- a/pyo3-introspection/tests/test.rs +++ b/pyo3-introspection/tests/test.rs @@ -42,9 +42,10 @@ fn pytests_stubs() -> Result<()> { file_name.display() ) }); + // We normalize line jumps for compatibility with Windows assert_eq!( - &expected_file_content.replace('\r', ""), // Windows compatibility - actual_file_content, + expected_file_content.replace('\r', ""), + actual_file_content.replace('\r', ""), "The content of file {} is different", file_name.display() ) diff --git a/pyo3-macros-backend/src/introspection.rs b/pyo3-macros-backend/src/introspection.rs index 4888417cb08..303e952b814 100644 --- a/pyo3-macros-backend/src/introspection.rs +++ b/pyo3-macros-backend/src/introspection.rs @@ -19,7 +19,7 @@ use std::collections::HashMap; use std::hash::{Hash, Hasher}; use std::mem::take; use std::sync::atomic::{AtomicUsize, Ordering}; -use syn::{Attribute, Ident}; +use syn::{Attribute, Ident, Type, TypePath}; static GLOBAL_COUNTER_FOR_UNIQUE_NAMES: AtomicUsize = AtomicUsize::new(0); @@ -42,7 +42,9 @@ pub fn module_introspection_code<'a>( .zip(members_cfg_attrs) .filter_map(|(member, attributes)| { if attributes.is_empty() { - Some(IntrospectionNode::IntrospectionId(Some(member))) + Some(IntrospectionNode::IntrospectionId(Some(ident_to_type( + member, + )))) } else { None // TODO: properly interpret cfg attributes } @@ -64,7 +66,10 @@ pub fn class_introspection_code( IntrospectionNode::Map( [ ("type", IntrospectionNode::String("class".into())), - ("id", IntrospectionNode::IntrospectionId(Some(ident))), + ( + "id", + IntrospectionNode::IntrospectionId(Some(ident_to_type(ident))), + ), ("name", IntrospectionNode::String(name.into())), ] .into(), @@ -74,23 +79,47 @@ pub fn class_introspection_code( pub fn function_introspection_code( pyo3_crate_path: &PyO3CratePath, - ident: &Ident, + ident: Option<&Ident>, name: &str, signature: &FunctionSignature<'_>, + first_argument: Option<&'static str>, + decorators: impl IntoIterator, + parent: Option<&Type>, ) -> TokenStream { - IntrospectionNode::Map( - [ - ("type", IntrospectionNode::String("function".into())), - ("id", IntrospectionNode::IntrospectionId(Some(ident))), - ("name", IntrospectionNode::String(name.into())), - ("arguments", arguments_introspection_data(signature)), - ] - .into(), - ) - .emit(pyo3_crate_path) + let mut desc = HashMap::from([ + ("type", IntrospectionNode::String("function".into())), + ("name", IntrospectionNode::String(name.into())), + ( + "arguments", + arguments_introspection_data(signature, first_argument), + ), + ]); + if let Some(ident) = ident { + desc.insert( + "id", + IntrospectionNode::IntrospectionId(Some(ident_to_type(ident))), + ); + } + let decorators = decorators + .into_iter() + .map(|d| IntrospectionNode::String(d.into())) + .collect::>(); + if !decorators.is_empty() { + desc.insert("decorators", IntrospectionNode::List(decorators)); + } + if let Some(parent) = parent { + desc.insert( + "parent", + IntrospectionNode::IntrospectionId(Some(Cow::Borrowed(parent))), + ); + } + IntrospectionNode::Map(desc).emit(pyo3_crate_path) } -fn arguments_introspection_data<'a>(signature: &'a FunctionSignature<'a>) -> IntrospectionNode<'a> { +fn arguments_introspection_data<'a>( + signature: &'a FunctionSignature<'a>, + first_argument: Option<&'a str>, +) -> IntrospectionNode<'a> { let mut argument_desc = signature.arguments.iter().filter_map(|arg| { if let FnArg::Regular(arg) = arg { Some(arg) @@ -105,6 +134,12 @@ fn arguments_introspection_data<'a>(signature: &'a FunctionSignature<'a>) -> Int let mut kwonlyargs = Vec::new(); let mut kwarg = None; + if let Some(first_argument) = first_argument { + posonlyargs.push(IntrospectionNode::Map( + [("name", IntrospectionNode::String(first_argument.into()))].into(), + )); + } + for (i, param) in signature .python_signature .positional_parameters @@ -184,7 +219,7 @@ fn argument_introspection_data<'a>( enum IntrospectionNode<'a> { String(Cow<'a, str>), - IntrospectionId(Option<&'a Ident>), + IntrospectionId(Option>), Map(HashMap<&'static str, IntrospectionNode<'a>>), List(Vec>), } @@ -333,3 +368,13 @@ fn unique_element_id() -> u64 { .hash(&mut hasher); // If there are multiple elements in the same call site hasher.finish() } + +fn ident_to_type(ident: &Ident) -> Cow<'static, Type> { + Cow::Owned( + TypePath { + path: ident.clone().into(), + qself: None, + } + .into(), + ) +} diff --git a/pyo3-macros-backend/src/pyfunction.rs b/pyo3-macros-backend/src/pyfunction.rs index 301819f42dd..e512ca1cabc 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -230,7 +230,15 @@ pub fn impl_wrap_pyfunction( let name = &func.sig.ident; #[cfg(feature = "experimental-inspect")] - let introspection = function_introspection_code(pyo3_path, name, &name.to_string(), &signature); + let introspection = function_introspection_code( + pyo3_path, + Some(name), + &name.to_string(), + &signature, + None, + [] as [String; 0], + None, + ); #[cfg(not(feature = "experimental-inspect"))] let introspection = quote! {}; #[cfg(feature = "experimental-inspect")] diff --git a/pyo3-macros-backend/src/pyimpl.rs b/pyo3-macros-backend/src/pyimpl.rs index 72f06721ec4..90b0d961cd8 100644 --- a/pyo3-macros-backend/src/pyimpl.rs +++ b/pyo3-macros-backend/src/pyimpl.rs @@ -1,14 +1,19 @@ use std::collections::HashSet; +#[cfg(feature = "experimental-inspect")] +use crate::introspection::function_introspection_code; +#[cfg(feature = "experimental-inspect")] +use crate::method::{FnSpec, FnType}; use crate::utils::{has_attribute, has_attribute_with_namespace, Ctx, PyO3CratePath}; use crate::{ attributes::{take_pyo3_options, CrateAttribute}, konst::{ConstAttributes, ConstSpec}, pyfunction::PyFunctionOptions, - pymethod::{self, is_proto_method, MethodAndMethodDef, MethodAndSlotDef}, + pymethod::{ + self, is_proto_method, GeneratedPyMethod, MethodAndMethodDef, MethodAndSlotDef, PyMethod, + }, }; use proc_macro2::TokenStream; -use pymethod::GeneratedPyMethod; use quote::{format_ident, quote}; use syn::ImplItemFn; use syn::{ @@ -110,7 +115,7 @@ pub fn impl_methods( methods_type: PyClassMethodsType, options: PyImplOptions, ) -> syn::Result { - let mut trait_impls = Vec::new(); + let mut extra_fragments = Vec::new(); let mut proto_impls = Vec::new(); let mut methods = Vec::new(); let mut associated_methods = Vec::new(); @@ -125,9 +130,10 @@ pub fn impl_methods( 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)? - { + let method = PyMethod::parse(&mut meth.sig, &mut meth.attrs, fun_options)?; + #[cfg(feature = "experimental-inspect")] + extra_fragments.push(method_introspection_code(&method.spec, ty, ctx)); + match pymethod::gen_py_method(ty, method, &meth.attrs, ctx)? { GeneratedPyMethod::Method(MethodAndMethodDef { associated_method, method_def, @@ -139,7 +145,7 @@ pub fn impl_methods( GeneratedPyMethod::SlotTraitImpl(method_name, token_stream) => { implemented_proto_fragments.insert(method_name); let attrs = get_cfg_attributes(&meth.attrs); - trait_impls.push(quote!(#(#attrs)* #token_stream)); + extra_fragments.push(quote!(#(#attrs)* #token_stream)); } GeneratedPyMethod::Proto(MethodAndSlotDef { associated_method, @@ -193,7 +199,7 @@ pub fn impl_methods( }; Ok(quote! { - #(#trait_impls)* + #(#extra_fragments)* #items @@ -336,3 +342,52 @@ pub(crate) fn get_cfg_attributes(attrs: &[syn::Attribute]) -> Vec<&syn::Attribut .filter(|attr| attr.path().is_ident("cfg")) .collect() } + +#[cfg(feature = "experimental-inspect")] +fn method_introspection_code(spec: &FnSpec<'_>, parent: &syn::Type, ctx: &Ctx) -> TokenStream { + let Ctx { pyo3_path, .. } = ctx; + + // We introduce self/cls argument and setup decorators + let name = spec.python_name.to_string(); + let mut first_argument = None; + let mut decorators = Vec::new(); + match &spec.tp { + FnType::Getter(_) => { + first_argument = Some("self"); + decorators.push("property".into()); + } + FnType::Setter(_) => { + first_argument = Some("self"); + decorators.push(format!("{name}.setter")); + } + FnType::Fn(_) => { + first_argument = Some("self"); + } + FnType::FnNew | FnType::FnNewClass(_) => { + first_argument = Some("cls"); + } + FnType::FnClass(_) => { + first_argument = Some("cls"); + decorators.push("classmethod".into()); + } + FnType::FnStatic => { + decorators.push("staticmethod".into()); + } + FnType::FnModule(_) => (), // TODO: not sure this can happen + FnType::ClassAttribute => { + first_argument = Some("cls"); + // TODO: this combination only works with Python 3.9-3.11 https://docs.python.org/3.11/library/functions.html#classmethod + decorators.push("classmethod".into()); + decorators.push("property".into()); + } + } + function_introspection_code( + pyo3_path, + None, + &name, + &spec.signature, + first_argument, + decorators, + Some(parent), + ) +} diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index 15cce6f365f..434a96e9dde 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -40,7 +40,7 @@ pub enum GeneratedPyMethod { pub struct PyMethod<'a> { kind: PyMethodKind, method_name: String, - spec: FnSpec<'a>, + pub spec: FnSpec<'a>, } enum PyMethodKind { @@ -160,11 +160,13 @@ enum PyMethodProtoKind { } impl<'a> PyMethod<'a> { - fn parse( + pub fn parse( sig: &'a mut syn::Signature, meth_attrs: &mut Vec, options: PyFunctionOptions, ) -> Result { + check_generic(sig)?; + ensure_function_options_valid(&options)?; let spec = FnSpec::parse(sig, meth_attrs, options)?; let method_name = spec.python_name.to_string(); @@ -187,14 +189,10 @@ pub fn is_proto_method(name: &str) -> bool { pub fn gen_py_method( cls: &syn::Type, - sig: &mut syn::Signature, - meth_attrs: &mut Vec, - options: PyFunctionOptions, + method: PyMethod<'_>, + meth_attrs: &[syn::Attribute], ctx: &Ctx, ) -> Result { - check_generic(sig)?; - ensure_function_options_valid(&options)?; - let method = PyMethod::parse(sig, meth_attrs, options)?; let spec = &method.spec; let Ctx { pyo3_path, .. } = ctx; diff --git a/pytests/src/pyclasses.rs b/pytests/src/pyclasses.rs index 4c3398b6627..4e681b2c941 100644 --- a/pytests/src/pyclasses.rs +++ b/pytests/src/pyclasses.rs @@ -103,6 +103,45 @@ impl ClassWithDict { } } +#[pyclass] +struct ClassWithDecorators { + attr: usize, +} + +#[pymethods] +impl ClassWithDecorators { + #[new] + #[classmethod] + fn new(_cls: Bound<'_, PyType>) -> Self { + Self { attr: 0 } + } + + #[getter] + fn get_attr(&self) -> usize { + self.attr + } + + #[setter] + fn set_attr(&mut self, value: usize) { + self.attr = value; + } + + #[classmethod] + fn cls_method(_cls: &Bound<'_, PyType>) -> usize { + 1 + } + + #[staticmethod] + fn static_method() -> usize { + 2 + } + + #[classattr] + fn cls_attribute() -> usize { + 3 + } +} + #[pymodule(gil_used = false)] pub mod pyclasses { #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] @@ -110,6 +149,7 @@ pub mod pyclasses { use super::ClassWithDict; #[pymodule_export] use super::{ - AssertingBaseClass, ClassWithoutConstructor, EmptyClass, PyClassIter, PyClassThreadIter, + AssertingBaseClass, ClassWithDecorators, ClassWithoutConstructor, EmptyClass, PyClassIter, + PyClassThreadIter, }; } diff --git a/pytests/stubs/pyclasses.pyi b/pytests/stubs/pyclasses.pyi index 2a36e0b4540..13b52f1c4e2 100644 --- a/pytests/stubs/pyclasses.pyi +++ b/pytests/stubs/pyclasses.pyi @@ -1,5 +1,31 @@ -class AssertingBaseClass: ... +class AssertingBaseClass: + def __new__(cls, /, expected_type): ... + +class ClassWithDecorators: + def __new__(cls, /): ... + @property + def attr(self, /): ... + @attr.setter + def attr(self, /, value): ... + @classmethod + @property + def cls_attribute(cls, /): ... + @classmethod + def cls_method(cls, /): ... + @staticmethod + def static_method(): ... + class ClassWithoutConstructor: ... -class EmptyClass: ... -class PyClassIter: ... -class PyClassThreadIter: ... + +class EmptyClass: + def __len__(self, /): ... + def __new__(cls, /): ... + def method(self, /): ... + +class PyClassIter: + def __new__(cls, /): ... + def __next__(self, /): ... + +class PyClassThreadIter: + def __new__(cls, /): ... + def __next__(self, /): ... diff --git a/pytests/tests/test_pyclasses.py b/pytests/tests/test_pyclasses.py index 9f611b634b6..a641c9770a4 100644 --- a/pytests/tests/test_pyclasses.py +++ b/pytests/tests/test_pyclasses.py @@ -121,3 +121,32 @@ def test_dict(): d.foo = 42 assert d.__dict__ == {"foo": 42} + + +def test_getter(benchmark): + obj = pyclasses.ClassWithDecorators() + benchmark(lambda: obj.attr) + + +def test_setter(benchmark): + obj = pyclasses.ClassWithDecorators() + + def set_attr(): + obj.attr = 42 + + benchmark(set_attr) + + +def test_class_attribute(benchmark): + cls = pyclasses.ClassWithDecorators + benchmark(lambda: cls.cls_attribute) + + +def test_class_method(benchmark): + cls = pyclasses.ClassWithDecorators + benchmark(lambda: cls.cls_method()) + + +def test_static_method(benchmark): + cls = pyclasses.ClassWithDecorators + benchmark(lambda: cls.static_method()) From 775f03bd0e1f34d9524f4757ee69c8749229a407 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Wed, 14 May 2025 09:18:24 -0400 Subject: [PATCH 694/936] docs: fixup typo on object.md (#5143) --- guide/src/class/object.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/guide/src/class/object.md b/guide/src/class/object.md index 3b9f1aafa40..a3a136fe015 100644 --- a/guide/src/class/object.md +++ b/guide/src/class/object.md @@ -77,8 +77,8 @@ To automatically generate the `__str__` implementation using a `Display` trait i # use pyo3::prelude::*; # # #[allow(dead_code)] -# #[pyclass(str)] -# struct Coordinate { +#[pyclass(str)] +struct Coordinate { x: i32, y: i32, z: i32, @@ -104,8 +104,8 @@ For convenience, a shorthand format string can be passed to `str` as `str=" Date: Sat, 17 May 2025 03:11:58 +0200 Subject: [PATCH 695/936] ci: updates for Rust 1.87 (#5147) --- tests/ui/invalid_proto_pymethods.stderr | 22 +++---- tests/ui/pyclass_send.stderr | 84 ++++++++++++------------- 2 files changed, 53 insertions(+), 53 deletions(-) diff --git a/tests/ui/invalid_proto_pymethods.stderr b/tests/ui/invalid_proto_pymethods.stderr index 82c99c2ddc3..1f564d18ea0 100644 --- a/tests/ui/invalid_proto_pymethods.stderr +++ b/tests/ui/invalid_proto_pymethods.stderr @@ -22,6 +22,17 @@ error: `text_signature` cannot be used with magic method `__bool__` 46 | #[pyo3(name = "__bool__", text_signature = "")] | ^^^^^^^^^^^^^^ +error[E0592]: duplicate definitions with name `__pymethod___richcmp____` + --> tests/ui/invalid_proto_pymethods.rs:55:1 + | +55 | #[pymethods] + | ^^^^^^^^^^^^ + | | + | duplicate definitions for `__pymethod___richcmp____` + | other definition for `__pymethod___richcmp____` + | + = note: this error originates in the macro `::pyo3::impl_::pyclass::generate_pyclass_richcompare_slot` which comes from the expansion of the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) + error[E0034]: multiple applicable items in scope --> tests/ui/invalid_proto_pymethods.rs:55:1 | @@ -40,17 +51,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[E0592]: duplicate definitions with name `__pymethod___richcmp____` - --> tests/ui/invalid_proto_pymethods.rs:55:1 - | -55 | #[pymethods] - | ^^^^^^^^^^^^ - | | - | duplicate definitions for `__pymethod___richcmp____` - | other definition for `__pymethod___richcmp____` - | - = note: this error originates in the macro `::pyo3::impl_::pyclass::generate_pyclass_richcompare_slot` which comes from the expansion of the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) - error[E0034]: multiple applicable items in scope --> tests/ui/invalid_proto_pymethods.rs:55:1 | diff --git a/tests/ui/pyclass_send.stderr b/tests/ui/pyclass_send.stderr index 1623a5b2183..334c36daa13 100644 --- a/tests/ui/pyclass_send.stderr +++ b/tests/ui/pyclass_send.stderr @@ -1,24 +1,3 @@ -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` -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 `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 | @@ -40,27 +19,6 @@ 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]: `*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` -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 `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 | @@ -119,3 +77,45 @@ note: required by a bound in `SendablePyClass` | pub struct SendablePyClass(PhantomData); | ^^^^ required by this bound in `SendablePyClass` = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0277]: `*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` +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 `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 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` +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 `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` From 37c33d806a055bd661abc0767dbdba6df0706aa8 Mon Sep 17 00:00:00 2001 From: Code Apprentice Date: Mon, 19 May 2025 11:45:32 -0600 Subject: [PATCH 696/936] 4877 Bump hasbrown to 0.15 (#5152) * Some whitespace things * Bump hashbrown to 0.15 * Use RandomState as hasher * More explicit RandomState * Add newsfragments * Fix changelog filename * Fix spelling * Fix changelog filename again * Don't override indexmap and hashbrown versions in nox tests * Format noxfile.py * Update src/conversions/hashbrown.rs Co-authored-by: David Hewitt --------- Co-authored-by: David Hewitt --- Cargo.toml | 10 +++++----- newsfragments/5152.packaging.md | 1 + noxfile.py | 4 ---- src/conversions/hashbrown.rs | 14 +++++++++----- 4 files changed, 15 insertions(+), 14 deletions(-) create mode 100644 newsfragments/5152.packaging.md diff --git a/Cargo.toml b/Cargo.toml index 55e4a29170c..6f2038b9119 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,12 +32,12 @@ inventory = { version = "0.3.5", optional = true } # crate integrations that can be added using the eponymous features anyhow = { version = "1.0.1", optional = true } -bigdecimal = {version = "0.4", optional = true } +bigdecimal = { version = "0.4", optional = true } chrono = { version = "0.4.25", 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.6.8, < 0.7", optional = true } -hashbrown = { version = ">= 0.14.5, < 0.16", optional = true } +hashbrown = { version = ">= 0.15.0, < 0.16", optional = true, default-features = false } 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 } @@ -48,9 +48,9 @@ rust_decimal = { version = "1.15", default-features = false, optional = true } time = { version = "0.3.38", default-features = false, optional = true } serde = { version = "1.0", optional = true } smallvec = { version = "1.0", optional = true } -uuid = { version = "1.11.0", optional = true } +uuid = { version = "1.11.0", optional = true } lock_api = { version = "0.4", optional = true } -parking_lot = { version = "0.12", optional = true} +parking_lot = { version = "0.12", optional = true } [target.'cfg(not(target_has_atomic = "64"))'.dependencies] portable-atomic = "1.0" @@ -70,7 +70,7 @@ futures = "0.3.28" tempfile = "3.12.0" static_assertions = "1.1.0" uuid = { version = "1.10.0", features = ["v4"] } -parking_lot = { version = "0.12.3", features = ["arc_lock"]} +parking_lot = { version = "0.12.3", features = ["arc_lock"] } [build-dependencies] pyo3-build-config = { path = "pyo3-build-config", version = "=0.25.0", features = ["resolve-config"] } diff --git a/newsfragments/5152.packaging.md b/newsfragments/5152.packaging.md new file mode 100644 index 00000000000..464c3081a33 --- /dev/null +++ b/newsfragments/5152.packaging.md @@ -0,0 +1 @@ +Bump hashbrown to 0.15.0 \ No newline at end of file diff --git a/noxfile.py b/noxfile.py index 94e0f392044..c2b95fe0842 100644 --- a/noxfile.py +++ b/noxfile.py @@ -21,7 +21,6 @@ Tuple, ) - import nox import nox.command @@ -35,7 +34,6 @@ nox.options.sessions = ["test", "clippy", "rustfmt", "ruff", "docs"] - PYO3_DIR = Path(__file__).parent PYO3_TARGET = Path(os.environ.get("CARGO_TARGET_DIR", PYO3_DIR / "target")).absolute() PYO3_GUIDE_SRC = PYO3_DIR / "guide" / "src" @@ -675,8 +673,6 @@ def set_msrv_package_versions(session: nox.Session): min_pkg_versions = { "trybuild": "1.0.89", "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/src/conversions/hashbrown.rs b/src/conversions/hashbrown.rs index c302d28d2a1..d9972a68865 100644 --- a/src/conversions/hashbrown.rs +++ b/src/conversions/hashbrown.rs @@ -134,11 +134,13 @@ where mod tests { use super::*; use crate::types::IntoPyDict; + use std::collections::hash_map::RandomState; #[test] fn test_hashbrown_hashmap_into_pyobject() { Python::with_gil(|py| { - let mut map = hashbrown::HashMap::::new(); + let mut map = + hashbrown::HashMap::::with_hasher(RandomState::new()); map.insert(1, 1); let py_map = (&map).into_pyobject(py).unwrap(); @@ -160,7 +162,8 @@ mod tests { #[test] fn test_hashbrown_hashmap_into_dict() { Python::with_gil(|py| { - let mut map = hashbrown::HashMap::::new(); + let mut map = + hashbrown::HashMap::::with_hasher(RandomState::new()); map.insert(1, 1); let py_map = map.into_py_dict(py).unwrap(); @@ -182,11 +185,11 @@ mod tests { fn test_extract_hashbrown_hashset() { Python::with_gil(|py| { let set = PySet::new(py, [1, 2, 3, 4, 5]).unwrap(); - let hash_set: hashbrown::HashSet = set.extract().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 hash_set: hashbrown::HashSet = set.extract().unwrap(); + let hash_set: hashbrown::HashSet = set.extract().unwrap(); assert_eq!(hash_set, [1, 2, 3, 4, 5].iter().copied().collect()); }); } @@ -194,7 +197,8 @@ mod tests { #[test] fn test_hashbrown_hashset_into_pyobject() { Python::with_gil(|py| { - let hs: hashbrown::HashSet = [1, 2, 3, 4, 5].iter().cloned().collect(); + let hs: hashbrown::HashSet = + [1, 2, 3, 4, 5].iter().cloned().collect(); let hso = hs.clone().into_pyobject(py).unwrap(); From 3b40ba5817475ba7da9f2e18a9d9000ddf8fb112 Mon Sep 17 00:00:00 2001 From: Anthony Shaw Date: Mon, 19 May 2025 10:47:58 -0700 Subject: [PATCH 697/936] Add Windows ARM64 Build configuration (#5145) * Add Windows ARM64 Build configuration * Add change notes * add llvm install * fix yml syntax * correct `os` in `if` clause * add llvm version * fix clippy, ignore coverage for now * correct `if` condition --------- Co-authored-by: David Hewitt --- .github/workflows/build.yml | 12 ++++++++++++ .github/workflows/ci.yml | 21 ++++++++++++++++++++- newsfragments/5145.packaging.md | 1 + 3 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 newsfragments/5145.packaging.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 60744cadbf7..37da958f35a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -69,6 +69,14 @@ jobs: run: | echo "CARGO_BUILD_TARGET=i686-pc-windows-msvc" >> $GITHUB_ENV + # windows on arm image contains x86-64 libclang + - name: Install LLVM and Clang + if: inputs.os == 'windows-11-arm' + uses: KyleMayes/install-llvm-action@v2 + with: + # to match windows-2022 images + version: "18" + - name: Install zoneinfo backport for Python 3.7 / 3.8 if: contains(fromJSON('["3.7", "3.8"]'), inputs.python-version) run: python -m pip install backports.zoneinfo @@ -132,6 +140,8 @@ jobs: - if: ${{ github.event_name != 'merge_group' }} name: Generate coverage report + # needs investigation why llvm-cov fails on windows-11-arm + continue-on-error: ${{ inputs.os == 'windows-11-arm' }} run: cargo llvm-cov --package=pyo3 --package=pyo3-build-config @@ -143,6 +153,8 @@ jobs: - if: ${{ github.event_name != 'merge_group' }} name: Upload coverage report uses: codecov/codecov-action@v5 + # needs investigation why llvm-cov fails on windows-11-arm + continue-on-error: ${{ inputs.os == 'windows-11-arm' }} with: files: coverage.json name: ${{ inputs.os }}/${{ inputs.python-version }}/${{ inputs.rust }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 428b78e5c9d..70628644447 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -133,6 +133,11 @@ jobs: python-architecture: "x86", rust-target: "i686-pc-windows-msvc", }, + { + os: "windows-11-arm", + python-architecture: "arm64", + rust-target: "aarch64-pc-windows-msvc", + }, ] include: # Run beta clippy as a way to detect any incoming lints which may affect downstream users @@ -159,6 +164,13 @@ jobs: - uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'CI-save-pr-cache') }} + # windows on arm image contains x86-64 libclang + - name: Install LLVM and Clang + if: matrix.platform.os == 'windows-11-arm' + uses: KyleMayes/install-llvm-action@v2 + with: + # to match windows-2022 images + version: "18" - run: python -m pip install --upgrade pip && pip install nox - run: nox -s clippy-all env: @@ -385,7 +397,14 @@ jobs: python-architecture: "arm64", rust-target: "aarch64-unknown-linux-gnu", } - + - rust: stable + python-version: "3.13" + platform: + { + os: "windows-11-arm", + python-architecture: "arm64", + rust-target: "aarch64-pc-windows-msvc", + } exclude: # ubuntu-latest (24.04) no longer supports 3.7 - python-version: "3.7" diff --git a/newsfragments/5145.packaging.md b/newsfragments/5145.packaging.md new file mode 100644 index 00000000000..a11116acc28 --- /dev/null +++ b/newsfragments/5145.packaging.md @@ -0,0 +1 @@ +add support for Windows on ARM64. \ No newline at end of file From f00808d0f8c7c88d165aada15b9b8f0e199ec02b Mon Sep 17 00:00:00 2001 From: yogevm15 <45556766+yogevm15@users.noreply.github.com> Date: Tue, 20 May 2025 00:05:14 +0200 Subject: [PATCH 698/936] Introspection: modules associated constants (#5150) * Adds introspection for modules associated constants * Add support for `Final` constants. * Document changes * Review fixes * Fix typo --- newsfragments/5150.added.md | 1 + pyo3-introspection/src/introspection.rs | 23 ++++++++++++++-- pyo3-introspection/src/model.rs | 7 +++++ pyo3-introspection/src/stubs.rs | 26 ++++++++++++++++-- pyo3-macros-backend/src/introspection.rs | 35 ++++++++++++++++++++++++ pyo3-macros-backend/src/method.rs | 28 ++----------------- pyo3-macros-backend/src/module.rs | 10 +++++-- pyo3-macros-backend/src/utils.rs | 28 +++++++++++++++++++ pytests/src/consts.rs | 10 +++++++ pytests/src/lib.rs | 3 +- pytests/stubs/consts.pyi | 4 +++ 11 files changed, 141 insertions(+), 34 deletions(-) create mode 100644 newsfragments/5150.added.md create mode 100644 pytests/src/consts.rs create mode 100644 pytests/stubs/consts.pyi diff --git a/newsfragments/5150.added.md b/newsfragments/5150.added.md new file mode 100644 index 00000000000..dec859ce549 --- /dev/null +++ b/newsfragments/5150.added.md @@ -0,0 +1 @@ +- Add support for module associated consts introspection. \ No newline at end of file diff --git a/pyo3-introspection/src/introspection.rs b/pyo3-introspection/src/introspection.rs index 665109d0a66..b6d9bfb6e18 100644 --- a/pyo3-introspection/src/introspection.rs +++ b/pyo3-introspection/src/introspection.rs @@ -1,4 +1,4 @@ -use crate::model::{Argument, Arguments, Class, Function, Module, VariableLengthArgument}; +use crate::model::{Argument, Arguments, Class, Const, Function, Module, VariableLengthArgument}; use anyhow::{bail, ensure, Context, Result}; use goblin::elf::Elf; use goblin::mach::load_command::CommandVariant; @@ -44,11 +44,12 @@ fn parse_chunks(chunks: &[Chunk], main_module_name: &str) -> Result { if let Chunk::Module { name, members, + consts, id: _, } = chunk { if name == main_module_name { - return convert_module(name, members, &chunks_by_id, &chunks_by_parent); + return convert_module(name, members, consts, &chunks_by_id, &chunks_by_parent); } } } @@ -58,6 +59,7 @@ fn parse_chunks(chunks: &[Chunk], main_module_name: &str) -> Result { fn convert_module( name: &str, members: &[String], + consts: &[ConstChunk], chunks_by_id: &HashMap<&str, &Chunk>, chunks_by_parent: &HashMap<&str, Vec<&Chunk>>, ) -> Result { @@ -69,11 +71,19 @@ fn convert_module( chunks_by_id, chunks_by_parent, )?; + Ok(Module { name: name.into(), modules, classes, functions, + consts: consts + .iter() + .map(|c| Const { + name: c.name.clone(), + value: c.value.clone(), + }) + .collect(), }) } @@ -91,11 +101,13 @@ fn convert_members( Chunk::Module { name, members, + consts, id: _, } => { modules.push(convert_module( name, members, + consts, chunks_by_id, chunks_by_parent, )?); @@ -354,6 +366,7 @@ enum Chunk { id: String, name: String, members: Vec, + consts: Vec, }, Class { id: String, @@ -371,6 +384,12 @@ enum Chunk { }, } +#[derive(Deserialize)] +struct ConstChunk { + name: String, + value: String, +} + #[derive(Deserialize)] struct ChunkArguments { #[serde(default)] diff --git a/pyo3-introspection/src/model.rs b/pyo3-introspection/src/model.rs index 021475b9392..dc98198ca90 100644 --- a/pyo3-introspection/src/model.rs +++ b/pyo3-introspection/src/model.rs @@ -4,6 +4,7 @@ pub struct Module { pub modules: Vec, pub classes: Vec, pub functions: Vec, + pub consts: Vec, } #[derive(Debug, Eq, PartialEq, Clone, Hash)] @@ -20,6 +21,12 @@ pub struct Function { pub arguments: Arguments, } +#[derive(Debug, Eq, PartialEq, Clone, Hash)] +pub struct Const { + pub name: String, + pub value: String, +} + #[derive(Debug, Eq, PartialEq, Clone, Hash)] pub struct Arguments { /// Arguments before / diff --git a/pyo3-introspection/src/stubs.rs b/pyo3-introspection/src/stubs.rs index cc0a11ebd38..8be82da1f11 100644 --- a/pyo3-introspection/src/stubs.rs +++ b/pyo3-introspection/src/stubs.rs @@ -1,5 +1,5 @@ -use crate::model::{Argument, Class, Function, Module, VariableLengthArgument}; -use std::collections::HashMap; +use crate::model::{Argument, Class, Const, Function, Module, VariableLengthArgument}; +use std::collections::{BTreeSet, HashMap}; use std::path::{Path, PathBuf}; /// Generates the [type stubs](https://typing.readthedocs.io/en/latest/source/stubs.html) of a given module. @@ -32,6 +32,7 @@ fn add_module_stub_files( /// Generates the module stubs to a String, not including submodules fn module_stubs(module: &Module) -> String { + let mut modules_to_import = BTreeSet::new(); let mut elements = Vec::new(); for class in &module.classes { elements.push(class_stubs(class)); @@ -39,9 +40,21 @@ fn module_stubs(module: &Module) -> String { for function in &module.functions { elements.push(function_stubs(function)); } + for konst in &module.consts { + elements.push(const_stubs(konst, &mut modules_to_import)); + } - // We insert two line jumps (i.e. empty strings) only above and below multiple line elements (classes with methods, functions with decorators) let mut output = String::new(); + + for module_to_import in &modules_to_import { + output.push_str(&format!("import {module_to_import}\n")); + } + + if !modules_to_import.is_empty() { + output.push('\n') + } + + // We insert two line jumps (i.e. empty strings) only above and below multiple line elements (classes with methods, functions with decorators) for element in elements { let is_multiline = element.contains('\n'); if is_multiline && !output.is_empty() && !output.ends_with("\n\n") { @@ -53,6 +66,7 @@ fn module_stubs(module: &Module) -> String { output.push('\n'); } } + // We remove a line jump at the end if they are two if output.ends_with("\n\n") { output.pop(); @@ -111,6 +125,12 @@ fn function_stubs(function: &Function) -> String { buffer } +fn const_stubs(konst: &Const, modules_to_import: &mut BTreeSet) -> String { + modules_to_import.insert("typing".to_string()); + let Const { name, value } = konst; + format!("{name}: typing.Final = {value}") +} + fn argument_stub(argument: &Argument) -> String { let mut output = argument.name.clone(); if let Some(default_value) = &argument.default_value { diff --git a/pyo3-macros-backend/src/introspection.rs b/pyo3-macros-backend/src/introspection.rs index 303e952b814..dcb76e668b1 100644 --- a/pyo3-macros-backend/src/introspection.rs +++ b/pyo3-macros-backend/src/introspection.rs @@ -19,6 +19,7 @@ use std::collections::HashMap; use std::hash::{Hash, Hasher}; use std::mem::take; use std::sync::atomic::{AtomicUsize, Ordering}; +use syn::ext::IdentExt; use syn::{Attribute, Ident, Type, TypePath}; static GLOBAL_COUNTER_FOR_UNIQUE_NAMES: AtomicUsize = AtomicUsize::new(0); @@ -28,6 +29,9 @@ pub fn module_introspection_code<'a>( name: &str, members: impl IntoIterator, members_cfg_attrs: impl IntoIterator>, + consts: impl IntoIterator, + consts_values: impl IntoIterator, + consts_cfg_attrs: impl IntoIterator>, ) -> TokenStream { IntrospectionNode::Map( [ @@ -52,6 +56,23 @@ pub fn module_introspection_code<'a>( .collect(), ), ), + ( + "consts", + IntrospectionNode::List( + consts + .into_iter() + .zip(consts_values) + .zip(consts_cfg_attrs) + .filter_map(|((ident, value), attributes)| { + if attributes.is_empty() { + Some(const_introspection_code(ident, value)) + } else { + None // TODO: properly interpret cfg attributes + } + }) + .collect(), + ), + ), ] .into(), ) @@ -116,6 +137,20 @@ pub fn function_introspection_code( IntrospectionNode::Map(desc).emit(pyo3_crate_path) } +fn const_introspection_code<'a>(ident: &'a Ident, value: &'a String) -> IntrospectionNode<'a> { + IntrospectionNode::Map( + [ + ("type", IntrospectionNode::String("const".into())), + ( + "name", + IntrospectionNode::String(ident.unraw().to_string().into()), + ), + ("value", IntrospectionNode::String(value.into())), + ] + .into(), + ) +} + fn arguments_introspection_data<'a>( signature: &'a FunctionSignature<'a>, first_argument: Option<&'a str>, diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index 0ab5135e93a..911ad58fdcf 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -7,7 +7,7 @@ use quote::{format_ident, quote, quote_spanned, ToTokens}; use syn::{ext::IdentExt, spanned::Spanned, Ident, Result}; use crate::pyversions::is_abi3_before; -use crate::utils::{Ctx, LitCStr}; +use crate::utils::{expr_to_python, Ctx, LitCStr}; use crate::{ attributes::{FromPyWithAttribute, TextSignatureAttribute, TextSignatureAttributeValue}, params::{impl_arg_params, Holders}, @@ -34,31 +34,7 @@ impl RegularArg<'_> { .. } = self { - match arg_default { - // literal values - syn::Expr::Lit(syn::ExprLit { lit, .. }) => match lit { - syn::Lit::Str(s) => s.token().to_string(), - syn::Lit::Char(c) => c.token().to_string(), - syn::Lit::Int(i) => i.base10_digits().to_string(), - syn::Lit::Float(f) => f.base10_digits().to_string(), - syn::Lit::Bool(b) => { - if b.value() { - "True".to_string() - } else { - "False".to_string() - } - } - _ => "...".to_string(), - }, - // None - syn::Expr::Path(syn::ExprPath { qself, path, .. }) - if qself.is_none() && path.is_ident("None") => - { - "None".to_string() - } - // others, unsupported yet so defaults to `...` - _ => "...".to_string(), - } + expr_to_python(arg_default) } else if let RegularArg { option_wrapped_type: Some(..), .. diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index 860a3b6d857..dd22778465b 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -2,6 +2,7 @@ #[cfg(feature = "experimental-inspect")] use crate::introspection::{introspection_id_const, module_introspection_code}; +use crate::utils::expr_to_python; use crate::{ attributes::{ self, kw, take_attributes, take_pyo3_options, CrateAttribute, GILUsedAttribute, @@ -150,6 +151,7 @@ pub fn pymodule_module_impl( let mut pymodule_init = None; let mut module_consts = Vec::new(); + let mut module_consts_values = Vec::new(); let mut module_consts_cfg_attrs = Vec::new(); for item in &mut *items { @@ -293,8 +295,8 @@ pub fn pymodule_module_impl( if !find_and_remove_attribute(&mut item.attrs, "pymodule_export") { continue; } - module_consts.push(item.ident.clone()); + module_consts_values.push(expr_to_python(&item.expr)); module_consts_cfg_attrs.push(get_cfg_attributes(&item.attrs)); } Item::Static(item) => { @@ -349,6 +351,9 @@ pub fn pymodule_module_impl( &name.to_string(), &module_items, &module_items_cfg_attrs, + &module_consts, + &module_consts_values, + &module_consts_cfg_attrs, ); #[cfg(not(feature = "experimental-inspect"))] let introspection = quote! {}; @@ -432,7 +437,8 @@ pub fn pymodule_function_impl( ); #[cfg(feature = "experimental-inspect")] - let introspection = module_introspection_code(pyo3_path, &name.to_string(), &[], &[]); + let introspection = + module_introspection_code(pyo3_path, &name.to_string(), &[], &[], &[], &[], &[]); #[cfg(not(feature = "experimental-inspect"))] let introspection = quote! {}; #[cfg(feature = "experimental-inspect")] diff --git a/pyo3-macros-backend/src/utils.rs b/pyo3-macros-backend/src/utils.rs index 09f86158834..0f4a267cac7 100644 --- a/pyo3-macros-backend/src/utils.rs +++ b/pyo3-macros-backend/src/utils.rs @@ -393,3 +393,31 @@ impl TypeExt for syn::Type { self } } + +pub fn expr_to_python(expr: &syn::Expr) -> String { + match expr { + // literal values + syn::Expr::Lit(syn::ExprLit { lit, .. }) => match lit { + syn::Lit::Str(s) => s.token().to_string(), + syn::Lit::Char(c) => c.token().to_string(), + syn::Lit::Int(i) => i.base10_digits().to_string(), + syn::Lit::Float(f) => f.base10_digits().to_string(), + syn::Lit::Bool(b) => { + if b.value() { + "True".to_string() + } else { + "False".to_string() + } + } + _ => "...".to_string(), + }, + // None + syn::Expr::Path(syn::ExprPath { qself, path, .. }) + if qself.is_none() && path.is_ident("None") => + { + "None".to_string() + } + // others, unsupported yet so defaults to `...` + _ => "...".to_string(), + } +} diff --git a/pytests/src/consts.rs b/pytests/src/consts.rs new file mode 100644 index 00000000000..a0bdf3a9d3e --- /dev/null +++ b/pytests/src/consts.rs @@ -0,0 +1,10 @@ +use pyo3::pymodule; + +#[pymodule] +pub mod consts { + #[pymodule_export] + pub const PI: f64 = std::f64::consts::PI; // Exports PI constant as part of the module + + #[pymodule_export] + pub const SIMPLE: &str = "SIMPLE"; +} diff --git a/pytests/src/lib.rs b/pytests/src/lib.rs index 4112c90400e..9a406d9fe05 100644 --- a/pytests/src/lib.rs +++ b/pytests/src/lib.rs @@ -5,6 +5,7 @@ use pyo3::wrap_pymodule; pub mod awaitable; pub mod buf_and_str; pub mod comparisons; +mod consts; pub mod datetime; pub mod dict_iter; pub mod enums; @@ -22,7 +23,7 @@ mod pyo3_pytests { use super::*; #[pymodule_export] - use {pyclasses::pyclasses, pyfunctions::pyfunctions}; + use {consts::consts, pyclasses::pyclasses, pyfunctions::pyfunctions}; // Inserting to sys.modules allows importing submodules nicely from Python // e.g. import pyo3_pytests.buf_and_str as bas diff --git a/pytests/stubs/consts.pyi b/pytests/stubs/consts.pyi new file mode 100644 index 00000000000..9cf89e44883 --- /dev/null +++ b/pytests/stubs/consts.pyi @@ -0,0 +1,4 @@ +import typing + +PI: typing.Final = ... +SIMPLE: typing.Final = "SIMPLE" From a4fe418ad19fabc9dc5853d95d6c5fe7f8792fa6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 20 May 2025 17:57:57 +0200 Subject: [PATCH 699/936] build(deps): update criterion requirement in /pyo3-benches (#5155) Updates the requirements on [criterion](https://github.com/bheisler/criterion.rs) to permit the latest version. - [Changelog](https://github.com/bheisler/criterion.rs/blob/master/CHANGELOG.md) - [Commits](https://github.com/bheisler/criterion.rs/compare/0.5.1...0.6.0) --- updated-dependencies: - dependency-name: criterion dependency-version: 0.6.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyo3-benches/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyo3-benches/Cargo.toml b/pyo3-benches/Cargo.toml index 24ec9b5d76e..9f0c4c67b08 100644 --- a/pyo3-benches/Cargo.toml +++ b/pyo3-benches/Cargo.toml @@ -14,7 +14,7 @@ pyo3-build-config = { path = "../pyo3-build-config" } [dev-dependencies] codspeed-criterion-compat = "2.3" -criterion = "0.5.1" +criterion = "0.6.0" num-bigint = "0.4.3" rust_decimal = { version = "1.0.0", default-features = false } hashbrown = "0.15" From 11c9863b08f98ac3e793c78f7df788b7a6437afd Mon Sep 17 00:00:00 2001 From: Cheuk Ting Ho Date: Tue, 20 May 2025 22:35:07 +0100 Subject: [PATCH 700/936] Catching Invalid Asyncs (#5156) * Catching invalid asyncs * Catching invalid asyncs (adding missing file) * formating * adding changelog file * Update newsfragments/5156.fixed.md Co-authored-by: Daniel McCarney * make sure no experimental-async feature * revert pytest/test_awaitable.py changes * make sure not using experimental-async feature --------- Co-authored-by: Daniel McCarney --- newsfragments/5156.fixed.md | 1 + pyo3-macros-backend/src/method.rs | 7 ------- pyo3-macros-backend/src/pyfunction.rs | 6 ++++++ pyo3-macros-backend/src/pymethod.rs | 7 +++++++ tests/test_compile_error.rs | 2 ++ tests/ui/invalid_async.rs | 20 ++++++++++++++++++++ tests/ui/invalid_async.stderr | 11 +++++++++++ 7 files changed, 47 insertions(+), 7 deletions(-) create mode 100644 newsfragments/5156.fixed.md create mode 100644 tests/ui/invalid_async.rs create mode 100644 tests/ui/invalid_async.stderr diff --git a/newsfragments/5156.fixed.md b/newsfragments/5156.fixed.md new file mode 100644 index 00000000000..2c6049ef855 --- /dev/null +++ b/newsfragments/5156.fixed.md @@ -0,0 +1 @@ +Catching `async` declaration when not using `experimental-async` \ No newline at end of file diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index 911ad58fdcf..d2ddb3179a4 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -669,13 +669,6 @@ impl<'a> FnSpec<'a> { } } - if self.asyncness.is_some() { - ensure_spanned!( - cfg!(feature = "experimental-async"), - self.asyncness.span() => "async functions are only supported with the `experimental-async` feature" - ); - } - let rust_call = |args: Vec, holders: &mut Holders| { let mut self_arg = || self.tp.self_arg(cls, ExtractErrorMode::Raise, holders, ctx); diff --git a/pyo3-macros-backend/src/pyfunction.rs b/pyo3-macros-backend/src/pyfunction.rs index e512ca1cabc..82d9bceabb8 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -258,6 +258,12 @@ pub fn impl_wrap_pyfunction( }; let wrapper_ident = format_ident!("__pyfunction_{}", spec.name); + if spec.asyncness.is_some() { + ensure_spanned!( + cfg!(feature = "experimental-async"), + spec.asyncness.span() => "async functions are only supported with the `experimental-async` feature" + ); + } let wrapper = spec.get_wrapper_function(&wrapper_ident, None, ctx)?; let methoddef = spec.get_methoddef(wrapper_ident, &spec.get_doc(&func.attrs, ctx), ctx); diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index 434a96e9dde..a34e22b1e3d 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -196,6 +196,13 @@ pub fn gen_py_method( let spec = &method.spec; let Ctx { pyo3_path, .. } = ctx; + if spec.asyncness.is_some() { + ensure_spanned!( + cfg!(feature = "experimental-async"), + spec.asyncness.span() => "async functions are only supported with the `experimental-async` feature" + ); + } + Ok(match (method.kind, &spec.tp) { // Class attributes go before protos so that class attributes can be used to set proto // method to None. diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index c6990430366..0053f86e7bb 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -41,6 +41,8 @@ fn test_compile_errors() { // output changes with async feature #[cfg(all(Py_LIMITED_API, feature = "experimental-async"))] t.compile_fail("tests/ui/abi3_nativetype_inheritance.rs"); + #[cfg(not(any(feature = "experimental-async")))] + t.compile_fail("tests/ui/invalid_async.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")))] diff --git a/tests/ui/invalid_async.rs b/tests/ui/invalid_async.rs new file mode 100644 index 00000000000..2c7375fd2f9 --- /dev/null +++ b/tests/ui/invalid_async.rs @@ -0,0 +1,20 @@ +use pyo3::prelude::*; + +#[pyfunction] +async fn check(){} + +#[pyclass] +pub(crate) struct AsyncRange { + count: i32, + target: i32, +} +#[pymethods] +impl AsyncRange { + async fn __anext__(mut _pyself: PyRefMut<'_, Self>) -> PyResult { + Ok(0) + } + + async fn foo(&self) {} +} + +fn main() {} \ No newline at end of file diff --git a/tests/ui/invalid_async.stderr b/tests/ui/invalid_async.stderr new file mode 100644 index 00000000000..5573cdf388a --- /dev/null +++ b/tests/ui/invalid_async.stderr @@ -0,0 +1,11 @@ +error: async functions are only supported with the `experimental-async` feature + --> tests/ui/invalid_async.rs:4:1 + | +4 | async fn check(){} + | ^^^^^ + +error: async functions are only supported with the `experimental-async` feature + --> tests/ui/invalid_async.rs:13:5 + | +13 | async fn __anext__(mut _pyself: PyRefMut<'_, Self>) -> PyResult { + | ^^^^^ From 9e2f03327da00406db0a3b396b577aa6717e5f60 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers <7943856+bschoenmaeckers@users.noreply.github.com> Date: Wed, 21 May 2025 18:09:09 +0200 Subject: [PATCH 701/936] Allow `is_instance_of` for types that only implement `PyTypeCheck` (#5146) --- newsfragments/5146.changed.md | 1 + src/types/any.rs | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 newsfragments/5146.changed.md diff --git a/newsfragments/5146.changed.md b/newsfragments/5146.changed.md new file mode 100644 index 00000000000..72958216001 --- /dev/null +++ b/newsfragments/5146.changed.md @@ -0,0 +1 @@ +Allow `is_instance_of` for types that only implement `PyTypeCheck` diff --git a/src/types/any.rs b/src/types/any.rs index 936c9d07040..4dd9c594c6c 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -904,7 +904,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// /// This is equivalent to the Python expression `isinstance(self, T)`, /// if the type `T` is known at compile time. - fn is_instance_of(&self) -> bool; + fn is_instance_of(&self) -> bool; /// Checks whether this object is an instance of exactly type `T`. /// @@ -1558,8 +1558,8 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { } #[inline] - fn is_instance_of(&self) -> bool { - T::is_type_of(self) + fn is_instance_of(&self) -> bool { + T::type_check(self) } #[inline] From 2c2f39f4e89d6c495a0dfe6eba254bd1918ac32b Mon Sep 17 00:00:00 2001 From: Code Apprentice Date: Fri, 23 May 2025 05:41:12 -0600 Subject: [PATCH 702/936] Frame ffi (#5154) * add missing pyframe ffi functions * Rearrange pyframe stuff to match CPython structure * Remove duplicate declaration * Clean up duplicate declaration of PyFrameObject * Fix imports * Update changelog * Conditional compilation for `use` statement that matches its usage * Match `use` conditional compilation with usage * Fix another `use` statement * Don't need cfg_attr() * Fix another conditional compilation * Conditional compilation for `use` * Another conditional compile * More conditional compile checks * One more * Remove unused import Co-authored-by: David Hewitt * Changelog for removed items * Remove unnecessary wording in comments Co-authored-by: David Hewitt --------- Co-authored-by: daniel sonbolian Co-authored-by: David Hewitt --- newsfragments/5154.added.md | 3 ++ newsfragments/5154.removed.md | 1 + pyo3-ffi/src/cpython/frameobject.rs | 50 +----------------------- pyo3-ffi/src/cpython/mod.rs | 1 - pyo3-ffi/src/cpython/pyframe.rs | 59 +++++++++++++++++++++++++++++ pyo3-ffi/src/lib.rs | 2 + pyo3-ffi/src/pyframe.rs | 11 +++--- pyo3-ffi/src/pystate.rs | 5 ++- pyo3-ffi/src/pytypedefs.rs | 4 ++ 9 files changed, 79 insertions(+), 57 deletions(-) create mode 100644 newsfragments/5154.added.md create mode 100644 newsfragments/5154.removed.md create mode 100644 pyo3-ffi/src/pytypedefs.rs diff --git a/newsfragments/5154.added.md b/newsfragments/5154.added.md new file mode 100644 index 00000000000..97147e17117 --- /dev/null +++ b/newsfragments/5154.added.md @@ -0,0 +1,3 @@ +* added missing ffi functions for `PyFrameObject` but without + including unstable API from python 3.13 +* moved ffi functions to correct files to mach CPython as of 3.13 \ No newline at end of file diff --git a/newsfragments/5154.removed.md b/newsfragments/5154.removed.md new file mode 100644 index 00000000000..a625a742c0c --- /dev/null +++ b/newsfragments/5154.removed.md @@ -0,0 +1 @@ +* Removed access to internals of PyFrameObject \ No newline at end of file diff --git a/pyo3-ffi/src/cpython/frameobject.rs b/pyo3-ffi/src/cpython/frameobject.rs index 993e93c838b..99ab63b9254 100644 --- a/pyo3-ffi/src/cpython/frameobject.rs +++ b/pyo3-ffi/src/cpython/frameobject.rs @@ -1,12 +1,13 @@ #[cfg(not(GraalPy))] use crate::cpython::code::PyCodeObject; +#[cfg(not(GraalPy))] use crate::object::*; #[cfg(not(GraalPy))] use crate::pystate::PyThreadState; +use crate::PyFrameObject; #[cfg(not(any(PyPy, GraalPy, Py_3_11)))] use std::os::raw::c_char; use std::os::raw::c_int; -use std::ptr::addr_of_mut; #[cfg(not(any(PyPy, GraalPy, Py_3_11)))] pub type PyFrameState = c_char; @@ -20,55 +21,10 @@ pub struct PyTryBlock { pub b_level: c_int, } -#[repr(C)] -#[cfg(not(any(PyPy, GraalPy, Py_3_11)))] -pub struct PyFrameObject { - pub ob_base: PyVarObject, - pub f_back: *mut PyFrameObject, - pub f_code: *mut PyCodeObject, - pub f_builtins: *mut PyObject, - pub f_globals: *mut PyObject, - pub f_locals: *mut PyObject, - pub f_valuestack: *mut *mut PyObject, - - #[cfg(not(Py_3_10))] - pub f_stacktop: *mut *mut PyObject, - pub f_trace: *mut PyObject, - #[cfg(Py_3_10)] - pub f_stackdepth: c_int, - pub f_trace_lines: c_char, - pub f_trace_opcodes: c_char, - - pub f_gen: *mut PyObject, - - pub f_lasti: c_int, - pub f_lineno: c_int, - pub f_iblock: c_int, - #[cfg(not(Py_3_10))] - pub f_executing: c_char, - #[cfg(Py_3_10)] - pub f_state: PyFrameState, - pub f_blockstack: [PyTryBlock; crate::CO_MAXBLOCKS], - pub f_localsplus: [*mut PyObject; 1], -} - -#[cfg(any(PyPy, GraalPy, Py_3_11))] -opaque_struct!(pub PyFrameObject); - // skipped _PyFrame_IsRunnable // skipped _PyFrame_IsExecuting // skipped _PyFrameHasCompleted -#[cfg_attr(windows, link(name = "pythonXY"))] -extern "C" { - pub static mut PyFrame_Type: PyTypeObject; -} - -#[inline] -pub unsafe fn PyFrame_Check(op: *mut PyObject) -> c_int { - (Py_TYPE(op) == addr_of_mut!(PyFrame_Type)) as c_int -} - extern "C" { #[cfg(not(GraalPy))] #[cfg_attr(PyPy, link_name = "PyPyFrame_New")] @@ -89,8 +45,6 @@ extern "C" { pub fn PyFrame_FastToLocals(f: *mut PyFrameObject); // skipped _PyFrame_DebugMallocStats - #[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/cpython/mod.rs b/pyo3-ffi/src/cpython/mod.rs index adaf8bc8861..d2b2274628b 100644 --- a/pyo3-ffi/src/cpython/mod.rs +++ b/pyo3-ffi/src/cpython/mod.rs @@ -72,7 +72,6 @@ pub use self::object::*; pub use self::objimpl::*; pub use self::pydebug::*; pub use self::pyerrors::*; -#[cfg(all(Py_3_11, not(PyPy)))] pub use self::pyframe::*; #[cfg(any(not(PyPy), Py_3_13))] pub use self::pyhash::*; diff --git a/pyo3-ffi/src/cpython/pyframe.rs b/pyo3-ffi/src/cpython/pyframe.rs index f0c38be47be..a0479b50b6b 100644 --- a/pyo3-ffi/src/cpython/pyframe.rs +++ b/pyo3-ffi/src/cpython/pyframe.rs @@ -1,3 +1,62 @@ +#[cfg(any(Py_3_11, all(Py_3_9, not(PyPy))))] +use crate::PyFrameObject; +use crate::{PyObject, PyTypeObject, Py_TYPE}; +#[cfg(Py_3_12)] +use std::os::raw::c_char; +use std::os::raw::c_int; +use std::ptr::addr_of_mut; + // NB used in `_PyEval_EvalFrameDefault`, maybe we remove this too. #[cfg(all(Py_3_11, not(PyPy)))] opaque_struct!(pub _PyInterpreterFrame); + +#[cfg_attr(windows, link(name = "pythonXY"))] +extern "C" { + pub static mut PyFrame_Type: PyTypeObject; + + #[cfg(Py_3_13)] + pub static mut PyFrameLocalsProxy_Type: PyTypeObject; +} + +#[inline] +pub unsafe fn PyFrame_Check(op: *mut PyObject) -> c_int { + (Py_TYPE(op) == addr_of_mut!(PyFrame_Type)) as c_int +} + +#[cfg(Py_3_13)] +#[inline] +pub unsafe fn PyFrameLocalsProxy_Check(op: *mut PyObject) -> c_int { + (Py_TYPE(op) == addr_of_mut!(PyFrameLocalsProxy_Type)) as c_int +} + +extern "C" { + #[cfg(all(Py_3_9, not(PyPy)))] + pub fn PyFrame_GetBack(frame: *mut PyFrameObject) -> *mut PyFrameObject; + + #[cfg(Py_3_11)] + pub fn PyFrame_GetLocals(frame: *mut PyFrameObject) -> *mut PyObject; + + #[cfg(Py_3_11)] + pub fn PyFrame_GetGlobals(frame: *mut PyFrameObject) -> *mut PyObject; + + #[cfg(Py_3_11)] + pub fn PyFrame_GetBuiltins(frame: *mut PyFrameObject) -> *mut PyObject; + + #[cfg(Py_3_11)] + pub fn PyFrame_GetGenerator(frame: *mut PyFrameObject) -> *mut PyObject; + + #[cfg(Py_3_11)] + pub fn PyFrame_GetLasti(frame: *mut PyFrameObject) -> c_int; + + #[cfg(Py_3_12)] + pub fn PyFrame_GetVar(frame: *mut PyFrameObject, name: *mut PyObject) -> *mut PyObject; + + #[cfg(Py_3_12)] + pub fn PyFrame_GetVarString(frame: *mut PyFrameObject, name: *mut c_char) -> *mut PyObject; + + // skipped PyUnstable_InterpreterFrame_GetCode + // skipped PyUnstable_InterpreterFrame_GetLasti + // skipped PyUnstable_InterpreterFrame_GetLine + // skipped PyUnstable_ExecutableKinds + +} diff --git a/pyo3-ffi/src/lib.rs b/pyo3-ffi/src/lib.rs index f78c918a8c5..95f59d35192 100644 --- a/pyo3-ffi/src/lib.rs +++ b/pyo3-ffi/src/lib.rs @@ -447,6 +447,7 @@ pub use self::pyport::*; pub use self::pystate::*; pub use self::pystrtod::*; pub use self::pythonrun::*; +pub use self::pytypedefs::*; pub use self::rangeobject::*; pub use self::refcount::*; pub use self::setobject::*; @@ -540,6 +541,7 @@ mod pythonrun; mod pystrtod; // skipped pythread.h // skipped pytime.h +mod pytypedefs; mod rangeobject; mod refcount; mod setobject; diff --git a/pyo3-ffi/src/pyframe.rs b/pyo3-ffi/src/pyframe.rs index 1693b20b0af..8ab5abce37f 100644 --- a/pyo3-ffi/src/pyframe.rs +++ b/pyo3-ffi/src/pyframe.rs @@ -1,16 +1,15 @@ +#[allow(unused_imports)] +use crate::object::PyObject; #[cfg(not(GraalPy))] #[cfg(any(Py_3_10, all(Py_3_9, not(Py_LIMITED_API))))] use crate::PyCodeObject; -#[cfg(not(Py_LIMITED_API))] use crate::PyFrameObject; use std::os::raw::c_int; -#[cfg(Py_LIMITED_API)] -opaque_struct!(pub PyFrameObject); - extern "C" { - pub fn PyFrame_GetLineNumber(f: *mut PyFrameObject) -> c_int; + pub fn PyFrame_GetLineNumber(frame: *mut PyFrameObject) -> c_int; + #[cfg(not(GraalPy))] #[cfg(any(Py_3_10, all(Py_3_9, not(Py_LIMITED_API))))] - pub fn PyFrame_GetCode(f: *mut PyFrameObject) -> *mut PyCodeObject; + pub fn PyFrame_GetCode(frame: *mut PyFrameObject) -> *mut PyCodeObject; } diff --git a/pyo3-ffi/src/pystate.rs b/pyo3-ffi/src/pystate.rs index cc16e554ca9..080fa11201c 100644 --- a/pyo3-ffi/src/pystate.rs +++ b/pyo3-ffi/src/pystate.rs @@ -1,9 +1,10 @@ -#[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; +#[cfg(all(Py_3_10, not(PyPy), not(Py_LIMITED_API)))] +use crate::PyFrameObject; + #[cfg(not(PyPy))] use std::os::raw::c_long; diff --git a/pyo3-ffi/src/pytypedefs.rs b/pyo3-ffi/src/pytypedefs.rs new file mode 100644 index 00000000000..7172724ce1f --- /dev/null +++ b/pyo3-ffi/src/pytypedefs.rs @@ -0,0 +1,4 @@ +// TODO: Created this file as part of fixing pyframe.rs and cpython/pyframe.rs +// TODO: Finish defining or moving declarations now in Include/pytypedefs.h + +opaque_struct!(pub PyFrameObject); From f881298a40ed583436aed4c7740a3f67d433cf5a Mon Sep 17 00:00:00 2001 From: Tim Felgentreff Date: Fri, 23 May 2025 16:57:24 +0200 Subject: [PATCH 703/936] Updates to better support ormsgpack on GraalPy. (#5121) * Expose the vectorcall functions also on GraalPy * Add PyBytes_AS_STRING, seen in ormsgpack * Call Py_Is function on GraalPy --- newsfragments/5121.added.md | 1 + newsfragments/5121.changed.md | 2 ++ pyo3-ffi/src/abstract_.rs | 3 +-- pyo3-ffi/src/compat/py_3_9.rs | 3 +-- pyo3-ffi/src/cpython/abstract_.rs | 22 +++++++++++----------- pyo3-ffi/src/cpython/bytesobject.rs | 11 ++++++++++- pyo3-ffi/src/object.rs | 4 ++-- src/types/bytes.rs | 15 +++++++++++++++ 8 files changed, 43 insertions(+), 18 deletions(-) create mode 100644 newsfragments/5121.added.md create mode 100644 newsfragments/5121.changed.md diff --git a/newsfragments/5121.added.md b/newsfragments/5121.added.md new file mode 100644 index 00000000000..fd79e5a8ea7 --- /dev/null +++ b/newsfragments/5121.added.md @@ -0,0 +1 @@ +Add `PyBytes_AS_STRING` diff --git a/newsfragments/5121.changed.md b/newsfragments/5121.changed.md new file mode 100644 index 00000000000..49b317cb96b --- /dev/null +++ b/newsfragments/5121.changed.md @@ -0,0 +1,2 @@ +Enable vectorcall methods on GraalPy +Call Py_Is function on GraalPy diff --git a/pyo3-ffi/src/abstract_.rs b/pyo3-ffi/src/abstract_.rs index 84fb98a1b4e..59639d14de8 100644 --- a/pyo3-ffi/src/abstract_.rs +++ b/pyo3-ffi/src/abstract_.rs @@ -25,7 +25,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")] @@ -94,7 +93,7 @@ extern "C" { kwnames: *mut PyObject, ) -> *mut PyObject; - #[cfg(any(Py_3_12, all(Py_3_9, not(any(Py_LIMITED_API, PyPy, GraalPy)))))] + #[cfg(any(Py_3_12, all(Py_3_9, not(any(Py_LIMITED_API, PyPy)))))] pub fn PyObject_VectorcallMethod( name: *mut PyObject, args: *const *mut PyObject, diff --git a/pyo3-ffi/src/compat/py_3_9.rs b/pyo3-ffi/src/compat/py_3_9.rs index 285f2b2ae7e..6b3521cc167 100644 --- a/pyo3-ffi/src/compat/py_3_9.rs +++ b/pyo3-ffi/src/compat/py_3_9.rs @@ -1,7 +1,6 @@ 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 )); @@ -12,7 +11,7 @@ compat_function!( ); compat_function!( - originally_defined_for(all(Py_3_9, not(any(Py_LIMITED_API, PyPy, GraalPy)))); + originally_defined_for(all(Py_3_9, not(any(Py_LIMITED_API, PyPy)))); #[inline] pub unsafe fn PyObject_CallMethodNoArgs(obj: *mut crate::PyObject, name: *mut crate::PyObject) -> *mut crate::PyObject { diff --git a/pyo3-ffi/src/cpython/abstract_.rs b/pyo3-ffi/src/cpython/abstract_.rs index 6ada1a754ef..b9d9dd47dc9 100644 --- a/pyo3-ffi/src/cpython/abstract_.rs +++ b/pyo3-ffi/src/cpython/abstract_.rs @@ -1,12 +1,12 @@ use crate::{PyObject, Py_ssize_t}; -#[cfg(any(all(Py_3_8, not(any(PyPy, GraalPy))), not(Py_3_11)))] +#[cfg(any(all(Py_3_8, not(PyPy)), not(Py_3_11)))] use std::os::raw::c_char; use std::os::raw::c_int; #[cfg(not(Py_3_11))] use crate::Py_buffer; -#[cfg(all(Py_3_8, not(any(PyPy, GraalPy))))] +#[cfg(all(Py_3_8, not(PyPy)))] use crate::{ vectorcallfunc, PyCallable_Check, PyThreadState, PyThreadState_GET, PyTuple_Check, PyType_HasFeature, Py_TPFLAGS_HAVE_VECTORCALL, @@ -23,7 +23,7 @@ extern "C" { const _PY_FASTCALL_SMALL_STACK: size_t = 5; extern "C" { - #[cfg(all(Py_3_8, not(any(PyPy, GraalPy))))] + #[cfg(all(Py_3_8, not(PyPy)))] pub fn _Py_CheckFunctionResult( tstate: *mut PyThreadState, callable: *mut PyObject, @@ -31,7 +31,7 @@ extern "C" { where_: *const c_char, ) -> *mut PyObject; - #[cfg(all(Py_3_8, not(any(PyPy, GraalPy))))] + #[cfg(all(Py_3_8, not(PyPy)))] pub fn _PyObject_MakeTpCall( tstate: *mut PyThreadState, callable: *mut PyObject, @@ -52,7 +52,7 @@ pub unsafe fn PyVectorcall_NARGS(n: size_t) -> Py_ssize_t { n.try_into().expect("cannot fail due to mask") } -#[cfg(all(Py_3_8, not(any(PyPy, GraalPy))))] +#[cfg(all(Py_3_8, not(PyPy)))] #[inline(always)] pub unsafe fn PyVectorcall_Function(callable: *mut PyObject) -> Option { assert!(!callable.is_null()); @@ -67,7 +67,7 @@ pub unsafe fn PyVectorcall_Function(callable: *mut PyObject) -> Option *mut PyObject { _PyObject_VectorcallTstate( @@ -166,7 +166,7 @@ extern "C" { pub fn _PyObject_CallNoArg(func: *mut PyObject) -> *mut PyObject; } -#[cfg(all(Py_3_8, not(any(PyPy, GraalPy))))] +#[cfg(all(Py_3_8, not(PyPy)))] #[inline(always)] pub unsafe fn PyObject_CallOneArg(func: *mut PyObject, arg: *mut PyObject) -> *mut PyObject { assert!(!arg.is_null()); @@ -177,7 +177,7 @@ pub unsafe fn PyObject_CallOneArg(func: *mut PyObject, arg: *mut PyObject) -> *m _PyObject_VectorcallTstate(tstate, func, args, nargsf, std::ptr::null_mut()) } -#[cfg(all(Py_3_9, not(any(PyPy, GraalPy))))] +#[cfg(all(Py_3_9, not(PyPy)))] #[inline(always)] pub unsafe fn PyObject_CallMethodNoArgs( self_: *mut PyObject, @@ -191,7 +191,7 @@ pub unsafe fn PyObject_CallMethodNoArgs( ) } -#[cfg(all(Py_3_9, not(any(PyPy, GraalPy))))] +#[cfg(all(Py_3_9, not(PyPy)))] #[inline(always)] pub unsafe fn PyObject_CallMethodOneArg( self_: *mut PyObject, @@ -219,7 +219,7 @@ extern "C" { pub fn PyObject_LengthHint(o: *mut PyObject, arg1: Py_ssize_t) -> Py_ssize_t; #[cfg(not(Py_3_11))] // moved to src/buffer.rs from 3.11 - #[cfg(all(Py_3_9, not(any(PyPy, GraalPy))))] + #[cfg(all(Py_3_9, not(PyPy)))] pub fn PyObject_CheckBuffer(obj: *mut PyObject) -> c_int; } diff --git a/pyo3-ffi/src/cpython/bytesobject.rs b/pyo3-ffi/src/cpython/bytesobject.rs index dd78e646a12..9e233311ac1 100644 --- a/pyo3-ffi/src/cpython/bytesobject.rs +++ b/pyo3-ffi/src/cpython/bytesobject.rs @@ -1,6 +1,6 @@ use crate::object::*; use crate::Py_ssize_t; -#[cfg(not(any(PyPy, GraalPy, Py_LIMITED_API)))] +#[cfg(not(Py_LIMITED_API))] use std::os::raw::c_char; use std::os::raw::c_int; @@ -23,3 +23,12 @@ extern "C" { #[cfg_attr(PyPy, link_name = "_PyPyBytes_Resize")] pub fn _PyBytes_Resize(bytes: *mut *mut PyObject, newsize: Py_ssize_t) -> c_int; } + +#[cfg(not(Py_LIMITED_API))] +#[inline] +pub unsafe fn PyBytes_AS_STRING(op: *mut PyObject) -> *const c_char { + #[cfg(not(any(PyPy, GraalPy)))] + return &(*op.cast::()).ob_sval as *const c_char; + #[cfg(any(PyPy, GraalPy))] + return crate::PyBytes_AsString(op); +} diff --git a/pyo3-ffi/src/object.rs b/pyo3-ffi/src/object.rs index 5fbf45db617..c4c5abc361c 100644 --- a/pyo3-ffi/src/object.rs +++ b/pyo3-ffi/src/object.rs @@ -147,13 +147,13 @@ pub struct PyVarObject { // skipped private _PyVarObject_CAST #[inline] -#[cfg(not(all(PyPy, Py_3_10)))] +#[cfg(not(any(GraalPy, 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() } -#[cfg(all(PyPy, Py_3_10))] +#[cfg(any(GraalPy, all(PyPy, Py_3_10)))] #[cfg_attr(docsrs, doc(cfg(all())))] extern "C" { #[cfg_attr(PyPy, link_name = "PyPy_Is")] diff --git a/src/types/bytes.rs b/src/types/bytes.rs index 4e2ce9c96e7..16e0c42f8b1 100644 --- a/src/types/bytes.rs +++ b/src/types/bytes.rs @@ -365,4 +365,19 @@ mod tests { assert_eq!(*b, py_string); }) } + + #[test] + #[cfg(not(Py_LIMITED_API))] + fn test_as_string() { + Python::with_gil(|py| { + let b = b"hello, world".as_slice(); + let py_bytes = PyBytes::new(py, b); + unsafe { + assert_eq!( + ffi::PyBytes_AsString(py_bytes.as_ptr()) as *const std::os::raw::c_char, + ffi::PyBytes_AS_STRING(py_bytes.as_ptr()) as *const std::os::raw::c_char + ); + } + }) + } } From b8b7546c8a0d23a4b0bf8297a346be27b0cd1e04 Mon Sep 17 00:00:00 2001 From: Fabio Valentini Date: Mon, 26 May 2025 12:16:51 +0200 Subject: [PATCH 704/936] conversions: fix FromPyObject impl for uuid::Uuid for big-endian (#5161) The "to_le()" conversion on the u128 seems to be just wrong. --- newsfragments/5161.fixed.md | 1 + src/conversions/uuid.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 newsfragments/5161.fixed.md diff --git a/newsfragments/5161.fixed.md b/newsfragments/5161.fixed.md new file mode 100644 index 00000000000..1a40c79bbe8 --- /dev/null +++ b/newsfragments/5161.fixed.md @@ -0,0 +1 @@ +Fixed FromPyObject impl for `uuid::Uuid` on big-endian architectures diff --git a/src/conversions/uuid.rs b/src/conversions/uuid.rs index 44ac0bae25a..116bc1e2b29 100644 --- a/src/conversions/uuid.rs +++ b/src/conversions/uuid.rs @@ -85,7 +85,7 @@ impl FromPyObject<'_> for Uuid { if obj.is_instance(uuid_cls)? { let uuid_int: u128 = obj.getattr(intern!(py, "int"))?.extract()?; - Ok(Uuid::from_u128(uuid_int.to_le())) + Ok(Uuid::from_u128(uuid_int)) } else { Err(PyTypeError::new_err("Expected a `uuid.UUID` instance.")) } From 3914daff760fc23aae4602378b4c010332baa920 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 26 May 2025 12:29:12 -0600 Subject: [PATCH 705/936] Remove unnecessary warning filter in pytests (#5165) --- pytests/pytest.ini | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 pytests/pytest.ini diff --git a/pytests/pytest.ini b/pytests/pytest.ini deleted file mode 100644 index 3d62037f722..00000000000 --- a/pytests/pytest.ini +++ /dev/null @@ -1,3 +0,0 @@ -# see https://github.com/PyO3/pyo3/issues/5094 for details -[pytest] -filterwarnings = ignore::DeprecationWarning:pytest_asyncio.* \ No newline at end of file From daa3ee7897011c95775ce7617b4c1c7cadd8db49 Mon Sep 17 00:00:00 2001 From: "Larry Z." Date: Thu, 29 May 2025 02:02:03 +0100 Subject: [PATCH 706/936] Add `#[pyo3(warn(...))]` to `#[pyfunction]` (#4316) (#4364) * Implement `#[pyo3(warn)]` and `#[pyo3(deprecated)]` attribute * Disallow multiple `deprecated` * Fix testing and docs * Remove redundant warn_with_cstr_bound Fix err/mod.rs * Remove pyo3(deprecated = ...) * fixup UI tests --------- Co-authored-by: David Hewitt Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> --- guide/src/class.md | 2 + guide/src/class/protocols.md | 2 + guide/src/function.md | 105 +++++++ newsfragments/4364.added.md | 1 + pyo3-macros-backend/src/attributes.rs | 3 + pyo3-macros-backend/src/method.rs | 10 + pyo3-macros-backend/src/pyclass.rs | 3 + pyo3-macros-backend/src/pyfunction.rs | 163 ++++++++++- pyo3-macros-backend/src/pymethod.rs | 30 +- pyo3-macros/src/lib.rs | 1 + src/tests/common.rs | 51 ++++ src/tests/hygiene/pyfunction.rs | 14 + src/tests/hygiene/pymethods.rs | 108 +++++++ tests/test_compile_error.rs | 2 + tests/test_methods.rs | 270 ++++++++++++++++++ tests/test_pyfunction.rs | 111 +++++++ tests/ui/invalid_pyfunction_signatures.stderr | 2 +- tests/ui/invalid_pyfunction_warn.rs | 47 +++ tests/ui/invalid_pyfunction_warn.stderr | 47 +++ tests/ui/invalid_pymethods_warn.rs | 21 ++ tests/ui/invalid_pymethods_warn.stderr | 11 + 21 files changed, 998 insertions(+), 6 deletions(-) create mode 100644 newsfragments/4364.added.md create mode 100644 tests/ui/invalid_pyfunction_warn.rs create mode 100644 tests/ui/invalid_pyfunction_warn.stderr create mode 100644 tests/ui/invalid_pymethods_warn.rs create mode 100644 tests/ui/invalid_pymethods_warn.stderr diff --git a/guide/src/class.md b/guide/src/class.md index 8772e857d7e..192f66cc287 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -809,6 +809,8 @@ Python::with_gil(|py| { > Note: if the method has a `Result` return type and returns an `Err`, PyO3 will panic during class creation. +> Note: `#[classattr]` does not work with [`#[pyo3(warn(...))]`](./function.md#warn) attribute. + If the class attribute is defined with `const` code only, one can also annotate associated constants: diff --git a/guide/src/class/protocols.md b/guide/src/class/protocols.md index 44aa9199e36..6b7e6c09fe1 100644 --- a/guide/src/class/protocols.md +++ b/guide/src/class/protocols.md @@ -428,6 +428,8 @@ cleared, as every cycle must contain at least one mutable reference. - `__traverse__(, pyo3::class::gc::PyVisit<'_>) -> Result<(), pyo3::class::gc::PyTraverseError>` - `__clear__() -> ()` +> Note: `__traverse__` does not work with [`#[pyo3(warn(...))]`](../function.md#warn). + Example: ```rust,no_run diff --git a/guide/src/function.md b/guide/src/function.md index a3121835215..96782895317 100644 --- a/guide/src/function.md +++ b/guide/src/function.md @@ -25,6 +25,7 @@ This chapter of the guide explains full usage of the `#[pyfunction]` attribute. - [`#[pyo3(signature = (...))]`](#signature) - [`#[pyo3(text_signature = "...")]`](#text_signature) - [`#[pyo3(pass_module)]`](#pass_module) + - [`#[pyo3(warn(message = "...", category = ...))]`](#warn) - [Per-argument options](#per-argument-options) - [Advanced function patterns](#advanced-function-patterns) - [`#[pyfn]` shorthand](#pyfn-shorthand) @@ -96,7 +97,111 @@ The `#[pyo3]` attribute can be used to modify properties of the generated Python m.add_function(wrap_pyfunction!(pyfunction_with_module, m)?) } ``` + - `#[pyo3(warn(message = "...", category = ...))]` + This option is used to display a warning when the function is used in Python. It is equivalent to [`warnings.warn(message, category)`](https://docs.python.org/3.13/library/warnings.html#warnings.warn). + The `message` parameter is a string that will be displayed when the function is called, and the `category` parameter is optional and has to be a subclass of [`Warning`](https://docs.python.org/3.12/library/exceptions.html#Warning). + When the `category` parameter is not provided, the warning will be defaulted to [`UserWarning`](https://docs.python.org/3.12/library/exceptions.html#UserWarning). + + > Note: when used with `#[pymethods]`, this attribute does not work with `#[classattr]` nor `__traverse__` magic method. + + The following are examples of using the `#[pyo3(warn)]` attribute: + + ```rust + use pyo3::prelude::*; + + #[pymodule] + mod raising_warning_fn { + use pyo3::prelude::pyfunction; + use pyo3::exceptions::PyFutureWarning; + + #[pyfunction] + #[pyo3(warn(message = "This is a warning message"))] + fn function_with_warning() -> usize { + 42 + } + + #[pyfunction] + #[pyo3(warn(message = "This function is warning with FutureWarning", category = PyFutureWarning))] + fn function_with_warning_and_custom_category() -> usize { + 42 + } + } + + # use pyo3::exceptions::{PyFutureWarning, PyUserWarning}; + # use pyo3::types::{IntoPyDict, PyList}; + # use pyo3::PyTypeInfo; + # + # fn catch_warning(py: Python<'_>, f: impl FnOnce(&Bound<'_, PyList>) -> ()) -> PyResult<()> { + # let warnings = py.import("warnings")?; + # let kwargs = [("record", true)].into_py_dict(py)?; + # let catch_warnings = warnings + # .getattr("catch_warnings")? + # .call((), Some(&kwargs))?; + # let list = catch_warnings.call_method0("__enter__")?.downcast_into()?; + # warnings.getattr("simplefilter")?.call1(("always",))?; // show all warnings + # f(&list); + # catch_warnings + # .call_method1("__exit__", (py.None(), py.None(), py.None())) + # .unwrap(); + # Ok(()) + # } + # + # macro_rules! assert_warnings { + # ($py:expr, $body:expr, [$(($category:ty, $message:literal)),+] $(,)? ) => { + # catch_warning($py, |list| { + # $body; + # let expected_warnings = [$((<$category as PyTypeInfo>::type_object($py), $message)),+]; + # assert_eq!(list.len(), expected_warnings.len()); + # for (warning, (category, message)) in list.iter().zip(expected_warnings) { + # assert!(warning.getattr("category").unwrap().is(&category)); + # assert_eq!( + # warning.getattr("message").unwrap().str().unwrap().to_string_lossy(), + # message + # ); + # } + # }).unwrap(); + # }; + # } + # + # Python::with_gil(|py| { + # assert_warnings!( + # py, + # { + # let m = pyo3::wrap_pymodule!(raising_warning_fn)(py); + # let f1 = m.getattr(py, "function_with_warning").unwrap(); + # let f2 = m.getattr(py, "function_with_warning_and_custom_category").unwrap(); + # f1.call0(py).unwrap(); + # f2.call0(py).unwrap(); + # }, + # [ + # (PyUserWarning, "This is a warning message"), + # ( + # PyFutureWarning, + # "This function is warning with FutureWarning" + # ) + # ] + # ); + # }); + ``` + + When the functions are called as the following, warnings will be displayed. + + ```python + import warnings + from raising_warning_fn import function_with_warning, function_with_warning_and_custom_category + + function_with_warning() + function_with_warning_and_custom_category() + ``` + + The warning output will be: + + ```plaintext + UserWarning: This is a warning message + FutureWarning: This function is warning with FutureWarning + ``` + ## Per-argument options 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: diff --git a/newsfragments/4364.added.md b/newsfragments/4364.added.md new file mode 100644 index 00000000000..503cb7ae2c5 --- /dev/null +++ b/newsfragments/4364.added.md @@ -0,0 +1 @@ +Added `#[pyo3(warn(message = "...", category = ...))]` attribute for automatic warnings generation for `#[pyfunction]` and `#[pymethods]`. \ No newline at end of file diff --git a/pyo3-macros-backend/src/attributes.rs b/pyo3-macros-backend/src/attributes.rs index ac3894d6419..711653fe430 100644 --- a/pyo3-macros-backend/src/attributes.rs +++ b/pyo3-macros-backend/src/attributes.rs @@ -48,6 +48,9 @@ pub mod kw { syn::custom_keyword!(weakref); syn::custom_keyword!(generic); syn::custom_keyword!(gil_used); + syn::custom_keyword!(warn); + syn::custom_keyword!(message); + syn::custom_keyword!(category); } fn take_int(read: &mut &str, tracker: &mut usize) -> String { diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index d2ddb3179a4..862dd3d89b6 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -6,6 +6,7 @@ use proc_macro2::{Span, TokenStream}; use quote::{format_ident, quote, quote_spanned, ToTokens}; use syn::{ext::IdentExt, spanned::Spanned, Ident, Result}; +use crate::pyfunction::{PyFunctionWarning, WarningFactory}; use crate::pyversions::is_abi3_before; use crate::utils::{expr_to_python, Ctx, LitCStr}; use crate::{ @@ -443,6 +444,7 @@ pub struct FnSpec<'a> { pub text_signature: Option, pub asyncness: Option, pub unsafety: Option, + pub warnings: Vec, } pub fn parse_method_receiver(arg: &syn::FnArg) -> Result { @@ -479,6 +481,7 @@ impl<'a> FnSpec<'a> { text_signature, name, signature, + warnings, .. } = options; @@ -522,6 +525,7 @@ impl<'a> FnSpec<'a> { text_signature, asyncness: sig.asyncness, unsafety: sig.unsafety, + warnings, }) } @@ -768,6 +772,8 @@ impl<'a> FnSpec<'a> { quote!(#func_name) }; + let warnings = self.warnings.build_py_warning(ctx); + Ok(match self.convention { CallingConvention::Noargs => { let mut holders = Holders::new(); @@ -790,6 +796,7 @@ impl<'a> FnSpec<'a> { ) -> #pyo3_path::PyResult<*mut #pyo3_path::ffi::PyObject> { let function = #rust_name; // Shadow the function name to avoid #3017 #init_holders + #warnings let result = #call; result } @@ -812,6 +819,7 @@ impl<'a> FnSpec<'a> { let function = #rust_name; // Shadow the function name to avoid #3017 #arg_convert #init_holders + #warnings let result = #call; result } @@ -833,6 +841,7 @@ impl<'a> FnSpec<'a> { let function = #rust_name; // Shadow the function name to avoid #3017 #arg_convert #init_holders + #warnings let result = #call; result } @@ -857,6 +866,7 @@ impl<'a> FnSpec<'a> { let function = #rust_name; // Shadow the function name to avoid #3017 #arg_convert #init_holders + #warnings let result = #call; let initializer: #pyo3_path::PyClassInitializer::<#cls> = result.convert(py)?; #pyo3_path::impl_::pymethods::tp_new_impl(py, initializer, _slf) diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index ab86138338b..ea2a4ae7cc3 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -1669,6 +1669,7 @@ fn complex_enum_struct_variant_new<'a>( text_signature: None, asyncness: None, unsafety: None, + warnings: vec![], }; crate::pymethod::impl_py_method_def_new(&variant_cls_type, &spec, ctx) @@ -1723,6 +1724,7 @@ fn complex_enum_tuple_variant_new<'a>( text_signature: None, asyncness: None, unsafety: None, + warnings: vec![], }; crate::pymethod::impl_py_method_def_new(&variant_cls_type, &spec, ctx) @@ -1747,6 +1749,7 @@ fn complex_enum_variant_field_getter<'a>( text_signature: None, asyncness: None, unsafety: None, + warnings: vec![], }; let property_type = crate::pymethod::PropertyType::Function { diff --git a/pyo3-macros-backend/src/pyfunction.rs b/pyo3-macros-backend/src/pyfunction.rs index 82d9bceabb8..b24ea21289d 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -1,6 +1,7 @@ +use crate::attributes::KeywordAttribute; #[cfg(feature = "experimental-inspect")] use crate::introspection::{function_introspection_code, introspection_id_const}; -use crate::utils::Ctx; +use crate::utils::{Ctx, LitCStr}; use crate::{ attributes::{ self, get_pyo3_options, take_attributes, take_pyo3_options, CrateAttribute, @@ -9,11 +10,13 @@ use crate::{ method::{self, CallingConvention, FnArg}, pymethod::check_generic, }; -use proc_macro2::TokenStream; -use quote::{format_ident, quote}; +use proc_macro2::{Span, TokenStream}; +use quote::{format_ident, quote, ToTokens}; +use std::cmp::PartialEq; +use std::ffi::CString; use syn::parse::{Parse, ParseStream}; use syn::punctuated::Punctuated; -use syn::{ext::IdentExt, spanned::Spanned, Result}; +use syn::{ext::IdentExt, spanned::Spanned, LitStr, Path, Result, Token}; mod signature; @@ -83,6 +86,149 @@ impl PyFunctionArgPyO3Attributes { } } +type PyFunctionWarningMessageAttribute = KeywordAttribute; +type PyFunctionWarningCategoryAttribute = KeywordAttribute; + +pub struct PyFunctionWarningAttribute { + pub message: PyFunctionWarningMessageAttribute, + pub category: Option, + pub span: Span, +} + +#[derive(PartialEq)] +pub enum PyFunctionWarningCategory { + Path(Path), + UserWarning, + DeprecationWarning, // TODO: unused for now, intended for pyo3(deprecated) special-case +} + +pub struct PyFunctionWarning { + pub message: LitStr, + pub category: PyFunctionWarningCategory, + pub span: Span, +} + +impl From for PyFunctionWarning { + fn from(value: PyFunctionWarningAttribute) -> Self { + Self { + message: value.message.value, + category: value + .category + .map_or(PyFunctionWarningCategory::UserWarning, |cat| { + PyFunctionWarningCategory::Path(cat.value) + }), + span: value.span, + } + } +} + +pub trait WarningFactory { + fn build_py_warning(&self, ctx: &Ctx) -> TokenStream; + fn span(&self) -> Span; +} + +impl WarningFactory for PyFunctionWarning { + fn build_py_warning(&self, ctx: &Ctx) -> TokenStream { + let message = &self.message.value(); + let c_message = LitCStr::new( + CString::new(message.clone()).unwrap(), + Spanned::span(&message), + ctx, + ); + let pyo3_path = &ctx.pyo3_path; + let category = match &self.category { + PyFunctionWarningCategory::Path(path) => quote! {#path}, + PyFunctionWarningCategory::UserWarning => { + quote! {#pyo3_path::exceptions::PyUserWarning} + } + PyFunctionWarningCategory::DeprecationWarning => { + quote! {#pyo3_path::exceptions::PyDeprecationWarning} + } + }; + quote! { + #pyo3_path::PyErr::warn(py, &<#category as #pyo3_path::PyTypeInfo>::type_object(py), #c_message, 1)?; + } + } + + fn span(&self) -> Span { + self.span + } +} + +impl WarningFactory for Vec { + fn build_py_warning(&self, ctx: &Ctx) -> TokenStream { + let warnings = self.iter().map(|warning| warning.build_py_warning(ctx)); + + quote! { + #(#warnings)* + } + } + + fn span(&self) -> Span { + self.iter() + .map(|val| val.span()) + .reduce(|acc, span| acc.join(span).unwrap_or(acc)) + .unwrap() + } +} + +impl Parse for PyFunctionWarningAttribute { + fn parse(input: ParseStream<'_>) -> Result { + let mut message: Option = None; + let mut category: Option = None; + + let span = input.parse::()?.span(); + + let content; + syn::parenthesized!(content in input); + + while !content.is_empty() { + let lookahead = content.lookahead1(); + + if lookahead.peek(attributes::kw::message) { + message = content + .parse::() + .map(Some)?; + } else if lookahead.peek(attributes::kw::category) { + category = content + .parse::() + .map(Some)?; + } else { + return Err(lookahead.error()); + } + + if content.peek(Token![,]) { + content.parse::()?; + } + } + + Ok(PyFunctionWarningAttribute { + message: message.ok_or(syn::Error::new( + content.span(), + "missing `message` in `warn` attribute", + ))?, + category, + span, + }) + } +} + +impl ToTokens for PyFunctionWarningAttribute { + fn to_tokens(&self, tokens: &mut TokenStream) { + let message_tokens = self.message.to_token_stream(); + let category_tokens = self + .category + .as_ref() + .map_or(quote! {}, |cat| cat.to_token_stream()); + + let token_stream = quote! { + warn(#message_tokens, #category_tokens) + }; + + tokens.extend(token_stream); + } +} + #[derive(Default)] pub struct PyFunctionOptions { pub pass_module: Option, @@ -90,6 +236,7 @@ pub struct PyFunctionOptions { pub signature: Option, pub text_signature: Option, pub krate: Option, + pub warnings: Vec, } impl Parse for PyFunctionOptions { @@ -109,6 +256,7 @@ pub enum PyFunctionOption { Signature(SignatureAttribute), TextSignature(TextSignatureAttribute), Crate(CrateAttribute), + Warning(PyFunctionWarningAttribute), } impl Parse for PyFunctionOption { @@ -124,6 +272,8 @@ impl Parse for PyFunctionOption { input.parse().map(PyFunctionOption::TextSignature) } else if lookahead.peek(syn::Token![crate]) { input.parse().map(PyFunctionOption::Crate) + } else if lookahead.peek(attributes::kw::warn) { + input.parse().map(PyFunctionOption::Warning) } else { Err(lookahead.error()) } @@ -159,6 +309,9 @@ impl PyFunctionOptions { PyFunctionOption::Signature(signature) => set_option!(signature), PyFunctionOption::TextSignature(text_signature) => set_option!(text_signature), PyFunctionOption::Crate(krate) => set_option!(krate), + PyFunctionOption::Warning(warning) => { + self.warnings.push(warning.into()); + } } } Ok(()) @@ -186,6 +339,7 @@ pub fn impl_wrap_pyfunction( signature, text_signature, krate, + warnings, } = options; let ctx = &Ctx::new(&krate, Some(&func.sig)); @@ -255,6 +409,7 @@ pub fn impl_wrap_pyfunction( text_signature, asyncness: func.sig.asyncness, unsafety: func.sig.unsafety, + warnings, }; let wrapper_ident = format_ident!("__pyfunction_{}", spec.name); diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index a34e22b1e3d..8d70ee49b0b 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -4,6 +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::pyfunction::WarningFactory; use crate::utils::{Ctx, LitCStr}; use crate::utils::{PythonDoc, TypeExt as _}; use crate::{ @@ -461,6 +462,11 @@ fn impl_traverse_slot( } } + ensure_spanned!( + spec.warnings.is_empty(), + spec.warnings.span() => "__traverse__ cannot be used with #[pyo3(warn)]" + ); + let rust_fn_ident = spec.name; let associated_method = quote! { @@ -542,6 +548,12 @@ pub(crate) fn impl_py_class_attribute( args[0].ty().span() => "#[classattr] can only have one argument (of type pyo3::Python)" ); + ensure_spanned!( + spec.warnings.is_empty(), + spec.warnings.span() + => "#[classattr] cannot be used with #[pyo3(warn)]" + ); + let name = &spec.name; let fncall = if py_arg.is_some() { quote!(function(py)) @@ -725,6 +737,12 @@ pub fn impl_py_setter_def( } } + let warnings = if let PropertyType::Function { spec, .. } = &property_type { + spec.warnings.build_py_warning(ctx) + } else { + quote!() + }; + let init_holders = holders.init_holders(ctx); let associated_method = quote! { #cfg_attrs @@ -740,6 +758,7 @@ pub fn impl_py_setter_def( })?; #init_holders #extract + #warnings let result = #setter_impl; #pyo3_path::impl_::callback::convert(py, result) } @@ -872,6 +891,8 @@ pub fn impl_py_getter_def( }; let init_holders = holders.init_holders(ctx); + let warnings = spec.warnings.build_py_warning(ctx); + let associated_method = quote! { #cfg_attrs unsafe fn #wrapper_ident( @@ -879,6 +900,7 @@ pub fn impl_py_getter_def( _slf: *mut #pyo3_path::ffi::PyObject ) -> #pyo3_path::PyResult<*mut #pyo3_path::ffi::PyObject> { #init_holders + #warnings let result = #body; result } @@ -1419,13 +1441,19 @@ fn generate_method_body( let rust_name = spec.name; 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 { + let body = if let Some(return_mode) = return_mode { return_mode.return_call_output(call, ctx) } else { quote! { let result = #call; #pyo3_path::impl_::callback::convert(py, result) } + }; + let warnings = spec.warnings.build_py_warning(ctx); + + Ok(quote! { + #warnings + #body }) } diff --git a/pyo3-macros/src/lib.rs b/pyo3-macros/src/lib.rs index 2621bea4c6e..5d16a28a97f 100644 --- a/pyo3-macros/src/lib.rs +++ b/pyo3-macros/src/lib.rs @@ -130,6 +130,7 @@ pub fn pymethods(attr: TokenStream, input: TokenStream) -> TokenStream { /// | `#[pyo3(name = "...")]` | Defines the name of the function in Python. | /// | `#[pyo3(text_signature = "...")]` | Defines the `__text_signature__` attribute of the function in Python. | /// | `#[pyo3(pass_module)]` | Passes the module containing the function as a `&PyModule` first argument to the function. | +/// | `#[pyo3(warn(message = "...", category = ...))]` | Generate warning given a message and a category | /// /// For more on exposing functions see the [function section of the guide][1]. /// diff --git a/src/tests/common.rs b/src/tests/common.rs index cd0374e9019..8dbd7427ab3 100644 --- a/src/tests/common.rs +++ b/src/tests/common.rs @@ -68,6 +68,57 @@ mod inner { }}; } + #[macro_export] + macro_rules! py_expect_warning { + ($py:expr, $($val:ident)+, $code:expr, [$(($warning_msg:literal, $warning_category:path)),+] $(,)?) => {{ + use pyo3::types::IntoPyDict; + let d = [$((stringify!($val), ($val.as_ref() as &Bound<'_, PyAny>).into_pyobject($py).expect("Failed to create test dict element")),)+].into_py_dict($py).expect("Failed to create test dict"); + py_expect_warning!($py, *d, $code, [$(($warning_msg, $warning_category)),+]) + }}; + ($py:expr, *$dict:expr, $code:expr, [$(($warning_msg:literal, $warning_category:path)),+] $(,)?) => {{ + let code_lines: Vec<&str> = $code.lines().collect(); + let indented_code: String = code_lines.iter() + .map(|line| format!(" {}", line)) // add 4 spaces indentation + .collect::>() + .join("\n"); + + let wrapped_code = format!(r#" +import warnings +with warnings.catch_warnings(record=True) as warning_record: +{} +"#, indented_code); + + $py.run(&std::ffi::CString::new(wrapped_code).unwrap(), None, Some(&$dict.as_borrowed())).expect("Failed to run warning testing code"); + let expected_warnings = [$(($warning_msg, <$warning_category as pyo3::PyTypeInfo>::type_object($py))),+]; + let warning_record: Bound<'_, pyo3::types::PyList> = $dict.get_item("warning_record").expect("Failed to capture warnings").expect("Failed to downcast to PyList").extract().unwrap(); + + assert_eq!(warning_record.len(), expected_warnings.len(), "Expecting {} warnings but got {}", expected_warnings.len(), warning_record.len()); + + for ((index, warning), (msg, category)) in warning_record.iter().enumerate().zip(expected_warnings.iter()) { + let actual_msg = warning.getattr("message").unwrap().str().unwrap().to_string_lossy().to_string(); + let actual_category = warning.getattr("category").unwrap(); + + assert_eq!(actual_msg, msg.to_string(), "Warning message mismatch at index {}, expecting `{}` but got `{}`", index, msg, actual_msg); + assert!(actual_category.is(category), "Warning category mismatch at index {}, expecting {:?} but got {:?}", index, category, actual_category); + } + }}; + } + + #[macro_export] + macro_rules! py_expect_warning_for_fn { + ($fn:ident, $($val:ident)+, [$(($warning_msg:literal, $warning_category:path)),+] $(,)?) => { + pyo3::Python::with_gil(|py| { + let f = wrap_pyfunction!($fn)(py).unwrap(); + py_expect_warning!( + py, + f, + "f()", + [$(($warning_msg, $warning_category)),+] + ); + }); + }; + } + // sys.unraisablehook not available until Python 3.8 #[cfg(all(feature = "macros", Py_3_8, not(Py_GIL_DISABLED)))] #[pyclass(crate = "pyo3")] diff --git a/src/tests/hygiene/pyfunction.rs b/src/tests/hygiene/pyfunction.rs index 2aabd111dd8..44784aefe1c 100644 --- a/src/tests/hygiene/pyfunction.rs +++ b/src/tests/hygiene/pyfunction.rs @@ -10,6 +10,20 @@ fn check_5012(x: i32) -> crate::PyResult { ::std::result::Result::Ok(x) } +#[crate::pyfunction] +#[pyo3(crate = "crate")] +#[pyo3(warn(message = "This is a warning message"))] +fn function_with_warning() {} + +#[crate::pyfunction(crate = "crate")] +#[pyo3(warn(message = "This is a warning message with custom category", category = crate::exceptions::PyFutureWarning))] +fn function_with_warning_and_category() {} + +#[crate::pyfunction(crate = "crate")] +#[pyo3(warn(message = "This is a warning message"))] +#[pyo3(warn(message = "This is another warning message", category = crate::exceptions::PyFutureWarning))] +fn multiple_warning_function() {} + #[test] fn invoke_wrap_pyfunction() { crate::Python::with_gil(|py| { diff --git a/src/tests/hygiene/pymethods.rs b/src/tests/hygiene/pymethods.rs index a5856e6413e..3f7361a395a 100644 --- a/src/tests/hygiene/pymethods.rs +++ b/src/tests/hygiene/pymethods.rs @@ -450,3 +450,111 @@ impl Dummy2 { "Dummy" } } + +#[crate::pyclass(crate = "crate")] +struct WarningDummy { + value: i32, +} + +#[cfg(not(Py_LIMITED_API))] +#[crate::pyclass(crate = "crate", extends=crate::exceptions::PyWarning)] +pub struct UserDefinedWarning {} + +#[cfg(not(Py_LIMITED_API))] +#[crate::pymethods(crate = "crate")] +impl UserDefinedWarning { + #[new] + #[pyo3(signature = (*_args, **_kwargs))] + fn new( + _args: crate::Bound<'_, crate::PyAny>, + _kwargs: ::std::option::Option>, + ) -> Self { + Self {} + } +} + +#[crate::pymethods(crate = "crate")] +impl WarningDummy { + #[new] + #[pyo3(warn(message = "this __new__ method raises warning"))] + fn new() -> Self { + Self { value: 0 } + } + + #[pyo3(warn(message = "this method raises warning"))] + fn method_with_warning(_slf: crate::PyRef<'_, Self>) {} + + #[pyo3(warn(message = "this method raises warning", category = crate::exceptions::PyFutureWarning))] + fn method_with_warning_and_custom_category(_slf: crate::PyRef<'_, Self>) {} + + #[cfg(not(Py_LIMITED_API))] + #[pyo3(warn(message = "this method raises user-defined warning", category = UserDefinedWarning))] + fn method_with_warning_and_user_defined_category(&self) {} + + #[staticmethod] + #[pyo3(warn(message = "this static method raises warning"))] + fn static_method() {} + + #[staticmethod] + #[pyo3(warn(message = "this class method raises warning"))] + fn class_method() {} + + #[getter] + #[pyo3(warn(message = "this getter raises warning"))] + fn get_value(&self) -> i32 { + self.value + } + + #[setter] + #[pyo3(warn(message = "this setter raises warning"))] + fn set_value(&mut self, value: i32) { + self.value = value; + } + + #[pyo3(warn(message = "this subscript op method raises warning"))] + fn __getitem__(&self, _key: i32) -> i32 { + 0 + } + + #[pyo3(warn(message = "the + op method raises warning"))] + fn __add__(&self, other: crate::PyRef<'_, Self>) -> Self { + Self { + value: self.value + other.value, + } + } + + #[pyo3(warn(message = "this __call__ method raises warning"))] + fn __call__(&self) -> i32 { + self.value + } + + #[pyo3(warn(message = "this method raises warning 1"))] + #[pyo3(warn(message = "this method raises warning 2", category = crate::exceptions::PyFutureWarning))] + fn multiple_warn_method(&self) {} +} + +#[crate::pyclass(crate = "crate")] +struct WarningDummy2; + +#[crate::pymethods(crate = "crate")] +impl WarningDummy2 { + #[new] + #[classmethod] + #[pyo3(warn(message = "this class-method __new__ method raises warning"))] + fn new(_cls: crate::Bound<'_, crate::types::PyType>) -> Self { + Self {} + } + + #[pyo3(warn(message = "this class-method raises warning 1"))] + #[pyo3(warn(message = "this class-method raises warning 2"))] + fn multiple_default_warnings_fn(&self) {} + + #[pyo3(warn(message = "this class-method raises warning"))] + #[pyo3(warn(message = "this class-method raises future warning", category = crate::exceptions::PyFutureWarning))] + fn multiple_warnings_fn(&self) {} + + #[cfg(not(Py_LIMITED_API))] + #[pyo3(warn(message = "this class-method raises future warning", category = crate::exceptions::PyFutureWarning))] + #[pyo3(warn(message = "this class-method raises user-defined warning", category = UserDefinedWarning))] + fn multiple_warnings_fn_with_custom_category(&self) {} +} diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index 0053f86e7bb..0608a1f0df4 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -80,4 +80,6 @@ fn test_compile_errors() { t.compile_fail("tests/ui/immutable_type.rs"); t.pass("tests/ui/ambiguous_associated_items.rs"); t.pass("tests/ui/pyclass_probe.rs"); + t.compile_fail("tests/ui/invalid_pyfunction_warn.rs"); + t.compile_fail("tests/ui/invalid_pymethods_warn.rs"); } diff --git a/tests/test_methods.rs b/tests/test_methods.rs index d6ce5e30aae..5f8398f6681 100644 --- a/tests/test_methods.rs +++ b/tests/test_methods.rs @@ -1,10 +1,15 @@ #![cfg(feature = "macros")] +#[cfg(not(Py_LIMITED_API))] +use pyo3::exceptions::PyWarning; +#[cfg(not(Py_GIL_DISABLED))] +use pyo3::exceptions::{PyFutureWarning, PyUserWarning}; use pyo3::prelude::*; use pyo3::py_run; use pyo3::types::PySequence; use pyo3::types::{IntoPyDict, PyDict, PyList, PySet, PyString, PyTuple, PyType}; use pyo3::BoundObject; +use pyo3_macros::pyclass; #[path = "../src/tests/common.rs"] mod common; @@ -1202,3 +1207,268 @@ fn test_issue_2988() { ) { } } + +#[cfg(not(Py_LIMITED_API))] +#[pyclass(extends=PyWarning)] +pub struct UserDefinedWarning {} + +#[cfg(not(Py_LIMITED_API))] +#[pymethods] +impl UserDefinedWarning { + #[new] + #[pyo3(signature = (*_args, **_kwargs))] + fn new(_args: Bound<'_, PyAny>, _kwargs: Option>) -> Self { + Self {} + } +} + +#[test] +#[cfg(not(Py_GIL_DISABLED))] // FIXME: enable once `warnings` is thread-safe +fn test_pymethods_warn() { + // We do not test #[classattr] nor __traverse__ + // because it doesn't make sense to implement deprecated methods for them. + + #[pyclass] + struct WarningMethodContainer { + value: i32, + } + + #[pymethods] + impl WarningMethodContainer { + #[new] + #[pyo3(warn(message = "this __new__ method raises warning"))] + fn new() -> Self { + Self { value: 0 } + } + + #[pyo3(warn(message = "this method raises warning"))] + fn method_with_warning(_slf: PyRef<'_, Self>) {} + + #[pyo3(warn(message = "this method raises warning", category = PyFutureWarning))] + fn method_with_warning_and_custom_category(_slf: PyRef<'_, Self>) {} + + #[cfg(not(Py_LIMITED_API))] + #[pyo3(warn(message = "this method raises user-defined warning", category = UserDefinedWarning))] + fn method_with_warning_and_user_defined_category(&self) {} + + #[staticmethod] + #[pyo3(warn(message = "this static method raises warning"))] + fn static_method() {} + + #[staticmethod] + #[pyo3(warn(message = "this class method raises warning"))] + fn class_method() {} + + #[getter] + #[pyo3(warn(message = "this getter raises warning"))] + fn get_value(&self) -> i32 { + self.value + } + + #[setter] + #[pyo3(warn(message = "this setter raises warning"))] + fn set_value(&mut self, value: i32) { + self.value = value; + } + + #[pyo3(warn(message = "this subscript op method raises warning"))] + fn __getitem__(&self, _key: i32) -> i32 { + 0 + } + + #[pyo3(warn(message = "the + op method raises warning"))] + fn __add__(&self, other: PyRef<'_, Self>) -> Self { + Self { + value: self.value + other.value, + } + } + + #[pyo3(warn(message = "this __call__ method raises warning"))] + fn __call__(&self) -> i32 { + self.value + } + } + + Python::with_gil(|py| { + let typeobj = py.get_type::(); + let obj = typeobj.call0().unwrap(); + + // FnType::Fn + py_expect_warning!( + py, + obj, + "obj.method_with_warning()", + [("this method raises warning", PyUserWarning)], + ); + + // FnType::Fn + py_expect_warning!( + py, + obj, + "obj.method_with_warning_and_custom_category()", + [("this method raises warning", PyFutureWarning)] + ); + + // FnType::Fn, user-defined warning + #[cfg(not(Py_LIMITED_API))] + py_expect_warning!( + py, + obj, + "obj.method_with_warning_and_user_defined_category()", + [( + "this method raises user-defined warning", + UserDefinedWarning + )] + ); + + // #[staticmethod], FnType::FnStatic + py_expect_warning!( + py, + typeobj, + "typeobj.static_method()", + [("this static method raises warning", PyUserWarning)] + ); + + // #[classmethod], FnType::FnClass + py_expect_warning!( + py, + typeobj, + "typeobj.class_method()", + [("this class method raises warning", PyUserWarning)] + ); + + // #[classmethod], FnType::FnClass + py_expect_warning!( + py, + obj, + "obj.class_method()", + [("this class method raises warning", PyUserWarning)] + ); + + // #[new], FnType::FnNew + py_expect_warning!( + py, + typeobj, + "typeobj()", + [("this __new__ method raises warning", PyUserWarning)] + ); + + // #[getter], FnType::Getter + py_expect_warning!( + py, + obj, + "val = obj.value", + [("this getter raises warning", PyUserWarning)] + ); + + // #[setter], FnType::Setter + py_expect_warning!( + py, + obj, + "obj.value = 10", + [("this setter raises warning", PyUserWarning)] + ); + + // PyMethodProtoKind::Slot + py_expect_warning!( + py, + obj, + "obj[0]", + [("this subscript op method raises warning", PyUserWarning)] + ); + + // PyMethodProtoKind::SlotFragment + py_expect_warning!( + py, + obj, + "obj + obj", + [("the + op method raises warning", PyUserWarning)] + ); + + // PyMethodProtoKind::Call + py_expect_warning!( + py, + obj, + "obj()", + [("this __call__ method raises warning", PyUserWarning)] + ); + }); + + #[pyclass] + struct WarningMethodContainer2 {} + + #[pymethods] + impl WarningMethodContainer2 { + #[new] + #[classmethod] + #[pyo3(warn(message = "this class-method __new__ method raises warning"))] + fn new(_cls: Bound<'_, PyType>) -> Self { + Self {} + } + } + + Python::with_gil(|py| { + let typeobj = py.get_type::(); + + // #[new], #[classmethod], FnType::FnNewClass + py_expect_warning!( + py, + typeobj, + "typeobj()", + [( + "this class-method __new__ method raises warning", + PyUserWarning + )] + ); + }); +} + +#[test] +#[cfg(not(Py_GIL_DISABLED))] // FIXME: enable once `warnings` is thread-safe +fn test_py_methods_multiple_warn() { + #[pyclass] + struct MultipleWarnContainer {} + + #[pymethods] + impl MultipleWarnContainer { + #[new] + fn new() -> Self { + Self {} + } + + #[pyo3(warn(message = "this method raises warning 1"))] + #[pyo3(warn(message = "this method raises warning 2", category = PyFutureWarning))] + fn multiple_warn_method(&self) {} + + #[cfg(not(Py_LIMITED_API))] + #[pyo3(warn(message = "this method raises FutureWarning", category = PyFutureWarning))] + #[pyo3(warn(message = "this method raises UserDefinedWarning", category = UserDefinedWarning))] + fn multiple_warn_custom_category_method(&self) {} + } + + Python::with_gil(|py| { + let typeobj = py.get_type::(); + let obj = typeobj.call0().unwrap(); + + py_expect_warning!( + py, + obj, + "obj.multiple_warn_method()", + [ + ("this method raises warning 1", PyUserWarning), + ("this method raises warning 2", PyFutureWarning) + ] + ); + + #[cfg(not(Py_LIMITED_API))] + py_expect_warning!( + py, + obj, + "obj.multiple_warn_custom_category_method()", + [ + ("this method raises FutureWarning", PyFutureWarning), + ("this method raises UserDefinedWarning", UserDefinedWarning) + ] + ); + }); +} diff --git a/tests/test_pyfunction.rs b/tests/test_pyfunction.rs index 3f57f66e976..b5af0c81836 100644 --- a/tests/test_pyfunction.rs +++ b/tests/test_pyfunction.rs @@ -5,6 +5,10 @@ use std::collections::HashMap; #[cfg(not(Py_LIMITED_API))] use pyo3::buffer::PyBuffer; +#[cfg(not(Py_LIMITED_API))] +use pyo3::exceptions::PyWarning; +#[cfg(not(Py_GIL_DISABLED))] +use pyo3::exceptions::{PyFutureWarning, PyUserWarning}; use pyo3::ffi::c_str; use pyo3::prelude::*; #[cfg(not(Py_LIMITED_API))] @@ -12,6 +16,7 @@ use pyo3::types::PyDateTime; #[cfg(not(any(Py_LIMITED_API, PyPy)))] use pyo3::types::PyFunction; use pyo3::types::{self, PyCFunction}; +use pyo3_macros::pyclass; #[path = "../src/tests/common.rs"] mod common; @@ -647,3 +652,109 @@ fn test_pyfunction_raw_ident() { py_assert!(py, m, "m.enum()"); }) } + +#[cfg(not(Py_LIMITED_API))] +#[pyclass(extends=PyWarning)] +pub struct UserDefinedWarning {} + +#[cfg(not(Py_LIMITED_API))] +#[pymethods] +impl UserDefinedWarning { + #[new] + #[pyo3(signature = (*_args, **_kwargs))] + fn new(_args: Bound<'_, PyAny>, _kwargs: Option>) -> Self { + Self {} + } +} + +#[test] +#[cfg(not(Py_GIL_DISABLED))] // FIXME: enable once `warnings` is thread-safe +fn test_pyfunction_warn() { + #[pyfunction] + #[pyo3(warn(message = "this function raises warning"))] + fn function_with_warning() {} + + py_expect_warning_for_fn!( + function_with_warning, + f, + [("this function raises warning", PyUserWarning)] + ); + + #[pyfunction] + #[pyo3(warn(message = "this function raises warning with category", category = PyFutureWarning))] + fn function_with_warning_with_category() {} + + py_expect_warning_for_fn!( + function_with_warning_with_category, + f, + [( + "this function raises warning with category", + PyFutureWarning + )] + ); + + #[pyfunction] + #[pyo3(warn(message = "custom deprecated category", category = pyo3::exceptions::PyDeprecationWarning))] + fn function_with_warning_with_custom_category() {} + + py_expect_warning_for_fn!( + function_with_warning_with_custom_category, + f, + [( + "custom deprecated category", + pyo3::exceptions::PyDeprecationWarning + )] + ); + + #[cfg(not(Py_LIMITED_API))] + #[pyfunction] + #[pyo3(warn(message = "this function raises user-defined warning", category = UserDefinedWarning))] + fn function_with_warning_and_user_defined_category() {} + + #[cfg(not(Py_LIMITED_API))] + py_expect_warning_for_fn!( + function_with_warning_and_user_defined_category, + f, + [( + "this function raises user-defined warning", + UserDefinedWarning + )] + ); +} + +#[test] +#[cfg(not(Py_GIL_DISABLED))] // FIXME: enable once `warnings` is thread-safe +fn test_pyfunction_multiple_warnings() { + #[pyfunction] + #[pyo3(warn(message = "this function raises warning"))] + #[pyo3(warn(message = "this function raises FutureWarning", category = PyFutureWarning))] + fn function_with_multiple_warnings() {} + + py_expect_warning_for_fn!( + function_with_multiple_warnings, + f, + [ + ("this function raises warning", PyUserWarning), + ("this function raises FutureWarning", PyFutureWarning) + ] + ); + + #[cfg(not(Py_LIMITED_API))] + #[pyfunction] + #[pyo3(warn(message = "this function raises FutureWarning", category = PyFutureWarning))] + #[pyo3(warn(message = "this function raises user-defined warning", category = UserDefinedWarning))] + fn function_with_multiple_custom_warnings() {} + + #[cfg(not(Py_LIMITED_API))] + py_expect_warning_for_fn!( + function_with_multiple_custom_warnings, + f, + [ + ("this function raises FutureWarning", PyFutureWarning), + ( + "this function raises user-defined warning", + UserDefinedWarning + ) + ] + ); +} diff --git a/tests/ui/invalid_pyfunction_signatures.stderr b/tests/ui/invalid_pyfunction_signatures.stderr index 97d0fd3b4af..60498ebd931 100644 --- a/tests/ui/invalid_pyfunction_signatures.stderr +++ b/tests/ui/invalid_pyfunction_signatures.stderr @@ -16,7 +16,7 @@ error: expected argument from function definition `y` but got argument `x` 13 | #[pyo3(signature = (x))] | ^ -error: expected one of: `name`, `pass_module`, `signature`, `text_signature`, `crate` +error: expected one of: `name`, `pass_module`, `signature`, `text_signature`, `crate`, `warn` --> tests/ui/invalid_pyfunction_signatures.rs:18:14 | 18 | #[pyfunction(x)] diff --git a/tests/ui/invalid_pyfunction_warn.rs b/tests/ui/invalid_pyfunction_warn.rs new file mode 100644 index 00000000000..fe1b72dbc3f --- /dev/null +++ b/tests/ui/invalid_pyfunction_warn.rs @@ -0,0 +1,47 @@ +use pyo3::prelude::*; + +#[pyfunction] +#[pyo3(warn)] +fn no_parenthesis_deprecated() {} + +#[pyfunction] +#[pyo3(warn())] +fn no_message_deprecated() {} + +#[pyfunction] +#[pyo3(warn(category = pyo3::exceptions::PyDeprecationWarning))] +fn no_message_deprecated_with_category() {} + +#[pyfunction] +#[pyo3(warn(category = pyo3::exceptions::PyDeprecationWarning, message = ,))] +fn empty_message_deprecated_with_category() {} + +#[pyfunction] +#[pyo3(warn(message = "deprecated function", category = ,))] +fn empty_category_deprecated_with_message() {} + +#[pyfunction] +#[pyo3(warn(message = "deprecated function", random_key))] +fn random_key_deprecated() {} + +#[pyclass] +struct DeprecatedMethodContainer {} + +#[pymethods] +impl DeprecatedMethodContainer { + #[classattr] + #[pyo3(warn(message = "deprecated class attr"))] + fn deprecated_class_attr() -> i32 { + 5 + } +} + +#[pymethods] +impl DeprecatedMethodContainer { + #[pyo3(warn(message = "deprecated __traverse__"))] + fn __traverse__(&self, _visit: pyo3::gc::PyVisit<'_>) -> Result<(), pyo3::PyTraverseError> { + Ok(()) + } +} + +fn main() {} diff --git a/tests/ui/invalid_pyfunction_warn.stderr b/tests/ui/invalid_pyfunction_warn.stderr new file mode 100644 index 00000000000..584abfcdeaf --- /dev/null +++ b/tests/ui/invalid_pyfunction_warn.stderr @@ -0,0 +1,47 @@ +error: unexpected end of input, expected parentheses + --> tests/ui/invalid_pyfunction_warn.rs:4:12 + | +4 | #[pyo3(warn)] + | ^ + +error: missing `message` in `warn` attribute + --> tests/ui/invalid_pyfunction_warn.rs:8:13 + | +8 | #[pyo3(warn())] + | ^ + +error: missing `message` in `warn` attribute + --> tests/ui/invalid_pyfunction_warn.rs:12:62 + | +12 | #[pyo3(warn(category = pyo3::exceptions::PyDeprecationWarning))] + | ^ + +error: expected string literal + --> tests/ui/invalid_pyfunction_warn.rs:16:74 + | +16 | #[pyo3(warn(category = pyo3::exceptions::PyDeprecationWarning, message = ,))] + | ^ + +error: expected identifier + --> tests/ui/invalid_pyfunction_warn.rs:20:57 + | +20 | #[pyo3(warn(message = "deprecated function", category = ,))] + | ^ + +error: expected `message` or `category` + --> tests/ui/invalid_pyfunction_warn.rs:24:46 + | +24 | #[pyo3(warn(message = "deprecated function", random_key))] + | ^^^^^^^^^^ + +error: #[classattr] cannot be used with #[pyo3(warn)] + --> tests/ui/invalid_pyfunction_warn.rs:33:12 + | +33 | #[pyo3(warn(message = "deprecated class attr"))] + | ^^^^ + +error: __traverse__ cannot be used with #[pyo3(warn)] + --> tests/ui/invalid_pyfunction_warn.rs:41:12 + | +41 | #[pyo3(warn(message = "deprecated __traverse__"))] + | ^^^^ diff --git a/tests/ui/invalid_pymethods_warn.rs b/tests/ui/invalid_pymethods_warn.rs new file mode 100644 index 00000000000..6c6f39f5dc0 --- /dev/null +++ b/tests/ui/invalid_pymethods_warn.rs @@ -0,0 +1,21 @@ +use pyo3::prelude::*; + +#[pyclass] +struct WarningMethodContainer {} + +#[pymethods] +impl WarningMethodContainer { + #[pyo3(warn(message = "warn on __traverse__"))] + fn __traverse__(&self) {} +} + +#[pymethods] +impl WarningMethodContainer { + #[classattr] + #[pyo3(warn(message = "warn for class attr"))] + fn a_class_attr(_py: pyo3::Python<'_>) -> i64 { + 5 + } +} + +fn main() {} diff --git a/tests/ui/invalid_pymethods_warn.stderr b/tests/ui/invalid_pymethods_warn.stderr new file mode 100644 index 00000000000..9e0c8f1d7e9 --- /dev/null +++ b/tests/ui/invalid_pymethods_warn.stderr @@ -0,0 +1,11 @@ +error: __traverse__ cannot be used with #[pyo3(warn)] + --> tests/ui/invalid_pymethods_warn.rs:8:12 + | +8 | #[pyo3(warn(message = "warn on __traverse__"))] + | ^^^^ + +error: #[classattr] cannot be used with #[pyo3(warn)] + --> tests/ui/invalid_pymethods_warn.rs:15:12 + | +15 | #[pyo3(warn(message = "warn for class attr"))] + | ^^^^ From 8373f451fdf9caf6b6ed3cb57b576aac067bad07 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Fri, 30 May 2025 13:28:38 -0400 Subject: [PATCH 707/936] If the bare abi3 feature is requested on a too-new Python, fall back to the maximum supported abi3 (#5144) --- newsfragments/5144.changed.md | 1 + noxfile.py | 3 +++ pyo3-build-config/src/impl_.rs | 3 +++ 3 files changed, 7 insertions(+) create mode 100644 newsfragments/5144.changed.md diff --git a/newsfragments/5144.changed.md b/newsfragments/5144.changed.md new file mode 100644 index 00000000000..3812e70197a --- /dev/null +++ b/newsfragments/5144.changed.md @@ -0,0 +1 @@ +When building with `abi3` for a Python version newer than pyo3 supports, we now automatically fall back to an abi3 build for the latest version. diff --git a/noxfile.py b/noxfile.py index c2b95fe0842..cbd499aa899 100644 --- a/noxfile.py +++ b/noxfile.py @@ -743,6 +743,9 @@ def test_version_limits(session: nox.Session): config_file.set("CPython", "3.15") _run_cargo(session, "check", env=env, expect_error=True) + # 3.15 CPython should build if abi3 is explicitly requested + _run_cargo(session, "check", "--features=pyo3/abi3", env=env) + # 3.15 CPython should build with forward compatibility env["PYO3_USE_ABI3_FORWARD_COMPATIBILITY"] = "1" _run_cargo(session, "check", env=env) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 9485b8a0394..4997fc936fc 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -730,6 +730,9 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) ); self.version = version; + } else if is_abi3() && self.version.minor > ABI3_MAX_MINOR { + warn!("Automatically falling back to abi3-py3{ABI3_MAX_MINOR} because current Python is higher than the maximum supported"); + self.version.minor = ABI3_MAX_MINOR; } Ok(()) From b050c874ec87f496a5e5daef97104d5d3d97cf3c Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers <7943856+bschoenmaeckers@users.noreply.github.com> Date: Tue, 3 Jun 2025 09:59:27 +0200 Subject: [PATCH 708/936] Add conversions for chrono's `Local` timezone (#5174) --- Cargo.toml | 4 ++ guide/src/features.md | 4 ++ newsfragments/5174.added.md | 1 + src/conversions/chrono.rs | 98 ++++++++++++++++++++++++++++++++++++- 4 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 newsfragments/5174.added.md diff --git a/Cargo.toml b/Cargo.toml index 6f2038b9119..22b62a00807 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,6 +51,7 @@ smallvec = { version = "1.0", optional = true } uuid = { version = "1.11.0", optional = true } lock_api = { version = "0.4", optional = true } parking_lot = { version = "0.12", optional = true } +iana-time-zone = { version = "0.1", optional = true, features = ["fallback"]} [target.'cfg(not(target_has_atomic = "64"))'.dependencies] portable-atomic = "1.0" @@ -121,6 +122,8 @@ py-clone = [] parking_lot = ["dep:parking_lot", "lock_api"] arc_lock = ["lock_api", "lock_api/arc_lock", "parking_lot?/arc_lock"] +chrono-local = ["chrono/clock", "dep:iana-time-zone"] + # Optimizes PyObject to Vec conversion and so on. nightly = [] @@ -133,6 +136,7 @@ full = [ "arc_lock", "bigdecimal", "chrono", + "chrono-local", "chrono-tz", "either", "experimental-async", diff --git a/guide/src/features.md b/guide/src/features.md index 450c1b4ea04..aad151baa58 100644 --- a/guide/src/features.md +++ b/guide/src/features.md @@ -138,6 +138,10 @@ Adds a dependency on [chrono](https://docs.rs/chrono). Enables a conversion from - [NaiveTime](https://docs.rs/chrono/latest/chrono/naive/struct.NaiveTime.html) -> [`PyTime`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyTime.html) - [DateTime](https://docs.rs/chrono/latest/chrono/struct.DateTime.html) -> [`PyDateTime`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyDateTime.html) +### `chrono-local` + +Enables conversion from and to [Local](https://docs.rs/chrono/latest/chrono/struct.Local.html) timezones. + ### `chrono-tz` Adds a dependency on [chrono-tz](https://docs.rs/chrono-tz). diff --git a/newsfragments/5174.added.md b/newsfragments/5174.added.md new file mode 100644 index 00000000000..09b7dec02e7 --- /dev/null +++ b/newsfragments/5174.added.md @@ -0,0 +1 @@ +Add conversions for chrono's `Local` timezone & `DateTime` instances. diff --git a/src/conversions/chrono.rs b/src/conversions/chrono.rs index bf99951e459..fab734422f2 100644 --- a/src/conversions/chrono.rs +++ b/src/conversions/chrono.rs @@ -43,15 +43,23 @@ use crate::conversion::IntoPyObject; use crate::exceptions::{PyTypeError, PyUserWarning, PyValueError}; -#[cfg(Py_LIMITED_API)] use crate::intern; use crate::types::any::PyAnyMethods; use crate::types::PyNone; use crate::types::{PyDate, PyDateTime, PyDelta, PyTime, PyTzInfo, PyTzInfoAccess}; #[cfg(not(Py_LIMITED_API))] use crate::types::{PyDateAccess, PyDeltaAccess, PyTimeAccess}; +#[cfg(feature = "chrono-local")] +use crate::{ + exceptions::PyRuntimeError, + sync::GILOnceCell, + types::{PyString, PyStringMethods}, + Py, +}; use crate::{ffi, Borrowed, Bound, FromPyObject, IntoPyObjectExt, PyAny, PyErr, PyResult, Python}; use chrono::offset::{FixedOffset, Utc}; +#[cfg(feature = "chrono-local")] +use chrono::Local; use chrono::{ DateTime, Datelike, Duration, LocalResult, NaiveDate, NaiveDateTime, NaiveTime, Offset, TimeZone, Timelike, @@ -387,7 +395,8 @@ impl FromPyObject<'_> for FixedOffset { // 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", (PyNone::get(ob.py()),))?; + let py_timedelta = + ob.call_method1(intern!(ob.py(), "utcoffset"), (PyNone::get(ob.py()),))?; if py_timedelta.is_none() { return Err(PyTypeError::new_err(format!( "{ob:?} is not a fixed offset timezone" @@ -433,6 +442,54 @@ impl FromPyObject<'_> for Utc { } } +#[cfg(feature = "chrono-local")] +impl<'py> IntoPyObject<'py> for Local { + type Target = PyTzInfo; + type Output = Borrowed<'static, 'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + static LOCAL_TZ: GILOnceCell> = GILOnceCell::new(); + let tz = LOCAL_TZ + .get_or_try_init(py, || { + let iana_name = iana_time_zone::get_timezone().map_err(|e| { + PyRuntimeError::new_err(format!("Could not get local timezone: {e}")) + })?; + PyTzInfo::timezone(py, iana_name).map(Bound::unbind) + })? + .bind_borrowed(py); + Ok(tz) + } +} + +#[cfg(feature = "chrono-local")] +impl<'py> IntoPyObject<'py> for &Local { + type Target = PyTzInfo; + type Output = Borrowed<'static, 'py, Self::Target>; + type Error = PyErr; + + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + (*self).into_pyobject(py) + } +} + +#[cfg(feature = "chrono-local")] +impl FromPyObject<'_> for Local { + fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { + let local_tz = Local.into_pyobject(ob.py())?; + if ob.eq(local_tz)? { + Ok(Local) + } else { + let name = local_tz.getattr("key")?.downcast_into::()?; + Err(PyValueError::new_err(format!( + "expected local timezone {}", + name.to_cow()? + ))) + } + } +} + struct DateArgs { year: i32, month: u8, @@ -1281,6 +1338,43 @@ mod tests { } }) } + + #[test] + #[cfg(all(feature = "chrono-local", not(target_os = "windows")))] + fn test_local_datetime_roundtrip( + year in 1i32..=9999i32, + month in 1u32..=12u32, + day in 1u32..=31u32, + hour in 0u32..=23u32, + min in 0u32..=59u32, + sec in 0u32..=59u32, + micro in 0u32..=1_999_999u32, + ) { + Python::with_gil(|py| { + let date_opt = NaiveDate::from_ymd_opt(year, month, day); + let time_opt = NaiveTime::from_hms_micro_opt(hour, min, sec, micro); + if let (Some(date), Some(time)) = (date_opt, time_opt) { + let dts = match NaiveDateTime::new(date, time).and_local_timezone(Local) { + LocalResult::None => return, + LocalResult::Single(dt) => [Some((dt, false)), None], + LocalResult::Ambiguous(dt1, dt2) => [Some((dt1, false)), Some((dt2, true))], + }; + for (dt, fold) in dts.iter().filter_map(|input| *input) { + // Wrap in CatchWarnings to avoid into_py firing warning for truncated leap second + 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 = if fold { + NaiveDateTime::new(date, expected_roundtrip_time).and_local_timezone(Local).latest() + } else { + NaiveDateTime::new(date, expected_roundtrip_time).and_local_timezone(Local).earliest() + }.unwrap(); + assert_eq!(expected_roundtrip_dt, roundtripped); + } + } + }) + } } } } From d2801efcea1242041eeb53e771dd91d7e75e3761 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 6 Jun 2025 13:08:02 +0100 Subject: [PATCH 709/936] ci: fixes 2025-06-06 (#5186) * fix nightly warning of inconsistent lifetime use * workaround Python 3.13.4 ffi-check issue on Windows --- .github/workflows/build.yml | 6 ++++-- .github/workflows/ci.yml | 4 +++- pyo3-macros-backend/src/intopyobject.rs | 4 ++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 37da958f35a..4b0ec314fd6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -46,8 +46,7 @@ jobs: python-version: ${{ inputs.python-version }} architecture: ${{ inputs.python-architecture }} # PyPy can have FFI changes within Python versions, which creates pain in CI - # 3.13.2 also had an ABI break so temporarily add this for 3.13 to ensure that we're using 3.13.3 - check-latest: ${{ startsWith(inputs.python-version, 'pypy') || startsWith(inputs.python-version, '3.13') }} + check-latest: ${{ startsWith(inputs.python-version, 'pypy') }} - name: Install nox run: python -m pip install --upgrade pip && pip install nox @@ -111,6 +110,9 @@ jobs: - '.github/workflows/build.yml' - name: Run pyo3-ffi-check + # Python 3.13.4 has an issue on Windows where the headers are configured to always be free-threaded + # https://discuss.python.org/t/heads-up-3-13-5-release-coming-soon/94535 + continue-on-error: ${{ inputs.python-version == '3.13' && (inputs.os == 'windows-latest' || inputs.os == 'windows-11-arm') }} # 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 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 70628644447..6c7ef5c3763 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -159,7 +159,9 @@ jobs: components: clippy,rust-src - uses: actions/setup-python@v5 with: - python-version: "3.13" + # FIXME: 3.13.4 has an issue on windows so pinned to 3.13.3 + # once 3.13.5 is out, unpin the patch version + python-version: "3.13.3" architecture: ${{ matrix.platform.python-architecture }} - uses: Swatinem/rust-cache@v2 with: diff --git a/pyo3-macros-backend/src/intopyobject.rs b/pyo3-macros-backend/src/intopyobject.rs index 2532812f6e1..2c12567e8b7 100644 --- a/pyo3-macros-backend/src/intopyobject.rs +++ b/pyo3-macros-backend/src/intopyobject.rs @@ -530,8 +530,8 @@ pub fn build_derive_into_pyobject(tokens: &DeriveInput) -> Resu type Error = #error; fn into_pyobject(self, py: #pyo3_path::Python<#lt_param>) -> ::std::result::Result< - ::Output, - ::Error, + >::Output, + >::Error, > { #body } From c9921680f2e1557c9c584842a280ee0eb8901abb Mon Sep 17 00:00:00 2001 From: Ben Beasley Date: Fri, 6 Jun 2025 11:23:07 -0400 Subject: [PATCH 710/936] Make PyObjectObRefcnt.refcnt_and_flags 64-bit-only (#5180) * Make PyObjectObRefcnt.refcnt_and_flags 64-bit-only Fix #5175 as described in https://github.com/PyO3/pyo3/issues/5175#issuecomment-2939918138. * Add a news fragment for #5180 --- newsfragments/5180.fixed.md | 1 + pyo3-ffi/src/object.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 newsfragments/5180.fixed.md diff --git a/newsfragments/5180.fixed.md b/newsfragments/5180.fixed.md new file mode 100644 index 00000000000..9be577d65ab --- /dev/null +++ b/newsfragments/5180.fixed.md @@ -0,0 +1 @@ +Fixed segmentation faults on 32-bit ix86 with Python 3.14 diff --git a/pyo3-ffi/src/object.rs b/pyo3-ffi/src/object.rs index c4c5abc361c..6d5dc781287 100644 --- a/pyo3-ffi/src/object.rs +++ b/pyo3-ffi/src/object.rs @@ -49,7 +49,7 @@ pub struct PyObjectObFlagsAndRefcnt { pub union PyObjectObRefcnt { #[cfg(all(target_pointer_width = "64", Py_3_14))] pub ob_refcnt_full: crate::PY_INT64_T, - #[cfg(Py_3_14)] + #[cfg(all(target_pointer_width = "64", Py_3_14))] pub refcnt_and_flags: PyObjectObFlagsAndRefcnt, pub ob_refcnt: Py_ssize_t, #[cfg(all(target_pointer_width = "64", not(Py_3_14)))] From 93e64067d4eb178b8704ab494595cd3e0af7c2a3 Mon Sep 17 00:00:00 2001 From: jesse Date: Fri, 6 Jun 2025 11:23:39 -0700 Subject: [PATCH 711/936] fix: typos in guide/docs (#5182) --- guide/src/class.md | 6 +++--- guide/src/conversions/traits.md | 4 ++-- guide/src/debugging.md | 2 +- guide/src/features.md | 2 +- guide/src/free-threading.md | 4 ++-- guide/src/migration.md | 6 +++--- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/guide/src/class.md b/guide/src/class.md index 192f66cc287..16f5039cc8e 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -55,7 +55,7 @@ enum HttpResponse { } // PyO3 also supports enums with Struct and Tuple variants -// These complex enums have sligtly different behavior from the simple enums above +// These complex enums have slightly 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, ... @@ -827,7 +827,7 @@ impl MyClass { ## Classes as function arguments -Free functions defined using `#[pyfunction]` interact with classes through the same mechanisms as the self parameters of instance methods, i.e. they can take GIL-bound references, GIL-bound reference wrappers or GIL-indepedent references: +Free functions defined using `#[pyfunction]` interact with classes through the same mechanisms as the self parameters of instance methods, i.e. they can take GIL-bound references, GIL-bound reference wrappers or GIL-independent references: ```rust,no_run # #![allow(dead_code)] @@ -859,7 +859,7 @@ fn increment_then_print_field(my_class: &Bound<'_, MyClass>) { println!("{}", my_class.borrow().my_field); } -// Take a GIL-indepedent reference when you want to store the reference elsewhere. +// Take a GIL-independent reference when you want to store the reference elsewhere. #[pyfunction] fn print_refcnt(my_class: Py, py: Python<'_>) { println!("{}", my_class.get_refcnt(py)); diff --git a/guide/src/conversions/traits.md b/guide/src/conversions/traits.md index b1b6cd31127..281116a9747 100644 --- a/guide/src/conversions/traits.md +++ b/guide/src/conversions/traits.md @@ -613,7 +613,7 @@ Additionally `IntoPyObject` can be derived for a reference to a struct or enum u ##### `#[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 argument must be the function identifier - 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` @@ -650,7 +650,7 @@ 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` + type Error = std::convert::Infallible; // the conversion error type, has to be convertible to `PyErr` fn into_pyobject(self, py: Python<'py>) -> Result { Ok(self.0.into_bound(py)) diff --git a/guide/src/debugging.md b/guide/src/debugging.md index 2cbf867d438..0cee7bd1783 100644 --- a/guide/src/debugging.md +++ b/guide/src/debugging.md @@ -68,7 +68,7 @@ For more information about how to use both `lldb` and `gdb` you can read the [gd ### Debugger specific setup -Depeding on your OS and your preferences you can use two different debuggers, `rust-gdb` or `rust-lldb`. +Depending on your OS and your preferences you can use two different debuggers, `rust-gdb` or `rust-lldb`. {{#tabs }} {{#tab name="Using rust-gdb" }} diff --git a/guide/src/features.md b/guide/src/features.md index aad151baa58..f60b07b6a40 100644 --- a/guide/src/features.md +++ b/guide/src/features.md @@ -77,7 +77,7 @@ This feature was introduced to ease migration. It was found that delayed referen ### `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. +This is a performance-oriented conditional compilation flag, e.g. [set via `$RUSTFLAGS`][set-configuration-options], which disabled the global reference pool and the associated 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` diff --git a/guide/src/free-threading.md b/guide/src/free-threading.md index 6df9ac01cfb..898755b6118 100644 --- a/guide/src/free-threading.md +++ b/guide/src/free-threading.md @@ -148,7 +148,7 @@ 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. +manner analogous to releasing and acquiring the GIL in the GIL-enabled build. 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 @@ -289,7 +289,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 -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 initialized once, it can be problematic in some contexts that [`GILOnceCell`] does not block like the standard library [`OnceLock`]. diff --git a/guide/src/migration.md b/guide/src/migration.md index 3beaa5bc07c..ee929e79d2b 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -645,7 +645,7 @@ impl PyClassAsyncIter {
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. +Interactions with Python objects implemented in Rust no longer need to go though `PyCell`. Instead interactions 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` @@ -657,7 +657,7 @@ To minimise breakage of code using the GIL Refs API, the `Bound` smart pointe To identify what to migrate, temporarily switch off the `gil-refs` feature to see deprecation warnings on [almost](#cases-where-pyo3-cannot-emit-gil-ref-deprecation-warnings) all uses of APIs accepting and producing GIL Refs . Over one or more PRs it should be possible to follow the deprecation hints to update code. Depending on your development environment, switching off the `gil-refs` feature may introduce [some very targeted breakages](#deactivating-the-gil-refs-feature), so you may need to fixup those first. For example, the following APIs have gained updated variants: -- `PyList::new`, `PyTyple::new` and similar constructors have replacements `PyList::new_bound`, `PyTuple::new_bound` etc. +- `PyList::new`, `PyTuple::new` and similar constructors have replacements `PyList::new_bound`, `PyTuple::new_bound` etc. - `FromPyObject::extract` has a new `FromPyObject::extract_bound` (see the section below) - The `PyTypeInfo` trait has had new `_bound` methods added to accept / return `Bound`. @@ -1082,7 +1082,7 @@ impl Object { } } -// It either forces us to release the GIL before aquiring it again. +// It either forces us to release the GIL before acquiring it again. let first = Python::with_gil(|py| Object::new(py)); let second = Python::with_gil(|py| Object::new(py)); drop(first); From 3947d5d422fe08f3aa257d954d4cb3fef9c58236 Mon Sep 17 00:00:00 2001 From: jesse Date: Fri, 6 Jun 2025 11:40:50 -0700 Subject: [PATCH 712/936] docs: add Kyle Barron libraries/packages to README.md (#5172) * readme: add Kyle Barron libraries/packages * remove from readme obstore ref '1' --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 47a74ec84a3..31ed8b7e47a 100644 --- a/README.md +++ b/README.md @@ -182,9 +182,14 @@ about this topic. - [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._ +- [pyo3-object_store](https://github.com/developmentseed/obstore/tree/main/pyo3-object_store) _Integration between [`object_store`](https://docs.rs/object_store) and [`pyo3`](https://github.com/PyO3/pyo3)._ ## Examples +- [arro3](https://github.com/kylebarron/arro3) _A minimal Python library for Apache Arrow, connecting to the Rust arrow crate._ + - [arro3-compute](https://github.com/kylebarron/arro3/tree/main/arro3-compute) _`arro3-compute`_ + - [arro3-core](https://github.com/kylebarron/arro3/tree/main/arro3-core) _`arro3-core`_ + - [arro3-io](https://github.com/kylebarron/arro3/tree/main/arro3-io) _`arro3-io`_ - [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 - [blake3-py](https://github.com/oconnor663/blake3-py) _Python bindings for the [BLAKE3](https://github.com/BLAKE3-team/BLAKE3) cryptographic hash function._ @@ -199,6 +204,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._ +- [geo-index](https://github.com/kylebarron/geo-index) _A Rust crate and [Python library](https://github.com/kylebarron/geo-index/tree/main/python) for packed, immutable, zero-copy spatial indexes._ - [granian](https://github.com/emmett-framework/granian) _A Rust HTTP server for Python applications._ - [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._ @@ -207,6 +213,7 @@ about this topic. - [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._ +- [obstore](https://github.com/developmentseed/obstore) _The simplest, highest-throughput Python interface to Amazon S3, Google Cloud Storage, Azure Storage, & other S3-compliant APIs, powered by Rust._ - [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._ From a9e3f8116c075e68523445b7927fe1bb7ff5ea91 Mon Sep 17 00:00:00 2001 From: Thomas Tanon Date: Wed, 18 Jun 2025 22:50:29 +0200 Subject: [PATCH 713/936] PyGetSetDef: Remove Eq and PartialEq (#5196) --- newsfragments/5196.removed.md | 1 + pyo3-ffi/src/descrobject.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 newsfragments/5196.removed.md diff --git a/newsfragments/5196.removed.md b/newsfragments/5196.removed.md new file mode 100644 index 00000000000..6784fb05709 --- /dev/null +++ b/newsfragments/5196.removed.md @@ -0,0 +1 @@ +Remove `Eq` and `PartialEq` implementations on `PyGetSetDef` \ No newline at end of file diff --git a/pyo3-ffi/src/descrobject.rs b/pyo3-ffi/src/descrobject.rs index f4a5ce00bf6..0b75f2961f2 100644 --- a/pyo3-ffi/src/descrobject.rs +++ b/pyo3-ffi/src/descrobject.rs @@ -14,7 +14,7 @@ pub type setter = /// Note that CPython may leave fields uninitialized. You must ensure that /// `name` != NULL before dereferencing or reading other fields. #[repr(C)] -#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[derive(Copy, Clone, Debug)] pub struct PyGetSetDef { pub name: *const c_char, pub get: Option, From 82eefff3a4382476476c2b3c95a4877219b8d5b3 Mon Sep 17 00:00:00 2001 From: Thomas Tanon Date: Thu, 19 Jun 2025 17:53:38 +0200 Subject: [PATCH 714/936] Use VisitMut in elide_lifetimes (#5200) * Use VisitMut in elide_lifetimes Simpler and more robust code * PyGetSetDef: Remove Eq and PartialEq * Remove changelog --- pyo3-macros-backend/Cargo.toml | 2 +- pyo3-macros-backend/src/utils.rs | 60 ++++---------------------------- 2 files changed, 8 insertions(+), 54 deletions(-) diff --git a/pyo3-macros-backend/Cargo.toml b/pyo3-macros-backend/Cargo.toml index d626fa1ebf7..878a0476dad 100644 --- a/pyo3-macros-backend/Cargo.toml +++ b/pyo3-macros-backend/Cargo.toml @@ -23,7 +23,7 @@ quote = { version = "1", default-features = false } [dependencies.syn] version = "2.0.59" # for `LitCStr` default-features = false -features = ["derive", "parsing", "printing", "clone-impls", "full", "extra-traits"] +features = ["derive", "parsing", "printing", "clone-impls", "full", "extra-traits", "visit-mut"] [build-dependencies] pyo3-build-config = { path = "../pyo3-build-config", version = "=0.25.0" } diff --git a/pyo3-macros-backend/src/utils.rs b/pyo3-macros-backend/src/utils.rs index 0f4a267cac7..1954e79c858 100644 --- a/pyo3-macros-backend/src/utils.rs +++ b/pyo3-macros-backend/src/utils.rs @@ -3,6 +3,7 @@ use proc_macro2::{Span, TokenStream}; use quote::{quote, ToTokens}; use std::ffi::CString; use syn::spanned::Spanned; +use syn::visit_mut::VisitMut; use syn::{punctuated::Punctuated, Token}; /// Macro inspired by `anyhow::anyhow!` to create a compiler error with the given span. @@ -334,62 +335,15 @@ pub(crate) trait TypeExt { 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(_) - | _ => {} + struct ElideLifetimesVisitor; + + impl VisitMut for ElideLifetimesVisitor { + fn visit_lifetime_mut(&mut self, l: &mut syn::Lifetime) { + *l = syn::Lifetime::new("'_", l.span()); } } - elide_lifetimes(&mut self); + ElideLifetimesVisitor.visit_type_mut(&mut self); self } } From 011916f4ff61b2cfb82e690ce2540efa72e5376b Mon Sep 17 00:00:00 2001 From: Thomas Tanon Date: Fri, 20 Jun 2025 09:25:49 +0200 Subject: [PATCH 715/936] Introspection: Adds basic input type annotations (#5089) * Adds basic input type annotations * Use elide_lifetimes instead of remove_bound_lifetimes * Replace Self with the current class type and ignore some magic methods * Changelog * Makes semver-check happy * Use VisitMut in replace_self --- Cargo.toml | 2 +- guide/src/class.md | 5 ++ newsfragments/5089.added.md | 1 + noxfile.py | 1 + pyo3-introspection/Cargo.toml | 3 + pyo3-introspection/src/introspection.rs | 5 +- pyo3-introspection/src/model.rs | 2 + pyo3-introspection/src/stubs.rs | 65 +++++++++------- pyo3-introspection/tests/test.rs | 30 +++++++- pyo3-macros-backend/src/introspection.rs | 94 +++++++++++++++++++++--- pyo3-macros-backend/src/pyclass.rs | 19 +++++ pyo3-macros-backend/src/pyimpl.rs | 19 ++++- pytests/stubs/pyclasses.pyi | 6 +- pytests/stubs/pyfunctions.pyi | 26 +++++-- src/conversion.rs | 17 +++++ src/conversions/std/num.rs | 21 ++++++ src/conversions/std/string.rs | 12 +++ src/impl_/extract_argument.rs | 17 +++++ src/type_object.rs | 11 +++ src/types/boolobject.rs | 3 + src/types/datetime.rs | 10 +++ src/types/float.rs | 8 +- src/types/iterator.rs | 2 + src/types/mapping.rs | 2 + src/types/sequence.rs | 2 + src/types/weakref/anyref.rs | 2 + src/types/weakref/proxy.rs | 2 + src/types/weakref/reference.rs | 2 + tests/ui/invalid_cancel_handle.stderr | 26 +++++++ 29 files changed, 368 insertions(+), 47 deletions(-) create mode 100644 newsfragments/5089.added.md diff --git a/Cargo.toml b/Cargo.toml index 22b62a00807..f35669b5cca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3" -version = "0.25.0" +version = "0.26.0-dev" description = "Bindings to Python interpreter" authors = ["PyO3 Project and Contributors "] readme = "README.md" diff --git a/guide/src/class.md b/guide/src/class.md index 16f5039cc8e..bbe80359696 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -1380,6 +1380,7 @@ impl pyo3::types::DerefToPyAny for MyClass {} unsafe impl pyo3::type_object::PyTypeInfo for MyClass { const NAME: &'static str = "MyClass"; const MODULE: ::std::option::Option<&'static str> = ::std::option::Option::None; + #[inline] fn type_object_raw(py: pyo3::Python<'_>) -> *mut pyo3::ffi::PyTypeObject { ::lazy_type_object() @@ -1395,6 +1396,8 @@ impl pyo3::PyClass for MyClass { impl<'a, 'py> pyo3::impl_::extract_argument::PyFunctionArgument<'a, 'py, false> for &'a MyClass { type Holder = ::std::option::Option>; + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = "MyClass"; #[inline] fn extract(obj: &'a pyo3::Bound<'py, PyAny>, holder: &'a mut Self::Holder) -> pyo3::PyResult { @@ -1405,6 +1408,8 @@ impl<'a, 'py> pyo3::impl_::extract_argument::PyFunctionArgument<'a, 'py, false> impl<'a, 'py> pyo3::impl_::extract_argument::PyFunctionArgument<'a, 'py, false> for &'a mut MyClass { type Holder = ::std::option::Option>; + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = "MyClass"; #[inline] fn extract(obj: &'a pyo3::Bound<'py, PyAny>, holder: &'a mut Self::Holder) -> pyo3::PyResult { diff --git a/newsfragments/5089.added.md b/newsfragments/5089.added.md new file mode 100644 index 00000000000..1ce0d3cb893 --- /dev/null +++ b/newsfragments/5089.added.md @@ -0,0 +1 @@ +Introspection: Adds very basic input type annotations (not all types are covered yet) \ No newline at end of file diff --git a/noxfile.py b/noxfile.py index cbd499aa899..6e32efc3327 100644 --- a/noxfile.py +++ b/noxfile.py @@ -864,6 +864,7 @@ def update_ui_tests(session: nox.Session): @nox.session(name="test-introspection") def test_introspection(session: nox.Session): session.install("maturin") + session.install("ruff") target = os.environ.get("CARGO_BUILD_TARGET") for options in ([], ["--release"]): if target is not None: diff --git a/pyo3-introspection/Cargo.toml b/pyo3-introspection/Cargo.toml index 98dfdebbd10..55004ac497b 100644 --- a/pyo3-introspection/Cargo.toml +++ b/pyo3-introspection/Cargo.toml @@ -14,5 +14,8 @@ goblin = "0.9.0" serde = { version = "1", features = ["derive"] } serde_json = "1" +[dev-dependencies] +tempfile = "3.12.0" + [lints] workspace = true diff --git a/pyo3-introspection/src/introspection.rs b/pyo3-introspection/src/introspection.rs index b6d9bfb6e18..41553059558 100644 --- a/pyo3-introspection/src/introspection.rs +++ b/pyo3-introspection/src/introspection.rs @@ -194,6 +194,7 @@ fn convert_argument(arg: &ChunkArgument) -> Argument { Argument { name: arg.name.clone(), default_value: arg.default.clone(), + annotation: arg.annotation.clone(), } } @@ -376,7 +377,7 @@ enum Chunk { #[serde(default)] id: Option, name: String, - arguments: ChunkArguments, + arguments: Box, #[serde(default)] parent: Option, #[serde(default)] @@ -409,4 +410,6 @@ struct ChunkArgument { name: String, #[serde(default)] default: Option, + #[serde(default)] + annotation: Option, } diff --git a/pyo3-introspection/src/model.rs b/pyo3-introspection/src/model.rs index dc98198ca90..85ed57901a1 100644 --- a/pyo3-introspection/src/model.rs +++ b/pyo3-introspection/src/model.rs @@ -46,6 +46,8 @@ pub struct Argument { pub name: String, /// Default value as a Python expression pub default_value: Option, + /// Type annotation as a Python expression + pub annotation: Option, } /// A variable length argument ie. *vararg or **kwarg diff --git a/pyo3-introspection/src/stubs.rs b/pyo3-introspection/src/stubs.rs index 8be82da1f11..e60223d27e7 100644 --- a/pyo3-introspection/src/stubs.rs +++ b/pyo3-introspection/src/stubs.rs @@ -34,28 +34,25 @@ fn add_module_stub_files( fn module_stubs(module: &Module) -> String { let mut modules_to_import = BTreeSet::new(); let mut elements = Vec::new(); + for konst in &module.consts { + elements.push(const_stubs(konst, &mut modules_to_import)); + } for class in &module.classes { - elements.push(class_stubs(class)); + elements.push(class_stubs(class, &mut modules_to_import)); } for function in &module.functions { - elements.push(function_stubs(function)); + elements.push(function_stubs(function, &mut modules_to_import)); } - for konst in &module.consts { - elements.push(const_stubs(konst, &mut modules_to_import)); - } - - let mut output = String::new(); - + let mut final_elements = Vec::new(); for module_to_import in &modules_to_import { - output.push_str(&format!("import {module_to_import}\n")); + final_elements.push(format!("import {module_to_import}")); } + final_elements.extend(elements); - if !modules_to_import.is_empty() { - output.push('\n') - } + let mut output = String::new(); // We insert two line jumps (i.e. empty strings) only above and below multiple line elements (classes with methods, functions with decorators) - for element in elements { + for element in final_elements { let is_multiline = element.contains('\n'); if is_multiline && !output.is_empty() && !output.ends_with("\n\n") { output.push('\n'); @@ -74,7 +71,7 @@ fn module_stubs(module: &Module) -> String { output } -fn class_stubs(class: &Class) -> String { +fn class_stubs(class: &Class, modules_to_import: &mut BTreeSet) -> String { let mut buffer = format!("class {}:", class.name); if class.methods.is_empty() { buffer.push_str(" ..."); @@ -83,22 +80,22 @@ fn class_stubs(class: &Class) -> String { for method in &class.methods { // We do the indentation buffer.push_str("\n "); - buffer.push_str(&function_stubs(method).replace('\n', "\n ")); + buffer.push_str(&function_stubs(method, modules_to_import).replace('\n', "\n ")); } buffer } -fn function_stubs(function: &Function) -> String { +fn function_stubs(function: &Function, modules_to_import: &mut BTreeSet) -> String { // Signature let mut parameters = Vec::new(); for argument in &function.arguments.positional_only_arguments { - parameters.push(argument_stub(argument)); + parameters.push(argument_stub(argument, modules_to_import)); } if !function.arguments.positional_only_arguments.is_empty() { parameters.push("/".into()); } for argument in &function.arguments.arguments { - parameters.push(argument_stub(argument)); + parameters.push(argument_stub(argument, modules_to_import)); } if let Some(argument) = &function.arguments.vararg { parameters.push(format!("*{}", variable_length_argument_stub(argument))); @@ -106,7 +103,7 @@ fn function_stubs(function: &Function) -> String { parameters.push("*".into()); } for argument in &function.arguments.keyword_only_arguments { - parameters.push(argument_stub(argument)); + parameters.push(argument_stub(argument, modules_to_import)); } if let Some(argument) = &function.arguments.kwarg { parameters.push(format!("**{}", variable_length_argument_stub(argument))); @@ -131,10 +128,22 @@ fn const_stubs(konst: &Const, modules_to_import: &mut BTreeSet) -> Strin format!("{name}: typing.Final = {value}") } -fn argument_stub(argument: &Argument) -> String { +fn argument_stub(argument: &Argument, modules_to_import: &mut BTreeSet) -> String { let mut output = argument.name.clone(); + if let Some(annotation) = &argument.annotation { + output.push_str(": "); + output.push_str(annotation); + if let Some((module, _)) = annotation.rsplit_once('.') { + // TODO: this is very naive + modules_to_import.insert(module.into()); + } + } if let Some(default_value) = &argument.default_value { - output.push('='); + output.push_str(if argument.annotation.is_some() { + " = " + } else { + "=" + }); output.push_str(default_value); } output @@ -158,10 +167,12 @@ mod tests { positional_only_arguments: vec![Argument { name: "posonly".into(), default_value: None, + annotation: None, }], arguments: vec![Argument { name: "arg".into(), default_value: None, + annotation: None, }], vararg: Some(VariableLengthArgument { name: "varargs".into(), @@ -169,6 +180,7 @@ mod tests { keyword_only_arguments: vec![Argument { name: "karg".into(), default_value: None, + annotation: Some("str".into()), }], kwarg: Some(VariableLengthArgument { name: "kwarg".into(), @@ -176,8 +188,8 @@ mod tests { }, }; assert_eq!( - "def func(posonly, /, arg, *varargs, karg, **kwarg): ...", - function_stubs(&function) + "def func(posonly, /, arg, *varargs, karg: str, **kwarg): ...", + function_stubs(&function, &mut BTreeSet::new()) ) } @@ -190,22 +202,25 @@ mod tests { positional_only_arguments: vec![Argument { name: "posonly".into(), default_value: Some("1".into()), + annotation: None, }], arguments: vec![Argument { name: "arg".into(), default_value: Some("True".into()), + annotation: None, }], vararg: None, keyword_only_arguments: vec![Argument { name: "karg".into(), default_value: Some("\"foo\"".into()), + annotation: Some("str".into()), }], kwarg: None, }, }; assert_eq!( - "def afunc(posonly=1, /, arg=True, *, karg=\"foo\"): ...", - function_stubs(&function) + "def afunc(posonly=1, /, arg=True, *, karg: str = \"foo\"): ...", + function_stubs(&function, &mut BTreeSet::new()) ) } } diff --git a/pyo3-introspection/tests/test.rs b/pyo3-introspection/tests/test.rs index 302efd1ea47..cf01329e9b1 100644 --- a/pyo3-introspection/tests/test.rs +++ b/pyo3-introspection/tests/test.rs @@ -1,8 +1,11 @@ -use anyhow::Result; +use anyhow::{ensure, Result}; use pyo3_introspection::{introspect_cdylib, module_stub_files}; use std::collections::HashMap; +use std::io::{Read, Seek, SeekFrom, Write}; use std::path::{Path, PathBuf}; +use std::process::Command; use std::{env, fs}; +use tempfile::NamedTempFile; #[test] fn pytests_stubs() -> Result<()> { @@ -42,6 +45,9 @@ fn pytests_stubs() -> Result<()> { file_name.display() ) }); + + let actual_file_content = format_with_ruff(actual_file_content)?; + // We normalize line jumps for compatibility with Windows assert_eq!( expected_file_content.replace('\r', ""), @@ -76,3 +82,25 @@ fn add_dir_files( } Ok(()) } + +fn format_with_ruff(code: &str) -> Result { + let temp_file = NamedTempFile::with_suffix(".pyi")?; + // Write to file + { + let mut file = temp_file.as_file(); + file.write_all(code.as_bytes())?; + file.flush()?; + file.seek(SeekFrom::Start(0))?; + } + ensure!( + Command::new("ruff") + .arg("format") + .arg(temp_file.path()) + .status()? + .success(), + "Failed to run ruff" + ); + let mut content = String::new(); + temp_file.as_file().read_to_string(&mut content)?; + Ok(content) +} diff --git a/pyo3-macros-backend/src/introspection.rs b/pyo3-macros-backend/src/introspection.rs index dcb76e668b1..bdd0eed6200 100644 --- a/pyo3-macros-backend/src/introspection.rs +++ b/pyo3-macros-backend/src/introspection.rs @@ -10,7 +10,7 @@ use crate::method::{FnArg, RegularArg}; use crate::pyfunction::FunctionSignature; -use crate::utils::PyO3CratePath; +use crate::utils::{PyO3CratePath, TypeExt}; use proc_macro2::{Span, TokenStream}; use quote::{format_ident, quote, ToTokens}; use std::borrow::Cow; @@ -20,6 +20,7 @@ use std::hash::{Hash, Hasher}; use std::mem::take; use std::sync::atomic::{AtomicUsize, Ordering}; use syn::ext::IdentExt; +use syn::visit_mut::{visit_type_mut, VisitMut}; use syn::{Attribute, Ident, Type, TypePath}; static GLOBAL_COUNTER_FOR_UNIQUE_NAMES: AtomicUsize = AtomicUsize::new(0); @@ -112,7 +113,7 @@ pub fn function_introspection_code( ("name", IntrospectionNode::String(name.into())), ( "arguments", - arguments_introspection_data(signature, first_argument), + arguments_introspection_data(signature, first_argument, parent), ), ]); if let Some(ident) = ident { @@ -154,6 +155,7 @@ fn const_introspection_code<'a>(ident: &'a Ident, value: &'a String) -> Introspe fn arguments_introspection_data<'a>( signature: &'a FunctionSignature<'a>, first_argument: Option<&'a str>, + class_type: Option<&Type>, ) -> IntrospectionNode<'a> { let mut argument_desc = signature.arguments.iter().filter_map(|arg| { if let FnArg::Regular(arg) = arg { @@ -186,7 +188,7 @@ fn arguments_introspection_data<'a>( } else { panic!("Less arguments than in python signature"); }; - let arg = argument_introspection_data(param, arg_desc); + let arg = argument_introspection_data(param, arg_desc, class_type); if i < signature.python_signature.positional_only_parameters { posonlyargs.push(arg); } else { @@ -206,7 +208,7 @@ fn arguments_introspection_data<'a>( } else { panic!("Less arguments than in python signature"); }; - kwonlyargs.push(argument_introspection_data(param, arg_desc)); + kwonlyargs.push(argument_introspection_data(param, arg_desc, class_type)); } if let Some(param) = &signature.python_signature.kwargs { @@ -241,6 +243,7 @@ fn arguments_introspection_data<'a>( fn argument_introspection_data<'a>( name: &'a str, desc: &'a RegularArg<'_>, + class_type: Option<&Type>, ) -> IntrospectionNode<'a> { let mut params: HashMap<_, _> = [("name", IntrospectionNode::String(name.into()))].into(); if desc.default_value.is_some() { @@ -249,12 +252,44 @@ fn argument_introspection_data<'a>( IntrospectionNode::String(desc.default_value().into()), ); } + if desc.from_py_with.is_none() { + // If from_py_with is set we don't know anything on the input type + if let Some(ty) = desc.option_wrapped_type { + // Special case to properly generate a `T | None` annotation + let mut ty = ty.clone(); + if let Some(class_type) = class_type { + replace_self(&mut ty, class_type); + } + ty = ty.elide_lifetimes(); + params.insert( + "annotation", + IntrospectionNode::InputType { + rust_type: ty, + nullable: true, + }, + ); + } else { + let mut ty = desc.ty.clone(); + if let Some(class_type) = class_type { + replace_self(&mut ty, class_type); + } + ty = ty.elide_lifetimes(); + params.insert( + "annotation", + IntrospectionNode::InputType { + rust_type: ty, + nullable: false, + }, + ); + } + } IntrospectionNode::Map(params) } enum IntrospectionNode<'a> { String(Cow<'a, str>), IntrospectionId(Option>), + InputType { rust_type: Type, nullable: bool }, Map(HashMap<&'static str, IntrospectionNode<'a>>), List(Vec>), } @@ -262,7 +297,7 @@ enum IntrospectionNode<'a> { impl IntrospectionNode<'_> { fn emit(self, pyo3_crate_path: &PyO3CratePath) -> TokenStream { let mut content = ConcatenationBuilder::default(); - self.add_to_serialization(&mut content); + self.add_to_serialization(&mut content, pyo3_crate_path); let content = content.into_token_stream(pyo3_crate_path); let static_name = format_ident!("PYO3_INTROSPECTION_0_{}", unique_element_id()); @@ -276,7 +311,11 @@ impl IntrospectionNode<'_> { } } - fn add_to_serialization(self, content: &mut ConcatenationBuilder) { + fn add_to_serialization( + self, + content: &mut ConcatenationBuilder, + pyo3_crate_path: &PyO3CratePath, + ) { match self { Self::String(string) => { content.push_str_to_escape(&string); @@ -286,10 +325,21 @@ impl IntrospectionNode<'_> { content.push_tokens(if let Some(ident) = ident { quote! { #ident::_PYO3_INTROSPECTION_ID } } else { - Ident::new("_PYO3_INTROSPECTION_ID", Span::call_site()).into_token_stream() + quote! { _PYO3_INTROSPECTION_ID } }); content.push_str("\""); } + Self::InputType { + rust_type, + nullable, + } => { + content.push_str("\""); + content.push_tokens(quote! { <#rust_type as #pyo3_crate_path::impl_::extract_argument::PyFunctionArgument>::INPUT_TYPE }); + if nullable { + content.push_str(" | None"); + } + content.push_str("\""); + } Self::Map(map) => { content.push_str("{"); for (i, (key, value)) in map.into_iter().enumerate() { @@ -298,7 +348,7 @@ impl IntrospectionNode<'_> { } content.push_str_to_escape(key); content.push_str(":"); - value.add_to_serialization(content); + value.add_to_serialization(content, pyo3_crate_path); } content.push_str("}"); } @@ -308,7 +358,7 @@ impl IntrospectionNode<'_> { if i > 0 { content.push_str(","); } - value.add_to_serialization(content); + value.add_to_serialization(content, pyo3_crate_path); } content.push_str("]"); } @@ -413,3 +463,29 @@ fn ident_to_type(ident: &Ident) -> Cow<'static, Type> { .into(), ) } + +// Replace Self in types with the given type +fn replace_self(ty: &mut Type, self_target: &Type) { + struct SelfReplacementVisitor<'a> { + self_target: &'a Type, + } + + impl<'a> VisitMut for SelfReplacementVisitor<'a> { + fn visit_type_mut(&mut self, ty: &mut Type) { + if let syn::Type::Path(type_path) = ty { + if type_path.qself.is_none() + && type_path.path.segments.len() == 1 + && type_path.path.segments[0].ident == "Self" + && type_path.path.segments[0].arguments.is_empty() + { + // It is Self + *ty = self.self_target.clone(); + return; + } + } + visit_type_mut(self, ty); + } + } + + SelfReplacementVisitor { self_target }.visit_type_mut(ty); +} diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index ea2a4ae7cc3..d4ea0e08098 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -2136,12 +2136,27 @@ impl<'a> PyClassImplsBuilder<'a> { fn impl_extractext(&self, ctx: &Ctx) -> TokenStream { let Ctx { pyo3_path, .. } = ctx; let cls = self.cls; + + let input_type = if cfg!(feature = "experimental-inspect") { + let cls_name = get_class_python_name(cls, self.attr).to_string(); + let full_name = if let Some(ModuleAttribute { value, .. }) = &self.attr.options.module { + let value = value.value(); + format!("{value}.{cls_name}") + } else { + cls_name + }; + quote! { const INPUT_TYPE: &'static str = #full_name; } + } else { + quote! {} + }; if self.attr.options.frozen.is_some() { quote! { 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>>; + #input_type + #[inline] fn extract(obj: &'a #pyo3_path::Bound<'py, #pyo3_path::PyAny>, holder: &'a mut Self::Holder) -> #pyo3_path::PyResult { #pyo3_path::impl_::extract_argument::extract_pyclass_ref(obj, holder) @@ -2154,6 +2169,8 @@ impl<'a> PyClassImplsBuilder<'a> { { type Holder = ::std::option::Option<#pyo3_path::PyRef<'py, #cls>>; + #input_type + #[inline] fn extract(obj: &'a #pyo3_path::Bound<'py, #pyo3_path::PyAny>, holder: &'a mut Self::Holder) -> #pyo3_path::PyResult { #pyo3_path::impl_::extract_argument::extract_pyclass_ref(obj, holder) @@ -2164,6 +2181,8 @@ impl<'a> PyClassImplsBuilder<'a> { { type Holder = ::std::option::Option<#pyo3_path::PyRefMut<'py, #cls>>; + #input_type + #[inline] fn extract(obj: &'a #pyo3_path::Bound<'py, #pyo3_path::PyAny>, holder: &'a mut Self::Holder) -> #pyo3_path::PyResult { #pyo3_path::impl_::extract_argument::extract_pyclass_ref_mut(obj, holder) diff --git a/pyo3-macros-backend/src/pyimpl.rs b/pyo3-macros-backend/src/pyimpl.rs index 90b0d961cd8..12f2271b360 100644 --- a/pyo3-macros-backend/src/pyimpl.rs +++ b/pyo3-macros-backend/src/pyimpl.rs @@ -347,8 +347,25 @@ pub(crate) fn get_cfg_attributes(attrs: &[syn::Attribute]) -> Vec<&syn::Attribut fn method_introspection_code(spec: &FnSpec<'_>, parent: &syn::Type, ctx: &Ctx) -> TokenStream { let Ctx { pyo3_path, .. } = ctx; - // We introduce self/cls argument and setup decorators let name = spec.python_name.to_string(); + if matches!( + name.as_str(), + "__richcmp__" + | "__concat__" + | "__repeat__" + | "__inplace_concat__" + | "__inplace_repeat__" + | "__getbuffer__" + | "__releasebuffer__" + | "__traverse__" + | "__clear__" + ) { + // This is not a magic Python method, ignore for now + // TODO: properly implement + return quote! {}; + } + + // We introduce self/cls argument and setup decorators let mut first_argument = None; let mut decorators = Vec::new(); match &spec.tp { diff --git a/pytests/stubs/pyclasses.pyi b/pytests/stubs/pyclasses.pyi index 13b52f1c4e2..e1c1bacc38e 100644 --- a/pytests/stubs/pyclasses.pyi +++ b/pytests/stubs/pyclasses.pyi @@ -1,12 +1,14 @@ +import typing + class AssertingBaseClass: - def __new__(cls, /, expected_type): ... + def __new__(cls, /, expected_type: typing.Any): ... class ClassWithDecorators: def __new__(cls, /): ... @property def attr(self, /): ... @attr.setter - def attr(self, /, value): ... + def attr(self, /, value: int): ... @classmethod @property def cls_attribute(cls, /): ... diff --git a/pytests/stubs/pyfunctions.pyi b/pytests/stubs/pyfunctions.pyi index 5fb5e6c474c..b74b2f1fc61 100644 --- a/pytests/stubs/pyfunctions.pyi +++ b/pytests/stubs/pyfunctions.pyi @@ -1,8 +1,22 @@ +import typing + def args_kwargs(*args, **kwargs): ... def none(): ... -def positional_only(a, /, b): ... -def simple(a, b=None, *, c=None): ... -def simple_args(a, b=None, *args, c=None): ... -def simple_args_kwargs(a, b=None, *args, c=None, **kwargs): ... -def simple_kwargs(a, b=None, c=None, **kwargs): ... -def with_typed_args(a=False, b=0, c=0.0, d=""): ... +def positional_only(a: typing.Any, /, b: typing.Any): ... +def simple( + a: typing.Any, b: typing.Any | None = None, *, c: typing.Any | None = None +): ... +def simple_args( + a: typing.Any, b: typing.Any | None = None, *args, c: typing.Any | None = None +): ... +def simple_args_kwargs( + a: typing.Any, + b: typing.Any | None = None, + *args, + c: typing.Any | None = None, + **kwargs, +): ... +def simple_kwargs( + a: typing.Any, b: typing.Any | None = None, c: typing.Any | None = None, **kwargs +): ... +def with_typed_args(a: bool = False, b: int = 0, c: float = 0.0, d: str = ""): ... diff --git a/src/conversion.rs b/src/conversion.rs index 165175fae54..5fba9b8dc30 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -277,6 +277,13 @@ impl<'py, T> IntoPyObjectExt<'py> for T where T: IntoPyObject<'py> {} /// 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 { + /// Provides the type hint information for this type when it appears as an argument. + /// + /// For example, `Vec` would be `collections.abc.Sequence[int]`. + /// The default value is `typing.Any`, which is correct for any type. + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = "typing.Any"; + /// Extracts `Self` from the bound smart pointer `obj`. /// /// Implementors are encouraged to implement this method and leave `extract` defaulted, as @@ -339,6 +346,13 @@ mod from_py_object_bound_sealed { /// Similarly, users should typically not call these trait methods and should instead /// use this via the `extract` method on `Bound` and `Py`. pub trait FromPyObjectBound<'a, 'py>: Sized + from_py_object_bound_sealed::Sealed { + /// Provides the type hint information for this type when it appears as an argument. + /// + /// For example, `Vec` would be `collections.abc.Sequence[int]`. + /// The default value is `typing.Any`, which is correct for any type. + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = "typing.Any"; + /// Extracts `Self` from the bound smart pointer `obj`. /// /// Users are advised against calling this method directly: instead, use this via @@ -363,6 +377,9 @@ impl<'py, T> FromPyObjectBound<'_, 'py> for T where T: FromPyObject<'py>, { + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = T::INPUT_TYPE; + fn from_py_object_bound(ob: Borrowed<'_, 'py, PyAny>) -> PyResult { Self::extract_bound(&ob) } diff --git a/src/conversions/std/num.rs b/src/conversions/std/num.rs index ea5798cac99..b7a8d0b472b 100644 --- a/src/conversions/std/num.rs +++ b/src/conversions/std/num.rs @@ -46,6 +46,9 @@ macro_rules! int_fits_larger_int { } impl FromPyObject<'_> for $rust_type { + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = <$larger_type>::INPUT_TYPE; + fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { let val: $larger_type = obj.extract()?; <$rust_type>::try_from(val) @@ -121,6 +124,9 @@ macro_rules! int_convert_u64_or_i64 { } } impl FromPyObject<'_> for $rust_type { + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = "int"; + fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult<$rust_type> { extract_int!(obj, !0, $pylong_as_ll_or_ull, $force_index_call) } @@ -171,6 +177,9 @@ macro_rules! int_fits_c_long { } impl<'py> FromPyObject<'py> for $rust_type { + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = "int"; + fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { let val: c_long = extract_int!(obj, -1, ffi::PyLong_AsLong)?; <$rust_type>::try_from(val) @@ -245,6 +254,9 @@ impl<'py> IntoPyObject<'py> for &'_ u8 { } impl FromPyObject<'_> for u8 { + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = "int"; + 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())) @@ -367,6 +379,9 @@ mod fast_128bit_int_conversion { } impl FromPyObject<'_> for $rust_type { + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = "int"; + fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult<$rust_type> { let num = unsafe { ffi::PyNumber_Index(ob.as_ptr()).assume_owned_or_err(ob.py())? }; @@ -476,6 +491,9 @@ mod slow_128bit_int_conversion { } impl FromPyObject<'_> for $rust_type { + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = "int"; + fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult<$rust_type> { let py = ob.py(); unsafe { @@ -555,6 +573,9 @@ macro_rules! nonzero_int_impl { } impl FromPyObject<'_> for $nonzero_type { + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = <$primitive_type>::INPUT_TYPE; + fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { let val: $primitive_type = obj.extract()?; <$nonzero_type>::try_from(val) diff --git a/src/conversions/std/string.rs b/src/conversions/std/string.rs index 8936bd35000..48e47e5e46d 100644 --- a/src/conversions/std/string.rs +++ b/src/conversions/std/string.rs @@ -138,6 +138,9 @@ impl<'py> IntoPyObject<'py> for &String { #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] impl<'a> crate::conversion::FromPyObjectBound<'a, '_> for &'a str { + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = "str"; + fn from_py_object_bound(ob: crate::Borrowed<'a, '_, PyAny>) -> PyResult { ob.downcast::()?.to_str() } @@ -149,6 +152,9 @@ impl<'a> crate::conversion::FromPyObjectBound<'a, '_> for &'a str { } impl<'a> crate::conversion::FromPyObjectBound<'a, '_> for Cow<'a, str> { + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = "str"; + fn from_py_object_bound(ob: crate::Borrowed<'a, '_, PyAny>) -> PyResult { ob.downcast::()?.to_cow() } @@ -162,6 +168,9 @@ impl<'a> crate::conversion::FromPyObjectBound<'a, '_> for Cow<'a, str> { /// Allows extracting strings from Python objects. /// Accepts Python `str` and `unicode` objects. impl FromPyObject<'_> for String { + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = "str"; + fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { obj.downcast::()?.to_cow().map(Cow::into_owned) } @@ -173,6 +182,9 @@ impl FromPyObject<'_> for String { } impl FromPyObject<'_> for char { + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = "str"; + fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { let s = obj.downcast::()?.to_cow()?; let mut iter = s.chars(); diff --git a/src/impl_/extract_argument.rs b/src/impl_/extract_argument.rs index 7107c51b26a..5dc67677cfb 100644 --- a/src/impl_/extract_argument.rs +++ b/src/impl_/extract_argument.rs @@ -22,6 +22,11 @@ type PyArg<'py> = Borrowed<'py, 'py, PyAny>; /// There exists a trivial blanket implementation for `T: FromPyObject` with `Holder = ()`. pub trait PyFunctionArgument<'a, 'py, const IS_OPTION: bool>: Sized + 'a { type Holder: FunctionArgumentHolder; + + /// Provides the type hint information for which Python types are allowed. + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str; + fn extract(obj: &'a Bound<'py, PyAny>, holder: &'a mut Self::Holder) -> PyResult; } @@ -31,6 +36,9 @@ where { type Holder = (); + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = T::INPUT_TYPE; + #[inline] fn extract(obj: &'a Bound<'py, PyAny>, _: &'a mut ()) -> PyResult { obj.extract() @@ -43,6 +51,9 @@ where { type Holder = (); + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = T::PYTHON_TYPE; + #[inline] fn extract(obj: &'a Bound<'py, PyAny>, _: &'a mut ()) -> PyResult { obj.downcast().map_err(Into::into) @@ -55,6 +66,9 @@ where { type Holder = T::Holder; + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = "typing.Any | None"; + #[inline] fn extract(obj: &'a Bound<'py, PyAny>, holder: &'a mut T::Holder) -> PyResult { if obj.is_none() { @@ -69,6 +83,9 @@ where impl<'a> PyFunctionArgument<'a, '_, false> for &'a str { type Holder = Option>; + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = "str"; + #[inline] fn extract( obj: &'a Bound<'_, PyAny>, diff --git a/src/type_object.rs b/src/type_object.rs index 778fea7152c..2d7c0f9ef2c 100644 --- a/src/type_object.rs +++ b/src/type_object.rs @@ -43,6 +43,10 @@ pub unsafe trait PyTypeInfo: Sized { /// Module name, if any. const MODULE: Option<&'static str>; + /// Provides the full python type paths. + #[cfg(feature = "experimental-inspect")] + const PYTHON_TYPE: &'static str = "typing.Any"; + /// Returns the PyTypeObject instance for this type. fn type_object_raw(py: Python<'_>) -> *mut ffi::PyTypeObject; @@ -86,6 +90,10 @@ pub trait PyTypeCheck { /// Name of self. This is used in error messages, for example. const NAME: &'static str; + /// Provides the full python type of the allowed values. + #[cfg(feature = "experimental-inspect")] + const PYTHON_TYPE: &'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)`. @@ -98,6 +106,9 @@ where { const NAME: &'static str = ::NAME; + #[cfg(feature = "experimental-inspect")] + const PYTHON_TYPE: &'static str = ::PYTHON_TYPE; + #[inline] fn type_check(object: &Bound<'_, PyAny>) -> bool { T::is_type_of(object) diff --git a/src/types/boolobject.rs b/src/types/boolobject.rs index 28806f8c4cf..6fc2e87b2d8 100644 --- a/src/types/boolobject.rs +++ b/src/types/boolobject.rs @@ -172,6 +172,9 @@ impl<'py> IntoPyObject<'py> for &bool { /// /// Fails with `TypeError` if the input is not a Python `bool`. impl FromPyObject<'_> for bool { + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = "bool"; + fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { let err = match obj.downcast::() { Ok(obj) => return Ok(obj.is_true()), diff --git a/src/types/datetime.rs b/src/types/datetime.rs index 8690952807d..8a9c4166aa6 100644 --- a/src/types/datetime.rs +++ b/src/types/datetime.rs @@ -239,6 +239,8 @@ pyobject_native_type_named!(PyDate); #[cfg(Py_LIMITED_API)] impl PyTypeCheck for PyDate { const NAME: &'static str = "PyDate"; + #[cfg(feature = "experimental-inspect")] + const PYTHON_TYPE: &'static str = "datetime.date"; fn type_check(object: &Bound<'_, PyAny>) -> bool { let py = object.py(); @@ -338,6 +340,8 @@ pyobject_native_type_named!(PyDateTime); #[cfg(Py_LIMITED_API)] impl PyTypeCheck for PyDateTime { const NAME: &'static str = "PyDateTime"; + #[cfg(feature = "experimental-inspect")] + const PYTHON_TYPE: &'static str = "datetime.datetime"; fn type_check(object: &Bound<'_, PyAny>) -> bool { let py = object.py(); @@ -587,6 +591,8 @@ pyobject_native_type_named!(PyTime); #[cfg(Py_LIMITED_API)] impl PyTypeCheck for PyTime { const NAME: &'static str = "PyTime"; + #[cfg(feature = "experimental-inspect")] + const PYTHON_TYPE: &'static str = "datetime.time"; fn type_check(object: &Bound<'_, PyAny>) -> bool { let py = object.py(); @@ -771,6 +777,8 @@ pyobject_native_type_named!(PyTzInfo); #[cfg(Py_LIMITED_API)] impl PyTypeCheck for PyTzInfo { const NAME: &'static str = "PyTzInfo"; + #[cfg(feature = "experimental-inspect")] + const PYTHON_TYPE: &'static str = "datetime.tzinfo"; fn type_check(object: &Bound<'_, PyAny>) -> bool { let py = object.py(); @@ -885,6 +893,8 @@ pyobject_native_type_named!(PyDelta); #[cfg(Py_LIMITED_API)] impl PyTypeCheck for PyDelta { const NAME: &'static str = "PyDelta"; + #[cfg(feature = "experimental-inspect")] + const PYTHON_TYPE: &'static str = "datetime.timedelta"; fn type_check(object: &Bound<'_, PyAny>) -> bool { let py = object.py(); diff --git a/src/types/float.rs b/src/types/float.rs index 3753572904b..2cc95246996 100644 --- a/src/types/float.rs +++ b/src/types/float.rs @@ -102,8 +102,11 @@ impl<'py> IntoPyObject<'py> for &f64 { } impl<'py> FromPyObject<'py> for f64 { + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = "float"; + // PyFloat_AsDouble returns -1.0 upon failure - #![allow(clippy::float_cmp)] + #[allow(clippy::float_cmp)] fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { // On non-limited API, .value() uses PyFloat_AS_DOUBLE which // allows us to have an optimized fast path for the case when @@ -164,6 +167,9 @@ impl<'py> IntoPyObject<'py> for &f32 { } impl<'py> FromPyObject<'py> for f32 { + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = "float"; + fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { Ok(obj.extract::()? as f32) } diff --git a/src/types/iterator.rs b/src/types/iterator.rs index 6731fff2c48..6b7cf722337 100644 --- a/src/types/iterator.rs +++ b/src/types/iterator.rs @@ -119,6 +119,8 @@ impl<'py> IntoIterator for &Bound<'py, PyIterator> { impl PyTypeCheck for PyIterator { const NAME: &'static str = "Iterator"; + #[cfg(feature = "experimental-inspect")] + const PYTHON_TYPE: &'static str = "collections.abc.Iterator"; fn type_check(object: &Bound<'_, PyAny>) -> bool { unsafe { ffi::PyIter_Check(object.as_ptr()) != 0 } diff --git a/src/types/mapping.rs b/src/types/mapping.rs index baf3a023ce9..5950db72743 100644 --- a/src/types/mapping.rs +++ b/src/types/mapping.rs @@ -168,6 +168,8 @@ fn get_mapping_abc(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> { impl PyTypeCheck for PyMapping { const NAME: &'static str = "Mapping"; + #[cfg(feature = "experimental-inspect")] + const PYTHON_TYPE: &'static str = "collections.abc.Mapping"; #[inline] fn type_check(object: &Bound<'_, PyAny>) -> bool { diff --git a/src/types/sequence.rs b/src/types/sequence.rs index c5fef02bd5e..2546b483baf 100644 --- a/src/types/sequence.rs +++ b/src/types/sequence.rs @@ -377,6 +377,8 @@ fn get_sequence_abc(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> { impl PyTypeCheck for PySequence { const NAME: &'static str = "Sequence"; + #[cfg(feature = "experimental-inspect")] + const PYTHON_TYPE: &'static str = "collections.abc.Sequence"; #[inline] fn type_check(object: &Bound<'_, PyAny>) -> bool { diff --git a/src/types/weakref/anyref.rs b/src/types/weakref/anyref.rs index d496c175376..9a481d2b0c0 100644 --- a/src/types/weakref/anyref.rs +++ b/src/types/weakref/anyref.rs @@ -18,6 +18,8 @@ pyobject_native_type_named!(PyWeakref); impl PyTypeCheck for PyWeakref { const NAME: &'static str = "weakref"; + #[cfg(feature = "experimental-inspect")] + const PYTHON_TYPE: &'static str = "weakref.ProxyTypes"; fn type_check(object: &Bound<'_, PyAny>) -> bool { unsafe { ffi::PyWeakref_Check(object.as_ptr()) > 0 } diff --git a/src/types/weakref/proxy.rs b/src/types/weakref/proxy.rs index f60321fdd3d..ceab81e37a5 100644 --- a/src/types/weakref/proxy.rs +++ b/src/types/weakref/proxy.rs @@ -22,6 +22,8 @@ pyobject_native_type_named!(PyWeakrefProxy); impl PyTypeCheck for PyWeakrefProxy { const NAME: &'static str = "weakref.ProxyTypes"; + #[cfg(feature = "experimental-inspect")] + const PYTHON_TYPE: &'static str = "weakref.ProxyType | weakref.CallableProxyType"; fn type_check(object: &Bound<'_, PyAny>) -> bool { unsafe { ffi::PyWeakref_CheckProxy(object.as_ptr()) > 0 } diff --git a/src/types/weakref/reference.rs b/src/types/weakref/reference.rs index a4540f7eaf9..eb4eb53a170 100644 --- a/src/types/weakref/reference.rs +++ b/src/types/weakref/reference.rs @@ -35,6 +35,8 @@ pyobject_native_type_named!(PyWeakrefReference); #[cfg(any(PyPy, GraalPy, Py_LIMITED_API))] impl PyTypeCheck for PyWeakrefReference { const NAME: &'static str = "weakref.ReferenceType"; + #[cfg(feature = "experimental-inspect")] + const PYTHON_TYPE: &'static str = "weakref.ReferenceType"; fn type_check(object: &Bound<'_, PyAny>) -> bool { unsafe { ffi::PyWeakref_CheckRef(object.as_ptr()) > 0 } diff --git a/tests/ui/invalid_cancel_handle.stderr b/tests/ui/invalid_cancel_handle.stderr index a023d128bf2..ca9274f1785 100644 --- a/tests/ui/invalid_cancel_handle.stderr +++ b/tests/ui/invalid_cancel_handle.stderr @@ -22,6 +22,32 @@ error: `from_py_with` and `cancel_handle` cannot be specified together 24 | #[pyo3(cancel_handle, from_py_with = cancel_handle)] _param: pyo3::coroutine::CancelHandle, | ^^^^^^^^^^^^^ +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 `PyClass` is not implemented for `CancelHandle` + | + = 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<'_, '_, false>` + +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, 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<'_, '_, false>` + error[E0308]: mismatched types --> tests/ui/invalid_cancel_handle.rs:16:1 | From bc419502dd36fe15a74498712551ff6e03038e30 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 20 Jun 2025 10:21:11 +0100 Subject: [PATCH 716/936] bump MSRV to 1.74 (#5171) * bump MSRV to 1.74 * fmt * newsfragmen * simplify dependencies * fix toml formatting * clippy * fix resolve job * fmt * fix temporary in `c_str!` macro * more clippy fixes * more clippy & UI test fixes * fixup doctest * Update src/exceptions.rs Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> * be more permissive in doctest lints --------- Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- Cargo.toml | 18 +++- build.rs | 1 - examples/Cargo.toml | 2 +- examples/decorator/Cargo.toml | 2 +- examples/getitem/Cargo.toml | 2 +- examples/maturin-starter/Cargo.toml | 2 +- examples/plugin/Cargo.toml | 6 +- examples/setuptools-rust-starter/Cargo.toml | 2 +- examples/word-count/Cargo.toml | 2 +- newsfragments/5171.packaging.md | 2 + noxfile.py | 28 +---- pyo3-build-config/Cargo.toml | 3 +- pyo3-build-config/src/impl_.rs | 4 +- pyo3-build-config/src/lib.rs | 16 +-- pyo3-ffi/Cargo.toml | 6 +- pyo3-ffi/build.rs | 5 +- pyo3-ffi/examples/sequential/Cargo.toml | 2 +- pyo3-ffi/examples/string-sum/Cargo.toml | 2 +- pyo3-ffi/src/cpython/genobject.rs | 1 + pyo3-ffi/src/cpython/pystate.rs | 3 +- pyo3-ffi/src/lib.rs | 21 +--- pyo3-ffi/src/pystate.rs | 4 +- pyo3-macros-backend/Cargo.toml | 5 +- pyo3-macros-backend/src/pyclass.rs | 1 - pyo3-macros/Cargo.toml | 2 +- pytests/Cargo.toml | 2 +- pytests/src/misc.rs | 7 +- src/conversions/jiff.rs | 3 +- src/conversions/num_bigint.rs | 12 +-- src/err/impls.rs | 2 +- src/exceptions.rs | 6 +- src/gil.rs | 34 +++--- src/lib.rs | 4 +- src/sync.rs | 8 -- src/types/boolobject.rs | 4 +- src/types/string.rs | 2 +- src/types/weakref/anyref.rs | 20 ++-- src/types/weakref/proxy.rs | 82 +++++++------- src/types/weakref/reference.rs | 34 +++--- tests/test_buffer.rs | 2 +- tests/test_buffer_protocol.rs | 2 +- tests/test_declarative_module.rs | 21 ++-- tests/test_enum.rs | 2 +- tests/test_gc.rs | 2 +- tests/test_pyfunction.rs | 2 +- tests/ui/invalid_pymethods.rs | 4 +- tests/ui/invalid_pymethods.stderr | 112 +++++++++----------- 48 files changed, 221 insertions(+), 290 deletions(-) create mode 100644 newsfragments/5171.packaging.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6c7ef5c3763..a7a245350dd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,7 +45,7 @@ jobs: python-version: "3.13" - 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"))["workspace"]["package"]["rust-version"])'` >> $GITHUB_OUTPUT semver-checks: if: github.ref != 'refs/heads/main' diff --git a/Cargo.toml b/Cargo.toml index f35669b5cca..f7c8cfcd391 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,14 +10,23 @@ repository = "https://github.com/pyo3/pyo3" documentation = "https://docs.rs/crate/pyo3/" 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"] +exclude = [ + "/.gitignore", + ".cargo/config", + "/codecov.yml", + "/Makefile", + "/pyproject.toml", + "/noxfile.py", + "/.github", + "/tests/test_compile_error.rs", + "/tests/ui", +] edition = "2021" -rust-version = "1.63" +rust-version.workspace = true [dependencies] libc = "0.2.62" 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.25.0" } @@ -144,6 +153,7 @@ full = [ "eyre", "hashbrown", "indexmap", + "jiff-02", "lock_api", "num-bigint", "num-complex", @@ -154,6 +164,7 @@ full = [ "rust_decimal", "serde", "smallvec", + "time", "uuid", ] @@ -167,6 +178,7 @@ members = [ "pytests", "examples", ] +package.rust-version = "1.74" [package.metadata.docs.rs] no-default-features = true diff --git a/build.rs b/build.rs index fd28b03b79d..25e3f54e331 100644 --- a/build.rs +++ b/build.rs @@ -41,7 +41,6 @@ fn configure_pyo3() -> Result<()> { println!("{cfg}") } - // Emit cfgs like `invalid_from_utf8_lint` print_feature_cfgs(); // Make `cargo test` etc work on macOS with Xcode bundled Python diff --git a/examples/Cargo.toml b/examples/Cargo.toml index f6c77eb609c..b17fd4887ad 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -3,7 +3,7 @@ name = "pyo3-examples" version = "0.0.0" publish = false edition = "2021" -rust-version = "1.63" +rust-version = "1.74" [dev-dependencies] pyo3 = { path = "..", features = ["auto-initialize", "extension-module"] } diff --git a/examples/decorator/Cargo.toml b/examples/decorator/Cargo.toml index 785895121a3..1c3586b884b 100644 --- a/examples/decorator/Cargo.toml +++ b/examples/decorator/Cargo.toml @@ -2,7 +2,7 @@ name = "decorator" version = "0.1.0" edition = "2021" -rust-version = "1.63" +rust-version = "1.74" [lib] name = "decorator" diff --git a/examples/getitem/Cargo.toml b/examples/getitem/Cargo.toml index 99430483171..fa35cf10ffc 100644 --- a/examples/getitem/Cargo.toml +++ b/examples/getitem/Cargo.toml @@ -2,7 +2,7 @@ name = "getitem" version = "0.1.0" edition = "2021" -rust-version = "1.63" +rust-version = "1.74" [lib] name = "getitem" diff --git a/examples/maturin-starter/Cargo.toml b/examples/maturin-starter/Cargo.toml index ee1ab9aff06..f65b0ed3ba7 100644 --- a/examples/maturin-starter/Cargo.toml +++ b/examples/maturin-starter/Cargo.toml @@ -2,7 +2,7 @@ name = "maturin-starter" version = "0.1.0" edition = "2021" -rust-version = "1.63" +rust-version = "1.74" [lib] name = "maturin_starter" diff --git a/examples/plugin/Cargo.toml b/examples/plugin/Cargo.toml index 062dab1ff21..4cc6b003ed5 100644 --- a/examples/plugin/Cargo.toml +++ b/examples/plugin/Cargo.toml @@ -2,11 +2,11 @@ name = "plugin_example" version = "0.1.0" edition = "2021" -rust-version = "1.63" +rust-version = "1.74" [dependencies] -pyo3={path="../../", features=["macros"]} -plugin_api={path="plugin_api"} +pyo3 = { path = "../../", features = ["macros"] } +plugin_api = { path = "plugin_api" } [workspace] diff --git a/examples/setuptools-rust-starter/Cargo.toml b/examples/setuptools-rust-starter/Cargo.toml index ffabd8df849..6132cf878e2 100644 --- a/examples/setuptools-rust-starter/Cargo.toml +++ b/examples/setuptools-rust-starter/Cargo.toml @@ -2,7 +2,7 @@ name = "setuptools-rust-starter" version = "0.1.0" edition = "2021" -rust-version = "1.63" +rust-version = "1.74" [lib] name = "setuptools_rust_starter" diff --git a/examples/word-count/Cargo.toml b/examples/word-count/Cargo.toml index 8d79c8a4ff9..514bcfe06e2 100644 --- a/examples/word-count/Cargo.toml +++ b/examples/word-count/Cargo.toml @@ -2,7 +2,7 @@ name = "word-count" version = "0.1.0" edition = "2021" -rust-version = "1.63" +rust-version = "1.74" [lib] name = "word_count" diff --git a/newsfragments/5171.packaging.md b/newsfragments/5171.packaging.md new file mode 100644 index 00000000000..cb502462c1a --- /dev/null +++ b/newsfragments/5171.packaging.md @@ -0,0 +1,2 @@ +Update MSRV to 1.74. +Drop `once_cell` dependency. diff --git a/noxfile.py b/noxfile.py index 6e32efc3327..cfaee2ad167 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1,4 +1,3 @@ -from contextlib import contextmanager import json import os import re @@ -7,6 +6,7 @@ import sys import sysconfig import tempfile +from contextlib import contextmanager from functools import lru_cache from glob import glob from pathlib import Path @@ -21,7 +21,6 @@ Tuple, ) -import nox import nox.command try: @@ -460,14 +459,6 @@ def docs(session: nox.Session) -> None: features = "full" - if get_rust_version()[:2] >= (1, 67): - # time needs MSRC 1.67+ - features += ",time" - - if get_rust_version()[:2] >= (1, 70): - # jiff needs MSRC 1.70+ - features += ",jiff-02" - shutil.rmtree(PYO3_DOCS_TARGET, ignore_errors=True) _run_cargo( session, @@ -670,10 +661,7 @@ def set_msrv_package_versions(session: nox.Session): *(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 = { - "trybuild": "1.0.89", - "allocator-api2": "0.2.10", - } + min_pkg_versions = {} # run cargo update first to ensure that everything is at highest # possible version, so that this matches what CI will resolve to. @@ -857,8 +845,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-02,time", env=env) - _run_cargo(session, *command, "--features=abi3,full,jiff-02,time", env=env) + _run_cargo(session, *command, "--features=full", env=env) + _run_cargo(session, *command, "--features=abi3,full", env=env) @nox.session(name="test-introspection") @@ -929,14 +917,6 @@ def _get_feature_sets() -> Tuple[Optional[str], ...]: # multiple-pymethods not supported on wasm features += ",multiple-pymethods" - if get_rust_version()[:2] >= (1, 67): - # time needs MSRC 1.67+ - features += ",time" - - if get_rust_version()[:2] >= (1, 70): - # jiff needs MSRC 1.70+ - features += ",jiff-02" - if is_rust_nightly(): features += ",nightly" diff --git a/pyo3-build-config/Cargo.toml b/pyo3-build-config/Cargo.toml index 8070860a1e9..3786b4e6f7c 100644 --- a/pyo3-build-config/Cargo.toml +++ b/pyo3-build-config/Cargo.toml @@ -9,10 +9,9 @@ repository = "https://github.com/pyo3/pyo3" categories = ["api-bindings", "development-tools::ffi"] license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.63" +rust-version.workspace = true [dependencies] -once_cell = "1" python3-dll-a = { version = "0.2.12", optional = true } target-lexicon = "0.13" diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 4997fc936fc..fc3cae925e3 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -848,7 +848,7 @@ fn have_python_interpreter() -> bool { /// Must be called from a PyO3 crate build script. fn is_abi3() -> bool { cargo_env_var("CARGO_FEATURE_ABI3").is_some() - || env_var("PYO3_USE_ABI3_FORWARD_COMPATIBILITY").map_or(false, |os_str| os_str == "1") + || env_var("PYO3_USE_ABI3_FORWARD_COMPATIBILITY").is_some_and(|os_str| os_str == "1") } /// Gets the minimum supported Python version from PyO3 `abi3-py*` features. @@ -1467,7 +1467,7 @@ fn search_lib_dir(path: impl AsRef, cross: &CrossCompileConfig) -> Result< 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()], - Ok(f) if f.metadata().map_or(false, |metadata| metadata.is_dir()) => { + Ok(f) if f.metadata().is_ok_and(|metadata| metadata.is_dir()) => { let file_name = f.file_name(); let file_name = file_name.to_string_lossy(); if file_name == "build" || file_name == "lib" { diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index f47c16f425d..22f0d333522 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -16,9 +16,7 @@ use std::{ path::{Path, PathBuf}, }; -use std::{env, process::Command, str::FromStr}; - -use once_cell::sync::OnceCell; +use std::{env, process::Command, str::FromStr, sync::OnceLock}; pub use impl_::{ cross_compiling_from_to, find_all_sysconfigdata, parse_sysconfigdata, BuildFlag, BuildFlags, @@ -109,10 +107,10 @@ fn _add_python_framework_link_args( /// 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`. +/// Because this will never change in a given compilation run, this is cached in a `OnceLock`. #[cfg(feature = "resolve-config")] pub fn get() -> &'static InterpreterConfig { - static CONFIG: OnceCell = OnceCell::new(); + static CONFIG: OnceLock = OnceLock::new(); CONFIG.get_or_init(|| { // Check if we are in a build script and cross compiling to a different target. let cross_compile_config_path = resolve_cross_compile_config_path(); @@ -183,10 +181,6 @@ fn print_feature_cfg(minor_version_required: u32, cfg: &str) { /// so this function is unstable. #[doc(hidden)] pub fn print_feature_cfgs() { - 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 @@ -201,7 +195,7 @@ pub fn print_feature_cfgs() { /// - #[doc(hidden)] pub fn print_expected_cfgs() { - if rustc_minor_version().map_or(false, |version| version < 80) { + if rustc_minor_version().is_some_and(|version| version < 80) { // rustc 1.80.0 stabilized `rustc-check-cfg` feature, don't emit before return; } @@ -280,7 +274,7 @@ pub mod pyo3_build_script_impl { } fn rustc_minor_version() -> Option { - static RUSTC_MINOR_VERSION: OnceCell> = OnceCell::new(); + static RUSTC_MINOR_VERSION: OnceLock> = OnceLock::new(); *RUSTC_MINOR_VERSION.get_or_init(|| { let rustc = env::var_os("RUSTC")?; let output = Command::new(rustc).arg("--version").output().ok()?; diff --git a/pyo3-ffi/Cargo.toml b/pyo3-ffi/Cargo.toml index 647b76fbdcd..91c1517786a 100644 --- a/pyo3-ffi/Cargo.toml +++ b/pyo3-ffi/Cargo.toml @@ -10,7 +10,7 @@ categories = ["api-bindings", "development-tools::ffi"] license = "MIT OR Apache-2.0" edition = "2021" links = "python" -rust-version = "1.63" +rust-version.workspace = true [dependencies] libc = "0.2.62" @@ -51,8 +51,8 @@ workspace = true [package.metadata.cpython] min-version = "3.7" -max-version = "3.14" # inclusive +max-version = "3.14" # inclusive [package.metadata.pypy] min-version = "3.9" -max-version = "3.11" # inclusive +max-version = "3.11" # inclusive diff --git a/pyo3-ffi/build.rs b/pyo3-ffi/build.rs index 6776cd80476..455089ee5a2 100644 --- a/pyo3-ffi/build.rs +++ b/pyo3-ffi/build.rs @@ -63,7 +63,7 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { = 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"), + ensure!(env_var("PYO3_USE_ABI3_FORWARD_COMPATIBILITY").is_some_and(|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", @@ -192,7 +192,7 @@ fn emit_link_config(interpreter_config: &InterpreterConfig) -> Result<()> { fn configure_pyo3() -> Result<()> { let interpreter_config = resolve_interpreter_config()?; - if env_var("PYO3_PRINT_CONFIG").map_or(false, |os_str| os_str == "1") { + if env_var("PYO3_PRINT_CONFIG").is_some_and(|os_str| os_str == "1") { print_config_and_exit(&interpreter_config); } @@ -215,7 +215,6 @@ fn configure_pyo3() -> Result<()> { println!("{line}"); } - // Emit cfgs like `invalid_from_utf8_lint` print_feature_cfgs(); Ok(()) diff --git a/pyo3-ffi/examples/sequential/Cargo.toml b/pyo3-ffi/examples/sequential/Cargo.toml index e62693303d9..950322ec0b7 100644 --- a/pyo3-ffi/examples/sequential/Cargo.toml +++ b/pyo3-ffi/examples/sequential/Cargo.toml @@ -2,7 +2,7 @@ name = "sequential" version = "0.1.0" edition = "2021" -rust-version = "1.63" +rust-version = "1.74" [lib] name = "sequential" diff --git a/pyo3-ffi/examples/string-sum/Cargo.toml b/pyo3-ffi/examples/string-sum/Cargo.toml index b0f784d26f2..29724e14c66 100644 --- a/pyo3-ffi/examples/string-sum/Cargo.toml +++ b/pyo3-ffi/examples/string-sum/Cargo.toml @@ -2,7 +2,7 @@ name = "string_sum" version = "0.1.0" edition = "2021" -rust-version = "1.63" +rust-version = "1.74" [lib] name = "string_sum" diff --git a/pyo3-ffi/src/cpython/genobject.rs b/pyo3-ffi/src/cpython/genobject.rs index 92f14d59e4b..05ee6f3f652 100644 --- a/pyo3-ffi/src/cpython/genobject.rs +++ b/pyo3-ffi/src/cpython/genobject.rs @@ -18,6 +18,7 @@ pub struct PyGenObject { pub gi_weakreflist: *mut PyObject, pub gi_name: *mut PyObject, pub gi_qualname: *mut PyObject, + #[allow(private_interfaces)] pub gi_exc_state: crate::cpython::pystate::_PyErr_StackItem, #[cfg(Py_3_11)] pub gi_origin_or_finalizer: *mut PyObject, diff --git a/pyo3-ffi/src/cpython/pystate.rs b/pyo3-ffi/src/cpython/pystate.rs index b8f6fd667b7..707d4945f49 100644 --- a/pyo3-ffi/src/cpython/pystate.rs +++ b/pyo3-ffi/src/cpython/pystate.rs @@ -31,8 +31,7 @@ pub const PyTrace_OPCODE: c_int = 7; #[cfg(not(PyPy))] #[repr(C)] #[derive(Clone, Copy)] -#[doc(hidden)] // TODO should be able to make pub(crate) after MSRV 1.74 -pub struct _PyErr_StackItem { +pub(crate) struct _PyErr_StackItem { #[cfg(not(Py_3_11))] exc_type: *mut PyObject, exc_value: *mut PyObject, diff --git a/pyo3-ffi/src/lib.rs b/pyo3-ffi/src/lib.rs index 95f59d35192..cd24fd86221 100644 --- a/pyo3-ffi/src/lib.rs +++ b/pyo3-ffi/src/lib.rs @@ -373,26 +373,13 @@ macro_rules! c_str { /// Private helper for `c_str!` macro. #[doc(hidden)] -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 = bytes.len(); - assert!( - !bytes.is_empty() && bytes[bytes.len() - 1] == b'\0', - "string is not nul-terminated" - ); - let mut i = 0; - let non_null_len = len - 1; - while i < non_null_len { - assert!(bytes[i] != b'\0', "string contains null bytes"); - i += 1; +pub const fn _cstr_from_utf8_with_nul_checked(s: &str) -> &std::ffi::CStr { + match std::ffi::CStr::from_bytes_with_nul(s.as_bytes()) { + Ok(cstr) => cstr, + Err(_) => panic!("string contains nul bytes"), } - - unsafe { CStr::from_bytes_with_nul_unchecked(bytes) } } -use std::ffi::CStr; - pub mod compat; mod impl_; diff --git a/pyo3-ffi/src/pystate.rs b/pyo3-ffi/src/pystate.rs index 080fa11201c..2a41f937257 100644 --- a/pyo3-ffi/src/pystate.rs +++ b/pyo3-ffi/src/pystate.rs @@ -102,13 +102,13 @@ impl Drop for HangThread { // 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))] + #[cfg(not(Py_3_14))] 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)))] + #[cfg(Py_3_14)] extern "C" { #[cfg_attr(PyPy, link_name = "PyPyGILState_Ensure")] pub fn PyGILState_Ensure() -> super::PyGILState_STATE; diff --git a/pyo3-macros-backend/Cargo.toml b/pyo3-macros-backend/Cargo.toml index 878a0476dad..506a2b8eb31 100644 --- a/pyo3-macros-backend/Cargo.toml +++ b/pyo3-macros-backend/Cargo.toml @@ -9,7 +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" +rust-version.workspace = true # Note: we use default-features = false for proc-macro related crates # not to depend on proc-macro itself. @@ -21,7 +21,8 @@ pyo3-build-config = { path = "../pyo3-build-config", version = "=0.25.0", featur quote = { version = "1", default-features = false } [dependencies.syn] -version = "2.0.59" # for `LitCStr` +# 2.0.59 for `LitCStr` +version = "2.0.59" default-features = false features = ["derive", "parsing", "printing", "clone-impls", "full", "extra-traits", "visit-mut"] diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index d4ea0e08098..449ede64832 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -2006,7 +2006,6 @@ fn pyclass_hash( 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() => diff --git a/pyo3-macros/Cargo.toml b/pyo3-macros/Cargo.toml index 7df15c23f76..a38dac4b3a1 100644 --- a/pyo3-macros/Cargo.toml +++ b/pyo3-macros/Cargo.toml @@ -9,7 +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" +rust-version.workspace = true [lib] proc-macro = true diff --git a/pytests/Cargo.toml b/pytests/Cargo.toml index 2763cdec2e3..b744c6e16cc 100644 --- a/pytests/Cargo.toml +++ b/pytests/Cargo.toml @@ -5,7 +5,7 @@ version = "0.1.0" description = "Python-based tests for PyO3" edition = "2021" publish = false -rust-version = "1.63" +rust-version = "1.74" [dependencies] pyo3 = { path = "../", features = ["extension-module", "experimental-inspect"] } diff --git a/pytests/src/misc.rs b/pytests/src/misc.rs index b3ef5ee283e..7beafe78020 100644 --- a/pytests/src/misc.rs +++ b/pytests/src/misc.rs @@ -12,8 +12,7 @@ fn issue_219() { #[pyclass] struct LockHolder { #[allow(unused)] - // Mutex needed for the MSRV - sender: std::sync::Mutex>, + sender: std::sync::mpsc::Sender<()>, } // This will hammer the GIL once the LockHolder is dropped. @@ -28,9 +27,7 @@ fn hammer_gil_in_thread() -> LockHolder { Python::with_gil(|_py| ()); } }); - LockHolder { - sender: std::sync::Mutex::new(sender), - } + LockHolder { sender } } #[pyfunction] diff --git a/src/conversions/jiff.rs b/src/conversions/jiff.rs index 814040e5fb4..83cacc83fe7 100644 --- a/src/conversions/jiff.rs +++ b/src/conversions/jiff.rs @@ -14,8 +14,7 @@ //! ``` //! //! 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. +//! The required jiff version may vary based on the version of PyO3. //! //! # Example: Convert a `datetime.datetime` to jiff `Zoned` //! diff --git a/src/conversions/num_bigint.rs b/src/conversions/num_bigint.rs index ebc4058b1a6..7dadf842c84 100644 --- a/src/conversions/num_bigint.rs +++ b/src/conversions/num_bigint.rs @@ -139,7 +139,7 @@ impl<'py> FromPyObject<'py> for BigInt { #[cfg(not(Py_LIMITED_API))] { let mut buffer = int_to_u32_vec::(num)?; - let sign = if buffer.last().copied().map_or(false, |last| last >> 31 != 0) { + let sign = if buffer.last().copied().is_some_and(|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) let mut elements = buffer.iter_mut(); @@ -195,7 +195,7 @@ impl<'py> FromPyObject<'py> for BigUint { if n_bits == 0 { return Ok(BigUint::from(0usize)); } - let bytes = int_to_py_bytes(num, (n_bits + 7) / 8, false)?; + let bytes = int_to_py_bytes(num, n_bits.div_ceil(8), false)?; Ok(BigUint::from_bytes_le(bytes.as_bytes())) } } @@ -212,7 +212,7 @@ fn int_to_u32_vec(long: &Bound<'_, PyInt>) -> PyResult(long: &Bound<'_, PyInt>) -> PyResult= 1.73 - let n_digits = { - let adjust = if n_bytes % 4 == 0 { 0 } else { 1 }; - (n_bytes_unsigned / 4) + adjust - }; + let n_digits = n_bytes_unsigned.div_ceil(4); buffer.reserve_exact(n_digits); unsafe { ffi::PyLong_AsNativeBytes( diff --git a/src/err/impls.rs b/src/err/impls.rs index 814544a86f4..36ae3c6255a 100644 --- a/src/err/impls.rs +++ b/src/err/impls.rs @@ -49,7 +49,7 @@ impl From for io::Error { impl From for PyErr { fn from(err: io::Error) -> PyErr { // If the error wraps a Python error we return it - if err.get_ref().map_or(false, |e| e.is::()) { + if err.get_ref().is_some_and(|e| e.is::()) { return *err.into_inner().unwrap().downcast().unwrap(); } match err.kind() { diff --git a/src/exceptions.rs b/src/exceptions.rs index 5f39d474a72..661604ef325 100644 --- a/src/exceptions.rs +++ b/src/exceptions.rs @@ -658,13 +658,13 @@ impl PyUnicodeDecodeError { /// # Examples /// /// ``` - /// #![cfg_attr(invalid_from_utf8_lint, allow(invalid_from_utf8))] /// use pyo3::prelude::*; /// use pyo3::exceptions::PyUnicodeDecodeError; /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { /// let invalid_utf8 = b"fo\xd8o"; + /// # #[allow(invalid_from_utf8)] /// let err = std::str::from_utf8(invalid_utf8).expect_err("should be invalid utf8"); /// let decode_err = PyUnicodeDecodeError::new_utf8(py, invalid_utf8, err)?; /// assert_eq!( @@ -1020,7 +1020,7 @@ mod tests { #[test] fn unicode_decode_error() { let invalid_utf8 = b"fo\xd8o"; - #[cfg_attr(invalid_from_utf8_lint, allow(invalid_from_utf8))] + #[allow(invalid_from_utf8)] let err = std::str::from_utf8(invalid_utf8).expect_err("should be invalid utf8"); Python::with_gil(|py| { let decode_err = PyUnicodeDecodeError::new_utf8(py, invalid_utf8, err).unwrap(); @@ -1077,7 +1077,7 @@ mod tests { test_exception!(PyUnicodeError); test_exception!(PyUnicodeDecodeError, |py| { let invalid_utf8 = b"fo\xd8o"; - #[cfg_attr(invalid_from_utf8_lint, allow(invalid_from_utf8))] + #[allow(invalid_from_utf8)] let err = std::str::from_utf8(invalid_utf8).expect_err("should be invalid utf8"); PyErr::from_value( PyUnicodeDecodeError::new_utf8(py, invalid_utf8, err) diff --git a/src/gil.rs b/src/gil.rs index a8a170a0bba..f62e13762f0 100644 --- a/src/gil.rs +++ b/src/gil.rs @@ -3,9 +3,9 @@ #[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(not(pyo3_disable_reference_pool))] +use std::sync::OnceLock; use std::{mem, ptr::NonNull, sync}; static START: sync::Once = sync::Once::new(); @@ -209,7 +209,7 @@ impl GILGuard { increment_gil_count(); #[cfg(not(pyo3_disable_reference_pool))] - if let Some(pool) = Lazy::get(&POOL) { + if let Some(pool) = POOL.get() { pool.update_counts(unsafe { Python::assume_gil_acquired() }); } GILGuard::Ensured { gstate } @@ -220,7 +220,7 @@ impl GILGuard { increment_gil_count(); let guard = GILGuard::Assumed; #[cfg(not(pyo3_disable_reference_pool))] - if let Some(pool) = Lazy::get(&POOL) { + if let Some(pool) = POOL.get() { pool.update_counts(guard.python()); } guard @@ -290,7 +290,12 @@ unsafe impl Send for ReferencePool {} unsafe impl Sync for ReferencePool {} #[cfg(not(pyo3_disable_reference_pool))] -static POOL: Lazy = Lazy::new(ReferencePool::new); +static POOL: OnceLock = OnceLock::new(); + +#[cfg(not(pyo3_disable_reference_pool))] +fn get_pool() -> &'static ReferencePool { + POOL.get_or_init(ReferencePool::new) +} /// A guard which can be used to temporarily release the GIL and restore on `Drop`. pub(crate) struct SuspendGIL { @@ -315,7 +320,7 @@ 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))] - if let Some(pool) = Lazy::get(&POOL) { + if let Some(pool) = POOL.get() { pool.update_counts(Python::assume_gil_acquired()); } } @@ -385,7 +390,7 @@ pub unsafe fn register_decref(obj: NonNull) { unsafe { ffi::Py_DECREF(obj.as_ptr()) } } else { #[cfg(not(pyo3_disable_reference_pool))] - POOL.register_decref(obj); + get_pool().register_decref(obj); #[cfg(all( pyo3_disable_reference_pool, not(pyo3_leak_on_drop_without_reference_pool) @@ -428,7 +433,7 @@ fn decrement_gil_count() { mod tests { use super::GIL_COUNT; #[cfg(not(pyo3_disable_reference_pool))] - use super::{gil_is_acquired, POOL}; + use super::{get_pool, gil_is_acquired}; use crate::{ffi, PyObject, Python}; use crate::{gil::GILGuard, types::any::PyAnyMethods}; use std::ptr::NonNull; @@ -441,7 +446,7 @@ mod tests { #[cfg(not(pyo3_disable_reference_pool))] fn pool_dec_refs_does_not_contain(obj: &PyObject) -> bool { - !POOL + !get_pool() .pending_decrefs .lock() .unwrap() @@ -452,7 +457,8 @@ mod tests { // 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 + get_pool() + .pending_decrefs .lock() .unwrap() .contains(&unsafe { NonNull::new_unchecked(obj.as_ptr()) }) @@ -634,10 +640,10 @@ mod tests { let capsule = unsafe { ffi::PyCapsule_New(ptr as _, std::ptr::null(), Some(capsule_drop)) }; - POOL.register_decref(NonNull::new(capsule).unwrap()); + get_pool().register_decref(NonNull::new(capsule).unwrap()); // Updating the counts will call decref on the capsule, which calls capsule_drop - POOL.update_counts(py); + get_pool().update_counts(py); }) } @@ -651,7 +657,7 @@ mod tests { // For GILGuard::acquire - POOL.register_decref(NonNull::new(obj.clone_ref(py).into_ptr()).unwrap()); + get_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(); @@ -659,7 +665,7 @@ mod tests { // For GILGuard::assume - POOL.register_decref(NonNull::new(obj.clone_ref(py).into_ptr()).unwrap()); + get_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() }; diff --git a/src/lib.rs b/src/lib.rs index 8d684b67c18..ea160ed0966 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,8 +4,7 @@ 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 +#![warn(unsafe_op_in_unsafe_fn)] // Deny some lints in doctests. // Use `#[allow(...)]` locally to override. #![doc(test(attr( @@ -16,6 +15,7 @@ warnings ), allow( + unused_imports, // to make imports already in the prelude explicit unused_variables, unused_assignments, unused_extern_crates, diff --git a/src/sync.rs b/src/sync.rs index e1a74eae7bd..ba8f3da3310 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -527,7 +527,6 @@ where } } -#[cfg(rustc_has_once_lock)] mod once_lock_ext_sealed { pub trait Sealed {} impl Sealed for std::sync::OnceLock {} @@ -550,7 +549,6 @@ pub trait OnceExt: Sealed { /// 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. /// @@ -637,15 +635,11 @@ impl OnceExt for parking_lot::Once { } } -#[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)) @@ -747,7 +741,6 @@ where }); } -#[cfg(rustc_has_once_lock)] #[cold] fn init_once_lock_py_attached<'a, F, T>( lock: &'a std::sync::OnceLock, @@ -1086,7 +1079,6 @@ mod tests { test_once!(parking_lot::Once::new(), parking_lot::OnceState::poisoned); } - #[cfg(rustc_has_once_lock)] #[cfg(not(target_arch = "wasm32"))] // We are building wasm Python with pthreads disabled #[test] fn test_once_lock_ext() { diff --git a/src/types/boolobject.rs b/src/types/boolobject.rs index 6fc2e87b2d8..09f1c14d59f 100644 --- a/src/types/boolobject.rs +++ b/src/types/boolobject.rs @@ -183,10 +183,10 @@ impl FromPyObject<'_> for bool { let is_numpy_bool = { let ty = obj.get_type(); - ty.module().map_or(false, |module| module == "numpy") + ty.module().is_ok_and(|module| module == "numpy") && ty .name() - .map_or(false, |name| name == "bool_" || name == "bool") + .is_ok_and(|name| name == "bool_" || name == "bool") }; if is_numpy_bool { diff --git a/src/types/string.rs b/src/types/string.rs index 774624df108..59300440231 100644 --- a/src/types/string.rs +++ b/src/types/string.rs @@ -488,7 +488,7 @@ impl PartialEq for Borrowed<'_, '_, PyString> { fn eq(&self, other: &str) -> bool { #[cfg(not(Py_3_13))] { - self.to_cow().map_or(false, |s| s == other) + self.to_cow().is_ok_and(|s| s == other) } #[cfg(Py_3_13)] diff --git a/src/types/weakref/anyref.rs b/src/types/weakref/anyref.rs index 9a481d2b0c0..b74ec53a939 100644 --- a/src/types/weakref/anyref.rs +++ b/src/types/weakref/anyref.rs @@ -382,10 +382,8 @@ mod tests { let obj = obj.unwrap(); assert!(obj.is_some()); - assert!( - obj.map_or(false, |obj| ptr::eq(obj.as_ptr(), object.as_ptr()) - && obj.is_exact_instance(&class)) - ); + assert!(obj.is_some_and(|obj| ptr::eq(obj.as_ptr(), object.as_ptr()) + && obj.is_exact_instance(&class))); } drop(object); @@ -426,10 +424,8 @@ mod tests { let obj = unsafe { reference.upgrade_as_unchecked::() }; assert!(obj.is_some()); - assert!( - obj.map_or(false, |obj| ptr::eq(obj.as_ptr(), object.as_ptr()) - && obj.is_exact_instance(&class)) - ); + assert!(obj.is_some_and(|obj| ptr::eq(obj.as_ptr(), object.as_ptr()) + && obj.is_exact_instance(&class))); } drop(object); @@ -467,7 +463,7 @@ mod tests { assert!(not_call_retrievable || reference.call0()?.is(&object)); assert!(reference.upgrade().is_some()); - assert!(reference.upgrade().map_or(false, |obj| obj.is(&object))); + assert!(reference.upgrade().is_some_and(|obj| obj.is(&object))); drop(object); @@ -512,7 +508,7 @@ mod tests { let obj = obj.unwrap(); assert!(obj.is_some()); - assert!(obj.map_or(false, |obj| ptr::eq(obj.as_ptr(), object.as_ptr()))); + assert!(obj.is_some_and(|obj| ptr::eq(obj.as_ptr(), object.as_ptr()))); } drop(object); @@ -550,7 +546,7 @@ mod tests { let obj = unsafe { reference.upgrade_as_unchecked::() }; assert!(obj.is_some()); - assert!(obj.map_or(false, |obj| ptr::eq(obj.as_ptr(), object.as_ptr()))); + assert!(obj.is_some_and(|obj| ptr::eq(obj.as_ptr(), object.as_ptr()))); } drop(object); @@ -586,7 +582,7 @@ mod tests { assert!(not_call_retrievable || reference.call0()?.is(&object)); assert!(reference.upgrade().is_some()); - assert!(reference.upgrade().map_or(false, |obj| obj.is(&object))); + assert!(reference.upgrade().is_some_and(|obj| obj.is(&object))); drop(object); diff --git a/src/types/weakref/proxy.rs b/src/types/weakref/proxy.rs index ceab81e37a5..e577ae229c0 100644 --- a/src/types/weakref/proxy.rs +++ b/src/types/weakref/proxy.rs @@ -57,8 +57,7 @@ impl PyWeakrefProxy { /// 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)) + /// weakref.upgrade().is_some_and(|obj| obj.is(&foo)) /// ); /// /// let weakref2 = PyWeakrefProxy::new(&foo)?; @@ -120,8 +119,7 @@ impl PyWeakrefProxy { /// 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)) + /// weakref.upgrade().is_some_and(|obj| obj.is(&foo)) /// ); /// assert_eq!(py.eval(c_str!("counter"), None, None)?.extract::()?, 0); /// @@ -277,9 +275,9 @@ mod tests { assert!(reference .getattr("__callback__") .err() - .map_or(false, |err| err.is_instance_of::(py))); + .is_some_and(|err| err.is_instance_of::(py))); - assert!(reference.call0().err().map_or(false, |err| { + assert!(reference.call0().err().is_some_and(|err| { let result = err.is_instance_of::(py); #[cfg(not(Py_LIMITED_API))] let result = result @@ -294,16 +292,16 @@ mod tests { assert!(reference .getattr("__class__") .err() - .map_or(false, |err| err.is_instance_of::(py))); + .is_some_and(|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))); + .is_some_and(|err| err.is_instance_of::(py))); - assert!(reference.call0().err().map_or(false, |err| { + assert!(reference.call0().err().is_some_and(|err| { let result = err.is_instance_of::(py); #[cfg(not(Py_LIMITED_API))] let result = result @@ -331,10 +329,8 @@ mod tests { let obj = obj.unwrap(); assert!(obj.is_some()); - assert!( - obj.map_or(false, |obj| ptr::eq(obj.as_ptr(), object.as_ptr()) - && obj.is_exact_instance(&class)) - ); + assert!(obj.is_some_and(|obj| ptr::eq(obj.as_ptr(), object.as_ptr()) + && obj.is_exact_instance(&class))); } drop(object); @@ -365,10 +361,8 @@ mod tests { let obj = unsafe { reference.upgrade_as_unchecked::() }; assert!(obj.is_some()); - assert!( - obj.map_or(false, |obj| ptr::eq(obj.as_ptr(), object.as_ptr()) - && obj.is_exact_instance(&class)) - ); + assert!(obj.is_some_and(|obj| ptr::eq(obj.as_ptr(), object.as_ptr()) + && obj.is_exact_instance(&class))); } drop(object); @@ -392,7 +386,7 @@ mod tests { let reference = PyWeakrefProxy::new(&object)?; assert!(reference.upgrade().is_some()); - assert!(reference.upgrade().map_or(false, |obj| obj.is(&object))); + assert!(reference.upgrade().is_some_and(|obj| obj.is(&object))); drop(object); @@ -455,9 +449,9 @@ mod tests { assert!(reference .getattr("__callback__") .err() - .map_or(false, |err| err.is_instance_of::(py))); + .is_some_and(|err| err.is_instance_of::(py))); - assert!(reference.call0().err().map_or(false, |err| { + assert!(reference.call0().err().is_some_and(|err| { let result = err.is_instance_of::(py); #[cfg(not(Py_LIMITED_API))] let result = result @@ -472,16 +466,16 @@ mod tests { assert!(reference .getattr("__class__") .err() - .map_or(false, |err| err.is_instance_of::(py))); + .is_some_and(|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))); + .is_some_and(|err| err.is_instance_of::(py))); - assert!(reference.call0().err().map_or(false, |err| { + assert!(reference.call0().err().is_some_and(|err| { let result = err.is_instance_of::(py); #[cfg(not(Py_LIMITED_API))] let result = result @@ -507,7 +501,7 @@ mod tests { let obj = obj.unwrap(); assert!(obj.is_some()); - assert!(obj.map_or(false, |obj| ptr::eq(obj.as_ptr(), object.as_ptr()))); + assert!(obj.is_some_and(|obj| ptr::eq(obj.as_ptr(), object.as_ptr()))); } drop(object); @@ -535,7 +529,7 @@ mod tests { let obj = unsafe { reference.upgrade_as_unchecked::() }; assert!(obj.is_some()); - assert!(obj.map_or(false, |obj| ptr::eq(obj.as_ptr(), object.as_ptr()))); + assert!(obj.is_some_and(|obj| ptr::eq(obj.as_ptr(), object.as_ptr()))); } drop(object); @@ -557,7 +551,7 @@ mod tests { let reference = PyWeakrefProxy::new(object.bind(py))?; assert!(reference.upgrade().is_some()); - assert!(reference.upgrade().map_or(false, |obj| obj.is(&object))); + assert!(reference.upgrade().is_some_and(|obj| obj.is(&object))); drop(object); @@ -613,7 +607,7 @@ mod tests { assert!(reference .getattr("__callback__") .err() - .map_or(false, |err| err.is_instance_of::(py))); + .is_some_and(|err| err.is_instance_of::(py))); assert_eq!(reference.call0()?.to_string(), "This class is callable!"); @@ -623,19 +617,19 @@ mod tests { assert!(reference .getattr("__class__") .err() - .map_or(false, |err| err.is_instance_of::(py))); + .is_some_and(|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))); + .is_some_and(|err| err.is_instance_of::(py))); assert!(reference .call0() .err() - .map_or(false, |err| err.is_instance_of::(py) + .is_some_and(|err| err.is_instance_of::(py) & (err.value(py).to_string() == "weakly-referenced object no longer exists"))); @@ -658,10 +652,8 @@ mod tests { let obj = obj.unwrap(); assert!(obj.is_some()); - assert!( - obj.map_or(false, |obj| ptr::eq(obj.as_ptr(), object.as_ptr()) - && obj.is_exact_instance(&class)) - ); + assert!(obj.is_some_and(|obj| ptr::eq(obj.as_ptr(), object.as_ptr()) + && obj.is_exact_instance(&class))); } drop(object); @@ -692,10 +684,8 @@ mod tests { let obj = unsafe { reference.upgrade_as_unchecked::() }; assert!(obj.is_some()); - assert!( - obj.map_or(false, |obj| ptr::eq(obj.as_ptr(), object.as_ptr()) - && obj.is_exact_instance(&class)) - ); + assert!(obj.is_some_and(|obj| ptr::eq(obj.as_ptr(), object.as_ptr()) + && obj.is_exact_instance(&class))); } drop(object); @@ -719,7 +709,7 @@ mod tests { let reference = PyWeakrefProxy::new(&object)?; assert!(reference.upgrade().is_some()); - assert!(reference.upgrade().map_or(false, |obj| obj.is(&object))); + assert!(reference.upgrade().is_some_and(|obj| obj.is(&object))); drop(object); @@ -769,7 +759,7 @@ mod tests { assert!(reference .getattr("__callback__") .err() - .map_or(false, |err| err.is_instance_of::(py))); + .is_some_and(|err| err.is_instance_of::(py))); assert_eq!(reference.call0()?.to_string(), "This class is callable!"); @@ -779,19 +769,19 @@ mod tests { assert!(reference .getattr("__class__") .err() - .map_or(false, |err| err.is_instance_of::(py))); + .is_some_and(|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))); + .is_some_and(|err| err.is_instance_of::(py))); assert!(reference .call0() .err() - .map_or(false, |err| err.is_instance_of::(py) + .is_some_and(|err| err.is_instance_of::(py) & (err.value(py).to_string() == "weakly-referenced object no longer exists"))); @@ -812,7 +802,7 @@ mod tests { let obj = obj.unwrap(); assert!(obj.is_some()); - assert!(obj.map_or(false, |obj| ptr::eq(obj.as_ptr(), object.as_ptr()))); + assert!(obj.is_some_and(|obj| ptr::eq(obj.as_ptr(), object.as_ptr()))); } drop(object); @@ -840,7 +830,7 @@ mod tests { let obj = unsafe { reference.upgrade_as_unchecked::() }; assert!(obj.is_some()); - assert!(obj.map_or(false, |obj| ptr::eq(obj.as_ptr(), object.as_ptr()))); + assert!(obj.is_some_and(|obj| ptr::eq(obj.as_ptr(), object.as_ptr()))); } drop(object); @@ -862,7 +852,7 @@ mod tests { let reference = PyWeakrefProxy::new(object.bind(py))?; assert!(reference.upgrade().is_some()); - assert!(reference.upgrade().map_or(false, |obj| obj.is(&object))); + assert!(reference.upgrade().is_some_and(|obj| obj.is(&object))); drop(object); diff --git a/src/types/weakref/reference.rs b/src/types/weakref/reference.rs index eb4eb53a170..8a6dd94790d 100644 --- a/src/types/weakref/reference.rs +++ b/src/types/weakref/reference.rs @@ -69,8 +69,7 @@ impl PyWeakrefReference { /// 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)) + /// weakref.upgrade().is_some_and(|obj| obj.is(&foo)) /// ); /// /// let weakref2 = PyWeakrefReference::new(&foo)?; @@ -131,8 +130,7 @@ impl PyWeakrefReference { /// 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)) + /// weakref.upgrade().is_some_and(|obj| obj.is(&foo)) /// ); /// assert_eq!(py.eval(c_str!("counter"), None, None)?.extract::()?, 0); /// @@ -271,7 +269,7 @@ mod tests { assert!(reference .getattr("__callback__") - .map_or(false, |result| result.is_none())); + .is_ok_and(|result| result.is_none())); assert!(reference.call0()?.is(&object)); @@ -284,7 +282,7 @@ mod tests { assert!(reference .getattr("__callback__") - .map_or(false, |result| result.is_none())); + .is_ok_and(|result| result.is_none())); assert!(reference.call0()?.is_none()); @@ -307,10 +305,8 @@ mod tests { let obj = obj.unwrap(); assert!(obj.is_some()); - assert!( - obj.map_or(false, |obj| ptr::eq(obj.as_ptr(), object.as_ptr()) - && obj.is_exact_instance(&class)) - ); + assert!(obj.is_some_and(|obj| ptr::eq(obj.as_ptr(), object.as_ptr()) + && obj.is_exact_instance(&class))); } drop(object); @@ -341,10 +337,8 @@ mod tests { let obj = unsafe { reference.upgrade_as_unchecked::() }; assert!(obj.is_some()); - assert!( - obj.map_or(false, |obj| ptr::eq(obj.as_ptr(), object.as_ptr()) - && obj.is_exact_instance(&class)) - ); + assert!(obj.is_some_and(|obj| ptr::eq(obj.as_ptr(), object.as_ptr()) + && obj.is_exact_instance(&class))); } drop(object); @@ -369,7 +363,7 @@ mod tests { assert!(reference.call0()?.is(&object)); assert!(reference.upgrade().is_some()); - assert!(reference.upgrade().map_or(false, |obj| obj.is(&object))); + assert!(reference.upgrade().is_some_and(|obj| obj.is(&object))); drop(object); @@ -409,7 +403,7 @@ mod tests { assert!(reference .getattr("__callback__") - .map_or(false, |result| result.is_none())); + .is_ok_and(|result| result.is_none())); assert!(reference.call0()?.is(&object)); @@ -422,7 +416,7 @@ mod tests { assert!(reference .getattr("__callback__") - .map_or(false, |result| result.is_none())); + .is_ok_and(|result| result.is_none())); assert!(reference.call0()?.is_none()); @@ -443,7 +437,7 @@ mod tests { let obj = obj.unwrap(); assert!(obj.is_some()); - assert!(obj.map_or(false, |obj| ptr::eq(obj.as_ptr(), object.as_ptr()))); + assert!(obj.is_some_and(|obj| ptr::eq(obj.as_ptr(), object.as_ptr()))); } drop(object); @@ -471,7 +465,7 @@ mod tests { let obj = unsafe { reference.upgrade_as_unchecked::() }; assert!(obj.is_some()); - assert!(obj.map_or(false, |obj| ptr::eq(obj.as_ptr(), object.as_ptr()))); + assert!(obj.is_some_and(|obj| ptr::eq(obj.as_ptr(), object.as_ptr()))); } drop(object); @@ -494,7 +488,7 @@ mod tests { assert!(reference.call0()?.is(&object)); assert!(reference.upgrade().is_some()); - assert!(reference.upgrade().map_or(false, |obj| obj.is(&object))); + assert!(reference.upgrade().is_some_and(|obj| obj.is(&object))); drop(object); diff --git a/tests/test_buffer.rs b/tests/test_buffer.rs index 8591b6a0e1f..40bccf889b1 100644 --- a/tests/test_buffer.rs +++ b/tests/test_buffer.rs @@ -1,6 +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))] +#![warn(unsafe_op_in_unsafe_fn)] use pyo3::{buffer::PyBuffer, exceptions::PyBufferError, ffi, prelude::*}; use std::{ diff --git a/tests/test_buffer_protocol.rs b/tests/test_buffer_protocol.rs index f3c316d50c7..c68b7785450 100644 --- a/tests/test_buffer_protocol.rs +++ b/tests/test_buffer_protocol.rs @@ -1,6 +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))] +#![warn(unsafe_op_in_unsafe_fn)] use pyo3::buffer::PyBuffer; use pyo3::exceptions::PyBufferError; diff --git a/tests/test_declarative_module.rs b/tests/test_declarative_module.rs index c4c9cd1a31d..1f2ff1f29b6 100644 --- a/tests/test_declarative_module.rs +++ b/tests/test_declarative_module.rs @@ -1,11 +1,11 @@ #![cfg(feature = "macros")] -use std::sync::Once; +use std::sync::OnceLock; use pyo3::create_exception; use pyo3::exceptions::PyException; use pyo3::prelude::*; -use pyo3::sync::{GILOnceCell, OnceExt}; +use pyo3::sync::OnceLockExt; #[path = "../src/tests/common.rs"] mod common; @@ -164,18 +164,11 @@ mod declarative_module2 { } fn declarative_module(py: Python<'_>) -> &Bound<'_, PyModule> { - static MODULE: GILOnceCell> = GILOnceCell::new(); - 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) + static MODULE: OnceLock> = OnceLock::new(); + + MODULE + .get_or_init_py_attached(py, || pyo3::wrap_pymodule!(declarative_module)(py)) + .bind(py) } #[test] diff --git a/tests/test_enum.rs b/tests/test_enum.rs index c687a7daf11..1421856e63d 100644 --- a/tests/test_enum.rs +++ b/tests/test_enum.rs @@ -338,7 +338,7 @@ fn custom_eq() { 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) + rhs.to_cow().is_ok_and(|rhs| self.__str__() == rhs) } else if let Ok(rhs) = other.downcast::() { self == rhs.get() } else { diff --git a/tests/test_gc.rs b/tests/test_gc.rs index 8bc1e53cadb..76901fd1530 100644 --- a/tests/test_gc.rs +++ b/tests/test_gc.rs @@ -1,5 +1,5 @@ #![cfg(feature = "macros")] -#![cfg_attr(not(cargo_toml_lints), warn(unsafe_op_in_unsafe_fn))] +#![warn(unsafe_op_in_unsafe_fn)] use pyo3::class::PyTraverseError; use pyo3::class::PyVisit; diff --git a/tests/test_pyfunction.rs b/tests/test_pyfunction.rs index b5af0c81836..b817850d727 100644 --- a/tests/test_pyfunction.rs +++ b/tests/test_pyfunction.rs @@ -1,5 +1,5 @@ #![cfg(feature = "macros")] -#![cfg_attr(not(cargo_toml_lints), warn(unsafe_op_in_unsafe_fn))] +#![warn(unsafe_op_in_unsafe_fn)] use std::collections::HashMap; diff --git a/tests/ui/invalid_pymethods.rs b/tests/ui/invalid_pymethods.rs index 41786cd7895..75ad12c26d2 100644 --- a/tests/ui/invalid_pymethods.rs +++ b/tests/ui/invalid_pymethods.rs @@ -40,10 +40,12 @@ impl MyClass { } } +struct NotATypeObject; + #[pymethods] impl MyClass { #[classmethod] - fn classmethod_wrong_first_argument(_x: i32) -> Self { + fn classmethod_wrong_first_argument(_t: NotATypeObject) -> Self { Self {} } } diff --git a/tests/ui/invalid_pymethods.stderr b/tests/ui/invalid_pymethods.stderr index eb8f429c937..2b6e195ab7d 100644 --- a/tests/ui/invalid_pymethods.stderr +++ b/tests/ui/invalid_pymethods.stderr @@ -35,160 +35,154 @@ error: Expected `&Bound` or `Py` as the first argument to `#[cla | ^^ error: expected receiver for `#[getter]` - --> tests/ui/invalid_pymethods.rs:54:5 + --> tests/ui/invalid_pymethods.rs:56:5 | -54 | fn getter_without_receiver() {} +56 | fn getter_without_receiver() {} | ^^ error: expected receiver for `#[setter]` - --> tests/ui/invalid_pymethods.rs:60:5 + --> tests/ui/invalid_pymethods.rs:62:5 | -60 | fn setter_without_receiver() {} +62 | fn setter_without_receiver() {} | ^^ error: static method needs #[staticmethod] attribute - --> tests/ui/invalid_pymethods.rs:66:5 + --> tests/ui/invalid_pymethods.rs:68:5 | -66 | fn text_signature_on_call() {} +68 | fn text_signature_on_call() {} | ^^ error: `text_signature` not allowed with `getter` - --> tests/ui/invalid_pymethods.rs:72:12 + --> tests/ui/invalid_pymethods.rs:74:12 | -72 | #[pyo3(text_signature = "()")] +74 | #[pyo3(text_signature = "()")] | ^^^^^^^^^^^^^^ error: `text_signature` not allowed with `setter` - --> tests/ui/invalid_pymethods.rs:79:12 + --> tests/ui/invalid_pymethods.rs:81:12 | -79 | #[pyo3(text_signature = "()")] +81 | #[pyo3(text_signature = "()")] | ^^^^^^^^^^^^^^ error: `text_signature` not allowed with `classattr` - --> tests/ui/invalid_pymethods.rs:86:12 + --> tests/ui/invalid_pymethods.rs:88:12 | -86 | #[pyo3(text_signature = "()")] +88 | #[pyo3(text_signature = "()")] | ^^^^^^^^^^^^^^ error: expected a string literal or `None` - --> tests/ui/invalid_pymethods.rs:92:30 + --> tests/ui/invalid_pymethods.rs:94:30 | -92 | #[pyo3(text_signature = 1)] +94 | #[pyo3(text_signature = 1)] | ^ error: `text_signature` may only be specified once - --> tests/ui/invalid_pymethods.rs:99:12 - | -99 | #[pyo3(text_signature = None)] - | ^^^^^^^^^^^^^^ + --> tests/ui/invalid_pymethods.rs:101:12 + | +101 | #[pyo3(text_signature = None)] + | ^^^^^^^^^^^^^^ error: `signature` not allowed with `getter` - --> tests/ui/invalid_pymethods.rs:106:12 + --> tests/ui/invalid_pymethods.rs:108:12 | -106 | #[pyo3(signature = ())] +108 | #[pyo3(signature = ())] | ^^^^^^^^^ error: `signature` not allowed with `setter` - --> tests/ui/invalid_pymethods.rs:113:12 + --> tests/ui/invalid_pymethods.rs:115:12 | -113 | #[pyo3(signature = ())] +115 | #[pyo3(signature = ())] | ^^^^^^^^^ error: `signature` not allowed with `classattr` - --> tests/ui/invalid_pymethods.rs:120:12 + --> tests/ui/invalid_pymethods.rs:122:12 | -120 | #[pyo3(signature = ())] +122 | #[pyo3(signature = ())] | ^^^^^^^^^ error: `#[new]` may not be combined with `#[classmethod]` `#[staticmethod]`, `#[classattr]`, `#[getter]`, and `#[setter]` - --> tests/ui/invalid_pymethods.rs:126:7 + --> tests/ui/invalid_pymethods.rs:128:7 | -126 | #[new] +128 | #[new] | ^^^ error: `#[new]` does not take any arguments = help: did you mean `#[new] #[pyo3(signature = ())]`? - --> tests/ui/invalid_pymethods.rs:137:7 + --> tests/ui/invalid_pymethods.rs:139:7 | -137 | #[new(signature = ())] +139 | #[new(signature = ())] | ^^^ error: `#[new]` does not take any arguments = note: this was previously accepted and ignored - --> tests/ui/invalid_pymethods.rs:143:11 + --> tests/ui/invalid_pymethods.rs:145:11 | -143 | #[new = ()] // in this form there's no suggestion to move arguments to `#[pyo3()]` attribute +145 | #[new = ()] // in this form there's no suggestion to move arguments to `#[pyo3()]` attribute | ^ error: `#[classmethod]` does not take any arguments = help: did you mean `#[classmethod] #[pyo3(signature = ())]`? - --> tests/ui/invalid_pymethods.rs:149:7 + --> tests/ui/invalid_pymethods.rs:151:7 | -149 | #[classmethod(signature = ())] +151 | #[classmethod(signature = ())] | ^^^^^^^^^^^ error: `#[staticmethod]` does not take any arguments = help: did you mean `#[staticmethod] #[pyo3(signature = ())]`? - --> tests/ui/invalid_pymethods.rs:155:7 + --> tests/ui/invalid_pymethods.rs:157:7 | -155 | #[staticmethod(signature = ())] +157 | #[staticmethod(signature = ())] | ^^^^^^^^^^^^ error: `#[classattr]` does not take any arguments = help: did you mean `#[classattr] #[pyo3(signature = ())]`? - --> tests/ui/invalid_pymethods.rs:161:7 + --> tests/ui/invalid_pymethods.rs:163:7 | -161 | #[classattr(signature = ())] +163 | #[classattr(signature = ())] | ^^^^^^^^^ error: Python functions cannot have generic type parameters - --> tests/ui/invalid_pymethods.rs:167:23 + --> tests/ui/invalid_pymethods.rs:169:23 | -167 | fn generic_method(_value: T) {} +169 | fn generic_method(_value: T) {} | ^ error: Python functions cannot have `impl Trait` arguments - --> tests/ui/invalid_pymethods.rs:172:49 + --> tests/ui/invalid_pymethods.rs:174:49 | -172 | fn impl_trait_method_first_arg(_impl_trait: impl AsRef) {} +174 | fn impl_trait_method_first_arg(_impl_trait: impl AsRef) {} | ^^^^ error: Python functions cannot have `impl Trait` arguments - --> tests/ui/invalid_pymethods.rs:177:57 + --> tests/ui/invalid_pymethods.rs:179:57 | -177 | fn impl_trait_method_second_arg(&self, _impl_trait: impl AsRef) {} +179 | 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 + --> tests/ui/invalid_pymethods.rs:184:12 | -182 | #[pyo3(pass_module)] +184 | #[pyo3(pass_module)] | ^^^^^^^^^^^ error: Python objects are shared, so 'self' cannot be moved out of the Python interpreter. Try `&self`, `&mut self, `slf: PyRef<'_, Self>` or `slf: PyRefMut<'_, Self>`. - --> tests/ui/invalid_pymethods.rs:188:29 + --> tests/ui/invalid_pymethods.rs:190:29 | -188 | fn method_self_by_value(self) {} +190 | fn method_self_by_value(self) {} | ^^^^ error: macros cannot be used as items in `#[pymethods]` impl blocks = note: this was previously accepted and ignored - --> tests/ui/invalid_pymethods.rs:197:5 + --> tests/ui/invalid_pymethods.rs:199:5 | -197 | macro_invocation!(); +199 | macro_invocation!(); | ^^^^^^^^^^^^^^^^ -error[E0277]: the trait bound `i32: From>` is not satisfied - --> tests/ui/invalid_pymethods.rs:46:45 +error[E0277]: the trait bound `NotATypeObject: From>` is not satisfied + --> tests/ui/invalid_pymethods.rs:48:45 | -46 | fn classmethod_wrong_first_argument(_x: i32) -> Self { - | ^^^ the trait `From>` is not implemented for `i32` +48 | fn classmethod_wrong_first_argument(_t: NotATypeObject) -> Self { + | ^^^^^^^^^^^^^^ the trait `From>` is not implemented for `NotATypeObject` | - = 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` + = note: required for `BoundRef<'_, '_, PyType>` to implement `Into` From 5d9b4b0e37894845e3ce8bb653b4bd0eaec12bcb Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 20 Jun 2025 17:32:58 +0100 Subject: [PATCH 717/936] fix `check-guide` failures (#5203) --- .github/workflows/gh-pages.yml | 2 ++ guide/src/free-threading.md | 6 +++--- noxfile.py | 12 ++++++++++++ 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index 56814be8715..e5980dbec74 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -45,6 +45,8 @@ jobs: nox -s ${{ github.event_name == 'release' && 'build-guide' || 'check-guide' }} env: PYO3_VERSION_TAG: ${{ steps.prepare_tag.outputs.tag_name }} + # allows lychee to get better rate limits from github + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Deploy docs and the guide if: ${{ github.event_name == 'release' }} diff --git a/guide/src/free-threading.md b/guide/src/free-threading.md index 898755b6118..6b2a790182f 100644 --- a/guide/src/free-threading.md +++ b/guide/src/free-threading.md @@ -393,15 +393,15 @@ interpreter. [`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 +[`Once::call_once`]: https://doc.rust-lang.org/stable/std/sync/struct.Once.html#method.call_once +[`Once::call_once_force`]: https://doc.rust-lang.org/stable/std/sync/struct.Once.html#method.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 +[`OnceLock::get_or_init`]: https://doc.rust-lang.org/stable/std/sync/struct.OnceLock.html#method.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 diff --git a/noxfile.py b/noxfile.py index cfaee2ad167..01697e71be4 100644 --- a/noxfile.py +++ b/noxfile.py @@ -508,6 +508,15 @@ def check_guide(session: nox.Session): remap_args = [] for key, value in remaps.items(): remap_args.extend(("--remap", f"{key} {value}")) + + # some http URL fragments fail to be loaded (needs javascript?) + lychee_exclusions = [ + "https://github.com/PyO3/pyo3/issues/1800#issuecomment-906786649", + "https://docs.rs/parking_lot/latest/parking_lot/type.Mutex.html#method.lock_arc", + "https://github.com/PyO3/pyo3/issues/1517#issuecomment-808664021", + "https://github.com/PyO3/maturin/blob/0dee40510083c03607834c821eea76964140a126/Readme.md#mixed-rustpython-projects", + ] + # check all links in the guide _run( session, @@ -515,6 +524,7 @@ def check_guide(session: nox.Session): "--include-fragments", str(PYO3_GUIDE_SRC), *remap_args, + "--exclude=" + "|".join(lychee_exclusions), "--accept=200,429", *session.posargs, ) @@ -526,7 +536,9 @@ def check_guide(session: nox.Session): str(PYO3_DOCS_TARGET), *remap_args, f"--exclude=file://{PYO3_DOCS_TARGET}", + # exclude some old http links from copyright notices, known to fail "--exclude=http://www.adobe.com/", + "--exclude=http://www.nhncorp.com/", "--accept=200,429", *session.posargs, ) From cadfb90a033b69f5121ca07fe5438050018b7492 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20=C5=A0im=C3=A1=C4=8Dek?= Date: Fri, 20 Jun 2025 19:06:55 +0200 Subject: [PATCH 718/936] Set the same maximum version for all interpreters (#5192) * Set the same maximum version for all interpreters * Remove test that would reject PyPy on 3.12 --- newsfragments/5192.packaging.md | 1 + noxfile.py | 4 ---- pyo3-ffi/build.rs | 10 ++-------- 3 files changed, 3 insertions(+), 12 deletions(-) create mode 100644 newsfragments/5192.packaging.md diff --git a/newsfragments/5192.packaging.md b/newsfragments/5192.packaging.md new file mode 100644 index 00000000000..12fb9a3808c --- /dev/null +++ b/newsfragments/5192.packaging.md @@ -0,0 +1 @@ +Set the same maximum supported version for alternative interpreters as for CPython diff --git a/noxfile.py b/noxfile.py index 01697e71be4..3b590dfa886 100644 --- a/noxfile.py +++ b/noxfile.py @@ -754,10 +754,6 @@ def test_version_limits(session: nox.Session): config_file.set("PyPy", "3.8") _run_cargo(session, "check", env=env, expect_error=True) - assert "3.12" not in PYPY_VERSIONS - config_file.set("PyPy", "3.12") - _run_cargo(session, "check", env=env, expect_error=True) - @nox.session(name="check-feature-powerset", venv_backend="none") def check_feature_powerset(session: nox.Session): diff --git a/pyo3-ffi/build.rs b/pyo3-ffi/build.rs index 455089ee5a2..8c935501954 100644 --- a/pyo3-ffi/build.rs +++ b/pyo3-ffi/build.rs @@ -23,10 +23,7 @@ const SUPPORTED_VERSIONS_CPYTHON: SupportedVersions = SupportedVersions { const SUPPORTED_VERSIONS_PYPY: SupportedVersions = SupportedVersions { min: PythonVersion { major: 3, minor: 9 }, - max: PythonVersion { - major: 3, - minor: 11, - }, + max: SUPPORTED_VERSIONS_CPYTHON.max, }; const SUPPORTED_VERSIONS_GRAALPY: SupportedVersions = SupportedVersions { @@ -34,10 +31,7 @@ const SUPPORTED_VERSIONS_GRAALPY: SupportedVersions = SupportedVersions { major: 3, minor: 10, }, - max: PythonVersion { - major: 3, - minor: 11, - }, + max: SUPPORTED_VERSIONS_CPYTHON.max, }; fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { From ab1e95297b354ef2d439331724544d5b83653fee Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 20 Jun 2025 21:34:30 +0100 Subject: [PATCH 719/936] docs: fix typo `nighty` -> `nightly` (#5204) --- guide/src/debugging.md | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/guide/src/debugging.md b/guide/src/debugging.md index 0cee7bd1783..c30a83c7ff9 100644 --- a/guide/src/debugging.md +++ b/guide/src/debugging.md @@ -59,7 +59,7 @@ For more information about how to use both `lldb` and `gdb` you can read the [gd ```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 . ``` @@ -90,10 +90,10 @@ Depending on your OS and your preferences you can use two different debuggers, ` ```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 ``` @@ -118,10 +118,10 @@ Depending on your OS and your preferences you can use two different debuggers, ` ```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 ``` @@ -237,7 +237,7 @@ 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. @@ -246,7 +246,7 @@ def update_launch_json(vscode_config_file_path=None): 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 @@ -255,7 +255,7 @@ def update_launch_json(vscode_config_file_path=None): # Get Python interpreter path python_path = sys.executable - + # Default debugger config debug_config = { "version": "0.2.0", @@ -270,7 +270,7 @@ def update_launch_json(vscode_config_file_path=None): }, { "name": "Launch Python with PyO3", - "type": "lldb", + "type": "lldb", "request": "launch", "program": python_path, "args": ["${file}"], @@ -283,13 +283,13 @@ def update_launch_json(vscode_config_file_path=None): # 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", []): @@ -297,15 +297,15 @@ def update_launch_json(vscode_config_file_path=None): 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}") @@ -315,7 +315,7 @@ def update_launch_json(vscode_config_file_path=None): 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. """ @@ -358,7 +358,7 @@ will need to compile the Rust standard library: ```bash rustup install nightly -rustup override set nighty +rustup override set nightly rustup component add rust-src ``` @@ -392,4 +392,4 @@ 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 +code. From 8fa2d60969858f719bfa3c7666f3e6bdfc128ca6 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers <7943856+bschoenmaeckers@users.noreply.github.com> Date: Sun, 22 Jun 2025 13:27:44 +0200 Subject: [PATCH 720/936] fix precision loss when converting `bigdecimal` into python (#5198) * fix precision loss when converting `bigdecimal` into python * Use tuple constructor * use version `0.4.7` --- Cargo.toml | 4 ++- newsfragments/5198.fixed.md | 1 + src/conversions/bigdecimal.rs | 46 ++++++++++++++++++++++++++++++++--- 3 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 newsfragments/5198.fixed.md diff --git a/Cargo.toml b/Cargo.toml index f7c8cfcd391..782c72c6cdc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,7 +41,7 @@ inventory = { version = "0.3.5", optional = true } # crate integrations that can be added using the eponymous features anyhow = { version = "1.0.1", optional = true } -bigdecimal = { version = "0.4", optional = true } +bigdecimal = { version = "0.4.7", optional = true } chrono = { version = "0.4.25", default-features = false, optional = true } chrono-tz = { version = ">= 0.10, < 0.11", default-features = false, optional = true } either = { version = "1.9", optional = true } @@ -131,6 +131,8 @@ py-clone = [] parking_lot = ["dep:parking_lot", "lock_api"] arc_lock = ["lock_api", "lock_api/arc_lock", "parking_lot?/arc_lock"] +bigdecimal = ["dep:bigdecimal", "num-bigint"] + chrono-local = ["chrono/clock", "dep:iana-time-zone"] # Optimizes PyObject to Vec conversion and so on. diff --git a/newsfragments/5198.fixed.md b/newsfragments/5198.fixed.md new file mode 100644 index 00000000000..fb07972894b --- /dev/null +++ b/newsfragments/5198.fixed.md @@ -0,0 +1 @@ +fix precision loss when converting `bigdecimal` into python diff --git a/src/conversions/bigdecimal.rs b/src/conversions/bigdecimal.rs index 129def772c8..576e7c61bf2 100644 --- a/src/conversions/bigdecimal.rs +++ b/src/conversions/bigdecimal.rs @@ -51,6 +51,7 @@ use std::str::FromStr; +use crate::types::PyTuple; use crate::{ exceptions::PyValueError, sync::GILOnceCell, @@ -58,13 +59,18 @@ use crate::{ Bound, FromPyObject, IntoPyObject, Py, PyAny, PyErr, PyResult, Python, }; use bigdecimal::BigDecimal; - -static DECIMAL_CLS: GILOnceCell> = GILOnceCell::new(); +use num_bigint::Sign; fn get_decimal_cls(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> { + static DECIMAL_CLS: GILOnceCell> = GILOnceCell::new(); DECIMAL_CLS.import(py, "decimal", "Decimal") } +fn get_invalid_operation_error_cls(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> { + static INVALID_OPERATION_CLS: GILOnceCell> = GILOnceCell::new(); + INVALID_OPERATION_CLS.import(py, "decimal", "InvalidOperation") +} + impl FromPyObject<'_> for BigDecimal { fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { let py_str = &obj.str()?; @@ -82,7 +88,19 @@ impl<'py> IntoPyObject<'py> for BigDecimal { fn into_pyobject(self, py: Python<'py>) -> Result { let cls = get_decimal_cls(py)?; - cls.call1((self.to_string(),)) + let (bigint, scale) = self.into_bigint_and_scale(); + if scale == 0 { + return cls.call1((bigint,)); + } + let exponent = scale.checked_neg().ok_or_else(|| { + get_invalid_operation_error_cls(py) + .map_or_else(|err| err, |cls| PyErr::from_type(cls.clone(), ())) + })?; + let (sign, digits) = bigint.to_radix_be(10); + let signed = matches!(sign, Sign::Minus).into_pyobject(py)?; + let digits = PyTuple::new(py, digits)?; + + cls.call1(((signed, digits, exponent),)) } } @@ -210,4 +228,26 @@ mod test_bigdecimal { assert!(roundtripped.is_err()); }) } + + #[test] + fn test_no_precision_loss() { + Python::with_gil(|py| { + let src = "1e4"; + let expected = get_decimal_cls(py) + .unwrap() + .call1((src,)) + .unwrap() + .call_method0("as_tuple") + .unwrap(); + let actual = src + .parse::() + .unwrap() + .into_pyobject(py) + .unwrap() + .call_method0("as_tuple") + .unwrap(); + + assert!(actual.eq(expected).unwrap()); + }); + } } From e486930301385b98c8a3c5103ff8d3898db3ebb9 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Mon, 23 Jun 2025 21:46:39 +0200 Subject: [PATCH 721/936] use `PyCallArgs` for `Py::call` and friends (#5206) --- newsfragments/5206.changed.md | 1 + src/instance.rs | 41 ++++++++++------------------------- 2 files changed, 12 insertions(+), 30 deletions(-) create mode 100644 newsfragments/5206.changed.md diff --git a/newsfragments/5206.changed.md b/newsfragments/5206.changed.md new file mode 100644 index 00000000000..dba13327867 --- /dev/null +++ b/newsfragments/5206.changed.md @@ -0,0 +1 @@ +use `PyCallArgs` for `Py::call` and friends so they're equivalent to their `Bound` counterpart \ No newline at end of file diff --git a/src/instance.rs b/src/instance.rs index 8dc6a2e73c1..71638903b77 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -1,3 +1,4 @@ +use crate::call::PyCallArgs; use crate::conversion::IntoPyObject; use crate::err::{self, PyErr, PyResult}; use crate::impl_::pycell::PyClassObject; @@ -5,7 +6,7 @@ use crate::internal_tricks::ptr_from_ref; 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}; +use crate::types::{DerefToPyAny, PyDict, PyString}; use crate::{ ffi, DowncastError, FromPyObject, PyAny, PyClass, PyClassInitializer, PyRef, PyRefMut, PyTypeInfo, Python, @@ -1490,30 +1491,19 @@ impl Py { kwargs: Option<&Bound<'py, PyDict>>, ) -> PyResult where - A: IntoPyObject<'py, Target = PyTuple>, + A: PyCallArgs<'py>, { - 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) + 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<'py, N>(&self, py: Python<'py>, args: N) -> PyResult + pub fn call1<'py, A>(&self, py: Python<'py>, args: A) -> PyResult where - N: IntoPyObject<'py, Target = PyTuple>, + A: PyCallArgs<'py>, { - 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) + self.bind(py).as_any().call1(args).map(Bound::unbind) } /// Calls the object without arguments. @@ -1538,16 +1528,11 @@ impl Py { ) -> PyResult where N: IntoPyObject<'py, Target = PyString>, - A: IntoPyObject<'py, Target = PyTuple>, + A: PyCallArgs<'py>, { self.bind(py) .as_any() - .call_method( - name, - // FIXME(icxolu): remove explicit args conversion - args.into_pyobject(py).map_err(Into::into)?.into_bound(), - kwargs, - ) + .call_method(name, args, kwargs) .map(Bound::unbind) } @@ -1560,15 +1545,11 @@ 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: PyCallArgs<'py>, { self.bind(py) .as_any() - .call_method1( - name, - // FIXME(icxolu): remove explicit args conversion - args.into_pyobject(py).map_err(Into::into)?.into_bound(), - ) + .call_method1(name, args) .map(Bound::unbind) } From e71873b3bbb260c3094b24006325435bbf982190 Mon Sep 17 00:00:00 2001 From: Andrew Burkett Date: Tue, 24 Jun 2025 10:14:58 -0700 Subject: [PATCH 722/936] Don't cross compile for win7 target (#5210) Don't treat `-pc-windows-msvc` to `-win7-windows-msvc` as a cross compilation. --- newsfragments/5210.changed.md | 1 + pyo3-build-config/src/impl_.rs | 13 +++++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 newsfragments/5210.changed.md diff --git a/newsfragments/5210.changed.md b/newsfragments/5210.changed.md new file mode 100644 index 00000000000..166065fb197 --- /dev/null +++ b/newsfragments/5210.changed.md @@ -0,0 +1 @@ +Don't necessarily treat win7 target as a cross-compilation diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index fc3cae925e3..14ab9f7f7b7 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::{Architecture, Environment, OperatingSystem}; +use target_lexicon::{Architecture, Environment, OperatingSystem, Vendor}; use crate::{ bail, ensure, @@ -956,7 +956,9 @@ impl CrossCompileConfig { // e.g. x86_64-unknown-linux-musl on x86_64-unknown-linux-gnu host // x86_64-pc-windows-gnu on x86_64-pc-windows-msvc host let mut compatible = host.architecture == target.architecture - && host.vendor == target.vendor + && (host.vendor == target.vendor + // Don't treat `-pc-` to `-win7-` as cross-compiling + || (host.vendor == Vendor::Pc && target.vendor.as_str() == "win7")) && host.operating_system == target.operating_system; // Not cross-compiling to compile for 32-bit Python from windows 64-bit @@ -2995,6 +2997,13 @@ mod tests { ) .unwrap() .is_none()); + + assert!(cross_compiling_from_to( + &triple!("x86_64-pc-windows-msvc"), + &triple!("x86_64-win7-windows-msvc"), + ) + .unwrap() + .is_none()); } #[test] From 9a24cfcf97fa5d2e81e7665502b5f6072eabe0cf Mon Sep 17 00:00:00 2001 From: Richard Harris Date: Thu, 26 Jun 2025 10:23:21 +0200 Subject: [PATCH 723/936] docs: remove doc refs to PyCell (#5213) --- Architecture.md | 14 +++++++------- guide/src/class/object.md | 2 +- src/pyclass_init.rs | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/Architecture.md b/Architecture.md index be9d2b53d82..db7c8b4bb17 100644 --- a/Architecture.md +++ b/Architecture.md @@ -85,23 +85,23 @@ To realize object-oriented programming in C, all Python objects have `ob_base: P first field in their structure definition. Thanks to this guarantee, casting `*mut A` to `*mut PyObject` is valid if `A` is a Python object. -To ensure this guarantee, we have a wrapper struct `PyCell` in [`src/pycell.rs`] which is roughly: +To ensure this guarantee, we have a wrapper struct `PyClassObject` in [`src/pycell/impl_.rs`] which is roughly: ```rust #[repr(C)] -pub struct PyCell { +pub struct PyClassObject { ob_base: crate::ffi::PyObject, inner: T, } ``` -Thus, when copying a Rust struct to a Python object, we first allocate `PyCell` on the Python heap and then +Thus, when copying a Rust struct to a Python object, we first allocate `PyClassObject` on the Python heap and then move `T` into it. -Also, `PyCell` provides [RefCell](https://doc.rust-lang.org/std/cell/struct.RefCell.html)-like methods -to ensure Rust's borrow rules. -See [the documentation](https://docs.rs/pyo3/latest/pyo3/pycell/struct.PyCell.html) for more. -`PyCell` requires that `T` implements `PyClass`. +The primary way to interact with Python objects implemented in Rust is through the `Bound<'py, T>` smart pointer. +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. + +`Bound<'py, T>` requires that `T` implements `PyClass`. This trait is somewhat complex and derives many traits, but the most important one is `PyTypeInfo` in [`src/type_object.rs`]. `PyTypeInfo` is also implemented for built-in types. diff --git a/guide/src/class/object.md b/guide/src/class/object.md index a3a136fe015..6d688c3ea99 100644 --- a/guide/src/class/object.md +++ b/guide/src/class/object.md @@ -133,7 +133,7 @@ impl Number { fn __repr__(slf: &Bound<'_, Self>) -> PyResult { // This is the equivalent of `self.__class__.__name__` in Python. let class_name: Bound<'_, PyString> = slf.get_type().qualname()?; - // To access fields of the Rust struct, we need to borrow the `PyCell`. + // To access fields of the Rust struct, we need to borrow from the Bound object. Ok(format!("{}({})", class_name, slf.borrow().0)) } } diff --git a/src/pyclass_init.rs b/src/pyclass_init.rs index 3a3253fffbc..a4094487bc9 100644 --- a/src/pyclass_init.rs +++ b/src/pyclass_init.rs @@ -145,7 +145,7 @@ impl PyClassInitializer { PyClassInitializer::new(subclass_value, self) } - /// Creates a new PyCell and initializes it. + /// Creates a new class object and initializes it. pub(crate) fn create_class_object(self, py: Python<'_>) -> PyResult> where T: PyClass, From 9f1f1d00d7de83db3f78c3707647114593f5c1bb Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Thu, 26 Jun 2025 17:30:15 +0100 Subject: [PATCH 724/936] ci: skip fragment checking in links for now (#5214) --- noxfile.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/noxfile.py b/noxfile.py index 3b590dfa886..98d19f085b6 100644 --- a/noxfile.py +++ b/noxfile.py @@ -509,22 +509,15 @@ def check_guide(session: nox.Session): for key, value in remaps.items(): remap_args.extend(("--remap", f"{key} {value}")) - # some http URL fragments fail to be loaded (needs javascript?) - lychee_exclusions = [ - "https://github.com/PyO3/pyo3/issues/1800#issuecomment-906786649", - "https://docs.rs/parking_lot/latest/parking_lot/type.Mutex.html#method.lock_arc", - "https://github.com/PyO3/pyo3/issues/1517#issuecomment-808664021", - "https://github.com/PyO3/maturin/blob/0dee40510083c03607834c821eea76964140a126/Readme.md#mixed-rustpython-projects", - ] - # check all links in the guide _run( session, "lychee", - "--include-fragments", + # FIXME: would be nice to use `--include-fragments` here, but we've had + # a lot of flaky failures from it - see https://github.com/lycheeverse/lychee/issues/1746 + # "--include-fragments", str(PYO3_GUIDE_SRC), *remap_args, - "--exclude=" + "|".join(lychee_exclusions), "--accept=200,429", *session.posargs, ) From cb2295089c56de6e9c5f3e7c6731b60a0d7a43f0 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sat, 28 Jun 2025 07:26:08 +0200 Subject: [PATCH 725/936] ci: updates for Rust 1.88 (#5215) --- tests/ui/invalid_frozen_pyclass_borrow.stderr | 49 ++++++++++++++++--- tests/ui/invalid_pymethod_enum.stderr | 34 ++++++++++--- tests/ui/static_ref.stderr | 20 ++------ 3 files changed, 74 insertions(+), 29 deletions(-) diff --git a/tests/ui/invalid_frozen_pyclass_borrow.stderr b/tests/ui/invalid_frozen_pyclass_borrow.stderr index 52a0623f282..9e86e8f3b3e 100644 --- a/tests/ui/invalid_frozen_pyclass_borrow.stderr +++ b/tests/ui/invalid_frozen_pyclass_borrow.stderr @@ -8,33 +8,49 @@ error[E0271]: type mismatch resolving `::Frozen == False` --> tests/ui/invalid_frozen_pyclass_borrow.rs:11:19 | 11 | fn mut_method(&mut self) {} - | ^ expected `False`, found `True` + | ^ type mismatch resolving `::Frozen == False` | +note: expected this to be `False` + --> tests/ui/invalid_frozen_pyclass_borrow.rs:3:1 + | +3 | #[pyclass(frozen)] + | ^^^^^^^^^^^^^^^^^^ 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` + = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0271]: type mismatch resolving `::Frozen == False` --> tests/ui/invalid_frozen_pyclass_borrow.rs:9:1 | 9 | #[pymethods] - | ^^^^^^^^^^^^ expected `False`, found `True` + | ^^^^^^^^^^^^ type mismatch resolving `::Frozen == False` + | +note: expected this to be `False` + --> tests/ui/invalid_frozen_pyclass_borrow.rs:3:1 | +3 | #[pyclass(frozen)] + | ^^^^^^^^^^^^^^^^^^ 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) + = note: this error originates in the attribute macro `pymethods` which comes from the expansion of the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0271]: type mismatch resolving `::Frozen == False` --> tests/ui/invalid_frozen_pyclass_borrow.rs:15:31 | 15 | let borrow = foo.bind(py).borrow_mut(); - | ^^^^^^^^^^ expected `False`, found `True` + | ^^^^^^^^^^ type mismatch resolving `::Frozen == False` + | +note: expected this to be `False` + --> tests/ui/invalid_frozen_pyclass_borrow.rs:3:1 | +3 | #[pyclass(frozen)] + | ^^^^^^^^^^^^^^^^^^ note: required by a bound in `pyo3::Bound::<'py, T>::borrow_mut` --> src/instance.rs | @@ -43,13 +59,19 @@ note: required by a bound in `pyo3::Bound::<'py, T>::borrow_mut` | where | T: PyClass, | ^^^^^^^^^^^^^^ required by this bound in `Bound::<'py, T>::borrow_mut` + = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0271]: type mismatch resolving `::Frozen == False` --> tests/ui/invalid_frozen_pyclass_borrow.rs:25:33 | 25 | let borrow = child.bind(py).borrow_mut(); - | ^^^^^^^^^^ expected `False`, found `True` + | ^^^^^^^^^^ type mismatch resolving `::Frozen == False` | +note: expected this to be `False` + --> tests/ui/invalid_frozen_pyclass_borrow.rs:21:1 + | +21 | #[pyclass(frozen, extends = MutableBase)] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ note: required by a bound in `pyo3::Bound::<'py, T>::borrow_mut` --> src/instance.rs | @@ -58,13 +80,19 @@ note: required by a bound in `pyo3::Bound::<'py, T>::borrow_mut` | where | T: PyClass, | ^^^^^^^^^^^^^^ required by this bound in `Bound::<'py, T>::borrow_mut` + = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0271]: type mismatch resolving `::Frozen == True` --> tests/ui/invalid_frozen_pyclass_borrow.rs:29:11 | 29 | class.get(); - | ^^^ expected `True`, found `False` + | ^^^ type mismatch resolving `::Frozen == True` + | +note: expected this to be `True` + --> tests/ui/invalid_frozen_pyclass_borrow.rs:18:1 | +18 | #[pyclass(subclass)] + | ^^^^^^^^^^^^^^^^^^^^ note: required by a bound in `pyo3::Py::::get` --> src/instance.rs | @@ -73,13 +101,19 @@ note: required by a bound in `pyo3::Py::::get` | where | T: PyClass + Sync, | ^^^^^^^^^^^^^ required by this bound in `Py::::get` + = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0271]: type mismatch resolving `::Frozen == True` --> tests/ui/invalid_frozen_pyclass_borrow.rs:33:11 | 33 | class.get(); - | ^^^ expected `True`, found `False` + | ^^^ type mismatch resolving `::Frozen == True` + | +note: expected this to be `True` + --> tests/ui/invalid_frozen_pyclass_borrow.rs:18:1 | +18 | #[pyclass(subclass)] + | ^^^^^^^^^^^^^^^^^^^^ note: required by a bound in `pyo3::Bound::<'py, T>::get` --> src/instance.rs | @@ -88,3 +122,4 @@ note: required by a bound in `pyo3::Bound::<'py, T>::get` | where | T: PyClass + Sync, | ^^^^^^^^^^^^^ required by this bound in `Bound::<'py, T>::get` + = 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_pymethod_enum.stderr b/tests/ui/invalid_pymethod_enum.stderr index bc377d2a055..1bc513abc6a 100644 --- a/tests/ui/invalid_pymethod_enum.stderr +++ b/tests/ui/invalid_pymethod_enum.stderr @@ -2,48 +2,70 @@ error[E0271]: type mismatch resolving `::Frozen == False --> tests/ui/invalid_pymethod_enum.rs:11:24 | 11 | fn mutate_in_place(&mut self) { - | ^ expected `False`, found `True` + | ^ type mismatch resolving `::Frozen == False` | +note: expected this to be `False` + --> tests/ui/invalid_pymethod_enum.rs:3:1 + | +3 | #[pyclass] + | ^^^^^^^^^^ 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` + = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0271]: type mismatch resolving `::Frozen == False` --> tests/ui/invalid_pymethod_enum.rs:9:1 | 9 | #[pymethods] - | ^^^^^^^^^^^^ expected `False`, found `True` + | ^^^^^^^^^^^^ type mismatch resolving `::Frozen == False` + | +note: expected this to be `False` + --> tests/ui/invalid_pymethod_enum.rs:3:1 | +3 | #[pyclass] + | ^^^^^^^^^^ 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) + = note: this error originates in the attribute macro `pymethods` which comes from the expansion of the attribute macro `pyclass` (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` + | ^ type mismatch resolving `::Frozen == False` | +note: expected this to be `False` + --> tests/ui/invalid_pymethod_enum.rs:19:1 + | +19 | #[pyclass] + | ^^^^^^^^^^ 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` + = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0271]: type mismatch resolving `::Frozen == False` --> tests/ui/invalid_pymethod_enum.rs:25:1 | 25 | #[pymethods] - | ^^^^^^^^^^^^ expected `False`, found `True` + | ^^^^^^^^^^^^ type mismatch resolving `::Frozen == False` + | +note: expected this to be `False` + --> tests/ui/invalid_pymethod_enum.rs:19:1 | +19 | #[pyclass] + | ^^^^^^^^^^ 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) + = note: this error originates in the attribute macro `pymethods` which comes from the expansion of the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/static_ref.stderr b/tests/ui/static_ref.stderr index fdcc61bd20a..d32b7508c4b 100644 --- a/tests/ui/static_ref.stderr +++ b/tests/ui/static_ref.stderr @@ -11,30 +11,18 @@ error[E0521]: borrowed data escapes outside of function | = note: this error originates in the attribute macro `pyfunction` (in Nightly builds, run with -Z macro-backtrace for more info) -error[E0597]: `output[_]` does not live long enough +error[E0716]: temporary value dropped while borrowed --> tests/ui/static_ref.rs:4:1 | 4 | #[pyfunction] | ^^^^^^^^^^^^- | | | - | | `output[_]` dropped here while still borrowed - | borrowed value does not live long enough - | argument requires that `output[_]` is borrowed for `'static` + | | temporary value is freed at the end of this statement + | creates a temporary value which is freed while still in use + | argument requires that borrow lasts for `'static` | = note: this error originates in the attribute macro `pyfunction` (in Nightly builds, run with -Z macro-backtrace for more info) -error[E0597]: `holder_0` does not live long enough - --> tests/ui/static_ref.rs:5:15 - | -4 | #[pyfunction] - | ------------- - | | | - | | `holder_0` dropped here while still borrowed - | binding `holder_0` declared here - | argument requires that `holder_0` is borrowed for `'static` -5 | fn static_ref(list: &'static Bound<'_, PyList>) -> usize { - | ^^^^^^^ borrowed value does not live long enough - error[E0521]: borrowed data escapes outside of function --> tests/ui/static_ref.rs:9:1 | From c7ef9636746b0150032bb115be14d0baeb18ea4c Mon Sep 17 00:00:00 2001 From: Samuel Lijin Date: Sat, 28 Jun 2025 14:40:25 -0700 Subject: [PATCH 726/936] fix(pymodule): improve safety of PyModule::from_code (#4777) * fix(pymodule): improve safety of PyModule::from_code If `PyImport_ExecCodeModuleEx` is called with an empty filename or module name, references to any Python variables defined in this context may break assumptions in standard library code. Notably, if `inspect.stack()` is called while any stack frame holds a reference to a variable declared in this Python snippet, and `file_name` is empty, then `inspect.stack()` will throw while trying to resolve the file in which said variable was defined. The `exec` builtin handles this by defaulting `file_name` to `` and `module_name` to `` - these are not the most obvious defaults, but in the spirit of consistency and providing pyo3 users with a safe API, it makes sense for `PyModule::from_code` to do the same. * add test + update examples --------- Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> --- guide/src/conversions/traits.md | 8 ++++---- .../python-from-rust/calling-existing-code.md | 6 +++--- guide/src/python-from-rust/function-calls.md | 4 ++-- newsfragments/4777.changed.md | 1 + src/types/any.rs | 10 +++++----- src/types/module.rs | 19 +++++++++++++++++++ 6 files changed, 34 insertions(+), 14 deletions(-) create mode 100644 newsfragments/4777.changed.md diff --git a/guide/src/conversions/traits.md b/guide/src/conversions/traits.md index 281116a9747..b8d7190e3dd 100644 --- a/guide/src/conversions/traits.md +++ b/guide/src/conversions/traits.md @@ -60,7 +60,7 @@ struct RustyStruct { # c_str!("class Foo: # def __init__(self): # self.my_string = 'test'"), -# c_str!(""), +# c_str!(""), # c_str!(""), # )?; # @@ -119,7 +119,7 @@ struct RustyStruct { # def __init__(self): # self.name = 'test' # self['key'] = 'test2'"), -# c_str!(""), +# c_str!(""), # c_str!(""), # )?; # @@ -349,7 +349,7 @@ enum RustyEnum<'py> { # self.x = 0 # self.y = 1 # self.z = 2"), -# c_str!(""), +# c_str!(""), # c_str!(""), # )?; # @@ -373,7 +373,7 @@ enum RustyEnum<'py> { # def __init__(self): # self.x = 3 # self.y = 4"), -# c_str!(""), +# c_str!(""), # c_str!(""), # )?; # diff --git a/guide/src/python-from-rust/calling-existing-code.md b/guide/src/python-from-rust/calling-existing-code.md index 4eba33f11c3..a33c24d9e22 100644 --- a/guide/src/python-from-rust/calling-existing-code.md +++ b/guide/src/python-from-rust/calling-existing-code.md @@ -265,8 +265,8 @@ fn main() -> PyResult<()> { ))); 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(py, py_foo, c_str!("utils.foo"), c_str!("utils.foo"))?; - let app: Py = PyModule::from_code(py, py_app, c_str!(""), c_str!(""))? + PyModule::from_code(py, py_foo, c_str!("foo.py"), c_str!("utils.foo"))?; + let app: Py = PyModule::from_code(py, py_app, c_str!("app.py"), c_str!(""))? .getattr("run")? .into(); app.call0(py) @@ -305,7 +305,7 @@ fn main() -> PyResult<()> { .getattr("path")? .downcast_into::()?; syspath.insert(0, path)?; - let app: Py = PyModule::from_code(py, py_app.as_c_str(), c_str!(""), c_str!(""))? + let app: Py = PyModule::from_code(py, py_app.as_c_str(), c_str!("app.py"), c_str!(""))? .getattr("run")? .into(); app.call0(py) diff --git a/guide/src/python-from-rust/function-calls.md b/guide/src/python-from-rust/function-calls.md index b64c062eec7..a8f8e13e872 100644 --- a/guide/src/python-from-rust/function-calls.md +++ b/guide/src/python-from-rust/function-calls.md @@ -36,7 +36,7 @@ fn main() -> PyResult<()> { print('called with kwargs', kwargs) if args == () and kwargs == {}: print('called with no arguments')"), - c_str!(""), + c_str!("example.py"), c_str!(""), )? .getattr("example")? @@ -83,7 +83,7 @@ fn main() -> PyResult<()> { print('called with kwargs', kwargs) if args == () and kwargs == {}: print('called with no arguments')"), - c_str!(""), + c_str!("example.py"), c_str!(""), )? .getattr("example")? diff --git a/newsfragments/4777.changed.md b/newsfragments/4777.changed.md new file mode 100644 index 00000000000..b4e2163a3b8 --- /dev/null +++ b/newsfragments/4777.changed.md @@ -0,0 +1 @@ +`PyModule::from_code` now defaults `file_name` to `` if empty. Ref #4769 \ No newline at end of file diff --git a/src/types/any.rs b/src/types/any.rs index 4dd9c594c6c..c0e91341594 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -457,7 +457,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { - /// let module = PyModule::from_code(py, CODE, c_str!(""), c_str!(""))?; + /// let module = PyModule::from_code(py, CODE, c_str!("func.py"), c_str!(""))?; /// let fun = module.getattr("function")?; /// let args = ("hello",); /// let kwargs = PyDict::new(py); @@ -514,7 +514,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { - /// let module = PyModule::from_code(py, CODE, c_str!(""), c_str!(""))?; + /// let module = PyModule::from_code(py, CODE, c_str!("func.py"), c_str!(""))?; /// let fun = module.getattr("function")?; /// let args = ("hello",); /// let result = fun.call1(args)?; @@ -553,7 +553,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { - /// let module = PyModule::from_code(py, CODE, c_str!(""), c_str!(""))?; + /// let module = PyModule::from_code(py, CODE, c_str!("a.py"), c_str!(""))?; /// let instance = module.getattr("a")?; /// let args = ("hello",); /// let kwargs = PyDict::new(py); @@ -599,7 +599,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { - /// let module = PyModule::from_code(py, CODE, c_str!(""), c_str!(""))?; + /// let module = PyModule::from_code(py, CODE, c_str!("a.py"), c_str!(""))?; /// let instance = module.getattr("a")?; /// let result = instance.call_method0("method")?; /// assert_eq!(result.extract::()?, "called with no arguments"); @@ -636,7 +636,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// /// # fn main() -> PyResult<()> { /// Python::with_gil(|py| { - /// let module = PyModule::from_code(py, CODE, c_str!(""), c_str!(""))?; + /// let module = PyModule::from_code(py, CODE, c_str!("a.py"), c_str!(""))?; /// let instance = module.getattr("a")?; /// let args = ("hello",); /// let result = instance.call_method1("method", args)?; diff --git a/src/types/module.rs b/src/types/module.rs index cae3554da5b..ef081d12c3e 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -1,3 +1,5 @@ +use pyo3_ffi::c_str; + use crate::err::{PyErr, PyResult}; use crate::ffi_ptr_ext::FfiPtrExt; use crate::impl_::callback::IntoPyCallbackOutput; @@ -95,6 +97,8 @@ impl PyModule { /// containing the Python code passed to `code` /// and pretending to live at `file_name`. /// + /// If `file_name` is empty, it will be set to ``. + /// ///
///
⚠ ️
///
@@ -154,6 +158,11 @@ impl PyModule {
         file_name: &CStr,
         module_name: &CStr,
     ) -> PyResult> {
+        let file_name = if file_name.is_empty() {
+            c_str!("")
+        } else {
+            file_name
+        };
         unsafe {
             let code = ffi::Py_CompileString(code.as_ptr(), file_name.as_ptr(), ffi::Py_file_input)
                 .assume_owned_or_err(py)?;
@@ -539,6 +548,8 @@ fn __name__(py: Python<'_>) -> &Bound<'_, PyString> {
 
 #[cfg(test)]
 mod tests {
+    use pyo3_ffi::c_str;
+
     use crate::{
         types::{module::PyModuleMethods, PyModule},
         Python,
@@ -565,4 +576,12 @@ mod tests {
                 .ends_with("site.py"));
         })
     }
+
+    #[test]
+    fn module_from_code_empty_file() {
+        Python::with_gil(|py| {
+            let builtins = PyModule::from_code(py, c_str!(""), c_str!(""), c_str!("")).unwrap();
+            assert_eq!(builtins.filename().unwrap(), "");
+        })
+    }
 }

From c818f4c90b9b9b9a63d8e5c8fa3d962573b104ca Mon Sep 17 00:00:00 2001
From: Shiyan Xu <2701446+xushiyan@users.noreply.github.com>
Date: Mon, 30 Jun 2025 11:26:56 -0500
Subject: [PATCH 727/936] docs: add `hudi-rs` to the readme example list
 (#5219)

---
 README.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/README.md b/README.md
index 31ed8b7e47a..266e61bdde4 100644
--- a/README.md
+++ b/README.md
@@ -209,6 +209,7 @@ about this topic.
 - [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._
+- [hudi-rs](https://github.com/apache/hudi-rs) _The native Rust implementation for Apache Hudi, with C++ & Python API bindings._
 - [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._

From 2af040bf85c721cbb9bbae9708e480a4be10efdc Mon Sep 17 00:00:00 2001
From: Icxolu <10486322+Icxolu@users.noreply.github.com>
Date: Tue, 1 Jul 2025 21:28:02 +0200
Subject: [PATCH 728/936] rename `Python::with_gil` to `Python::attach` (#5209)

---
 Cargo.toml                                    |   2 +-
 README.md                                     |   2 +-
 examples/plugin/src/main.rs                   |   2 +-
 guide/src/async-await.md                      |   4 +-
 guide/src/building-and-distribution.md        |   2 +-
 .../multiple-python-versions.md               |   2 +-
 guide/src/class.md                            |  40 +++----
 guide/src/class/numeric.md                    |   2 +-
 guide/src/class/object.md                     |   2 +-
 guide/src/class/protocols.md                  |   4 +-
 guide/src/conversions/traits.md               |  28 ++---
 guide/src/ecosystem/logging.md                |   2 +-
 guide/src/exception.md                        |  10 +-
 guide/src/features.md                         |   2 +-
 guide/src/free-threading.md                   |  12 +-
 guide/src/function.md                         |   6 +-
 guide/src/function/error-handling.md          |  10 +-
 guide/src/function/signature.md               |   6 +-
 guide/src/migration.md                        |  12 +-
 guide/src/module.md                           |   2 +-
 guide/src/parallelism.md                      |   4 +-
 guide/src/performance.md                      |  20 ++--
 .../python-from-rust/calling-existing-code.md |  20 ++--
 guide/src/python-from-rust/function-calls.md  |   4 +-
 guide/src/trait-bounds.md                     |  30 ++---
 guide/src/types.md                            |  18 +--
 newsfragments/5209.changed.md                 |   1 +
 pyo3-benches/benches/bench_any.rs             |   4 +-
 pyo3-benches/benches/bench_bigint.rs          |  12 +-
 pyo3-benches/benches/bench_call.rs            |  16 +--
 pyo3-benches/benches/bench_comparisons.rs     |   4 +-
 pyo3-benches/benches/bench_decimal.rs         |   2 +-
 pyo3-benches/benches/bench_dict.rs            |  14 +--
 pyo3-benches/benches/bench_err.rs             |   2 +-
 pyo3-benches/benches/bench_extract.rs         |  24 ++--
 pyo3-benches/benches/bench_frompyobject.rs    |  12 +-
 pyo3-benches/benches/bench_gil.rs             |   6 +-
 pyo3-benches/benches/bench_intern.rs          |   4 +-
 pyo3-benches/benches/bench_intopyobject.rs    |   6 +-
 pyo3-benches/benches/bench_list.rs            |  14 +--
 pyo3-benches/benches/bench_pyclass.rs         |   2 +-
 pyo3-benches/benches/bench_pyobject.rs        |  12 +-
 pyo3-benches/benches/bench_set.rs             |  10 +-
 pyo3-benches/benches/bench_tuple.rs           |  24 ++--
 pyo3-macros-backend/src/pymethod.rs           |   4 +-
 pytests/src/misc.rs                           |   4 +-
 src/buffer.rs                                 |   8 +-
 src/call.rs                                   |   4 +-
 src/conversion.rs                             |   4 +-
 src/conversions/anyhow.rs                     |  12 +-
 src/conversions/bigdecimal.rs                 |  12 +-
 src/conversions/chrono.rs                     |  62 +++++------
 src/conversions/chrono_tz.rs                  |   8 +-
 src/conversions/either.rs                     |   4 +-
 src/conversions/eyre.rs                       |  12 +-
 src/conversions/hashbrown.rs                  |   8 +-
 src/conversions/indexmap.rs                   |   6 +-
 src/conversions/jiff.rs                       |  58 +++++-----
 src/conversions/num_bigint.rs                 |  10 +-
 src/conversions/num_complex.rs                |  16 +--
 src/conversions/num_rational.rs               |  20 ++--
 src/conversions/ordered_float.rs              |  14 +--
 src/conversions/rust_decimal.rs               |  12 +-
 src/conversions/serde.rs                      |   6 +-
 src/conversions/smallvec.rs                   |   8 +-
 src/conversions/std/array.rs                  |  16 +--
 src/conversions/std/ipaddr.rs                 |   4 +-
 src/conversions/std/map.rs                    |   8 +-
 src/conversions/std/num.rs                    |  64 +++++------
 src/conversions/std/osstr.rs                  |   4 +-
 src/conversions/std/path.rs                   |   6 +-
 src/conversions/std/set.rs                    |   6 +-
 src/conversions/std/slice.rs                  |   8 +-
 src/conversions/std/string.rs                 |  12 +-
 src/conversions/std/time.rs                   |  16 +--
 src/conversions/std/vec.rs                    |   4 +-
 src/conversions/time.rs                       |  56 +++++-----
 src/conversions/uuid.rs                       |   2 +-
 src/coroutine/waker.rs                        |   2 +-
 src/err/err_state.rs                          |  10 +-
 src/err/impls.rs                              |   4 +-
 src/err/mod.rs                                |  44 ++++----
 src/exceptions.rs                             |  30 ++---
 src/ffi/tests.rs                              |  20 ++--
 src/gil.rs                                    |  34 +++---
 src/impl_/coroutine.rs                        |   4 +-
 src/impl_/extract_argument.rs                 |   6 +-
 src/impl_/pymethods.rs                        |   2 +-
 src/impl_/pymodule.rs                         |   4 +-
 src/instance.rs                               | 104 +++++++++---------
 src/lib.rs                                    |   4 +-
 src/macros.rs                                 |   6 +-
 src/marker.rs                                 |  77 +++++++------
 src/marshal.rs                                |   4 +-
 src/pybacked.rs                               |  38 +++----
 src/pycell.rs                                 |  24 ++--
 src/pycell/impl_.rs                           |   8 +-
 src/pyclass_init.rs                           |   6 +-
 src/sync.rs                                   |  70 ++++++------
 src/tests/common.rs                           |   2 +-
 src/tests/hygiene/pyfunction.rs               |   2 +-
 src/types/any.rs                              |  78 ++++++-------
 src/types/boolobject.rs                       |   6 +-
 src/types/bytearray.rs                        |  28 ++---
 src/types/bytes.rs                            |  18 +--
 src/types/capsule.rs                          |  30 ++---
 src/types/code.rs                             |   2 +-
 src/types/complex.rs                          |  18 +--
 src/types/datetime.rs                         |  10 +-
 src/types/dict.rs                             |  82 +++++++-------
 src/types/ellipsis.rs                         |   6 +-
 src/types/float.rs                            |   6 +-
 src/types/frozenset.rs                        |  16 +--
 src/types/function.rs                         |   2 +-
 src/types/genericalias.rs                     |   2 +-
 src/types/iterator.rs                         |  26 ++---
 src/types/list.rs                             | 104 +++++++++---------
 src/types/mapping.rs                          |  16 +--
 src/types/mappingproxy.rs                     |  46 ++++----
 src/types/mod.rs                              |   2 +-
 src/types/module.rs                           |  14 +--
 src/types/none.rs                             |   8 +-
 src/types/notimplemented.rs                   |   6 +-
 src/types/num.rs                              |   4 +-
 src/types/range.rs                            |   4 +-
 src/types/sequence.rs                         |  62 +++++------
 src/types/set.rs                              |  28 ++---
 src/types/slice.rs                            |   4 +-
 src/types/string.rs                           |  44 ++++----
 src/types/traceback.rs                        |   8 +-
 src/types/tuple.rs                            |  68 ++++++------
 src/types/typeobject.rs                       |  16 +--
 src/types/weakref/anyref.rs                   |  20 ++--
 src/types/weakref/proxy.rs                    |  38 +++----
 src/types/weakref/reference.rs                |  20 ++--
 src/version.rs                                |   4 +-
 tests/test_anyhow.rs                          |   4 +-
 tests/test_append_to_inittab.rs               |   4 +-
 tests/test_arithmetics.rs                     |  20 ++--
 tests/test_buffer.rs                          |   2 +-
 tests/test_buffer_protocol.rs                 |   8 +-
 tests/test_bytes.rs                           |  12 +-
 tests/test_class_attributes.rs                |  14 +--
 tests/test_class_basics.rs                    |  54 ++++-----
 tests/test_class_comparisons.rs               |  16 +--
 tests/test_class_conversion.rs                |  12 +-
 tests/test_class_formatting.rs                |  14 +--
 tests/test_class_new.rs                       |  22 ++--
 tests/test_coroutine.rs                       |  22 ++--
 tests/test_datetime.rs                        |  16 +--
 tests/test_datetime_import.rs                 |   2 +-
 tests/test_declarative_module.rs              |   8 +-
 tests/test_default_impls.rs                   |   4 +-
 tests/test_enum.rs                            |  40 +++----
 tests/test_exceptions.rs                      |   6 +-
 tests/test_field_cfg.rs                       |   4 +-
 tests/test_frompy_intopy_roundtrip.rs         |  14 +--
 tests/test_frompyobject.rs                    |  60 +++++-----
 tests/test_gc.rs                              |  40 +++----
 tests/test_getter_setter.rs                   |  20 ++--
 tests/test_inheritance.rs                     |  22 ++--
 tests/test_intopyobject.rs                    |  18 +--
 tests/test_macro_docs.rs                      |   2 +-
 tests/test_macros.rs                          |   2 +-
 tests/test_mapping.rs                         |   8 +-
 tests/test_methods.rs                         |  34 +++---
 tests/test_module.rs                          |  26 ++---
 tests/test_multiple_pymethods.rs              |   2 +-
 tests/test_proto_methods.rs                   |  50 ++++-----
 tests/test_pyerr_debug_unformattable.rs       |   2 +-
 tests/test_pyfunction.rs                      |  40 +++----
 tests/test_pyself.rs                          |   6 +-
 tests/test_sequence.rs                        |  30 ++---
 tests/test_serde.rs                           |   4 +-
 tests/test_static_slots.rs                    |   6 +-
 tests/test_string.rs                          |   2 +-
 tests/test_super.rs                           |   2 +-
 tests/test_text_signature.rs                  |  22 ++--
 tests/test_variable_arguments.rs              |   4 +-
 tests/test_various.rs                         |  16 +--
 tests/ui/invalid_closure.rs                   |   4 +-
 tests/ui/invalid_intern_arg.rs                |   2 +-
 tests/ui/invalid_intern_arg.stderr            |  18 +--
 tests/ui/invalid_pycallargs.rs                |   2 +-
 tests/ui/invalid_result_conversion.rs         |   2 +-
 tests/ui/not_send.rs                          |   2 +-
 tests/ui/not_send2.rs                         |   2 +-
 tests/ui/traverse.stderr                      |  10 +-
 tests/ui/wrong_aspyref_lifetimes.rs           |   6 +-
 tests/ui/wrong_aspyref_lifetimes.stderr       |  12 +-
 190 files changed, 1526 insertions(+), 1516 deletions(-)
 create mode 100644 newsfragments/5209.changed.md

diff --git a/Cargo.toml b/Cargo.toml
index 782c72c6cdc..9f253b27ded 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -122,7 +122,7 @@ abi3-py314 = ["abi3", "pyo3-build-config/abi3-py314", "pyo3-ffi/abi3-py314"]
 # Automatically generates `python3.dll` import libraries for Windows targets.
 generate-import-lib = ["pyo3-ffi/generate-import-lib"]
 
-# Changes `Python::with_gil` to automatically initialize the Python interpreter if needed.
+# Changes `Python::attach` to automatically initialize the Python interpreter if needed.
 auto-initialize = []
 
 # Enables `Clone`ing references to Python objects `Py` which panics if the GIL is not held.
diff --git a/README.md b/README.md
index 266e61bdde4..fdbc17ec432 100644
--- a/README.md
+++ b/README.md
@@ -152,7 +152,7 @@ use pyo3::types::IntoPyDict;
 use pyo3::ffi::c_str;
 
 fn main() -> PyResult<()> {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let sys = py.import("sys")?;
         let version: String = sys.getattr("version")?.extract()?;
 
diff --git a/examples/plugin/src/main.rs b/examples/plugin/src/main.rs
index 59442549e6d..beb04305f67 100644
--- a/examples/plugin/src/main.rs
+++ b/examples/plugin/src/main.rs
@@ -11,7 +11,7 @@ fn main() -> Result<(), Box> {
     //import path for python
     let path = Path::new("./python_plugin/");
     //do useful work
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         //add the current directory to import path of Python (do not use this in production!)
         let syspath: Bound = py.import("sys")?.getattr("path")?.extract()?;
         syspath.insert(0, &path)?;
diff --git a/guide/src/async-await.md b/guide/src/async-await.md
index 1e0772b8a09..59ea99b6ef8 100644
--- a/guide/src/async-await.md
+++ b/guide/src/async-await.md
@@ -39,7 +39,7 @@ However, there is an exception for method receivers, so async methods can accept
 
 Even if it is not possible to pass a `py: Python<'py>` parameter to `async fn`, the GIL is still held during the execution of the future – it's also the case for regular `fn` without `Python<'py>`/`Bound<'py, PyAny>` parameter, yet the GIL is held.
 
-It is still possible to get a `Python` marker using [`Python::with_gil`]({{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.with_gil); because `with_gil` is reentrant and optimized, the cost will be negligible.
+It is still possible to get a `Python` marker using [`Python::attach`]({{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.attach); because `attach` is reentrant and optimized, the cost will be negligible.
 
 ## Release the GIL across `.await`
 
@@ -66,7 +66,7 @@ where
 
     fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll {
         let waker = cx.waker();
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             py.allow_threads(|| pin!(&mut self.0).poll(&mut Context::from_waker(waker)))
         })
     }
diff --git a/guide/src/building-and-distribution.md b/guide/src/building-and-distribution.md
index b9ea56916ba..45a82b22cb7 100644
--- a/guide/src/building-and-distribution.md
+++ b/guide/src/building-and-distribution.md
@@ -278,7 +278,7 @@ If you encounter these or other complications when linking the interpreter stati
 
 ### Import your module when embedding the Python interpreter
 
-When you run your Rust binary with an embedded interpreter, any `#[pymodule]` created modules won't be accessible to import unless added to a table called `PyImport_Inittab` before the embedded interpreter is initialized. This will cause Python statements in your embedded interpreter such as `import your_new_module` to fail. You can call the macro [`append_to_inittab`]({{#PYO3_DOCS_URL}}/pyo3/macro.append_to_inittab.html) with your module before initializing the Python interpreter to add the module function into that table. (The Python interpreter will be initialized by calling `prepare_freethreaded_python`, `with_embedded_python_interpreter`, or `Python::with_gil` with the [`auto-initialize`](features.md#auto-initialize) feature enabled.)
+When you run your Rust binary with an embedded interpreter, any `#[pymodule]` created modules won't be accessible to import unless added to a table called `PyImport_Inittab` before the embedded interpreter is initialized. This will cause Python statements in your embedded interpreter such as `import your_new_module` to fail. You can call the macro [`append_to_inittab`]({{#PYO3_DOCS_URL}}/pyo3/macro.append_to_inittab.html) with your module before initializing the Python interpreter to add the module function into that table. (The Python interpreter will be initialized by calling `prepare_freethreaded_python`, `with_embedded_python_interpreter`, or `Python::attach` with the [`auto-initialize`](features.md#auto-initialize) feature enabled.)
 
 ## Cross Compiling
 
diff --git a/guide/src/building-and-distribution/multiple-python-versions.md b/guide/src/building-and-distribution/multiple-python-versions.md
index a7879941d0c..999f9f06ff9 100644
--- a/guide/src/building-and-distribution/multiple-python-versions.md
+++ b/guide/src/building-and-distribution/multiple-python-versions.md
@@ -97,7 +97,7 @@ PyO3 provides the APIs [`Python::version()`] and [`Python::version_info()`] to q
 ```rust
 use pyo3::Python;
 
-Python::with_gil(|py| {
+Python::attach(|py| {
     // PyO3 supports Python 3.7 and up.
     assert!(py.version_info() >= (3, 7));
     assert!(py.version_info() >= (3, 7, 0));
diff --git a/guide/src/class.md b/guide/src/class.md
index bbe80359696..1e522ce6143 100644
--- a/guide/src/class.md
+++ b/guide/src/class.md
@@ -221,7 +221,7 @@ struct MyClass {
     #[pyo3(get)]
     num: i32,
 }
-Python::with_gil(|py| {
+Python::attach(|py| {
     let obj = Bound::new(py, MyClass { num: 3 }).unwrap();
     {
         let obj_ref = obj.borrow(); // Get PyRef
@@ -253,12 +253,12 @@ struct MyClass {
 }
 
 fn return_myclass() -> Py {
-    Python::with_gil(|py| Py::new(py, MyClass { num: 1 }).unwrap())
+    Python::attach(|py| Py::new(py, MyClass { num: 1 }).unwrap())
 }
 
 let obj = return_myclass();
 
-Python::with_gil(move |py| {
+Python::attach(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,7 +280,7 @@ struct FrozenCounter {
     value: AtomicUsize,
 }
 
-let py_counter: Py = Python::with_gil(|py| {
+let py_counter: Py = Python::attach(|py| {
     let counter = FrozenCounter {
         value: AtomicUsize::new(0),
     };
@@ -290,7 +290,7 @@ 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));
+Python::attach(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.
@@ -423,7 +423,7 @@ impl SubSubClass {
         }
     }
 }
-# Python::with_gil(|py| {
+# Python::attach(|py| {
 #     let subsub = pyo3::Py::new(py, SubSubClass::new()).unwrap();
 #     pyo3::py_run!(py, subsub, "assert subsub.method1() == 10");
 #     pyo3::py_run!(py, subsub, "assert subsub.method2() == 150");
@@ -473,7 +473,7 @@ impl DictWithCounter {
         dict.set_item(key, value)
     }
 }
-# Python::with_gil(|py| {
+# Python::attach(|py| {
 #     let cnt = pyo3::Py::new(py, DictWithCounter::new()).unwrap();
 #     pyo3::py_run!(py, cnt, "cnt.set('abc', 10); assert cnt['abc'] == 10")
 # });
@@ -529,7 +529,7 @@ impl MyDict {
 
     // some custom methods that use `private` here...
 }
-# Python::with_gil(|py| {
+# Python::attach(|py| {
 #     let cls = py.get_type::();
 #     pyo3::py_run!(py, cls, "cls(a=1, b=2)")
 # });
@@ -800,7 +800,7 @@ impl MyClass {
     }
 }
 
-Python::with_gil(|py| {
+Python::attach(|py| {
     let my_class = py.get_type::();
     pyo3::py_run!(py, my_class, "assert my_class.my_attribute == 'hello'")
 });
@@ -973,7 +973,7 @@ impl MyClass {
 }
 #
 # fn main() -> PyResult<()> {
-#     Python::with_gil(|py| {
+#     Python::attach(|py| {
 #         let inspect = PyModule::import(py, "inspect")?.getattr("signature")?;
 #         let module = PyModule::new(py, "my_module")?;
 #         module.add_class::()?;
@@ -1096,7 +1096,7 @@ enum MyEnum {
     OtherVariant,
 }
 
-Python::with_gil(|py| {
+Python::attach(|py| {
     let x = Py::new(py, MyEnum::Variant).unwrap();
     let y = Py::new(py, MyEnum::OtherVariant).unwrap();
     let cls = py.get_type::();
@@ -1119,7 +1119,7 @@ enum MyEnum {
     OtherVariant = 10,
 }
 
-Python::with_gil(|py| {
+Python::attach(|py| {
     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#"
@@ -1140,7 +1140,7 @@ enum MyEnum{
     OtherVariant,
 }
 
-Python::with_gil(|py| {
+Python::attach(|py| {
     let cls = py.get_type::();
     let x = Py::new(py, MyEnum::Variant).unwrap();
     pyo3::py_run!(py, cls x, r#"
@@ -1167,7 +1167,7 @@ impl MyEnum {
     }
 }
 
-Python::with_gil(|py| {
+Python::attach(|py| {
     let cls = py.get_type::();
     pyo3::py_run!(py, cls, "assert repr(cls.Answer) == '42'")
 })
@@ -1184,7 +1184,7 @@ enum MyEnum {
     Variant,
 }
 
-Python::with_gil(|py| {
+Python::attach(|py| {
     let x = Py::new(py, MyEnum::Variant).unwrap();
     let cls = py.get_type::();
     pyo3::py_run!(py, x cls, r#"
@@ -1207,7 +1207,7 @@ enum MyEnum{
     C,
 }
 
-Python::with_gil(|py| {
+Python::attach(|py| {
     let cls = py.get_type::();
     let a = Py::new(py, MyEnum::A).unwrap();
     let b = Py::new(py, MyEnum::B).unwrap();
@@ -1263,7 +1263,7 @@ enum Shape {
 }
 
 # #[cfg(Py_3_10)]
-Python::with_gil(|py| {
+Python::attach(|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::();
@@ -1305,7 +1305,7 @@ enum MyEnum {
     Variant { i: i32 },
 }
 
-Python::with_gil(|py| {
+Python::attach(|py| {
     let x = Py::new(py, MyEnum::Variant { i: 42 }).unwrap();
     let cls = py.get_type::();
     pyo3::py_run!(py, x cls, r#"
@@ -1332,7 +1332,7 @@ enum Shape {
 }
 
 # #[cfg(Py_3_10)]
-Python::with_gil(|py| {
+Python::attach(|py| {
     let cls = py.get_type::();
     pyo3::py_run!(py, cls, r#"
         circle = cls.Circle()
@@ -1452,7 +1452,7 @@ impl pyo3::impl_::pyclass::PyClassImpl for MyClass {
     }
 }
 
-# Python::with_gil(|py| {
+# Python::attach(|py| {
 #     let cls = py.get_type::();
 #     pyo3::py_run!(py, cls, "assert cls.__name__ == 'MyClass'")
 # });
diff --git a/guide/src/class/numeric.md b/guide/src/class/numeric.md
index 124bb8d27a6..2928a3b6260 100644
--- a/guide/src/class/numeric.md
+++ b/guide/src/class/numeric.md
@@ -385,7 +385,7 @@ fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
 # use pyo3::PyTypeInfo;
 #
 # fn main() -> PyResult<()> {
-#     Python::with_gil(|py| -> PyResult<()> {
+#     Python::attach(|py| -> PyResult<()> {
 #         let globals = PyModule::import(py, "__main__")?.dict();
 #         globals.set_item("Number", Number::type_object(py))?;
 #
diff --git a/guide/src/class/object.md b/guide/src/class/object.md
index 6d688c3ea99..1c47c44a6e2 100644
--- a/guide/src/class/object.md
+++ b/guide/src/class/object.md
@@ -277,7 +277,7 @@ impl Number {
 }
 
 # fn main() -> PyResult<()> {
-#     Python::with_gil(|py| {
+#     Python::attach(|py| {
 #         let x = &Bound::new(py, Number(4))?;
 #         let y = &Bound::new(py, Number(4))?;
 #         assert!(x.eq(y)?);
diff --git a/guide/src/class/protocols.md b/guide/src/class/protocols.md
index 6b7e6c09fe1..d0e07919862 100644
--- a/guide/src/class/protocols.md
+++ b/guide/src/class/protocols.md
@@ -215,7 +215,7 @@ impl Container {
     }
 }
 
-# Python::with_gil(|py| {
+# Python::attach(|py| {
 #     let container = Container { iter: vec![1, 2, 3, 4] };
 #     let inst = pyo3::Py::new(py, container).unwrap();
 #     pyo3::py_run!(py, inst, "assert list(inst) == [1, 2, 3, 4]");
@@ -458,7 +458,7 @@ impl ClassWithGCSupport {
 
 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.
+i.e. `Python::attach` 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).
 
diff --git a/guide/src/conversions/traits.md b/guide/src/conversions/traits.md
index b8d7190e3dd..6565026e89c 100644
--- a/guide/src/conversions/traits.md
+++ b/guide/src/conversions/traits.md
@@ -12,7 +12,7 @@ fails, so usually you will use something like
 # use pyo3::prelude::*;
 # use pyo3::types::PyList;
 # fn main() -> PyResult<()> {
-#     Python::with_gil(|py| {
+#     Python::attach(|py| {
 #         let list = PyList::new(py, b"foo")?;
 let v: Vec = list.extract()?;
 #         assert_eq!(&v, &[102, 111, 111]);
@@ -54,7 +54,7 @@ struct RustyStruct {
 }
 #
 # fn main() -> PyResult<()> {
-#     Python::with_gil(|py| -> PyResult<()> {
+#     Python::attach(|py| -> PyResult<()> {
 #         let module = PyModule::from_code(
 #             py,
 #             c_str!("class Foo:
@@ -86,7 +86,7 @@ struct RustyStruct {
 #
 # use pyo3::types::PyDict;
 # fn main() -> PyResult<()> {
-#     Python::with_gil(|py| -> PyResult<()> {
+#     Python::attach(|py| -> PyResult<()> {
 #         let dict = PyDict::new(py);
 #         dict.set_item("my_string", "test")?;
 #
@@ -112,7 +112,7 @@ struct RustyStruct {
 }
 #
 # fn main() -> PyResult<()> {
-#     Python::with_gil(|py| -> PyResult<()> {
+#     Python::attach(|py| -> PyResult<()> {
 #         let module = PyModule::from_code(
 #             py,
 #             c_str!("class Foo(dict):
@@ -156,7 +156,7 @@ struct RustyStruct {
 }
 #
 # fn main() -> PyResult<()> {
-#     Python::with_gil(|py| -> PyResult<()> {
+#     Python::attach(|py| -> PyResult<()> {
 #         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");
@@ -182,7 +182,7 @@ struct RustyTuple(String, String);
 
 # use pyo3::types::PyTuple;
 # fn main() -> PyResult<()> {
-#     Python::with_gil(|py| -> PyResult<()> {
+#     Python::attach(|py| -> PyResult<()> {
 #         let tuple = PyTuple::new(py, vec!["test", "test2"])?;
 #
 #         let rustytuple: RustyTuple = tuple.extract()?;
@@ -205,7 +205,7 @@ struct RustyTuple((String,));
 
 # use pyo3::types::PyTuple;
 # fn main() -> PyResult<()> {
-#     Python::with_gil(|py| -> PyResult<()> {
+#     Python::attach(|py| -> PyResult<()> {
 #         let tuple = PyTuple::new(py, vec!["test"])?;
 #
 #         let rustytuple: RustyTuple = tuple.extract()?;
@@ -237,7 +237,7 @@ struct RustyTransparentStruct {
 
 # use pyo3::types::PyString;
 # fn main() -> PyResult<()> {
-#     Python::with_gil(|py| -> PyResult<()> {
+#     Python::attach(|py| -> PyResult<()> {
 #         let s = PyString::new(py, "test");
 #
 #         let tup: RustyTransparentTupleStruct = s.extract()?;
@@ -292,7 +292,7 @@ enum RustyEnum<'py> {
 #
 # use pyo3::types::{PyBytes, PyString};
 # fn main() -> PyResult<()> {
-#     Python::with_gil(|py| -> PyResult<()> {
+#     Python::attach(|py| -> PyResult<()> {
 #         {
 #             let thing = 42_u8.into_pyobject(py)?;
 #             let rust_thing: RustyEnum<'_> = thing.extract()?;
@@ -425,7 +425,7 @@ enum RustyEnum {
 }
 #
 # fn main() -> PyResult<()> {
-#     Python::with_gil(|py| -> PyResult<()> {
+#     Python::attach(|py| -> PyResult<()> {
 #         {
 #             let thing = 42_u8.into_pyobject(py)?;
 #             let rust_thing: RustyEnum = thing.extract()?;
@@ -515,7 +515,7 @@ struct RustyStruct {
 #
 # use pyo3::types::PyDict;
 # fn main() -> PyResult<()> {
-#     Python::with_gil(|py| -> PyResult<()> {
+#     Python::attach(|py| -> PyResult<()> {
 #         // Filled case
 #         let dict = PyDict::new(py);
 #         dict.set_item("value", (1,)).unwrap();
@@ -681,7 +681,7 @@ use pyo3::types::{PyBool, PyInt};
 let ints: Vec = vec![1, 2, 3, 4];
 let bools = vec![true, false, false, true];
 
-Python::with_gil(|py| {
+Python::attach(|py| {
     let ints_as_pyint: Vec> = ints
         .iter()
         .map(|x| Ok(x.into_pyobject(py)?))
@@ -696,7 +696,7 @@ Python::with_gil(|py| {
 });
 ```
 
-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.
+In this example if we wanted to combine `ints_as_pyints` and `bools_as_pybool` into a single `Vec>` to return from the `Python::attach` 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:
 
@@ -726,7 +726,7 @@ where
         .collect()
 }
 
-let vec_of_pyobjs: Vec> = Python::with_gil(|py| {
+let vec_of_pyobjs: Vec> = Python::attach(|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![];
diff --git a/guide/src/ecosystem/logging.md b/guide/src/ecosystem/logging.md
index e7d5c3c1e01..ba164e78e79 100644
--- a/guide/src/ecosystem/logging.md
+++ b/guide/src/ecosystem/logging.md
@@ -78,7 +78,7 @@ fn main() -> PyResult<()> {
     warn!("Something spooky happened!");
 
     // Log some messages from Python
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         py.run(
             "
 import logging
diff --git a/guide/src/exception.md b/guide/src/exception.md
index 2517ebde8b4..01552f1bf62 100644
--- a/guide/src/exception.md
+++ b/guide/src/exception.md
@@ -24,7 +24,7 @@ use pyo3::exceptions::PyException;
 create_exception!(mymodule, CustomError, PyException);
 
 # fn main() -> PyResult<()> {
-Python::with_gil(|py| {
+Python::attach(|py| {
     let ctx = [("CustomError", py.get_type::())].into_py_dict(py)?;
     pyo3::py_run!(
         py,
@@ -65,7 +65,7 @@ You can also manually write and fetch errors in the Python interpreter's global
 use pyo3::{Python, PyErr};
 use pyo3::exceptions::PyTypeError;
 
-Python::with_gil(|py| {
+Python::attach(|py| {
     PyTypeError::new_err("Error").restore(py);
     assert!(PyErr::occurred(py));
     drop(PyErr::fetch(py));
@@ -82,7 +82,7 @@ use pyo3::prelude::*;
 use pyo3::types::{PyBool, PyList};
 
 # fn main() -> PyResult<()> {
-Python::with_gil(|py| {
+Python::attach(|py| {
     assert!(PyBool::new(py, true).is_instance_of::());
     let list = PyList::new(py, &[1, 2, 3, 4])?;
     assert!(!list.is_instance_of::());
@@ -97,7 +97,7 @@ To check the type of an exception, you can similarly do:
 ```rust,no_run
 # use pyo3::exceptions::PyTypeError;
 # use pyo3::prelude::*;
-# Python::with_gil(|py| {
+# Python::attach(|py| {
 # let err = PyTypeError::new_err(());
 err.is_instance_of::(py);
 # });
@@ -166,7 +166,7 @@ impl CustomError {
 }
 
 # fn main() -> PyResult<()> {
-Python::with_gil(|py| {
+Python::attach(|py| {
     let ctx = [("CustomError", py.get_type::())].into_py_dict(py)?;
     pyo3::py_run!(
         py,
diff --git a/guide/src/features.md b/guide/src/features.md
index f60b07b6a40..8339816310c 100644
--- a/guide/src/features.md
+++ b/guide/src/features.md
@@ -45,7 +45,7 @@ section for further detail.
 
 ### `auto-initialize`
 
-This feature changes [`Python::with_gil`]({{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.with_gil) to automatically initialize a Python interpreter (by calling [`prepare_freethreaded_python`]({{#PYO3_DOCS_URL}}/pyo3/fn.prepare_freethreaded_python.html)) if needed.
+This feature changes [`Python::attach`]({{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.attach) to automatically initialize a Python interpreter (by calling [`prepare_freethreaded_python`]({{#PYO3_DOCS_URL}}/pyo3/fn.prepare_freethreaded_python.html)) if needed.
 
 If you do not enable this feature, you should call `pyo3::prepare_freethreaded_python()` before attempting to call any other Python APIs.
 
diff --git a/guide/src/free-threading.md b/guide/src/free-threading.md
index 6b2a790182f..96866d32f04 100644
--- a/guide/src/free-threading.md
+++ b/guide/src/free-threading.md
@@ -136,7 +136,7 @@ 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 [`Python::with_gil`] and [`GILOnceCell`] is
+in functions and types like [`Python::attach`] and [`GILOnceCell`] is
 historical.
 
 Instead, you should think about whether or not a Rust thread is attached to a
@@ -158,7 +158,7 @@ simultaneously interacting with the interpreter.
 
 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`]
+Python runtime, you can register a thread using the [`Python::attach`]
 function. Threads created via the Python [`threading`] module do not not need to
 do this, and pyo3 will handle setting up the [`Python<'py>`] token when CPython
 calls into your extension.
@@ -322,7 +322,7 @@ let mut cache = RuntimeCache {
     cache: None
 };
 
-Python::with_gil(|py| {
+Python::attach(|py| {
     // guaranteed to be called once and only once
     cache.once.call_once_py_attached(py, || {
         cache.cache = Some(PyDict::new(py).unbind());
@@ -353,7 +353,7 @@ use std::cell::RefCell;
 static OBJECTS: GILProtected>>> =
     GILProtected::new(RefCell::new(Vec::new()));
 
-Python::with_gil(|py| {
+Python::attach(|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();
@@ -372,7 +372,7 @@ use std::sync::Mutex;
 
 static OBJECTS: Mutex>> = Mutex::new(Vec::new());
 
-Python::with_gil(|py| {
+Python::attach(|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();
@@ -403,6 +403,6 @@ interpreter.
 [`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#method.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::attach`]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.attach
 [`Python<'py>`]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html
 [`threading`]: https://docs.python.org/3/library/threading.html
diff --git a/guide/src/function.md b/guide/src/function.md
index 96782895317..ca533f80cc9 100644
--- a/guide/src/function.md
+++ b/guide/src/function.md
@@ -59,7 +59,7 @@ The `#[pyo3]` attribute can be used to modify properties of the generated Python
         m.add_function(wrap_pyfunction!(no_args_py, m)?)
     }
 
-    # Python::with_gil(|py| {
+    # Python::attach(|py| {
     #     let m = pyo3::wrap_pymodule!(module_with_functions)(py);
     #     assert!(m.getattr(py, "no_args").is_ok());
     #     assert!(m.getattr(py, "no_args_py").is_err());
@@ -164,7 +164,7 @@ The `#[pyo3]` attribute can be used to modify properties of the generated Python
     #     };
     # }
     # 
-    # Python::with_gil(|py| {
+    # Python::attach(|py| {
     #     assert_warnings!(
     #         py,
     #         {
@@ -224,7 +224,7 @@ The `#[pyo3]` attribute can be used on individual arguments to modify properties
         argument
     }
 
-    # Python::with_gil(|py| {
+    # Python::attach(|py| {
     #     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 0d7fae7976e..990dc787dac 100644
--- a/guide/src/function/error-handling.md
+++ b/guide/src/function/error-handling.md
@@ -43,7 +43,7 @@ fn check_positive(x: i32) -> PyResult<()> {
 }
 #
 # fn main(){
-# 	Python::with_gil(|py|{
+# 	Python::attach(|py|{
 # 		let fun = pyo3::wrap_pyfunction!(check_positive, py).unwrap();
 # 		fun.call1((-1,)).unwrap_err();
 # 		fun.call1((1,)).unwrap();
@@ -71,7 +71,7 @@ fn parse_int(x: &str) -> Result {
 }
 
 # fn main() {
-#     Python::with_gil(|py| {
+#     Python::attach(|py| {
 #         let fun = pyo3::wrap_pyfunction!(parse_int, py).unwrap();
 #         let value: usize = fun.call1(("5",)).unwrap().extract().unwrap();
 #         assert_eq!(value, 5);
@@ -131,7 +131,7 @@ fn connect(s: String) -> Result<(), CustomIOError> {
 }
 
 fn main() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let fun = pyo3::wrap_pyfunction!(connect, py).unwrap();
         let err = fun.call1(("0.0.0.0",)).unwrap_err();
         assert!(err.is_instance_of::(py));
@@ -158,7 +158,7 @@ fn parse_int(s: String) -> PyResult {
 # use pyo3::exceptions::PyValueError;
 #
 # fn main() {
-#     Python::with_gil(|py| {
+#     Python::attach(|py| {
 #         assert_eq!(parse_int(String::from("1")).unwrap(), 1);
 #         assert_eq!(parse_int(String::from("1337")).unwrap(), 1337);
 #
@@ -223,7 +223,7 @@ fn wrapped_get_x() -> Result {
 }
 
 # fn main() {
-#     Python::with_gil(|py| {
+#     Python::attach(|py| {
 #         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 1e011a27840..873ff0f594d 100644
--- a/guide/src/function/signature.md
+++ b/guide/src/function/signature.md
@@ -139,7 +139,7 @@ fn add(a: u64, b: u64) -> u64 {
 }
 #
 # fn main() -> PyResult<()> {
-#     Python::with_gil(|py| {
+#     Python::attach(|py| {
 #         let fun = pyo3::wrap_pyfunction!(add, py)?;
 #
 #         let doc: String = fun.getattr("__doc__")?.extract()?;
@@ -187,7 +187,7 @@ fn add(a: u64, b: u64) -> u64 {
 }
 #
 # fn main() -> PyResult<()> {
-#     Python::with_gil(|py| {
+#     Python::attach(|py| {
 #         let fun = pyo3::wrap_pyfunction!(add, py)?;
 #
 #         let doc: String = fun.getattr("__doc__")?.extract()?;
@@ -229,7 +229,7 @@ fn add(a: u64, b: u64) -> u64 {
 }
 #
 # fn main() -> PyResult<()> {
-#     Python::with_gil(|py| {
+#     Python::attach(|py| {
 #         let fun = pyo3::wrap_pyfunction!(add, py)?;
 #
 #         let doc: String = fun.getattr("__doc__")?.extract()?;
diff --git a/guide/src/migration.md b/guide/src/migration.md
index ee929e79d2b..91c00ce5017 100644
--- a/guide/src/migration.md
+++ b/guide/src/migration.md
@@ -201,7 +201,7 @@ Before:
 # use pyo3::prelude::*;
 # use pyo3::types::PyTuple;
 # fn main() {
-# Python::with_gil(|py| {
+# Python::attach(|py| {
 // For example, for PyTuple. Many such APIs have been changed.
 let tup = PyTuple::new_bound(py, [1, 2, 3]);
 # })
@@ -214,7 +214,7 @@ After:
 # use pyo3::prelude::*;
 # use pyo3::types::PyTuple;
 # fn main() {
-# Python::with_gil(|py| {
+# Python::attach(|py| {
 // For example, for PyTuple. Many such APIs have been changed.
 let tup = PyTuple::new(py, [1, 2, 3]);
 # })
@@ -1160,7 +1160,7 @@ fn simple_function(a: i32, b: i32, c: i32) {}
 fn function_with_defaults(a: i32, b: i32, c: i32) {}
 
 # fn main() {
-#     Python::with_gil(|py| {
+#     Python::attach(|py| {
 #         let simple = wrap_pyfunction!(simple_function, py).unwrap();
 #         assert_eq!(simple.getattr("__text_signature__").unwrap().to_string(), "(a, b, c)");
 #         let defaulted = wrap_pyfunction!(function_with_defaults, py).unwrap();
@@ -1891,14 +1891,14 @@ To migrate, just pass a `py` argument to any calls to these methods.
 
 Before:
 ```rust,compile_fail
-# pyo3::Python::with_gil(|py| {
+# pyo3::Python::attach(|py| {
 py.None().get_refcnt();
 # })
 ```
 
 After:
 ```rust
-# pyo3::Python::with_gil(|py| {
+# pyo3::Python::attach(|py| {
 py.None().get_refcnt(py);
 # })
 ```
@@ -2018,7 +2018,7 @@ impl Names {
         self.names.append(&mut other.names)
     }
 }
-# Python::with_gil(|py| {
+# Python::attach(|py| {
 #     let names = Py::new(py, Names::new()).unwrap();
 #     pyo3::py_run!(py, names, r"
 #     try:
diff --git a/guide/src/module.md b/guide/src/module.md
index fa756e85920..e1d77d4adb5 100644
--- a/guide/src/module.md
+++ b/guide/src/module.md
@@ -85,7 +85,7 @@ fn func() -> String {
     "func".to_string()
 }
 
-# Python::with_gil(|py| {
+# Python::attach(|py| {
 #    use pyo3::wrap_pymodule;
 #    use pyo3::types::IntoPyDict;
 #    use pyo3::ffi::c_str;
diff --git a/guide/src/parallelism.md b/guide/src/parallelism.md
index 64ff1c8c9c0..c215211dce4 100644
--- a/guide/src/parallelism.md
+++ b/guide/src/parallelism.md
@@ -147,11 +147,11 @@ struct UserID {
     id: i64,
 }
 
-let allowed_ids: Vec = Python::with_gil(|outer_py| {
+let allowed_ids: Vec = Python::attach(|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| {
+            Python::attach(|inner_py| {
                 instance.borrow(inner_py).id > 5
             })
         }).collect()
diff --git a/guide/src/performance.md b/guide/src/performance.md
index 3c9b63b648a..0021a6c2033 100644
--- a/guide/src/performance.md
+++ b/guide/src/performance.md
@@ -55,7 +55,7 @@ fn frobnicate<'py>(value: &Bound<'py, PyAny>) -> PyResult> {
 
 ## Access to Bound implies access to GIL token
 
-Calling `Python::with_gil` is effectively a no-op when the GIL is already held, but checking that this is the case still has a cost. If an existing GIL token can not be accessed, for example when implementing a pre-existing trait, but a GIL-bound reference is available, this cost can be avoided by exploiting that access to GIL-bound reference gives zero-cost access to a GIL token via `Bound::py`.
+Calling `Python::attach` is effectively a no-op when the GIL is already held, but checking that this is the case still has a cost. If an existing GIL token can not be accessed, for example when implementing a pre-existing trait, but a GIL-bound reference is available, this cost can be avoided by exploiting that access to GIL-bound reference gives zero-cost access to a GIL token via `Bound::py`.
 
 For example, instead of writing
 
@@ -70,7 +70,7 @@ struct FooBound<'py>(Bound<'py, PyList>);
 
 impl PartialEq for FooBound<'_> {
     fn eq(&self, other: &Foo) -> bool {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let len = other.0.bind(py).len();
             self.0.len() == len
         })
@@ -116,18 +116,18 @@ PyO3 uses global mutable state to keep track of deferred reference count updates
 
 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
+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::attach` 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(py).unbind());
+let numbers: Py = Python::attach(|py| PyList::empty(py).unbind());
 
-Python::with_gil(|py| {
+Python::attach(|py| {
     numbers.bind(py).append(23).unwrap();
 });
 
-Python::with_gil(|py| {
+Python::attach(|py| {
     numbers.bind(py).append(42).unwrap();
 });
 ```
@@ -137,17 +137,17 @@ 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(py).unbind());
+let numbers: Py = Python::attach(|py| PyList::empty(py).unbind());
 
-Python::with_gil(|py| {
+Python::attach(|py| {
     numbers.bind(py).append(23).unwrap();
 });
 
-Python::with_gil(|py| {
+Python::attach(|py| {
     numbers.bind(py).append(42).unwrap();
 });
 
-Python::with_gil(move |py| {
+Python::attach(move |py| {
     drop(numbers);
 });
 ```
diff --git a/guide/src/python-from-rust/calling-existing-code.md b/guide/src/python-from-rust/calling-existing-code.md
index a33c24d9e22..5c1b2701e01 100644
--- a/guide/src/python-from-rust/calling-existing-code.md
+++ b/guide/src/python-from-rust/calling-existing-code.md
@@ -11,7 +11,7 @@ module available in your environment.
 use pyo3::prelude::*;
 
 fn main() -> PyResult<()> {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let builtins = PyModule::import(py, "builtins")?;
         let total: i32 = builtins
             .getattr("sum")?
@@ -36,7 +36,7 @@ use pyo3::prelude::*;
 use pyo3::ffi::c_str;
 
 # fn main() -> Result<(), ()> {
-Python::with_gil(|py| {
+Python::attach(|py| {
     let result = py
         .eval(c_str!("[i * 10 for i in range(5)]"), None, None)
         .map_err(|e| {
@@ -84,7 +84,7 @@ impl UserData {
     }
 }
 
-Python::with_gil(|py| {
+Python::attach(|py| {
     let userdata = UserData {
         id: 34,
         name: "Yu".to_string(),
@@ -113,7 +113,7 @@ use pyo3::{prelude::*, types::IntoPyDict};
 use pyo3_ffi::c_str;
 
 # fn main() -> PyResult<()> {
-Python::with_gil(|py| {
+Python::attach(|py| {
     let activators = PyModule::from_code(
         py,
         c_str!(r#"
@@ -172,7 +172,7 @@ fn foo(foo_module: &Bound<'_, PyModule>) -> PyResult<()> {
 
 fn main() -> PyResult<()> {
     pyo3::append_to_inittab!(foo);
-    Python::with_gil(|py| Python::run(py, c_str!("import foo; foo.add_one(6)"), None, None))
+    Python::attach(|py| Python::run(py, c_str!("import foo; foo.add_one(6)"), None, None))
 }
 ```
 
@@ -191,7 +191,7 @@ pub fn add_one(x: i64) -> i64 {
 }
 
 fn main() -> PyResult<()> {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         // Create new module
         let foo_module = PyModule::new(py, "foo")?;
         foo_module.add_function(wrap_pyfunction!(add_one, &foo_module)?)?;
@@ -264,7 +264,7 @@ fn main() -> PyResult<()> {
         "/python_app/utils/foo.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> {
+    let from_python = Python::attach(|py| -> PyResult> {
         PyModule::from_code(py, py_foo, c_str!("foo.py"), c_str!("utils.foo"))?;
         let app: Py = PyModule::from_code(py, py_app, c_str!("app.py"), c_str!(""))?
             .getattr("run")?
@@ -299,7 +299,7 @@ use std::ffi::CString;
 fn main() -> PyResult<()> {
     let path = Path::new("/usr/share/python_app");
     let py_app = CString::new(fs::read_to_string(path.join("app.py"))?)?;
-    let from_python = Python::with_gil(|py| -> PyResult> {
+    let from_python = Python::attach(|py| -> PyResult> {
         let syspath = py
             .import("sys")?
             .getattr("path")?
@@ -329,7 +329,7 @@ use pyo3::prelude::*;
 use pyo3::ffi::c_str;
 
 fn main() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let custom_manager = PyModule::from_code(
             py,
             c_str!(r#"
@@ -393,7 +393,7 @@ Alternatively, set Python's `signal` module to take the default action for a sig
 use pyo3::prelude::*;
 
 # fn main() -> PyResult<()> {
-Python::with_gil(|py| -> PyResult<()> {
+Python::attach(|py| -> PyResult<()> {
     let signal = py.import("signal")?;
     // Set SIGINT to have the default action
     signal
diff --git a/guide/src/python-from-rust/function-calls.md b/guide/src/python-from-rust/function-calls.md
index a8f8e13e872..ed8454fe431 100644
--- a/guide/src/python-from-rust/function-calls.md
+++ b/guide/src/python-from-rust/function-calls.md
@@ -26,7 +26,7 @@ fn main() -> PyResult<()> {
     let arg2 = "arg2";
     let arg3 = "arg3";
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let fun: Py = PyModule::from_code(
             py,
             c_str!("def example(*args, **kwargs):
@@ -73,7 +73,7 @@ fn main() -> PyResult<()> {
     let key2 = "key2";
     let val2 = 2;
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let fun: Py = PyModule::from_code(
             py,
             c_str!("def example(*args, **kwargs):
diff --git a/guide/src/trait-bounds.md b/guide/src/trait-bounds.md
index 18476eeb9f6..94f01caa03e 100644
--- a/guide/src/trait-bounds.md
+++ b/guide/src/trait-bounds.md
@@ -81,7 +81,7 @@ struct UserModel {
 impl Model for UserModel {
     fn set_variables(&mut self, var: &Vec) {
         println!("Rust calling Python to set the variables");
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             self.model
                 .bind(py)
                 .call_method("set_variables", (PyList::new(py, var).unwrap(),), None)
@@ -91,7 +91,7 @@ impl Model for UserModel {
 
     fn get_results(&self) -> Vec {
         println!("Rust calling Python to get the results");
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             self.model
                 .bind(py)
                 .call_method("get_results", (), None)
@@ -103,7 +103,7 @@ impl Model for UserModel {
 
     fn compute(&mut self) {
         println!("Rust calling Python to perform the computation");
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             self.model
                 .bind(py)
                 .call_method("compute", (), None)
@@ -180,7 +180,7 @@ This wrapper will also perform the type conversions between Python and Rust.
 # impl Model for UserModel {
 #  fn set_variables(&mut self, var: &Vec) {
 #      println!("Rust calling Python to set the variables");
-#      Python::with_gil(|py| {
+#      Python::attach(|py| {
 #          self.model.bind(py)
 #              .call_method("set_variables", (PyList::new(py, var).unwrap(),), None)
 #              .unwrap();
@@ -189,7 +189,7 @@ This wrapper will also perform the type conversions between Python and Rust.
 #
 #  fn get_results(&self) -> Vec {
 #      println!("Rust calling Python to get the results");
-#      Python::with_gil(|py| {
+#      Python::attach(|py| {
 #          self.model
 #              .bind(py)
 #              .call_method("get_results", (), None)
@@ -201,7 +201,7 @@ This wrapper will also perform the type conversions between Python and Rust.
 #
 #  fn compute(&mut self) {
 #      println!("Rust calling Python to perform the computation");
-#      Python::with_gil(|py| {
+#      Python::attach(|py| {
 #          self.model
 #              .bind(py)
 #              .call_method("compute", (), None)
@@ -347,7 +347,7 @@ We used in our `get_results` method the following call that performs the type co
 impl Model for UserModel {
     fn get_results(&self) -> Vec {
         println!("Rust calling Python to get the results");
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             self.model
                 .bind(py)
                 .call_method("get_results", (), None)
@@ -358,7 +358,7 @@ impl Model for UserModel {
     }
 #     fn set_variables(&mut self, var: &Vec) {
 #         println!("Rust calling Python to set the variables");
-#         Python::with_gil(|py| {
+#         Python::attach(|py| {
 #             self.model.bind(py)
 #                 .call_method("set_variables", (PyList::new(py, var).unwrap(),), None)
 #                 .unwrap();
@@ -367,7 +367,7 @@ impl Model for UserModel {
 #
 #     fn compute(&mut self) {
 #         println!("Rust calling Python to perform the computation");
-#         Python::with_gil(|py| {
+#         Python::attach(|py| {
 #             self.model
 #                 .bind(py)
 #                 .call_method("compute", (), None)
@@ -398,7 +398,7 @@ Let's break it down in order to perform better error handling:
 impl Model for UserModel {
     fn get_results(&self) -> Vec {
         println!("Get results from Rust calling Python");
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let py_result: Bound<'_, PyAny> = self
                 .model
                 .bind(py)
@@ -417,7 +417,7 @@ impl Model for UserModel {
     }
 #     fn set_variables(&mut self, var: &Vec) {
 #         println!("Rust calling Python to set the variables");
-#         Python::with_gil(|py| {
+#         Python::attach(|py| {
 #             let py_model = self.model.bind(py)
 #                 .call_method("set_variables", (PyList::new(py, var).unwrap(),), None)
 #                 .unwrap();
@@ -426,7 +426,7 @@ impl Model for UserModel {
 #
 #     fn compute(&mut self) {
 #         println!("Rust calling Python to perform the computation");
-#         Python::with_gil(|py| {
+#         Python::attach(|py| {
 #             self.model
 #                 .bind(py)
 #                 .call_method("compute", (), None)
@@ -514,7 +514,7 @@ impl UserModel {
 impl Model for UserModel {
     fn set_variables(&mut self, var: &Vec) {
         println!("Rust calling Python to set the variables");
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             self.model
                 .bind(py)
                 .call_method("set_variables", (PyList::new(py, var).unwrap(),), None)
@@ -524,7 +524,7 @@ impl Model for UserModel {
 
     fn get_results(&self) -> Vec {
         println!("Get results from Rust calling Python");
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let py_result: Bound<'_, PyAny> = self
                 .model
                 .bind(py)
@@ -544,7 +544,7 @@ impl Model for UserModel {
 
     fn compute(&mut self) {
         println!("Rust calling Python to perform the computation");
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             self.model
                 .bind(py)
                 .call_method("compute", (), None)
diff --git a/guide/src/types.md b/guide/src/types.md
index 06559d49d13..404779f2266 100644
--- a/guide/src/types.md
+++ b/guide/src/types.md
@@ -67,7 +67,7 @@ fn example<'py>(py: Python<'py>) -> PyResult<()> {
     drop(x); // release the original reference x
     Ok(())
 }
-# Python::with_gil(example).unwrap();
+# Python::attach(example).unwrap();
 ```
 
 Or, without the type annotations:
@@ -83,7 +83,7 @@ fn example(py: Python<'_>) -> PyResult<()> {
     drop(x);
     Ok(())
 }
-# Python::with_gil(example).unwrap();
+# Python::attach(example).unwrap();
 ```
 
 #### Function argument lifetimes
@@ -111,7 +111,7 @@ fn add<'py>(
 ) -> PyResult> {
     left.add(right)
 }
-# Python::with_gil(|py| {
+# Python::attach(|py| {
 #     let s = pyo3::types::PyString::new(py, "s");
 #     assert!(add(&s, &s).unwrap().eq("ss").unwrap());
 # })
@@ -125,7 +125,7 @@ fn add(left: &Bound<'_, PyAny>, right: &Bound<'_, PyAny>) -> PyResult
     let output: Bound<'_, PyAny> = left.add(right)?;
     Ok(output.unbind())
 }
-# Python::with_gil(|py| {
+# Python::attach(|py| {
 #     let s = pyo3::types::PyString::new(py, "s");
 #     assert!(add(&s, &s).unwrap().bind(py).eq("ss").unwrap());
 # })
@@ -155,7 +155,7 @@ for i in 0..=2 {
 }
 # Ok(())
 # }
-# Python::with_gil(example).unwrap();
+# Python::attach(example).unwrap();
 ```
 
 ### Casting between smart pointer types
@@ -231,7 +231,7 @@ use pyo3::types::PyList;
 fn get_first_item<'py>(list: &Bound<'py, PyList>) -> PyResult> {
     list.get_item(0)
 }
-# Python::with_gil(|py| {
+# Python::attach(|py| {
 #     let l = PyList::new(py, ["hello world"]).unwrap();
 #     assert!(get_first_item(&l).unwrap().eq("hello world").unwrap());
 # })
@@ -259,7 +259,7 @@ let _: &Bound<'py, PyTuple> = obj.downcast()?;
 let _: Bound<'py, PyTuple> = obj.downcast_into()?;
 # Ok(())
 # }
-# Python::with_gil(example).unwrap()
+# Python::attach(example).unwrap()
 ```
 
 Custom [`#[pyclass]`][pyclass] types implement [`PyTypeCheck`], so `.downcast()` also works for these types. The snippet below is the same as the snippet above casting instead to a custom type `MyClass`:
@@ -281,7 +281,7 @@ let _: &Bound<'py, MyClass> = obj.downcast()?;
 let _: Bound<'py, MyClass> = obj.downcast_into()?;
 # Ok(())
 # }
-# Python::with_gil(example).unwrap()
+# Python::attach(example).unwrap()
 ```
 
 ### Extracting Rust data from Python objects
@@ -302,7 +302,7 @@ let (x, y, z) = obj.extract::<(i32, i32, i32)>()?;
 assert_eq!((x, y, z), (1, 2, 3));
 # Ok(())
 # }
-# Python::with_gil(example).unwrap()
+# Python::attach(example).unwrap()
 ```
 
 To avoid copying data, [`#[pyclass]`][pyclass] types can directly reference Rust data stored within the Python objects without needing to `.extract()`. See the [corresponding documentation in the class section of the guide](./class.md#bound-and-interior-mutability)
diff --git a/newsfragments/5209.changed.md b/newsfragments/5209.changed.md
new file mode 100644
index 00000000000..17e097b2bc7
--- /dev/null
+++ b/newsfragments/5209.changed.md
@@ -0,0 +1 @@
+rename `Python::with_gil` to `Python::attach`
\ No newline at end of file
diff --git a/pyo3-benches/benches/bench_any.rs b/pyo3-benches/benches/bench_any.rs
index 4ed14493873..673f23e5fb7 100644
--- a/pyo3-benches/benches/bench_any.rs
+++ b/pyo3-benches/benches/bench_any.rs
@@ -62,7 +62,7 @@ fn find_object_type(obj: &Bound<'_, PyAny>) -> ObjectType {
 }
 
 fn bench_identify_object_type(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let obj = py.eval(c"object()", None, None).unwrap();
 
         b.iter(|| find_object_type(&obj));
@@ -72,7 +72,7 @@ fn bench_identify_object_type(b: &mut Bencher<'_>) {
 }
 
 fn bench_collect_generic_iterator(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let collection = py.eval(c"list(range(1 << 20))", None, None).unwrap();
 
         b.iter(|| {
diff --git a/pyo3-benches/benches/bench_bigint.rs b/pyo3-benches/benches/bench_bigint.rs
index d2c78f0ad4e..6227e95e496 100644
--- a/pyo3-benches/benches/bench_bigint.rs
+++ b/pyo3-benches/benches/bench_bigint.rs
@@ -7,7 +7,7 @@ use pyo3::prelude::*;
 use pyo3::types::PyDict;
 
 fn extract_bigint_extract_fail(bench: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let d = PyDict::new(py).into_any();
 
         bench.iter(|| match black_box(&d).extract::() {
@@ -18,7 +18,7 @@ fn extract_bigint_extract_fail(bench: &mut Bencher<'_>) {
 }
 
 fn extract_bigint_small(bench: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let int = py.eval(c"-42", None, None).unwrap();
 
         bench.iter_with_large_drop(|| black_box(&int).extract::().unwrap());
@@ -26,7 +26,7 @@ fn extract_bigint_small(bench: &mut Bencher<'_>) {
 }
 
 fn extract_bigint_big_negative(bench: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let int = py.eval(c"-10**300", None, None).unwrap();
 
         bench.iter_with_large_drop(|| black_box(&int).extract::().unwrap());
@@ -34,7 +34,7 @@ fn extract_bigint_big_negative(bench: &mut Bencher<'_>) {
 }
 
 fn extract_bigint_big_positive(bench: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let int = py.eval(c"10**300", None, None).unwrap();
 
         bench.iter_with_large_drop(|| black_box(&int).extract::().unwrap());
@@ -42,7 +42,7 @@ fn extract_bigint_big_positive(bench: &mut Bencher<'_>) {
 }
 
 fn extract_bigint_huge_negative(bench: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let int = py.eval(c"-10**3000", None, None).unwrap();
 
         bench.iter_with_large_drop(|| black_box(&int).extract::().unwrap());
@@ -50,7 +50,7 @@ fn extract_bigint_huge_negative(bench: &mut Bencher<'_>) {
 }
 
 fn extract_bigint_huge_positive(bench: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         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 b6e090a7dbd..92587b67437 100644
--- a/pyo3-benches/benches/bench_call.rs
+++ b/pyo3-benches/benches/bench_call.rs
@@ -14,7 +14,7 @@ macro_rules! test_module {
 }
 
 fn bench_call_0(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let module = test_module!(py, "def foo(): pass");
 
         let foo_module = &module.getattr("foo").unwrap();
@@ -28,7 +28,7 @@ fn bench_call_0(b: &mut Bencher<'_>) {
 }
 
 fn bench_call_1(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let module = test_module!(py, "def foo(a, b, c): pass");
 
         let foo_module = &module.getattr("foo").unwrap();
@@ -47,7 +47,7 @@ fn bench_call_1(b: &mut Bencher<'_>) {
 }
 
 fn bench_call(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let module = test_module!(py, "def foo(a, b, c, d, e): pass");
 
         let foo_module = &module.getattr("foo").unwrap();
@@ -69,7 +69,7 @@ fn bench_call(b: &mut Bencher<'_>) {
 }
 
 fn bench_call_one_arg(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let module = test_module!(py, "def foo(a): pass");
 
         let foo_module = &module.getattr("foo").unwrap();
@@ -84,7 +84,7 @@ fn bench_call_one_arg(b: &mut Bencher<'_>) {
 }
 
 fn bench_call_method_0(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let module = test_module!(
             py,
             "
@@ -105,7 +105,7 @@ class Foo:
 }
 
 fn bench_call_method_1(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let module = test_module!(
             py,
             "
@@ -133,7 +133,7 @@ class Foo:
 }
 
 fn bench_call_method(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let module = test_module!(
             py,
             "
@@ -162,7 +162,7 @@ class Foo:
 }
 
 fn bench_call_method_one_arg(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let module = test_module!(
             py,
             "
diff --git a/pyo3-benches/benches/bench_comparisons.rs b/pyo3-benches/benches/bench_comparisons.rs
index fbd473f06cf..581e9738b75 100644
--- a/pyo3-benches/benches/bench_comparisons.rs
+++ b/pyo3-benches/benches/bench_comparisons.rs
@@ -44,7 +44,7 @@ impl OrderedRichcmp {
 }
 
 fn bench_ordered_dunder_methods(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let obj1 = &Bound::new(py, OrderedDunderMethods(0)).unwrap().into_any();
         let obj2 = &Bound::new(py, OrderedDunderMethods(1)).unwrap().into_any();
 
@@ -53,7 +53,7 @@ fn bench_ordered_dunder_methods(b: &mut Bencher<'_>) {
 }
 
 fn bench_ordered_richcmp(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let obj1 = &Bound::new(py, OrderedRichcmp(0)).unwrap().into_any();
         let obj2 = &Bound::new(py, OrderedRichcmp(1)).unwrap().into_any();
 
diff --git a/pyo3-benches/benches/bench_decimal.rs b/pyo3-benches/benches/bench_decimal.rs
index 65fb47e23f7..6a288253a82 100644
--- a/pyo3-benches/benches/bench_decimal.rs
+++ b/pyo3-benches/benches/bench_decimal.rs
@@ -7,7 +7,7 @@ use pyo3::prelude::*;
 use pyo3::types::PyDict;
 
 fn decimal_via_extract(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let locals = PyDict::new(py);
         py.run(
             cr#"
diff --git a/pyo3-benches/benches/bench_dict.rs b/pyo3-benches/benches/bench_dict.rs
index 6a92cf21c5f..8867da1ee50 100644
--- a/pyo3-benches/benches/bench_dict.rs
+++ b/pyo3-benches/benches/bench_dict.rs
@@ -7,7 +7,7 @@ use pyo3::types::IntoPyDict;
 use pyo3::{prelude::*, types::PyMapping};
 
 fn iter_dict(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         const LEN: usize = 100_000;
         let dict = (0..LEN as u64)
             .map(|i| (i, i * 2))
@@ -24,7 +24,7 @@ fn iter_dict(b: &mut Bencher<'_>) {
 }
 
 fn dict_new(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         const LEN: usize = 50_000;
         b.iter_with_large_drop(|| {
             (0..LEN as u64)
@@ -36,7 +36,7 @@ fn dict_new(b: &mut Bencher<'_>) {
 }
 
 fn dict_get_item(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         const LEN: usize = 50_000;
         let dict = (0..LEN as u64)
             .map(|i| (i, i * 2))
@@ -57,7 +57,7 @@ fn dict_get_item(b: &mut Bencher<'_>) {
 }
 
 fn extract_hashmap(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         const LEN: usize = 100_000;
         let dict = (0..LEN as u64)
             .map(|i| (i, i * 2))
@@ -68,7 +68,7 @@ fn extract_hashmap(b: &mut Bencher<'_>) {
 }
 
 fn extract_btreemap(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         const LEN: usize = 100_000;
         let dict = (0..LEN as u64)
             .map(|i| (i, i * 2))
@@ -79,7 +79,7 @@ fn extract_btreemap(b: &mut Bencher<'_>) {
 }
 
 fn extract_hashbrown_map(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         const LEN: usize = 100_000;
         let dict = (0..LEN as u64)
             .map(|i| (i, i * 2))
@@ -90,7 +90,7 @@ fn extract_hashbrown_map(b: &mut Bencher<'_>) {
 }
 
 fn mapping_from_dict(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         const LEN: usize = 100_000;
         let dict = &(0..LEN as u64)
             .map(|i| (i, i * 2))
diff --git a/pyo3-benches/benches/bench_err.rs b/pyo3-benches/benches/bench_err.rs
index 998ed6975b0..543df090d48 100644
--- a/pyo3-benches/benches/bench_err.rs
+++ b/pyo3-benches/benches/bench_err.rs
@@ -3,7 +3,7 @@ use codspeed_criterion_compat::{criterion_group, criterion_main, Bencher, Criter
 use pyo3::{exceptions::PyValueError, prelude::*};
 
 fn err_new_restore_and_fetch(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         b.iter(|| {
             PyValueError::new_err("some exception message").restore(py);
             PyErr::fetch(py)
diff --git a/pyo3-benches/benches/bench_extract.rs b/pyo3-benches/benches/bench_extract.rs
index 2062ccba7b5..db1785a2331 100644
--- a/pyo3-benches/benches/bench_extract.rs
+++ b/pyo3-benches/benches/bench_extract.rs
@@ -8,7 +8,7 @@ use pyo3::{
 };
 
 fn extract_str_extract_success(bench: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let s = PyString::new(py, "Hello, World!").into_any();
 
         bench.iter(|| black_box(&s).extract::<&str>().unwrap());
@@ -16,7 +16,7 @@ fn extract_str_extract_success(bench: &mut Bencher<'_>) {
 }
 
 fn extract_str_extract_fail(bench: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let d = PyDict::new(py).into_any();
 
         bench.iter(|| match black_box(&d).extract::<&str>() {
@@ -28,7 +28,7 @@ fn extract_str_extract_fail(bench: &mut Bencher<'_>) {
 
 #[cfg(Py_3_10)]
 fn extract_str_downcast_success(bench: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let s = PyString::new(py, "Hello, World!").into_any();
 
         bench.iter(|| {
@@ -39,7 +39,7 @@ fn extract_str_downcast_success(bench: &mut Bencher<'_>) {
 }
 
 fn extract_str_downcast_fail(bench: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let d = PyDict::new(py).into_any();
 
         bench.iter(|| match black_box(&d).downcast::() {
@@ -50,7 +50,7 @@ fn extract_str_downcast_fail(bench: &mut Bencher<'_>) {
 }
 
 fn extract_int_extract_success(bench: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let int = 123i32.into_pyobject(py).unwrap();
 
         bench.iter(|| black_box(&int).extract::().unwrap());
@@ -58,7 +58,7 @@ fn extract_int_extract_success(bench: &mut Bencher<'_>) {
 }
 
 fn extract_int_extract_fail(bench: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let d = PyDict::new(py).into_any();
 
         bench.iter(|| match black_box(&d).extract::() {
@@ -69,7 +69,7 @@ fn extract_int_extract_fail(bench: &mut Bencher<'_>) {
 }
 
 fn extract_int_downcast_success(bench: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let int = 123i32.into_pyobject(py).unwrap();
 
         bench.iter(|| {
@@ -80,7 +80,7 @@ fn extract_int_downcast_success(bench: &mut Bencher<'_>) {
 }
 
 fn extract_int_downcast_fail(bench: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let d = PyDict::new(py).into_any();
 
         bench.iter(|| match black_box(&d).downcast::() {
@@ -91,7 +91,7 @@ fn extract_int_downcast_fail(bench: &mut Bencher<'_>) {
 }
 
 fn extract_float_extract_success(bench: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let float = 23.42f64.into_pyobject(py).unwrap();
 
         bench.iter(|| black_box(&float).extract::().unwrap());
@@ -99,7 +99,7 @@ fn extract_float_extract_success(bench: &mut Bencher<'_>) {
 }
 
 fn extract_float_extract_fail(bench: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let d = PyDict::new(py).into_any();
 
         bench.iter(|| match black_box(&d).extract::() {
@@ -110,7 +110,7 @@ fn extract_float_extract_fail(bench: &mut Bencher<'_>) {
 }
 
 fn extract_float_downcast_success(bench: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let float = 23.42f64.into_pyobject(py).unwrap();
 
         bench.iter(|| {
@@ -121,7 +121,7 @@ fn extract_float_downcast_success(bench: &mut Bencher<'_>) {
 }
 
 fn extract_float_downcast_fail(bench: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let d = PyDict::new(py).into_any();
 
         bench.iter(|| match black_box(&d).downcast::() {
diff --git a/pyo3-benches/benches/bench_frompyobject.rs b/pyo3-benches/benches/bench_frompyobject.rs
index 6411a391f9a..51bc825ffe9 100644
--- a/pyo3-benches/benches/bench_frompyobject.rs
+++ b/pyo3-benches/benches/bench_frompyobject.rs
@@ -16,7 +16,7 @@ enum ManyTypes {
 }
 
 fn enum_from_pyobject(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let any = PyString::new(py, "hello world").into_any();
 
         b.iter(|| black_box(&any).extract::().unwrap());
@@ -24,7 +24,7 @@ fn enum_from_pyobject(b: &mut Bencher<'_>) {
 }
 
 fn list_via_downcast(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let any = PyList::empty(py).into_any();
 
         b.iter(|| black_box(&any).downcast::().unwrap());
@@ -32,7 +32,7 @@ fn list_via_downcast(b: &mut Bencher<'_>) {
 }
 
 fn list_via_extract(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let any = PyList::empty(py).into_any();
 
         b.iter(|| black_box(&any).extract::>().unwrap());
@@ -40,7 +40,7 @@ fn list_via_extract(b: &mut Bencher<'_>) {
 }
 
 fn not_a_list_via_downcast(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let any = PyString::new(py, "foobar").into_any();
 
         b.iter(|| black_box(&any).downcast::().unwrap_err());
@@ -48,7 +48,7 @@ fn not_a_list_via_downcast(b: &mut Bencher<'_>) {
 }
 
 fn not_a_list_via_extract(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let any = PyString::new(py, "foobar").into_any();
 
         b.iter(|| black_box(&any).extract::>().unwrap_err());
@@ -62,7 +62,7 @@ enum ListOrNotList<'a> {
 }
 
 fn not_a_list_via_extract_enum(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let any = PyString::new(py, "foobar").into_any();
 
         b.iter(|| match black_box(&any).extract::>() {
diff --git a/pyo3-benches/benches/bench_gil.rs b/pyo3-benches/benches/bench_gil.rs
index cede8836f35..b8fc8496225 100644
--- a/pyo3-benches/benches/bench_gil.rs
+++ b/pyo3-benches/benches/bench_gil.rs
@@ -4,13 +4,13 @@ use pyo3::prelude::*;
 
 fn bench_clean_acquire_gil(b: &mut Bencher<'_>) {
     // Acquiring first GIL will also create a "clean" GILPool, so this measures the Python overhead.
-    b.iter(|| Python::with_gil(|_| {}));
+    b.iter(|| Python::attach(|_| {}));
 }
 
 fn bench_dirty_acquire_gil(b: &mut Bencher<'_>) {
-    let obj = Python::with_gil(|py| py.None());
+    let obj = Python::attach(|py| py.None());
     // Drop the returned clone of the object so that the reference pool has work to do.
-    b.iter(|| Python::with_gil(|py| obj.clone_ref(py)));
+    b.iter(|| Python::attach(|py| obj.clone_ref(py)));
 }
 
 fn criterion_benchmark(c: &mut Criterion) {
diff --git a/pyo3-benches/benches/bench_intern.rs b/pyo3-benches/benches/bench_intern.rs
index 1b7dc07370a..a135fe13278 100644
--- a/pyo3-benches/benches/bench_intern.rs
+++ b/pyo3-benches/benches/bench_intern.rs
@@ -7,7 +7,7 @@ use pyo3::prelude::*;
 use pyo3::intern;
 
 fn getattr_direct(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let sys = &py.import("sys").unwrap();
 
         b.iter(|| black_box(sys).getattr("version").unwrap());
@@ -15,7 +15,7 @@ fn getattr_direct(b: &mut Bencher<'_>) {
 }
 
 fn getattr_intern(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let sys = &py.import("sys").unwrap();
 
         b.iter(|| black_box(sys).getattr(intern!(py, "version")).unwrap());
diff --git a/pyo3-benches/benches/bench_intopyobject.rs b/pyo3-benches/benches/bench_intopyobject.rs
index 0e1fbad1a57..1ec88501bb2 100644
--- a/pyo3-benches/benches/bench_intopyobject.rs
+++ b/pyo3-benches/benches/bench_intopyobject.rs
@@ -7,7 +7,7 @@ use pyo3::prelude::*;
 use pyo3::types::PyBytes;
 
 fn bench_bytes_new(b: &mut Bencher<'_>, data: &[u8]) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         b.iter_with_large_drop(|| PyBytes::new(py, black_box(data)));
     });
 }
@@ -27,7 +27,7 @@ fn bytes_new_large(b: &mut Bencher<'_>) {
 }
 
 fn bench_bytes_into_pyobject(b: &mut Bencher<'_>, data: &[u8]) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         b.iter_with_large_drop(|| black_box(data).into_pyobject(py));
     });
 }
@@ -47,7 +47,7 @@ fn byte_slice_into_pyobject_large(b: &mut Bencher<'_>) {
 }
 
 fn vec_into_pyobject(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let bytes = (0..u8::MAX).collect::>();
         b.iter_with_large_drop(|| black_box(&bytes).clone().into_pyobject(py));
     });
diff --git a/pyo3-benches/benches/bench_list.rs b/pyo3-benches/benches/bench_list.rs
index f6df0cc4315..6f3b231d3e3 100644
--- a/pyo3-benches/benches/bench_list.rs
+++ b/pyo3-benches/benches/bench_list.rs
@@ -6,7 +6,7 @@ use pyo3::prelude::*;
 use pyo3::types::{PyList, PySequence};
 
 fn iter_list(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         const LEN: usize = 100_000;
         let list = PyList::new(py, 0..LEN).unwrap();
         let mut sum = 0;
@@ -20,14 +20,14 @@ fn iter_list(b: &mut Bencher<'_>) {
 }
 
 fn list_new(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         const LEN: usize = 50_000;
         b.iter_with_large_drop(|| PyList::new(py, 0..LEN));
     });
 }
 
 fn list_get_item(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         const LEN: usize = 50_000;
         let list = PyList::new(py, 0..LEN).unwrap();
         let mut sum = 0;
@@ -40,7 +40,7 @@ fn list_get_item(b: &mut Bencher<'_>) {
 }
 
 fn list_nth(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         const LEN: usize = 50;
         let list = PyList::new(py, 0..LEN).unwrap();
         let mut sum = 0;
@@ -53,7 +53,7 @@ fn list_nth(b: &mut Bencher<'_>) {
 }
 
 fn list_nth_back(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         const LEN: usize = 50;
         let list = PyList::new(py, 0..LEN).unwrap();
         let mut sum = 0;
@@ -67,7 +67,7 @@ fn list_nth_back(b: &mut Bencher<'_>) {
 
 #[cfg(not(Py_LIMITED_API))]
 fn list_get_item_unchecked(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         const LEN: usize = 50_000;
         let list = PyList::new(py, 0..LEN).unwrap();
         let mut sum = 0;
@@ -82,7 +82,7 @@ fn list_get_item_unchecked(b: &mut Bencher<'_>) {
 }
 
 fn sequence_from_list(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         const LEN: usize = 50_000;
         let list = &PyList::new(py, 0..LEN).unwrap();
         b.iter(|| black_box(list).downcast::().unwrap());
diff --git a/pyo3-benches/benches/bench_pyclass.rs b/pyo3-benches/benches/bench_pyclass.rs
index b917a4acc08..7e026f74bd1 100644
--- a/pyo3-benches/benches/bench_pyclass.rs
+++ b/pyo3-benches/benches/bench_pyclass.rs
@@ -27,7 +27,7 @@ impl MyClass {
 }
 
 pub fn first_time_init(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         b.iter(|| {
             // This is using an undocumented internal PyO3 API to measure pyclass performance; please
             // don't use this in your own code!
diff --git a/pyo3-benches/benches/bench_pyobject.rs b/pyo3-benches/benches/bench_pyobject.rs
index c731216c79f..f15097fb26f 100644
--- a/pyo3-benches/benches/bench_pyobject.rs
+++ b/pyo3-benches/benches/bench_pyobject.rs
@@ -11,7 +11,7 @@ use std::time::{Duration, Instant};
 use pyo3::prelude::*;
 
 fn drop_many_objects(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         b.iter(|| {
             for _ in 0..1000 {
                 drop(py.None());
@@ -22,11 +22,11 @@ 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()).collect::>()),
+        || Python::attach(|py| (0..1000).map(|_| py.None()).collect::>()),
         |objs| {
             drop(objs);
 
-            Python::with_gil(|_py| ());
+            Python::attach(|_py| ());
         },
         BatchSize::SmallInput,
     );
@@ -68,7 +68,7 @@ fn drop_many_objects_multiple_threads(b: &mut Bencher<'_>) {
 
         for _ in 0..iters {
             for sender in &sender {
-                let objs = Python::with_gil(|py| {
+                let objs = Python::attach(|py| {
                     (0..1000 / THREADS)
                         .map(|_| py.None())
                         .collect::>()
@@ -82,7 +82,7 @@ fn drop_many_objects_multiple_threads(b: &mut Bencher<'_>) {
             let start = Instant::now();
 
             loop {
-                Python::with_gil(|_py| ());
+                Python::attach(|_py| ());
 
                 let done = done.load(Ordering::Acquire);
                 if done - last_done == THREADS {
@@ -91,7 +91,7 @@ fn drop_many_objects_multiple_threads(b: &mut Bencher<'_>) {
                 }
             }
 
-            Python::with_gil(|_py| ());
+            Python::attach(|_py| ());
 
             duration += start.elapsed();
         }
diff --git a/pyo3-benches/benches/bench_set.rs b/pyo3-benches/benches/bench_set.rs
index 2d468740ea0..4352917231e 100644
--- a/pyo3-benches/benches/bench_set.rs
+++ b/pyo3-benches/benches/bench_set.rs
@@ -8,7 +8,7 @@ use std::{
 };
 
 fn set_new(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         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
@@ -18,7 +18,7 @@ fn set_new(b: &mut Bencher<'_>) {
 }
 
 fn iter_set(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         const LEN: usize = 100_000;
         let set = PySet::new(py, 0..LEN).unwrap();
         let mut sum = 0;
@@ -32,7 +32,7 @@ fn iter_set(b: &mut Bencher<'_>) {
 }
 
 fn extract_hashset(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         const LEN: usize = 100_000;
         let any = PySet::new(py, 0..LEN).unwrap().into_any();
         b.iter_with_large_drop(|| black_box(&any).extract::>());
@@ -40,7 +40,7 @@ fn extract_hashset(b: &mut Bencher<'_>) {
 }
 
 fn extract_btreeset(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         const LEN: usize = 100_000;
         let any = PySet::new(py, 0..LEN).unwrap().into_any();
         b.iter_with_large_drop(|| black_box(&any).extract::>());
@@ -48,7 +48,7 @@ fn extract_btreeset(b: &mut Bencher<'_>) {
 }
 
 fn extract_hashbrown_set(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         const LEN: usize = 100_000;
         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 cba7573c9d2..d3e37a0e653 100644
--- a/pyo3-benches/benches/bench_tuple.rs
+++ b/pyo3-benches/benches/bench_tuple.rs
@@ -6,7 +6,7 @@ use pyo3::prelude::*;
 use pyo3::types::{PyList, PySequence, PyTuple};
 
 fn iter_tuple(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         const LEN: usize = 100_000;
         let tuple = PyTuple::new(py, 0..LEN).unwrap();
         let mut sum = 0;
@@ -20,14 +20,14 @@ fn iter_tuple(b: &mut Bencher<'_>) {
 }
 
 fn tuple_new(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         const LEN: usize = 50_000;
         b.iter_with_large_drop(|| PyTuple::new(py, 0..LEN).unwrap());
     });
 }
 
 fn tuple_get_item(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         const LEN: usize = 50_000;
         let tuple = PyTuple::new(py, 0..LEN).unwrap();
         let mut sum = 0;
@@ -41,7 +41,7 @@ fn tuple_get_item(b: &mut Bencher<'_>) {
 
 #[cfg(not(any(Py_LIMITED_API, PyPy)))]
 fn tuple_get_item_unchecked(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         const LEN: usize = 50_000;
         let tuple = PyTuple::new(py, 0..LEN).unwrap();
         let mut sum = 0;
@@ -56,7 +56,7 @@ fn tuple_get_item_unchecked(b: &mut Bencher<'_>) {
 }
 
 fn tuple_get_borrowed_item(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         const LEN: usize = 50_000;
         let tuple = PyTuple::new(py, 0..LEN).unwrap();
         let mut sum = 0;
@@ -74,7 +74,7 @@ fn tuple_get_borrowed_item(b: &mut Bencher<'_>) {
 
 #[cfg(not(any(Py_LIMITED_API, PyPy)))]
 fn tuple_get_borrowed_item_unchecked(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         const LEN: usize = 50_000;
         let tuple = PyTuple::new(py, 0..LEN).unwrap();
         let mut sum = 0;
@@ -92,7 +92,7 @@ fn tuple_get_borrowed_item_unchecked(b: &mut Bencher<'_>) {
 }
 
 fn sequence_from_tuple(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         const LEN: usize = 50_000;
         let tuple = PyTuple::new(py, 0..LEN).unwrap().into_any();
         b.iter(|| black_box(&tuple).downcast::().unwrap());
@@ -100,7 +100,7 @@ fn sequence_from_tuple(b: &mut Bencher<'_>) {
 }
 
 fn tuple_new_list(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         const LEN: usize = 50_000;
         let tuple = PyTuple::new(py, 0..LEN).unwrap();
         b.iter_with_large_drop(|| PyList::new(py, tuple.iter_borrowed()));
@@ -108,7 +108,7 @@ fn tuple_new_list(b: &mut Bencher<'_>) {
 }
 
 fn tuple_to_list(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         const LEN: usize = 50_000;
         let tuple = PyTuple::new(py, 0..LEN).unwrap();
         b.iter_with_large_drop(|| tuple.to_list());
@@ -116,7 +116,7 @@ fn tuple_to_list(b: &mut Bencher<'_>) {
 }
 
 fn tuple_into_pyobject(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         b.iter(|| {
             (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12)
                 .into_pyobject(py)
@@ -126,7 +126,7 @@ fn tuple_into_pyobject(b: &mut Bencher<'_>) {
 }
 
 fn tuple_nth(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         const LEN: usize = 50;
         let list = PyTuple::new(py, 0..LEN).unwrap();
         let mut sum = 0;
@@ -139,7 +139,7 @@ fn tuple_nth(b: &mut Bencher<'_>) {
 }
 
 fn tuple_nth_back(b: &mut Bencher<'_>) {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         const LEN: usize = 50;
         let list = PyTuple::new(py, 0..LEN).unwrap();
         let mut sum = 0;
diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs
index 8d70ee49b0b..70aa813b143 100644
--- a/pyo3-macros-backend/src/pymethod.rs
+++ b/pyo3-macros-backend/src/pymethod.rs
@@ -444,7 +444,7 @@ fn impl_traverse_slot(
         return Err(syn::Error::new_spanned(py_arg.ty, "__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."));
+            inside implementations of `__traverse__`, i.e. `Python::attach` will panic."));
     }
 
     // check that the receiver does not try to smuggle an (implicit) `Python` token into here
@@ -458,7 +458,7 @@ fn impl_traverse_slot(
             "__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."
+            inside implementations of `__traverse__`, i.e. `Python::attach` will panic."
         }
     }
 
diff --git a/pytests/src/misc.rs b/pytests/src/misc.rs
index 7beafe78020..5ac6dcd5540 100644
--- a/pytests/src/misc.rs
+++ b/pytests/src/misc.rs
@@ -6,7 +6,7 @@ use pyo3::{
 #[pyfunction]
 fn issue_219() {
     // issue 219: acquiring GIL inside #[pyfunction] deadlocks.
-    Python::with_gil(|_| {});
+    Python::attach(|_| {});
 }
 
 #[pyclass]
@@ -24,7 +24,7 @@ fn hammer_gil_in_thread() -> LockHolder {
         // 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| ());
+            Python::attach(|_py| ());
         }
     });
     LockHolder { sender }
diff --git a/src/buffer.rs b/src/buffer.rs
index 6f74b698de7..6bef44cfa21 100644
--- a/src/buffer.rs
+++ b/src/buffer.rs
@@ -622,7 +622,7 @@ impl PyBuffer {
 
 impl Drop for PyBuffer {
     fn drop(&mut self) {
-        Python::with_gil(|_| unsafe { ffi::PyBuffer_Release(&mut *self.0) });
+        Python::attach(|_| unsafe { ffi::PyBuffer_Release(&mut *self.0) });
     }
 }
 
@@ -684,7 +684,7 @@ mod tests {
 
     #[test]
     fn test_debug() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let bytes = py.eval(ffi::c_str!("b'abcde'"), None, None).unwrap();
             let buffer: PyBuffer = PyBuffer::get(&bytes).unwrap();
             let expected = format!(
@@ -845,7 +845,7 @@ mod tests {
 
     #[test]
     fn test_bytes_buffer() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let bytes = py.eval(ffi::c_str!("b'abcde'"), None, None).unwrap();
             let buffer = PyBuffer::get(&bytes).unwrap();
             assert_eq!(buffer.dimensions(), 1);
@@ -877,7 +877,7 @@ mod tests {
 
     #[test]
     fn test_array_buffer() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let array = py
                 .import("array")
                 .unwrap()
diff --git a/src/call.rs b/src/call.rs
index cf9bb16ca3d..229e1002456 100644
--- a/src/call.rs
+++ b/src/call.rs
@@ -240,7 +240,7 @@ mod tests {
             wrap_pyfunction, Py, Python,
         };
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let f = wrap_pyfunction!(args_kwargs, py).unwrap();
 
             let args = PyTuple::new(py, [1, 2, 3]).unwrap();
@@ -282,7 +282,7 @@ mod tests {
             wrap_pyfunction, Py, Python,
         };
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let f = wrap_pyfunction!(args_kwargs, py).unwrap();
 
             let args = PyTuple::new(py, [1, 2, 3]).unwrap();
diff --git a/src/conversion.rs b/src/conversion.rs
index 5fba9b8dc30..9c264145667 100644
--- a/src/conversion.rs
+++ b/src/conversion.rs
@@ -244,7 +244,7 @@ impl<'py, T> IntoPyObjectExt<'py> for T where T: IntoPyObject<'py> {}
 /// use pyo3::types::PyString;
 ///
 /// # fn main() -> PyResult<()> {
-/// Python::with_gil(|py| {
+/// Python::attach(|py| {
 ///     // Calling `.extract()` on a `Bound` smart pointer
 ///     let obj: Bound<'_, PyString> = PyString::new(py, "blah");
 ///     let s: String = obj.extract()?;
@@ -438,7 +438,7 @@ impl<'py> IntoPyObject<'py> for () {
 ///
 /// let t = TestClass { num: 10 };
 ///
-/// Python::with_gil(|py| {
+/// Python::attach(|py| {
 ///     let pyvalue = Py::new(py, t).unwrap().to_object(py);
 ///     let t: TestClass = pyvalue.extract(py).unwrap();
 /// })
diff --git a/src/conversions/anyhow.rs b/src/conversions/anyhow.rs
index 0bf346d835a..5cae107b5b3 100644
--- a/src/conversions/anyhow.rs
+++ b/src/conversions/anyhow.rs
@@ -43,7 +43,7 @@
 //! }
 //!
 //! fn main() {
-//!     let error = Python::with_gil(|py| -> PyResult> {
+//!     let error = Python::attach(|py| -> PyResult> {
 //!         let fun = wrap_pyfunction!(py_open, py)?;
 //!         let text = fun.call1(("foo.txt",))?.extract::>()?;
 //!         Ok(text)
@@ -70,7 +70,7 @@
 //!     // An arbitrary example of a Python api you
 //!     // could call inside an application...
 //!     // This might return a `PyErr`.
-//!     let res = Python::with_gil(|py| {
+//!     let res = Python::attach(|py| {
 //!         let zlib = PyModule::import(py, "zlib")?;
 //!         let decompress = zlib.getattr("decompress")?;
 //!         let bytes = PyBytes::new(py, bytes);
@@ -144,7 +144,7 @@ mod test_anyhow {
         let expected_contents = format!("{err:?}");
         let pyerr = PyErr::from(err);
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let locals = [("err", pyerr)].into_py_dict(py).unwrap();
             let pyerr = py
                 .run(ffi::c_str!("raise err"), None, Some(&locals))
@@ -163,7 +163,7 @@ mod test_anyhow {
         let expected_contents = format!("{err:?}");
         let pyerr = PyErr::from(err);
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let locals = [("err", pyerr)].into_py_dict(py).unwrap();
             let pyerr = py
                 .run(ffi::c_str!("raise err"), None, Some(&locals))
@@ -177,7 +177,7 @@ mod test_anyhow {
         let origin_exc = PyValueError::new_err("Value Error");
         let err: anyhow::Error = origin_exc.into();
         let converted: PyErr = err.into();
-        assert!(Python::with_gil(
+        assert!(Python::attach(
             |py| converted.is_instance_of::(py)
         ))
     }
@@ -187,7 +187,7 @@ mod test_anyhow {
         let mut err: anyhow::Error = origin_exc.into();
         err = err.context("Context");
         let converted: PyErr = err.into();
-        assert!(Python::with_gil(
+        assert!(Python::attach(
             |py| converted.is_instance_of::(py)
         ))
     }
diff --git a/src/conversions/bigdecimal.rs b/src/conversions/bigdecimal.rs
index 576e7c61bf2..809cd167329 100644
--- a/src/conversions/bigdecimal.rs
+++ b/src/conversions/bigdecimal.rs
@@ -120,7 +120,7 @@ mod test_bigdecimal {
         ($name:ident, $rs:expr, $py:literal) => {
             #[test]
             fn $name() {
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let rs_orig = $rs;
                     let rs_dec = rs_orig.clone().into_pyobject(py).unwrap();
                     let locals = PyDict::new(py);
@@ -173,7 +173,7 @@ mod test_bigdecimal {
             number in 0..28u32
         ) {
             let num = BigDecimal::from(number);
-            Python::with_gil(|py| {
+            Python::attach(|py| {
                 let rs_dec = num.clone().into_pyobject(py).unwrap();
                 let locals = PyDict::new(py);
                 locals.set_item("rs_dec", &rs_dec).unwrap();
@@ -188,7 +188,7 @@ mod test_bigdecimal {
 
         #[test]
         fn test_integers(num in any::()) {
-            Python::with_gil(|py| {
+            Python::attach(|py| {
                 let py_num = num.into_pyobject(py).unwrap();
                 let roundtripped: BigDecimal = py_num.extract().unwrap();
                 let rs_dec = BigDecimal::from(num);
@@ -199,7 +199,7 @@ mod test_bigdecimal {
 
     #[test]
     fn test_nan() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let locals = PyDict::new(py);
             py.run(
                 ffi::c_str!("import decimal\npy_dec = decimal.Decimal(\"NaN\")"),
@@ -215,7 +215,7 @@ mod test_bigdecimal {
 
     #[test]
     fn test_infinity() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let locals = PyDict::new(py);
             py.run(
                 ffi::c_str!("import decimal\npy_dec = decimal.Decimal(\"Infinity\")"),
@@ -231,7 +231,7 @@ mod test_bigdecimal {
 
     #[test]
     fn test_no_precision_loss() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let src = "1e4";
             let expected = get_decimal_cls(py)
                 .unwrap()
diff --git a/src/conversions/chrono.rs b/src/conversions/chrono.rs
index fab734422f2..7036d2098bf 100644
--- a/src/conversions/chrono.rs
+++ b/src/conversions/chrono.rs
@@ -24,7 +24,7 @@
 //!
 //! fn main() -> PyResult<()> {
 //!     pyo3::prepare_freethreaded_python();
-//!     Python::with_gil(|py| {
+//!     Python::attach(|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);
@@ -606,7 +606,7 @@ mod tests {
         use crate::types::any::PyAnyMethods;
         use crate::types::dict::PyDictMethods;
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let locals = crate::types::PyDict::new(py);
             py.run(
                 ffi::c_str!("import zoneinfo; zi = zoneinfo.ZoneInfo('Europe/London')"),
@@ -627,7 +627,7 @@ mod tests {
     fn test_timezone_aware_to_naive_fails() {
         // Test that if a user tries to convert a python's timezone aware datetime into a naive
         // one, the conversion fails.
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let py_datetime =
                 new_py_datetime_ob(py, "datetime", (2022, 1, 1, 1, 0, 0, 0, python_utc(py)));
             // Now test that converting a PyDateTime with tzinfo to a NaiveDateTime fails
@@ -643,7 +643,7 @@ mod tests {
     fn test_naive_to_timezone_aware_fails() {
         // Test that if a user tries to convert a python's timezone aware datetime into a naive
         // one, the conversion fails.
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let py_datetime = new_py_datetime_ob(py, "datetime", (2022, 1, 1, 1, 0, 0, 0));
             // Now test that converting a PyDateTime with tzinfo to a NaiveDateTime fails
             let res: PyResult> = py_datetime.extract();
@@ -665,7 +665,7 @@ mod tests {
     fn test_invalid_types_fail() {
         // Test that if a user tries to convert a python's timezone aware datetime into a naive
         // one, the conversion fails.
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let none = py.None().into_bound(py);
             assert_eq!(
                 none.extract::().unwrap_err().to_string(),
@@ -709,7 +709,7 @@ mod tests {
         // 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| {
+            Python::attach(|py| {
                 let delta = delta.into_pyobject(py).unwrap();
                 let py_delta = new_py_datetime_ob(py, "timedelta", (py_days, py_seconds, py_ms));
                 assert!(
@@ -732,7 +732,7 @@ mod tests {
         check("delta max value", delta, 999999999, 86399, 999999);
 
         // Also check that trying to convert an out of bound value errors.
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             // min_value and max_value were deprecated in chrono 0.4.39
             #[allow(deprecated)]
             {
@@ -747,7 +747,7 @@ mod tests {
         // 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| {
+            Python::attach(|py| {
                 let py_delta = new_py_datetime_ob(py, "timedelta", (py_days, py_seconds, py_ms));
                 let py_delta: Duration = py_delta.extract().unwrap();
                 assert_eq!(py_delta, delta, "{name}: {py_delta} != {delta}");
@@ -774,7 +774,7 @@ mod tests {
 
         // This check is to assert that we can't construct every possible Duration from a PyDelta
         // since they have different bounds.
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let low_days: i32 = -1000000000;
             // This is possible
             assert!(panic::catch_unwind(|| Duration::days(low_days as i64)).is_ok());
@@ -804,7 +804,7 @@ mod tests {
     #[test]
     fn test_pyo3_date_into_pyobject() {
         let eq_ymd = |name: &'static str, year, month, day| {
-            Python::with_gil(|py| {
+            Python::attach(|py| {
                 let date = NaiveDate::from_ymd_opt(year, month, day)
                     .unwrap()
                     .into_pyobject(py)
@@ -827,7 +827,7 @@ mod tests {
     #[test]
     fn test_pyo3_date_frompyobject() {
         let eq_ymd = |name: &'static str, year, month, day| {
-            Python::with_gil(|py| {
+            Python::attach(|py| {
                 let py_date = new_py_datetime_ob(py, "date", (year, month, day));
                 let py_date: NaiveDate = py_date.extract().unwrap();
                 let date = NaiveDate::from_ymd_opt(year, month, day).unwrap();
@@ -843,7 +843,7 @@ mod tests {
 
     #[test]
     fn test_pyo3_datetime_into_pyobject_utc() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let check_utc =
                 |name: &'static str, year, month, day, hour, minute, second, ms, py_ms| {
                     let datetime = NaiveDate::from_ymd_opt(year, month, day)
@@ -889,7 +889,7 @@ mod tests {
 
     #[test]
     fn test_pyo3_datetime_into_pyobject_fixed_offset() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let check_fixed_offset =
                 |name: &'static str, year, month, day, hour, minute, second, ms, py_ms| {
                     let offset = FixedOffset::east_opt(3600).unwrap();
@@ -930,7 +930,7 @@ mod tests {
     #[test]
     #[cfg(all(Py_3_9, feature = "chrono-tz", not(windows)))]
     fn test_pyo3_datetime_into_pyobject_tz() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let datetime = NaiveDate::from_ymd_opt(2024, 12, 11)
                 .unwrap()
                 .and_hms_opt(23, 3, 13)
@@ -958,7 +958,7 @@ mod tests {
 
     #[test]
     fn test_pyo3_datetime_frompyobject_utc() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let year = 2014;
             let month = 5;
             let day = 6;
@@ -984,7 +984,7 @@ mod tests {
 
     #[test]
     fn test_pyo3_datetime_frompyobject_fixed_offset() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let year = 2014;
             let month = 5;
             let day = 6;
@@ -1027,7 +1027,7 @@ mod tests {
 
     #[test]
     fn test_pyo3_offset_fixed_into_pyobject() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             // Chrono offset
             let offset = FixedOffset::east_opt(3600)
                 .unwrap()
@@ -1052,7 +1052,7 @@ mod tests {
 
     #[test]
     fn test_pyo3_offset_fixed_frompyobject() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let py_timedelta = new_py_datetime_ob(py, "timedelta", (0, 3600, 0));
             let py_tzinfo = new_py_datetime_ob(py, "timezone", (py_timedelta,));
             let offset: FixedOffset = py_tzinfo.extract().unwrap();
@@ -1062,7 +1062,7 @@ mod tests {
 
     #[test]
     fn test_pyo3_offset_utc_into_pyobject() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let utc = Utc.into_pyobject(py).unwrap();
             let py_utc = python_utc(py);
             assert!(utc.is(&py_utc));
@@ -1071,7 +1071,7 @@ mod tests {
 
     #[test]
     fn test_pyo3_offset_utc_frompyobject() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let py_utc = python_utc(py);
             let py_utc: Utc = py_utc.extract().unwrap();
             assert_eq!(Utc, py_utc);
@@ -1089,7 +1089,7 @@ mod tests {
 
     #[test]
     fn test_pyo3_time_into_pyobject() {
-        Python::with_gil(|py| {
+        Python::attach(|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()
@@ -1119,7 +1119,7 @@ mod tests {
         let minute = 5;
         let second = 7;
         let micro = 999_999;
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let py_time = new_py_datetime_ob(py, "time", (hour, minute, second, micro));
             let py_time: NaiveTime = py_time.extract().unwrap();
             let time = NaiveTime::from_hms_micro_opt(hour, minute, second, micro).unwrap();
@@ -1176,7 +1176,7 @@ mod tests {
             // Range is limited to 1970 to 2038 due to windows limitations
             #[test]
             fn test_pyo3_offset_fixed_frompyobject_created_in_python(timestamp in 0..(i32::MAX as i64), timedelta in -86399i32..=86399i32) {
-                Python::with_gil(|py| {
+                Python::attach(|py| {
 
                     let globals = [("datetime", py.import("datetime").unwrap())].into_py_dict(py).unwrap();
                     let code = format!("datetime.datetime.fromtimestamp({timestamp}).replace(tzinfo=datetime.timezone(datetime.timedelta(seconds={timedelta})))");
@@ -1203,7 +1203,7 @@ mod tests {
             fn test_duration_roundtrip(days in -999999999i64..=999999999i64) {
                 // Test roundtrip conversion rust->python->rust for all allowed
                 // python values of durations (from -999999999 to 999999999 days),
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let dur = Duration::days(days);
                     let py_delta = dur.into_pyobject(py).unwrap();
                     let roundtripped: Duration = py_delta.extract().expect("Round trip");
@@ -1213,7 +1213,7 @@ mod tests {
 
             #[test]
             fn test_fixed_offset_roundtrip(secs in -86399i32..=86399i32) {
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let offset = FixedOffset::east_opt(secs).unwrap();
                     let py_offset = offset.into_pyobject(py).unwrap();
                     let roundtripped: FixedOffset = py_offset.extract().expect("Round trip");
@@ -1229,7 +1229,7 @@ mod tests {
             ) {
                 // Test roundtrip conversion rust->python->rust for all allowed
                 // python dates (from year 1 to year 9999)
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     // 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) {
@@ -1251,7 +1251,7 @@ mod tests {
                 // Python time has a resolution of microseconds, so we only test
                 // NaiveTimes with microseconds resolution, even if NaiveTime has nanosecond
                 // resolution.
-                Python::with_gil(|py| {
+                Python::attach(|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, |_| time.into_pyobject(py)).unwrap();
@@ -1273,7 +1273,7 @@ mod tests {
                 sec in 0u32..=60u32,
                 micro in 0u32..=999_999u32
             ) {
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let date_opt = NaiveDate::from_ymd_opt(year, month, day);
                     let time_opt = NaiveTime::from_hms_micro_opt(hour, min, sec, micro);
                     if let (Some(date), Some(time)) = (date_opt, time_opt) {
@@ -1295,7 +1295,7 @@ mod tests {
                 sec in 0u32..=59u32,
                 micro in 0u32..=1_999_999u32
             ) {
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let date_opt = NaiveDate::from_ymd_opt(year, month, day);
                     let time_opt = NaiveTime::from_hms_micro_opt(hour, min, sec, micro);
                     if let (Some(date), Some(time)) = (date_opt, time_opt) {
@@ -1322,7 +1322,7 @@ mod tests {
                 micro in 0u32..=1_999_999u32,
                 offset_secs in -86399i32..=86399i32
             ) {
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let date_opt = NaiveDate::from_ymd_opt(year, month, day);
                     let time_opt = NaiveTime::from_hms_micro_opt(hour, min, sec, micro);
                     let offset = FixedOffset::east_opt(offset_secs).unwrap();
@@ -1350,7 +1350,7 @@ mod tests {
                 sec in 0u32..=59u32,
                 micro in 0u32..=1_999_999u32,
             ) {
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let date_opt = NaiveDate::from_ymd_opt(year, month, day);
                     let time_opt = NaiveTime::from_hms_micro_opt(hour, min, sec, micro);
                     if let (Some(date), Some(time)) = (date_opt, time_opt) {
diff --git a/src/conversions/chrono_tz.rs b/src/conversions/chrono_tz.rs
index e8e5dc60e7a..e619c95c6fa 100644
--- a/src/conversions/chrono_tz.rs
+++ b/src/conversions/chrono_tz.rs
@@ -25,7 +25,7 @@
 //!
 //! fn main() -> PyResult<()> {
 //!     pyo3::prepare_freethreaded_python();
-//!     Python::with_gil(|py| {
+//!     Python::attach(|py| {
 //!         // Convert to Python
 //!         let py_tzinfo = Tz::Europe__Paris.into_pyobject(py)?;
 //!         // Convert back to Rust
@@ -84,7 +84,7 @@ mod tests {
 
     #[test]
     fn test_frompyobject() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             assert_eq!(
                 new_zoneinfo(py, "Europe/Paris").extract::().unwrap(),
                 Tz::Europe__Paris
@@ -116,7 +116,7 @@ mod tests {
             ]
         );
 
-        let dates = Python::with_gil(|py| {
+        let dates = Python::attach(|py| {
             let pydates = dates.map(|dt| dt.into_pyobject(py).unwrap());
             assert_eq!(
                 pydates
@@ -148,7 +148,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| {
+        Python::attach(|py| {
             let assert_eq = |l: Bound<'_, PyTzInfo>, r: Bound<'_, PyTzInfo>| {
                 assert!(l.eq(&r).unwrap(), "{l:?} != {r:?}");
             };
diff --git a/src/conversions/either.rs b/src/conversions/either.rs
index 7e8e3bfc2dc..6db97dc30a3 100644
--- a/src/conversions/either.rs
+++ b/src/conversions/either.rs
@@ -30,7 +30,7 @@
 //!
 //! fn main() -> PyResult<()> {
 //!     pyo3::prepare_freethreaded_python();
-//!     Python::with_gil(|py| {
+//!     Python::attach(|py| {
 //!         // Create a string and an int in Python.
 //!         let py_str = "crab".into_pyobject(py)?;
 //!         let py_int = 42i32.into_pyobject(py)?;
@@ -134,7 +134,7 @@ mod tests {
         type E1 = Either;
         type E2 = Either;
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let l = E::Left(42);
             let obj_l = (&l).into_pyobject(py).unwrap();
             assert_eq!(obj_l.extract::().unwrap(), 42);
diff --git a/src/conversions/eyre.rs b/src/conversions/eyre.rs
index 4a501de9c69..1ba8ec55152 100644
--- a/src/conversions/eyre.rs
+++ b/src/conversions/eyre.rs
@@ -45,7 +45,7 @@
 //! }
 //!
 //! fn main() {
-//!     let error = Python::with_gil(|py| -> PyResult> {
+//!     let error = Python::attach(|py| -> PyResult> {
 //!         let fun = wrap_pyfunction!(py_open, py)?;
 //!         let text = fun.call1(("foo.txt",))?.extract::>()?;
 //!         Ok(text)
@@ -72,7 +72,7 @@
 //!     // An arbitrary example of a Python api you
 //!     // could call inside an application...
 //!     // This might return a `PyErr`.
-//!     let res = Python::with_gil(|py| {
+//!     let res = Python::attach(|py| {
 //!         let zlib = PyModule::import(py, "zlib")?;
 //!         let decompress = zlib.getattr("decompress")?;
 //!         let bytes = PyBytes::new(py, bytes);
@@ -150,7 +150,7 @@ mod tests {
         let expected_contents = format!("{err:?}");
         let pyerr = PyErr::from(err);
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let locals = [("err", pyerr)].into_py_dict(py).unwrap();
             let pyerr = py
                 .run(ffi::c_str!("raise err"), None, Some(&locals))
@@ -169,7 +169,7 @@ mod tests {
         let expected_contents = format!("{err:?}");
         let pyerr = PyErr::from(err);
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let locals = [("err", pyerr)].into_py_dict(py).unwrap();
             let pyerr = py
                 .run(ffi::c_str!("raise err"), None, Some(&locals))
@@ -183,7 +183,7 @@ mod tests {
         let origin_exc = PyValueError::new_err("Value Error");
         let report: Report = origin_exc.into();
         let converted: PyErr = report.into();
-        assert!(Python::with_gil(
+        assert!(Python::attach(
             |py| converted.is_instance_of::(py)
         ))
     }
@@ -193,7 +193,7 @@ mod tests {
         let mut report: Report = origin_exc.into();
         report = report.wrap_err("Wrapped");
         let converted: PyErr = report.into();
-        assert!(Python::with_gil(
+        assert!(Python::attach(
             |py| converted.is_instance_of::(py)
         ))
     }
diff --git a/src/conversions/hashbrown.rs b/src/conversions/hashbrown.rs
index d9972a68865..fe3cf1e00d3 100644
--- a/src/conversions/hashbrown.rs
+++ b/src/conversions/hashbrown.rs
@@ -138,7 +138,7 @@ mod tests {
 
     #[test]
     fn test_hashbrown_hashmap_into_pyobject() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut map =
                 hashbrown::HashMap::::with_hasher(RandomState::new());
             map.insert(1, 1);
@@ -161,7 +161,7 @@ mod tests {
 
     #[test]
     fn test_hashbrown_hashmap_into_dict() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut map =
                 hashbrown::HashMap::::with_hasher(RandomState::new());
             map.insert(1, 1);
@@ -183,7 +183,7 @@ mod tests {
 
     #[test]
     fn test_extract_hashbrown_hashset() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             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());
@@ -196,7 +196,7 @@ mod tests {
 
     #[test]
     fn test_hashbrown_hashset_into_pyobject() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let hs: hashbrown::HashSet =
                 [1, 2, 3, 4, 5].iter().cloned().collect();
 
diff --git a/src/conversions/indexmap.rs b/src/conversions/indexmap.rs
index d6995c14db2..f9244b1b1d9 100644
--- a/src/conversions/indexmap.rs
+++ b/src/conversions/indexmap.rs
@@ -154,7 +154,7 @@ mod test_indexmap {
 
     #[test]
     fn test_indexmap_indexmap_into_pyobject() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut map = indexmap::IndexMap::::new();
             map.insert(1, 1);
 
@@ -179,7 +179,7 @@ mod test_indexmap {
 
     #[test]
     fn test_indexmap_indexmap_into_dict() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut map = indexmap::IndexMap::::new();
             map.insert(1, 1);
 
@@ -200,7 +200,7 @@ mod test_indexmap {
 
     #[test]
     fn test_indexmap_indexmap_insertion_order_round_trip() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let n = 20;
             let mut map = indexmap::IndexMap::::new();
 
diff --git a/src/conversions/jiff.rs b/src/conversions/jiff.rs
index 83cacc83fe7..2976139bcd8 100644
--- a/src/conversions/jiff.rs
+++ b/src/conversions/jiff.rs
@@ -29,7 +29,7 @@
 //! # #[cfg(not(windows))]
 //! fn main() -> PyResult<()> {
 //!     pyo3::prepare_freethreaded_python();
-//!     Python::with_gil(|py| {
+//!     Python::attach(|py| {
 //!         // Build some jiff values
 //!         let jiff_zoned = Zoned::now();
 //!         let jiff_span = 1.second();
@@ -480,7 +480,7 @@ mod tests {
         use crate::types::any::PyAnyMethods;
         use crate::types::dict::PyDictMethods;
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let locals = crate::types::PyDict::new(py);
             py.run(
                 ffi::c_str!("import zoneinfo; zi = zoneinfo.ZoneInfo('Europe/London')"),
@@ -501,7 +501,7 @@ mod tests {
     fn test_timezone_aware_to_naive_fails() {
         // Test that if a user tries to convert a python's timezone aware datetime into a naive
         // one, the conversion fails.
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let py_datetime =
                 new_py_datetime_ob(py, "datetime", (2022, 1, 1, 1, 0, 0, 0, python_utc(py)));
             // Now test that converting a PyDateTime with tzinfo to a NaiveDateTime fails
@@ -517,7 +517,7 @@ mod tests {
     fn test_naive_to_timezone_aware_fails() {
         // Test that if a user tries to convert a python's naive datetime into a timezone aware
         // one, the conversion fails.
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let py_datetime = new_py_datetime_ob(py, "datetime", (2022, 1, 1, 1, 0, 0, 0));
             let res: PyResult = py_datetime.extract();
             assert_eq!(
@@ -529,7 +529,7 @@ mod tests {
 
     #[test]
     fn test_invalid_types_fail() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let none = py.None().into_bound(py);
             assert_eq!(
                 none.extract::().unwrap_err().to_string(),
@@ -565,7 +565,7 @@ mod tests {
     #[test]
     fn test_pyo3_date_into_pyobject() {
         let eq_ymd = |name: &'static str, year, month, day| {
-            Python::with_gil(|py| {
+            Python::attach(|py| {
                 let date = Date::new(year, month, day)
                     .unwrap()
                     .into_pyobject(py)
@@ -588,7 +588,7 @@ mod tests {
     #[test]
     fn test_pyo3_date_frompyobject() {
         let eq_ymd = |name: &'static str, year, month, day| {
-            Python::with_gil(|py| {
+            Python::attach(|py| {
                 let py_date = new_py_datetime_ob(py, "date", (year, month, day));
                 let py_date: Date = py_date.extract().unwrap();
                 let date = Date::new(year, month, day).unwrap();
@@ -604,7 +604,7 @@ mod tests {
 
     #[test]
     fn test_pyo3_datetime_into_pyobject_utc() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let check_utc =
                 |name: &'static str, year, month, day, hour, minute, second, ms, py_ms| {
                     let datetime = DateTime::new(year, month, day, hour, minute, second, ms * 1000)
@@ -639,7 +639,7 @@ mod tests {
 
     #[test]
     fn test_pyo3_datetime_into_pyobject_fixed_offset() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let check_fixed_offset =
                 |name: &'static str, year, month, day, hour, minute, second, ms, py_ms| {
                     let offset = Offset::from_seconds(3600).unwrap();
@@ -672,7 +672,7 @@ mod tests {
     #[test]
     #[cfg(all(Py_3_9, not(windows)))]
     fn test_pyo3_datetime_into_pyobject_tz() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let datetime = DateTime::new(2024, 12, 11, 23, 3, 13, 0)
                 .unwrap()
                 .to_zoned(TimeZone::get("Europe/London").unwrap())
@@ -698,7 +698,7 @@ mod tests {
 
     #[test]
     fn test_pyo3_datetime_frompyobject_utc() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let year = 2014;
             let month = 5;
             let day = 6;
@@ -745,7 +745,7 @@ mod tests {
             ]
         );
 
-        let dates = Python::with_gil(|py| {
+        let dates = Python::attach(|py| {
             let pydates = dates.map(|dt| dt.into_pyobject(py).unwrap());
             assert_eq!(
                 pydates
@@ -777,7 +777,7 @@ mod tests {
 
     #[test]
     fn test_pyo3_datetime_frompyobject_fixed_offset() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let year = 2014;
             let month = 5;
             let day = 6;
@@ -803,7 +803,7 @@ mod tests {
 
     #[test]
     fn test_pyo3_offset_fixed_into_pyobject() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             // jiff offset
             let offset = Offset::from_seconds(3600)
                 .unwrap()
@@ -828,7 +828,7 @@ mod tests {
 
     #[test]
     fn test_pyo3_offset_fixed_frompyobject() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let py_timedelta = new_py_datetime_ob(py, "timedelta", (0, 3600, 0));
             let py_tzinfo = new_py_datetime_ob(py, "timezone", (py_timedelta,));
             let offset: Offset = py_tzinfo.extract().unwrap();
@@ -838,7 +838,7 @@ mod tests {
 
     #[test]
     fn test_pyo3_offset_utc_into_pyobject() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let utc = Offset::UTC.into_pyobject(py).unwrap();
             let py_utc = python_utc(py);
             assert!(utc.is(&py_utc));
@@ -847,7 +847,7 @@ mod tests {
 
     #[test]
     fn test_pyo3_offset_utc_frompyobject() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let py_utc = python_utc(py);
             let py_utc: Offset = py_utc.extract().unwrap();
             assert_eq!(Offset::UTC, py_utc);
@@ -865,7 +865,7 @@ mod tests {
 
     #[test]
     fn test_pyo3_time_into_pyobject() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let check_time = |name: &'static str, hour, minute, second, ms, py_ms| {
                 let time = Time::new(hour, minute, second, ms * 1000)
                     .unwrap()
@@ -885,7 +885,7 @@ mod tests {
         let minute = 5;
         let second = 7;
         let micro = 999_999;
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let py_time = new_py_datetime_ob(py, "time", (hour, minute, second, micro));
             let py_time: Time = py_time.extract().unwrap();
             let time = Time::new(hour, minute, second, micro * 1000).unwrap();
@@ -971,7 +971,7 @@ mod tests {
             // Range is limited to 1970 to 2038 due to windows limitations
             #[test]
             fn test_pyo3_offset_fixed_frompyobject_created_in_python(timestamp in 0..(i32::MAX as i64), timedelta in -86399i32..=86399i32) {
-                Python::with_gil(|py| {
+                Python::attach(|py| {
 
                     let globals = [("datetime", py.import("datetime").unwrap())].into_py_dict(py).unwrap();
                     let code = format!("datetime.datetime.fromtimestamp({timestamp}).replace(tzinfo=datetime.timezone(datetime.timedelta(seconds={timedelta})))");
@@ -992,7 +992,7 @@ mod tests {
             fn test_duration_roundtrip(days in -999999999i64..=999999999i64) {
                 // Test roundtrip conversion rust->python->rust for all allowed
                 // python values of durations (from -999999999 to 999999999 days),
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let dur = SignedDuration::new(days * 24 * 60 * 60, 0);
                     let py_delta = dur.into_pyobject(py).unwrap();
                     let roundtripped: SignedDuration = py_delta.extract().expect("Round trip");
@@ -1004,7 +1004,7 @@ mod tests {
             fn test_span_roundtrip(days in -999999999i64..=999999999i64) {
                 // Test roundtrip conversion rust->python->rust for all allowed
                 // python values of durations (from -999999999 to 999999999 days),
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     if let Ok(span) = Span::new().try_days(days) {
                         let relative_to = SpanRelativeTo::days_are_24_hours();
                         let jiff_duration = span.to_duration(relative_to).unwrap();
@@ -1017,7 +1017,7 @@ mod tests {
 
             #[test]
             fn test_fixed_offset_roundtrip(secs in -86399i32..=86399i32) {
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let offset = Offset::from_seconds(secs).unwrap();
                     let py_offset = offset.into_pyobject(py).unwrap();
                     let roundtripped: Offset = py_offset.extract().expect("Round trip");
@@ -1033,7 +1033,7 @@ mod tests {
             ) {
                 // Test roundtrip conversion rust->python->rust for all allowed
                 // python dates (from year 1 to year 9999)
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     if let Ok(date) = try_date(year, month, day) {
                         let py_date = date.into_pyobject(py).unwrap();
                         let roundtripped: Date = py_date.extract().expect("Round trip");
@@ -1049,7 +1049,7 @@ mod tests {
                 sec in 0u32..=59u32,
                 micro in 0u32..=1_999_999u32
             ) {
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     if let Ok(time) = try_time(hour, min, sec, micro) {
                         let py_time = time.into_pyobject(py).unwrap();
                         let roundtripped: Time = py_time.extract().expect("Round trip");
@@ -1068,7 +1068,7 @@ mod tests {
                 sec in 0u32..=60u32,
                 micro in 0u32..=999_999u32
             ) {
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let date_opt = try_date(year, month, day);
                     let time_opt = try_time(hour, min, sec, micro);
                     if let (Ok(date), Ok(time)) = (date_opt, time_opt) {
@@ -1090,7 +1090,7 @@ mod tests {
                 sec in 0u32..=59u32,
                 micro in 0u32..=1_999_999u32
             ) {
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let date_opt = try_date(year, month, day);
                     let time_opt = try_time(hour, min, sec, micro);
                     if let (Ok(date), Ok(time)) = (date_opt, time_opt) {
@@ -1113,7 +1113,7 @@ mod tests {
                 micro in 0u32..=1_999_999u32,
                 offset_secs in -86399i32..=86399i32
             ) {
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let date_opt = try_date(year, month, day);
                     let time_opt = try_time(hour, min, sec, micro);
                     let offset = Offset::from_seconds(offset_secs).unwrap();
@@ -1138,7 +1138,7 @@ mod tests {
                 min in 0u32..=59u32,
             ) {
 
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let transition_moment = transition.timestamp();
                     let zoned = (transition_moment - Span::new().hours(hour).minutes(min))
                         .to_zoned(timezone.clone());
diff --git a/src/conversions/num_bigint.rs b/src/conversions/num_bigint.rs
index 7dadf842c84..73c6ef44bee 100644
--- a/src/conversions/num_bigint.rs
+++ b/src/conversions/num_bigint.rs
@@ -346,7 +346,7 @@ mod tests {
 
     #[test]
     fn convert_biguint() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             // 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
@@ -359,7 +359,7 @@ mod tests {
 
     #[test]
     fn convert_bigint() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             // 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
@@ -401,7 +401,7 @@ mod tests {
 
     #[test]
     fn convert_index_class() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let index = python_index_class(py);
             let locals = PyDict::new(py);
             locals.set_item("index", index).unwrap();
@@ -414,7 +414,7 @@ mod tests {
 
     #[test]
     fn handle_zero() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let zero: BigInt = 0i32.into_pyobject(py).unwrap().extract().unwrap();
             assert_eq!(zero, BigInt::from(0));
         })
@@ -423,7 +423,7 @@ mod tests {
     /// `OverflowError` on converting Python int to BigInt, see issue #629
     #[test]
     fn check_overflow() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             macro_rules! test {
                 ($T:ty, $value:expr, $py:expr) => {
                     let value = $value;
diff --git a/src/conversions/num_complex.rs b/src/conversions/num_complex.rs
index e9fb1096ff5..6df4db5353b 100644
--- a/src/conversions/num_complex.rs
+++ b/src/conversions/num_complex.rs
@@ -54,7 +54,7 @@
 //! # use pyo3::types::PyComplex;
 //! #
 //! # fn main() -> PyResult<()> {
-//! #     Python::with_gil(|py| -> PyResult<()> {
+//! #     Python::attach(|py| -> PyResult<()> {
 //! #         let module = PyModule::new(py, "my_module")?;
 //! #
 //! #         module.add_function(&wrap_pyfunction!(get_eigenvalues, module)?)?;
@@ -204,7 +204,7 @@ mod tests {
 
     #[test]
     fn from_complex() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let complex = Complex::new(3.0, 1.2);
             let py_c = PyComplex::from_complex_bound(py, complex);
             assert_eq!(py_c.real(), 3.0);
@@ -213,7 +213,7 @@ mod tests {
     }
     #[test]
     fn to_from_complex() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let val = Complex::new(3.0f64, 1.2);
             let obj = val.into_pyobject(py).unwrap();
             assert_eq!(obj.extract::>().unwrap(), val);
@@ -221,14 +221,14 @@ mod tests {
     }
     #[test]
     fn from_complex_err() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let obj = vec![1i32].into_pyobject(py).unwrap();
             assert!(obj.extract::>().is_err());
         });
     }
     #[test]
     fn from_python_magic() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let module = PyModule::from_code(
                 py,
                 c_str!(
@@ -268,7 +268,7 @@ class C:
     }
     #[test]
     fn from_python_inherited_magic() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let module = PyModule::from_code(
                 py,
                 c_str!(
@@ -314,7 +314,7 @@ class C(First, IndexMixin): pass
         // Functions and lambdas implement the descriptor protocol in a way that makes
         // `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| {
+        Python::attach(|py| {
             let module = PyModule::from_code(
                 py,
                 c_str!(
@@ -339,7 +339,7 @@ class A:
     #[test]
     fn from_python_nondescriptor_magic() {
         // Magic methods don't need to implement the descriptor protocol, if they're callable.
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let module = PyModule::from_code(
                 py,
                 c_str!(
diff --git a/src/conversions/num_rational.rs b/src/conversions/num_rational.rs
index dfdd80cf54f..8371c48f838 100644
--- a/src/conversions/num_rational.rs
+++ b/src/conversions/num_rational.rs
@@ -121,7 +121,7 @@ mod tests {
     use proptest::prelude::*;
     #[test]
     fn test_negative_fraction() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let locals = PyDict::new(py);
             py.run(
                 ffi::c_str!("import fractions\npy_frac = fractions.Fraction(-0.125)"),
@@ -137,7 +137,7 @@ mod tests {
     }
     #[test]
     fn test_obj_with_incorrect_atts() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let locals = PyDict::new(py);
             py.run(
                 ffi::c_str!("not_fraction = \"contains_incorrect_atts\""),
@@ -152,7 +152,7 @@ mod tests {
 
     #[test]
     fn test_fraction_with_fraction_type() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let locals = PyDict::new(py);
             py.run(
                 ffi::c_str!(
@@ -171,7 +171,7 @@ mod tests {
 
     #[test]
     fn test_fraction_with_decimal() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let locals = PyDict::new(py);
             py.run(
                 ffi::c_str!("import fractions\n\nfrom decimal import Decimal\npy_frac = fractions.Fraction(Decimal(\"1.1\"))"),
@@ -188,7 +188,7 @@ mod tests {
 
     #[test]
     fn test_fraction_with_num_den() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let locals = PyDict::new(py);
             py.run(
                 ffi::c_str!("import fractions\npy_frac = fractions.Fraction(10,5)"),
@@ -206,7 +206,7 @@ mod tests {
     #[cfg(target_arch = "wasm32")]
     #[test]
     fn test_int_roundtrip() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let rs_frac = Ratio::new(1i32, 2);
             let py_frac = rs_frac.into_pyobject(py).unwrap();
             let roundtripped: Ratio = py_frac.extract().unwrap();
@@ -218,7 +218,7 @@ mod tests {
     #[cfg(target_arch = "wasm32")]
     #[test]
     fn test_big_int_roundtrip() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let rs_frac = Ratio::from_float(5.5).unwrap();
             let py_frac = rs_frac.clone().into_pyobject(py).unwrap();
             let roundtripped: Ratio = py_frac.extract().unwrap();
@@ -230,7 +230,7 @@ mod tests {
     proptest! {
         #[test]
         fn test_int_roundtrip(num in any::(), den in any::()) {
-            Python::with_gil(|py| {
+            Python::attach(|py| {
                 let rs_frac = Ratio::new(num, den);
                 let py_frac = rs_frac.into_pyobject(py).unwrap();
                 let roundtripped: Ratio = py_frac.extract().unwrap();
@@ -241,7 +241,7 @@ mod tests {
         #[test]
         #[cfg(feature = "num-bigint")]
         fn test_big_int_roundtrip(num in any::()) {
-            Python::with_gil(|py| {
+            Python::attach(|py| {
                 let rs_frac = Ratio::from_float(num).unwrap();
                 let py_frac = rs_frac.clone().into_pyobject(py).unwrap();
                 let roundtripped: Ratio = py_frac.extract().unwrap();
@@ -253,7 +253,7 @@ mod tests {
 
     #[test]
     fn test_infinity() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let locals = PyDict::new(py);
             let py_bound = py.run(
                 ffi::c_str!("import fractions\npy_frac = fractions.Fraction(\"Infinity\")"),
diff --git a/src/conversions/ordered_float.rs b/src/conversions/ordered_float.rs
index 79bce2c66c1..a4da92b5b14 100644
--- a/src/conversions/ordered_float.rs
+++ b/src/conversions/ordered_float.rs
@@ -113,7 +113,7 @@ mod test_ordered_float {
             fn $standard_test(inner_f: $float_type) {
                 let f = $constructor(inner_f);
 
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let f_py: Bound<'_, PyFloat>  = f.into_pyobject(py).unwrap();
 
                     py_run!(
@@ -139,7 +139,7 @@ mod test_ordered_float {
                 let inner_f = 10.0;
                 let f = $constructor(inner_f);
 
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let f_py: Bound<'_, PyFloat>  = f.into_pyobject(py).unwrap();
 
                     py_run!(
@@ -166,7 +166,7 @@ mod test_ordered_float {
                 let inner_ninf = <$float_type>::NEG_INFINITY;
                 let ninf = $constructor(inner_ninf);
 
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let pinf_py: Bound<'_, PyFloat>  = pinf.into_pyobject(py).unwrap();
                     let ninf_py: Bound<'_, PyFloat>  = ninf.into_pyobject(py).unwrap();
 
@@ -194,7 +194,7 @@ mod test_ordered_float {
                 let inner_nzero: $float_type = -0.0;
                 let nzero = $constructor(inner_nzero);
 
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let pzero_py: Bound<'_, PyFloat>  = pzero.into_pyobject(py).unwrap();
                     let nzero_py: Bound<'_, PyFloat>  = nzero.into_pyobject(py).unwrap();
 
@@ -266,7 +266,7 @@ mod test_ordered_float {
                 let inner_nan: $float_type = <$float_type>::NAN;
                 let nan = OrderedFloat(inner_nan);
 
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let nan_py: Bound<'_, PyFloat> = nan.into_pyobject(py).unwrap();
 
                     py_run!(
@@ -291,7 +291,7 @@ mod test_ordered_float {
         ($test_name:ident, $float_type:ty) => {
             #[test]
             fn $test_name() {
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let nan_py = py.eval(c_str!("float('nan')"), None, None).unwrap();
 
                     let nan_rs: PyResult> = nan_py.extract();
@@ -308,7 +308,7 @@ mod test_ordered_float {
         ($test_name:ident, $wrapper:ident, $float_type:ty) => {
             #[test]
             fn $test_name() {
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let py_64 = py
                         .import("sys")
                         .unwrap()
diff --git a/src/conversions/rust_decimal.rs b/src/conversions/rust_decimal.rs
index 8c5d870a2dd..597f469d21a 100644
--- a/src/conversions/rust_decimal.rs
+++ b/src/conversions/rust_decimal.rs
@@ -119,7 +119,7 @@ mod test_rust_decimal {
         ($name:ident, $rs:expr, $py:literal) => {
             #[test]
             fn $name() {
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let rs_orig = $rs;
                     let rs_dec = rs_orig.into_pyobject(py).unwrap();
                     let locals = PyDict::new(py);
@@ -163,7 +163,7 @@ mod test_rust_decimal {
             scale in 0..28u32
         ) {
             let num = Decimal::from_parts(lo, mid, high, negative, scale);
-            Python::with_gil(|py| {
+            Python::attach(|py| {
                 let rs_dec = num.into_pyobject(py).unwrap();
                 let locals = PyDict::new(py);
                 locals.set_item("rs_dec", &rs_dec).unwrap();
@@ -178,7 +178,7 @@ mod test_rust_decimal {
 
         #[test]
         fn test_integers(num in any::()) {
-            Python::with_gil(|py| {
+            Python::attach(|py| {
                 let py_num = num.into_pyobject(py).unwrap();
                 let roundtripped: Decimal = py_num.extract().unwrap();
                 let rs_dec = Decimal::new(num, 0);
@@ -189,7 +189,7 @@ mod test_rust_decimal {
 
     #[test]
     fn test_nan() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let locals = PyDict::new(py);
             py.run(
                 ffi::c_str!("import decimal\npy_dec = decimal.Decimal(\"NaN\")"),
@@ -205,7 +205,7 @@ mod test_rust_decimal {
 
     #[test]
     fn test_scientific_notation() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let locals = PyDict::new(py);
             py.run(
                 ffi::c_str!("import decimal\npy_dec = decimal.Decimal(\"1e3\")"),
@@ -222,7 +222,7 @@ mod test_rust_decimal {
 
     #[test]
     fn test_infinity() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let locals = PyDict::new(py);
             py.run(
                 ffi::c_str!("import decimal\npy_dec = decimal.Decimal(\"Infinity\")"),
diff --git a/src/conversions/serde.rs b/src/conversions/serde.rs
index a769bd70273..bb3f4f1c58f 100644
--- a/src/conversions/serde.rs
+++ b/src/conversions/serde.rs
@@ -23,7 +23,7 @@ where
     where
         S: Serializer,
     {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             self.try_borrow(py)
                 .map_err(|e| ser::Error::custom(e.to_string()))?
                 .serialize(serializer)
@@ -41,8 +41,6 @@ where
     {
         let deserialized = T::deserialize(deserializer)?;
 
-        Python::with_gil(|py| {
-            Py::new(py, deserialized).map_err(|e| de::Error::custom(e.to_string()))
-        })
+        Python::attach(|py| Py::new(py, deserialized).map_err(|e| de::Error::custom(e.to_string())))
     }
 }
diff --git a/src/conversions/smallvec.rs b/src/conversions/smallvec.rs
index d4229bc2865..25d97fc1a22 100644
--- a/src/conversions/smallvec.rs
+++ b/src/conversions/smallvec.rs
@@ -117,7 +117,7 @@ mod tests {
 
     #[test]
     fn test_smallvec_from_py_object() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             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]);
@@ -126,7 +126,7 @@ mod tests {
 
     #[test]
     fn test_smallvec_from_py_object_fails() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let dict = PyDict::new(py);
             let sv: PyResult> = dict.extract();
             assert_eq!(
@@ -138,7 +138,7 @@ mod tests {
 
     #[test]
     fn test_smallvec_into_pyobject() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let sv: SmallVec<[u64; 8]> = [1, 2, 3, 4, 5].iter().cloned().collect();
             let hso = sv.into_pyobject(py).unwrap();
             let l = PyList::new(py, [1, 2, 3, 4, 5]).unwrap();
@@ -148,7 +148,7 @@ mod tests {
 
     #[test]
     fn test_smallvec_intopyobject_impl() {
-        Python::with_gil(|py| {
+        Python::attach(|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::());
diff --git a/src/conversions/std/array.rs b/src/conversions/std/array.rs
index 1d88965e711..44a5a5fcb32 100644
--- a/src/conversions/std/array.rs
+++ b/src/conversions/std/array.rs
@@ -151,7 +151,7 @@ mod tests {
 
     #[test]
     fn test_extract_bytearray_to_array() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v: [u8; 33] = py
                 .eval(
                     ffi::c_str!("bytearray(b'abcabcabcabcabcabcabcabcabcabcabc')"),
@@ -167,7 +167,7 @@ mod tests {
 
     #[test]
     fn test_extract_small_bytearray_to_array() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v: [u8; 3] = py
                 .eval(ffi::c_str!("bytearray(b'abc')"), None, None)
                 .unwrap()
@@ -178,7 +178,7 @@ mod tests {
     }
     #[test]
     fn test_into_pyobject_array_conversion() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let array: [f32; 4] = [0.0, -16.0, 16.0, 42.0];
             let pyobject = array.into_pyobject(py).unwrap();
             let pylist = pyobject.downcast::().unwrap();
@@ -191,7 +191,7 @@ mod tests {
 
     #[test]
     fn test_extract_invalid_sequence_length() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v: PyResult<[u8; 3]> = py
                 .eval(ffi::c_str!("bytearray(b'abcdefg')"), None, None)
                 .unwrap()
@@ -205,7 +205,7 @@ mod tests {
 
     #[test]
     fn test_intopyobject_array_conversion() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let array: [f32; 4] = [0.0, -16.0, 16.0, 42.0];
             let pylist = array
                 .into_pyobject(py)
@@ -222,7 +222,7 @@ mod tests {
 
     #[test]
     fn test_array_intopyobject_impl() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let bytes: [u8; 6] = *b"foobar";
             let obj = bytes.into_pyobject(py).unwrap();
             assert!(obj.is_instance_of::());
@@ -237,7 +237,7 @@ mod tests {
 
     #[test]
     fn test_extract_non_iterable_to_array() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v = py.eval(ffi::c_str!("42"), None, None).unwrap();
             v.extract::().unwrap();
             v.extract::<[i32; 1]>().unwrap_err();
@@ -250,7 +250,7 @@ mod tests {
         #[crate::pyclass(crate = "crate")]
         struct Foo;
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let array: [Foo; 8] = [Foo, Foo, Foo, Foo, Foo, Foo, Foo, Foo];
             let list = array
                 .into_pyobject(py)
diff --git a/src/conversions/std/ipaddr.rs b/src/conversions/std/ipaddr.rs
index 0c479a2d836..699013c53e2 100644
--- a/src/conversions/std/ipaddr.rs
+++ b/src/conversions/std/ipaddr.rs
@@ -111,7 +111,7 @@ mod test_ipaddr {
 
     #[test]
     fn test_roundtrip() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             fn roundtrip(py: Python<'_>, ip: &str) {
                 let ip = IpAddr::from_str(ip).unwrap();
                 let py_cls = if ip.is_ipv4() {
@@ -136,7 +136,7 @@ mod test_ipaddr {
 
     #[test]
     fn test_from_pystring() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let py_str = PyString::new(py, "0:0:0:0:0:0:0:1");
             let ip: IpAddr = py_str.extract().unwrap();
             assert_eq!(ip, IpAddr::from_str("::1").unwrap());
diff --git a/src/conversions/std/map.rs b/src/conversions/std/map.rs
index b472957d1b1..937570b7a59 100644
--- a/src/conversions/std/map.rs
+++ b/src/conversions/std/map.rs
@@ -155,7 +155,7 @@ mod tests {
 
     #[test]
     fn test_hashmap_to_python() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut map = HashMap::::new();
             map.insert(1, 1);
 
@@ -177,7 +177,7 @@ mod tests {
 
     #[test]
     fn test_btreemap_to_python() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut map = BTreeMap::::new();
             map.insert(1, 1);
 
@@ -199,7 +199,7 @@ mod tests {
 
     #[test]
     fn test_hashmap_into_python() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut map = HashMap::::new();
             map.insert(1, 1);
 
@@ -220,7 +220,7 @@ mod tests {
 
     #[test]
     fn test_btreemap_into_py() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut map = BTreeMap::::new();
             map.insert(1, 1);
 
diff --git a/src/conversions/std/num.rs b/src/conversions/std/num.rs
index b7a8d0b472b..3ac8ac6d128 100644
--- a/src/conversions/std/num.rs
+++ b/src/conversions/std/num.rs
@@ -623,7 +623,7 @@ mod test_128bit_integers {
     proptest! {
         #[test]
         fn test_i128_roundtrip(x: i128) {
-            Python::with_gil(|py| {
+            Python::attach(|py| {
                 let x_py = x.into_pyobject(py).unwrap();
                 let locals = PyDict::new(py);
                 locals.set_item("x_py", &x_py).unwrap();
@@ -639,7 +639,7 @@ mod test_128bit_integers {
                 .prop_filter("Values must not be 0", |x| x != &0)
                 .prop_map(|x| NonZeroI128::new(x).unwrap())
         ) {
-            Python::with_gil(|py| {
+            Python::attach(|py| {
                 let x_py = x.into_pyobject(py).unwrap();
                 let locals = PyDict::new(py);
                 locals.set_item("x_py", &x_py).unwrap();
@@ -654,7 +654,7 @@ mod test_128bit_integers {
     proptest! {
         #[test]
         fn test_u128_roundtrip(x: u128) {
-            Python::with_gil(|py| {
+            Python::attach(|py| {
                 let x_py = x.into_pyobject(py).unwrap();
                 let locals = PyDict::new(py);
                 locals.set_item("x_py", &x_py).unwrap();
@@ -670,7 +670,7 @@ mod test_128bit_integers {
                 .prop_filter("Values must not be 0", |x| x != &0)
                 .prop_map(|x| NonZeroU128::new(x).unwrap())
         ) {
-            Python::with_gil(|py| {
+            Python::attach(|py| {
                 let x_py = x.into_pyobject(py).unwrap();
                 let locals = PyDict::new(py);
                 locals.set_item("x_py", &x_py).unwrap();
@@ -683,7 +683,7 @@ mod test_128bit_integers {
 
     #[test]
     fn test_i128_max() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v = i128::MAX;
             let obj = v.into_pyobject(py).unwrap();
             assert_eq!(v, obj.extract::().unwrap());
@@ -694,7 +694,7 @@ mod test_128bit_integers {
 
     #[test]
     fn test_i128_min() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v = i128::MIN;
             let obj = v.into_pyobject(py).unwrap();
             assert_eq!(v, obj.extract::().unwrap());
@@ -705,7 +705,7 @@ mod test_128bit_integers {
 
     #[test]
     fn test_u128_max() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v = u128::MAX;
             let obj = v.into_pyobject(py).unwrap();
             assert_eq!(v, obj.extract::().unwrap());
@@ -715,7 +715,7 @@ mod test_128bit_integers {
 
     #[test]
     fn test_i128_overflow() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             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));
@@ -724,7 +724,7 @@ mod test_128bit_integers {
 
     #[test]
     fn test_u128_overflow() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let obj = py.eval(ffi::c_str!("1 << 130"), None, None).unwrap();
             let err = obj.extract::().unwrap_err();
             assert!(err.is_instance_of::(py));
@@ -733,7 +733,7 @@ mod test_128bit_integers {
 
     #[test]
     fn test_nonzero_i128_max() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v = NonZeroI128::new(i128::MAX).unwrap();
             let obj = v.into_pyobject(py).unwrap();
             assert_eq!(v, obj.extract::().unwrap());
@@ -747,7 +747,7 @@ mod test_128bit_integers {
 
     #[test]
     fn test_nonzero_i128_min() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v = NonZeroI128::new(i128::MIN).unwrap();
             let obj = v.into_pyobject(py).unwrap();
             assert_eq!(v, obj.extract::().unwrap());
@@ -758,7 +758,7 @@ mod test_128bit_integers {
 
     #[test]
     fn test_nonzero_u128_max() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v = NonZeroU128::new(u128::MAX).unwrap();
             let obj = v.into_pyobject(py).unwrap();
             assert_eq!(v, obj.extract::().unwrap());
@@ -768,7 +768,7 @@ mod test_128bit_integers {
 
     #[test]
     fn test_nonzero_i128_overflow() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             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));
@@ -777,7 +777,7 @@ mod test_128bit_integers {
 
     #[test]
     fn test_nonzero_u128_overflow() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let obj = py.eval(ffi::c_str!("1 << 130"), None, None).unwrap();
             let err = obj.extract::().unwrap_err();
             assert!(err.is_instance_of::(py));
@@ -786,7 +786,7 @@ mod test_128bit_integers {
 
     #[test]
     fn test_nonzero_i128_zero_value() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let obj = py.eval(ffi::c_str!("0"), None, None).unwrap();
             let err = obj.extract::().unwrap_err();
             assert!(err.is_instance_of::(py));
@@ -795,7 +795,7 @@ mod test_128bit_integers {
 
     #[test]
     fn test_nonzero_u128_zero_value() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let obj = py.eval(ffi::c_str!("0"), None, None).unwrap();
             let err = obj.extract::().unwrap_err();
             assert!(err.is_instance_of::(py));
@@ -811,7 +811,7 @@ mod tests {
 
     #[test]
     fn test_u32_max() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v = u32::MAX;
             let obj = v.into_pyobject(py).unwrap();
             assert_eq!(v, obj.extract::().unwrap());
@@ -822,7 +822,7 @@ mod tests {
 
     #[test]
     fn test_i64_max() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v = i64::MAX;
             let obj = v.into_pyobject(py).unwrap();
             assert_eq!(v, obj.extract::().unwrap());
@@ -833,7 +833,7 @@ mod tests {
 
     #[test]
     fn test_i64_min() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v = i64::MIN;
             let obj = v.into_pyobject(py).unwrap();
             assert_eq!(v, obj.extract::().unwrap());
@@ -844,7 +844,7 @@ mod tests {
 
     #[test]
     fn test_u64_max() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v = u64::MAX;
             let obj = v.into_pyobject(py).unwrap();
             assert_eq!(v, obj.extract::().unwrap());
@@ -862,7 +862,7 @@ mod tests {
 
                 #[test]
                 fn from_py_string_type_error() {
-                    Python::with_gil(|py| {
+                    Python::attach(|py| {
                     let obj = ("123").into_pyobject(py).unwrap();
                     let err = obj.extract::<$t>().unwrap_err();
                     assert!(err.is_instance_of::(py));
@@ -871,7 +871,7 @@ mod tests {
 
                 #[test]
                 fn from_py_float_type_error() {
-                    Python::with_gil(|py| {
+                    Python::attach(|py| {
                     let obj = (12.3f64).into_pyobject(py).unwrap();
                     let err = obj.extract::<$t>().unwrap_err();
                     assert!(err.is_instance_of::(py));});
@@ -879,7 +879,7 @@ mod tests {
 
                 #[test]
                 fn to_py_object_and_back() {
-                    Python::with_gil(|py| {
+                    Python::attach(|py| {
                     let val = 123 as $t;
                     let obj = val.into_pyobject(py).unwrap();
                     assert_eq!(obj.extract::<$t>().unwrap(), val as $t);});
@@ -903,7 +903,7 @@ mod tests {
 
     #[test]
     fn test_nonzero_u32_max() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v = NonZeroU32::new(u32::MAX).unwrap();
             let obj = v.into_pyobject(py).unwrap();
             assert_eq!(v, obj.extract::().unwrap());
@@ -914,7 +914,7 @@ mod tests {
 
     #[test]
     fn test_nonzero_i64_max() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v = NonZeroI64::new(i64::MAX).unwrap();
             let obj = v.into_pyobject(py).unwrap();
             assert_eq!(v, obj.extract::().unwrap());
@@ -928,7 +928,7 @@ mod tests {
 
     #[test]
     fn test_nonzero_i64_min() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v = NonZeroI64::new(i64::MIN).unwrap();
             let obj = v.into_pyobject(py).unwrap();
             assert_eq!(v, obj.extract::().unwrap());
@@ -939,7 +939,7 @@ mod tests {
 
     #[test]
     fn test_nonzero_u64_max() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v = NonZeroU64::new(u64::MAX).unwrap();
             let obj = v.into_pyobject(py).unwrap();
             assert_eq!(v, obj.extract::().unwrap());
@@ -958,7 +958,7 @@ mod tests {
 
                 #[test]
                 fn from_py_string_type_error() {
-                    Python::with_gil(|py| {
+                    Python::attach(|py| {
                     let obj = ("123").into_pyobject(py).unwrap();
                     let err = obj.extract::<$t>().unwrap_err();
                     assert!(err.is_instance_of::(py));
@@ -967,7 +967,7 @@ mod tests {
 
                 #[test]
                 fn from_py_float_type_error() {
-                    Python::with_gil(|py| {
+                    Python::attach(|py| {
                     let obj = (12.3f64).into_pyobject(py).unwrap();
                     let err = obj.extract::<$t>().unwrap_err();
                     assert!(err.is_instance_of::(py));});
@@ -975,7 +975,7 @@ mod tests {
 
                 #[test]
                 fn to_py_object_and_back() {
-                    Python::with_gil(|py| {
+                    Python::attach(|py| {
                     let val = <$t>::new(123).unwrap();
                     let obj = val.into_pyobject(py).unwrap();
                     assert_eq!(obj.extract::<$t>().unwrap(), val);});
@@ -999,7 +999,7 @@ mod tests {
 
     #[test]
     fn test_i64_bool() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let obj = true.into_pyobject(py).unwrap();
             assert_eq!(1, obj.extract::().unwrap());
             let obj = false.into_pyobject(py).unwrap();
@@ -1009,7 +1009,7 @@ mod tests {
 
     #[test]
     fn test_i64_f64() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let obj = 12.34f64.into_pyobject(py).unwrap();
             let err = obj.extract::().unwrap_err();
             assert!(err.is_instance_of::(py));
diff --git a/src/conversions/std/osstr.rs b/src/conversions/std/osstr.rs
index 39fa56f373d..d9ee24226f6 100644
--- a/src/conversions/std/osstr.rs
+++ b/src/conversions/std/osstr.rs
@@ -181,7 +181,7 @@ mod tests {
     #[test]
     #[cfg(not(windows))]
     fn test_non_utf8_conversion() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             #[cfg(not(target_os = "wasi"))]
             use std::os::unix::ffi::OsStrExt;
             #[cfg(target_os = "wasi")]
@@ -200,7 +200,7 @@ mod tests {
 
     #[test]
     fn test_intopyobject_roundtrip() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             fn test_roundtrip<'py, T>(py: Python<'py>, obj: T)
             where
                 T: IntoPyObject<'py> + AsRef + Debug + Clone,
diff --git a/src/conversions/std/path.rs b/src/conversions/std/path.rs
index 3eb074d04d2..5a16aba1273 100644
--- a/src/conversions/std/path.rs
+++ b/src/conversions/std/path.rs
@@ -98,7 +98,7 @@ mod tests {
     #[test]
     #[cfg(not(windows))]
     fn test_non_utf8_conversion() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             use std::ffi::OsStr;
             #[cfg(not(target_os = "wasi"))]
             use std::os::unix::ffi::OsStrExt;
@@ -118,7 +118,7 @@ mod tests {
 
     #[test]
     fn test_intopyobject_roundtrip() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             fn test_roundtrip<'py, T>(py: Python<'py>, obj: T)
             where
                 T: IntoPyObject<'py> + AsRef + Debug + Clone,
@@ -138,7 +138,7 @@ mod tests {
 
     #[test]
     fn test_from_pystring() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let path = "Hello\0\n🐍";
             let pystring = PyString::new(py, path);
             let roundtrip: PathBuf = pystring.extract().unwrap();
diff --git a/src/conversions/std/set.rs b/src/conversions/std/set.rs
index ebb737f72c6..16c76032f16 100644
--- a/src/conversions/std/set.rs
+++ b/src/conversions/std/set.rs
@@ -145,7 +145,7 @@ mod tests {
 
     #[test]
     fn test_extract_hashset() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             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());
@@ -158,7 +158,7 @@ mod tests {
 
     #[test]
     fn test_extract_btreeset() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             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());
@@ -171,7 +171,7 @@ mod tests {
 
     #[test]
     fn test_set_into_pyobject() {
-        Python::with_gil(|py| {
+        Python::attach(|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 5bfa5a3d48d..8060f1e9ee8 100644
--- a/src/conversions/std/slice.rs
+++ b/src/conversions/std/slice.rs
@@ -99,7 +99,7 @@ mod tests {
 
     #[test]
     fn test_extract_bytes() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             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");
@@ -108,7 +108,7 @@ mod tests {
 
     #[test]
     fn test_cow_impl() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             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"));
@@ -136,7 +136,7 @@ mod tests {
 
     #[test]
     fn test_slice_intopyobject_impl() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let bytes: &[u8] = b"foobar";
             let obj = bytes.into_pyobject(py).unwrap();
             assert!(obj.is_instance_of::());
@@ -151,7 +151,7 @@ mod tests {
 
     #[test]
     fn test_cow_intopyobject_impl() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let borrowed_bytes = Cow::<[u8]>::Borrowed(b"foobar");
             let obj = borrowed_bytes.clone().into_pyobject(py).unwrap();
             assert!(obj.is_instance_of::());
diff --git a/src/conversions/std/string.rs b/src/conversions/std/string.rs
index 48e47e5e46d..2aa7e1a6de1 100644
--- a/src/conversions/std/string.rs
+++ b/src/conversions/std/string.rs
@@ -211,7 +211,7 @@ mod tests {
 
     #[test]
     fn test_cow_into_pyobject() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let s = "Hello Python";
             let py_string = Cow::Borrowed(s).into_pyobject(py).unwrap();
             assert_eq!(s, py_string.extract::>().unwrap());
@@ -222,7 +222,7 @@ mod tests {
 
     #[test]
     fn test_non_bmp() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let s = "\u{1F30F}";
             let py_string = s.into_pyobject(py).unwrap();
             assert_eq!(s, py_string.extract::().unwrap());
@@ -231,7 +231,7 @@ mod tests {
 
     #[test]
     fn test_extract_str() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let s = "Hello Python";
             let py_string = s.into_pyobject(py).unwrap();
 
@@ -242,7 +242,7 @@ mod tests {
 
     #[test]
     fn test_extract_char() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let ch = '😃';
             let py_string = ch.into_pyobject(py).unwrap();
             let ch2: char = py_string.extract().unwrap();
@@ -252,7 +252,7 @@ mod tests {
 
     #[test]
     fn test_extract_char_err() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let s = "Hello Python";
             let py_string = s.into_pyobject(py).unwrap();
             let err: crate::PyResult = py_string.extract();
@@ -265,7 +265,7 @@ mod tests {
 
     #[test]
     fn test_string_into_pyobject() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let s = "Hello Python";
             let s2 = s.to_owned();
             let s3 = &s2;
diff --git a/src/conversions/std/time.rs b/src/conversions/std/time.rs
index 53e3bf1a641..e46facbb6b8 100644
--- a/src/conversions/std/time.rs
+++ b/src/conversions/std/time.rs
@@ -141,7 +141,7 @@ mod tests {
 
     #[test]
     fn test_duration_frompyobject() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             assert_eq!(
                 new_timedelta(py, 0, 0, 0).extract::().unwrap(),
                 Duration::new(0, 0)
@@ -175,7 +175,7 @@ mod tests {
 
     #[test]
     fn test_duration_frompyobject_negative() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             assert_eq!(
                 new_timedelta(py, 0, -1, 0)
                     .extract::()
@@ -188,7 +188,7 @@ mod tests {
 
     #[test]
     fn test_duration_into_pyobject() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let assert_eq = |l: Bound<'_, PyAny>, r: Bound<'_, PyAny>| {
                 assert!(l.eq(r).unwrap());
             };
@@ -238,14 +238,14 @@ mod tests {
 
     #[test]
     fn test_duration_into_pyobject_overflow() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             assert!(Duration::MAX.into_pyobject(py).is_err());
         })
     }
 
     #[test]
     fn test_time_frompyobject() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             assert_eq!(
                 new_datetime(py, 1970, 1, 1, 0, 0, 0, 0)
                     .extract::()
@@ -271,7 +271,7 @@ mod tests {
 
     #[test]
     fn test_time_frompyobject_before_epoch() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             assert_eq!(
                 new_datetime(py, 1950, 1, 1, 0, 0, 0, 0)
                     .extract::()
@@ -284,7 +284,7 @@ mod tests {
 
     #[test]
     fn test_time_intopyobject() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let assert_eq = |l: Bound<'_, PyDateTime>, r: Bound<'_, PyDateTime>| {
                 assert!(l.eq(r).unwrap());
             };
@@ -352,7 +352,7 @@ mod tests {
         let big_system_time = UNIX_EPOCH
             .checked_add(Duration::new(300000000000, 0))
             .unwrap();
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             assert!(big_system_time.into_pyobject(py).is_err());
         })
     }
diff --git a/src/conversions/std/vec.rs b/src/conversions/std/vec.rs
index d77c899eda9..02f82439cfe 100644
--- a/src/conversions/std/vec.rs
+++ b/src/conversions/std/vec.rs
@@ -57,7 +57,7 @@ mod tests {
 
     #[test]
     fn test_vec_intopyobject_impl() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let bytes: Vec = b"foobar".to_vec();
             let obj = bytes.clone().into_pyobject(py).unwrap();
             assert!(obj.is_instance_of::());
@@ -72,7 +72,7 @@ mod tests {
 
     #[test]
     fn test_vec_reference_intopyobject_impl() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let bytes: Vec = b"foobar".to_vec();
             let obj = (&bytes).into_pyobject(py).unwrap();
             assert!(obj.is_instance_of::());
diff --git a/src/conversions/time.rs b/src/conversions/time.rs
index f077b3be55d..95a19259211 100644
--- a/src/conversions/time.rs
+++ b/src/conversions/time.rs
@@ -22,7 +22,7 @@
 //!
 //! fn main() -> PyResult<()> {
 //!     pyo3::prepare_freethreaded_python();
-//!     Python::with_gil(|py| {
+//!     Python::attach(|py| {
 //!         // Create a fixed date and time (2022-01-01 12:00:00 UTC)
 //!         let date = Date::from_calendar_date(2022, Month::January, 1).unwrap();
 //!         let time = Time::from_hms(12, 0, 0).unwrap();
@@ -761,7 +761,7 @@ mod tests {
     }
     #[test]
     fn test_time_duration_conversion() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             // Regular duration
             let duration = Duration::new(1, 500_000_000); // 1.5 seconds
             let (_, seconds, microseconds) = utils::extract_py_delta_from_duration(duration, py);
@@ -786,7 +786,7 @@ mod tests {
 
     #[test]
     fn test_time_duration_conversion_large_values() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             // Large duration (close to max)
             let large_duration = Duration::seconds(86_399_999_000_000); // Almost max
             let (days, _, _) = utils::extract_py_delta_from_duration(large_duration, py);
@@ -803,7 +803,7 @@ mod tests {
 
     #[test]
     fn test_time_duration_nanosecond_resolution() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             // Test nanosecond conversion to microseconds
             let duration = Duration::new(0, 1_234_567);
             let (_, _, microseconds) = utils::extract_py_delta_from_duration(duration, py);
@@ -814,7 +814,7 @@ mod tests {
 
     #[test]
     fn test_time_duration_from_python() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             // Create Python timedeltas with various values
             let datetime = py.import("datetime").unwrap();
             let timedelta = datetime.getattr(intern!(py, "timedelta")).unwrap();
@@ -836,7 +836,7 @@ mod tests {
 
     #[test]
     fn test_time_date_conversion() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             // Regular date
             let date = Date::from_calendar_date(2023, Month::April, 15).unwrap();
             let (year, month, day) = utils::extract_py_date_from_date(date, py);
@@ -861,7 +861,7 @@ mod tests {
 
     #[test]
     fn test_time_date_from_python() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let date1 = utils::create_date_from_py_date(py, 2023, 4, 15).unwrap();
             assert_eq!(date1.year(), 2023);
             assert_eq!(date1.month(), Month::April);
@@ -889,7 +889,7 @@ mod tests {
 
     #[test]
     fn test_time_date_invalid_values() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let invalid_date = utils::create_date_from_py_date(py, 2023, 2, 30);
             assert!(invalid_date.is_err());
 
@@ -901,7 +901,7 @@ mod tests {
 
     #[test]
     fn test_time_time_conversion() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             // Regular time
             let time = Time::from_hms_micro(14, 30, 45, 123456).unwrap();
             let (hour, minute, second, microsecond) = utils::extract_py_time_from_time(time, py);
@@ -931,7 +931,7 @@ mod tests {
 
     #[test]
     fn test_time_time_from_python() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let time1 = utils::create_time_from_py_time(py, 14, 30, 45, 123456).unwrap();
             assert_eq!(time1.hour(), 14);
             assert_eq!(time1.minute(), 30);
@@ -956,7 +956,7 @@ mod tests {
 
     #[test]
     fn test_time_time_invalid_values() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let result = utils::create_time_from_py_time(py, 24, 0, 0, 0);
             assert!(result.is_err());
             let result = utils::create_time_from_py_time(py, 12, 60, 0, 0);
@@ -970,7 +970,7 @@ mod tests {
 
     #[test]
     fn test_time_time_with_timezone() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             // Create Python time with timezone (just to ensure we can handle it properly)
             let datetime = py.import("datetime").unwrap();
             let time_type = datetime.getattr(intern!(py, "time")).unwrap();
@@ -988,7 +988,7 @@ mod tests {
 
     #[test]
     fn test_time_primitive_datetime_conversion() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             // Regular datetime
             let date = Date::from_calendar_date(2023, Month::April, 15).unwrap();
             let time = Time::from_hms_micro(14, 30, 45, 123456).unwrap();
@@ -1022,7 +1022,7 @@ mod tests {
 
     #[test]
     fn test_time_primitive_datetime_from_python() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let dt1 =
                 utils::create_primitive_date_time_from_py(py, 2023, 4, 15, 14, 30, 45, 123456)
                     .unwrap();
@@ -1045,7 +1045,7 @@ mod tests {
 
     #[test]
     fn test_time_utc_offset_conversion() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             // Test positive offset
             let offset = UtcOffset::from_hms(5, 30, 0).unwrap();
             let total_seconds = utils::extract_total_seconds_from_utcoffset(offset, py);
@@ -1060,7 +1060,7 @@ mod tests {
 
     #[test]
     fn test_time_utc_offset_from_python() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             // Create timezone objects
             let datetime = py.import("datetime").unwrap();
             let timezone = datetime.getattr(intern!(py, "timezone")).unwrap();
@@ -1091,7 +1091,7 @@ mod tests {
 
     #[test]
     fn test_time_offset_datetime_conversion() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             // Create an OffsetDateTime with +5:30 offset
             let date = Date::from_calendar_date(2023, Month::April, 15).unwrap();
             let time = Time::from_hms_micro(14, 30, 45, 123456).unwrap();
@@ -1160,7 +1160,7 @@ mod tests {
 
     #[test]
     fn test_time_offset_datetime_from_python() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             // Create Python datetime with timezone
             let datetime = py.import("datetime").unwrap();
             let datetime_type = datetime.getattr(intern!(py, "datetime")).unwrap();
@@ -1194,7 +1194,7 @@ mod tests {
 
     #[test]
     fn test_time_utc_datetime_conversion() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let date = Date::from_calendar_date(2023, Month::April, 15).unwrap();
             let time = Time::from_hms_micro(14, 30, 45, 123456).unwrap();
             let primitive_dt = PrimitiveDateTime::new(date, time);
@@ -1214,7 +1214,7 @@ mod tests {
 
     #[test]
     fn test_time_utc_datetime_from_python() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             // Create Python UTC datetime
             let datetime = py.import("datetime").unwrap();
             let datetime_type = datetime.getattr(intern!(py, "datetime")).unwrap();
@@ -1241,7 +1241,7 @@ mod tests {
 
     #[test]
     fn test_time_utc_datetime_non_utc_timezone() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             // Create Python datetime with non-UTC timezone
             let datetime = py.import("datetime").unwrap();
             let datetime_type = datetime.getattr(intern!(py, "datetime")).unwrap();
@@ -1272,7 +1272,7 @@ mod tests {
             #[test]
             fn test_time_duration_roundtrip(days in -9999i64..=9999i64, seconds in -86399i64..=86399i64, microseconds in -999999i64..=999999i64) {
                 // Generate a valid duration that should roundtrip successfully
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let duration = Duration::days(days) + Duration::seconds(seconds) + Duration::microseconds(microseconds);
 
                     // Skip if outside Python's timedelta bounds
@@ -1296,7 +1296,7 @@ mod tests {
                 year in 1i32..=9999,
                 month_num in 1u8..=12,
             ) {
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let month = match month_num {
                         1 => (Month::January, 31),
                         2 => {
@@ -1337,7 +1337,7 @@ mod tests {
                 second in 0u8..=59u8,
                 microsecond in 0u32..=999999u32
             ) {
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let time = Time::from_hms_micro(hour, minute, second, microsecond).unwrap();
                     let py_time = time.into_pyobject(py).unwrap();
                     let roundtripped: Time = py_time.extract().unwrap();
@@ -1355,7 +1355,7 @@ mod tests {
                 second in 0u8..=59u8,
                 microsecond in 0u32..=999999u32
             ) {
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let month = match month {
                         1 => Month::January,
                         2 => Month::February,
@@ -1392,7 +1392,7 @@ mod tests {
                     return Ok(());
                 }
 
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     if let Ok(offset) = UtcOffset::from_hms(hours, minutes, 0) {
                         let py_tz = offset.into_pyobject(py).unwrap();
                         let roundtripped: UtcOffset = py_tz.extract().unwrap();
@@ -1414,7 +1414,7 @@ mod tests {
                 tz_hour in -23i8..=23i8,
                 tz_minute in 0i8..=59i8
             ) {
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let month = match month {
                         1 => Month::January,
                         2 => Month::February,
@@ -1465,7 +1465,7 @@ mod tests {
                 second in 0u8..=59u8,
                 microsecond in 0u32..=999999u32
             ) {
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let month = match month {
                         1 => Month::January,
                         2 => Month::February,
diff --git a/src/conversions/uuid.rs b/src/conversions/uuid.rs
index 116bc1e2b29..2227617ba48 100644
--- a/src/conversions/uuid.rs
+++ b/src/conversions/uuid.rs
@@ -126,7 +126,7 @@ mod tests {
         ($name:ident, $rs:expr, $py:literal) => {
             #[test]
             fn $name() -> PyResult<()> {
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let rs_orig = $rs;
                     let rs_uuid = rs_orig.into_pyobject(py).unwrap();
                     let locals = PyDict::new(py);
diff --git a/src/coroutine/waker.rs b/src/coroutine/waker.rs
index e88ed71150f..9400d66c3d6 100644
--- a/src/coroutine/waker.rs
+++ b/src/coroutine/waker.rs
@@ -41,7 +41,7 @@ impl Wake for AsyncioWaker {
     }
 
     fn wake_by_ref(self: &Arc) {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             if let Some(loop_and_future) = self.0.get_or_init(py, || None) {
                 loop_and_future
                     .set_result(py)
diff --git a/src/err/err_state.rs b/src/err/err_state.rs
index 9353f032e93..f9a64c96d25 100644
--- a/src/err/err_state.rs
+++ b/src/err/err_state.rs
@@ -113,7 +113,7 @@ impl PyErrState {
                 };
 
                 let normalized_state =
-                    Python::with_gil(|py| PyErrStateInner::Normalized(state.normalize(py)));
+                    Python::attach(|py| PyErrStateInner::Normalized(state.normalize(py)));
 
                 // Safety: no other thread can access the inner value while we are normalizing it.
                 unsafe {
@@ -389,7 +389,7 @@ mod tests {
             }
         }
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             ERR.set(py, PyValueError::new_err(RecursiveArgs)).unwrap();
             ERR.get(py).expect("is set just above").value(py);
         })
@@ -413,13 +413,13 @@ mod tests {
             }
         }
 
-        Python::with_gil(|py| ERR.set(py, PyValueError::new_err(GILSwitchArgs)).unwrap());
+        Python::attach(|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| {
+                    Python::attach(|py| {
                         ERR.get(py).expect("is set just above").value(py);
                     });
                 })
@@ -432,7 +432,7 @@ mod tests {
 
         // We should never have deadlocked, and should be able to run
         // this assertion
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             assert!(ERR
                 .get(py)
                 .expect("is set above")
diff --git a/src/err/impls.rs b/src/err/impls.rs
index 36ae3c6255a..9d93ef33647 100644
--- a/src/err/impls.rs
+++ b/src/err/impls.rs
@@ -5,7 +5,7 @@ use std::io;
 /// Convert `PyErr` to `io::Error`
 impl From for io::Error {
     fn from(err: PyErr) -> Self {
-        let kind = Python::with_gil(|py| {
+        let kind = Python::attach(|py| {
             if err.is_instance_of::(py) {
                 io::ErrorKind::BrokenPipe
             } else if err.is_instance_of::(py) {
@@ -151,7 +151,7 @@ mod tests {
         use crate::types::any::PyAnyMethods;
 
         let check_err = |kind, expected_ty| {
-            Python::with_gil(|py| {
+            Python::attach(|py| {
                 let rust_err = io::Error::new(kind, "some error msg");
 
                 let py_err: PyErr = rust_err.into();
diff --git a/src/err/mod.rs b/src/err/mod.rs
index c4d5379cff6..19c51d25203 100644
--- a/src/err/mod.rs
+++ b/src/err/mod.rs
@@ -142,7 +142,7 @@ impl PyErr {
     ///     Err(PyErr::new::("Error message"))
     /// }
     /// #
-    /// # Python::with_gil(|py| {
+    /// # Python::attach(|py| {
     /// #     let fun = pyo3::wrap_pyfunction!(always_throws, py).unwrap();
     /// #     let err = fun.call0().expect_err("called a function that should always return an error but the return value was Ok");
     /// #     assert!(err.is_instance_of::(py))
@@ -160,7 +160,7 @@ impl PyErr {
     ///     Err(PyTypeError::new_err("Error message"))
     /// }
     /// #
-    /// # Python::with_gil(|py| {
+    /// # Python::attach(|py| {
     /// #     let fun = pyo3::wrap_pyfunction!(always_throws, py).unwrap();
     /// #     let err = fun.call0().expect_err("called a function that should always return an error but the return value was Ok");
     /// #     assert!(err.is_instance_of::(py))
@@ -212,7 +212,7 @@ impl PyErr {
     /// use pyo3::exceptions::PyTypeError;
     /// use pyo3::types::PyString;
     ///
-    /// Python::with_gil(|py| {
+    /// Python::attach(|py| {
     ///     // Case #1: Exception object
     ///     let err = PyErr::from_value(PyTypeError::new_err("some type error")
     ///         .value(py).clone().into_any());
@@ -251,7 +251,7 @@ impl PyErr {
     /// ```rust
     /// use pyo3::{prelude::*, exceptions::PyTypeError, types::PyType};
     ///
-    /// Python::with_gil(|py| {
+    /// Python::attach(|py| {
     ///     let err: PyErr = PyTypeError::new_err(("some type error",));
     ///     assert!(err.get_type(py).is(&PyType::new::(py)));
     /// });
@@ -267,7 +267,7 @@ impl PyErr {
     /// ```rust
     /// use pyo3::{exceptions::PyTypeError, PyErr, Python};
     ///
-    /// Python::with_gil(|py| {
+    /// Python::attach(|py| {
     ///     let err: PyErr = PyTypeError::new_err(("some type error",));
     ///     assert!(err.is_instance_of::(py));
     ///     assert_eq!(err.value(py).to_string(), "some type error");
@@ -298,7 +298,7 @@ impl PyErr {
     /// ```rust
     /// use pyo3::{exceptions::PyTypeError, Python};
     ///
-    /// Python::with_gil(|py| {
+    /// Python::attach(|py| {
     ///     let err = PyTypeError::new_err(("some type error",));
     ///     assert!(err.traceback(py).is_none());
     /// });
@@ -506,7 +506,7 @@ impl PyErr {
     /// # use pyo3::exceptions::PyRuntimeError;
     /// # fn failing_function() -> PyResult<()> { Err(PyRuntimeError::new_err("foo")) }
     /// # fn main() -> PyResult<()> {
-    /// Python::with_gil(|py| {
+    /// Python::attach(|py| {
     ///     match failing_function() {
     ///         Err(pyerr) => pyerr.write_unraisable(py, None),
     ///         Ok(..) => { /* do something here */ }
@@ -535,7 +535,7 @@ impl PyErr {
     /// # use pyo3::prelude::*;
     /// # use pyo3::ffi::c_str;
     /// # fn main() -> PyResult<()> {
-    /// Python::with_gil(|py| {
+    /// Python::attach(|py| {
     ///     let user_warning = py.get_type::();
     ///     PyErr::warn(py, &user_warning, c_str!("I am warning you"), 0)?;
     ///     Ok(())
@@ -599,7 +599,7 @@ impl PyErr {
     /// # Examples
     /// ```rust
     /// use pyo3::{exceptions::PyTypeError, PyErr, Python, prelude::PyAnyMethods};
-    /// Python::with_gil(|py| {
+    /// Python::attach(|py| {
     ///     let err: PyErr = PyTypeError::new_err(("some type error",));
     ///     let err_clone = err.clone_ref(py);
     ///     assert!(err.get_type(py).is(&err_clone.get_type(py)));
@@ -657,7 +657,7 @@ impl PyErr {
 
 impl std::fmt::Debug for PyErr {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             f.debug_struct("PyErr")
                 .field("type", &self.get_type(py))
                 .field("value", self.value(py))
@@ -682,7 +682,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| {
+        Python::attach(|py| {
             let value = self.value(py);
             let type_name = value.get_type().qualname().map_err(|_| std::fmt::Error)?;
             write!(f, "{type_name}")?;
@@ -856,12 +856,12 @@ mod tests {
 
     #[test]
     fn no_error() {
-        assert!(Python::with_gil(PyErr::take).is_none());
+        assert!(Python::attach(PyErr::take).is_none());
     }
 
     #[test]
     fn set_valueerror() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let err: PyErr = exceptions::PyValueError::new_err("some exception message");
             assert!(err.is_instance_of::(py));
             err.restore(py);
@@ -874,7 +874,7 @@ mod tests {
 
     #[test]
     fn invalid_error_type() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let err: PyErr = PyErr::new::(());
             assert!(err.is_instance_of::(py));
             err.restore(py);
@@ -890,7 +890,7 @@ mod tests {
 
     #[test]
     fn set_typeerror() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let err: PyErr = exceptions::PyTypeError::new_err(());
             err.restore(py);
             assert!(PyErr::occurred(py));
@@ -903,7 +903,7 @@ mod tests {
     fn fetching_panic_exception_resumes_unwind() {
         use crate::panic::PanicException;
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let err: PyErr = PanicException::new_err("new panic");
             err.restore(py);
             assert!(PyErr::occurred(py));
@@ -919,7 +919,7 @@ mod tests {
     fn fetching_normalized_panic_exception_resumes_unwind() {
         use crate::panic::PanicException;
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let err: PyErr = PanicException::new_err("new panic");
             // Restoring an error doesn't normalize it before Python 3.12,
             // so we have to explicitly test this case.
@@ -941,7 +941,7 @@ mod tests {
         //     traceback:  Some(\"Traceback (most recent call last):\\n  File \\\"\\\", line 1, in \\n\")
         // }
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let err = py
                 .run(ffi::c_str!("raise Exception('banana')"), None, None)
                 .expect_err("raising should have given us an error");
@@ -967,7 +967,7 @@ mod tests {
 
     #[test]
     fn err_display() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let err = py
                 .run(ffi::c_str!("raise Exception('banana')"), None, None)
                 .expect_err("raising should have given us an error");
@@ -989,7 +989,7 @@ mod tests {
 
     #[test]
     fn test_pyerr_matches() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let err = PyErr::new::("foo");
             assert!(err.matches(py, PyValueError::type_object(py)).unwrap());
 
@@ -1010,7 +1010,7 @@ mod tests {
 
     #[test]
     fn test_pyerr_cause() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let err = py
                 .run(ffi::c_str!("raise Exception('banana')"), None, None)
                 .expect_err("raising should have given us an error");
@@ -1046,7 +1046,7 @@ mod tests {
         // Note: although the warning filter is interpreter global, keeping the
         // GIL locked should prevent effects to be visible to other testing
         // threads.
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let cls = py.get_type::();
 
             // Reset warning filter to default state
diff --git a/src/exceptions.rs b/src/exceptions.rs
index 661604ef325..721dd33dbdd 100644
--- a/src/exceptions.rs
+++ b/src/exceptions.rs
@@ -62,7 +62,7 @@ macro_rules! impl_exception_boilerplate_bound {
 /// import_exception!(socket, gaierror);
 ///
 /// # fn main() -> pyo3::PyResult<()> {
-/// Python::with_gil(|py| {
+/// Python::attach(|py| {
 ///     let ctx = [("gaierror", py.get_type::())].into_py_dict(py)?;
 ///     pyo3::py_run!(py, *ctx, "import socket; assert gaierror is socket.gaierror");
 /// #   Ok(())
@@ -176,7 +176,7 @@ macro_rules! import_exception_bound {
 ///     Ok(())
 /// }
 /// # fn main() -> PyResult<()> {
-/// #     Python::with_gil(|py| -> PyResult<()> {
+/// #     Python::attach(|py| -> PyResult<()> {
 /// #         let fun = wrap_pyfunction!(raise_myerror, py)?;
 /// #         let locals = pyo3::types::PyDict::new(py);
 /// #         locals.set_item("MyError", py.get_type::())?;
@@ -331,7 +331,7 @@ fn always_throws() -> PyResult<()> {
     Err(Py", $name, "::new_err(message))
 }
 #
-# Python::with_gil(|py| {
+# Python::attach(|py| {
 #     let fun = pyo3::wrap_pyfunction!(always_throws, py).unwrap();
 #     let err = fun.call0().expect_err(\"called a function that should always return an error but the return value was Ok\");
 #     assert!(err.is_instance_of::(py))
@@ -355,7 +355,7 @@ use pyo3::prelude::*;
 use pyo3::exceptions::Py", $name, ";
 use pyo3::ffi::c_str;
 
-Python::with_gil(|py| {
+Python::attach(|py| {
     let result: PyResult<()> = py.run(c_str!(\"raise ", $name, "\"), None, None);
 
     let error_type = match result {
@@ -662,7 +662,7 @@ impl PyUnicodeDecodeError {
     /// use pyo3::exceptions::PyUnicodeDecodeError;
     ///
     /// # fn main() -> PyResult<()> {
-    /// Python::with_gil(|py| {
+    /// Python::attach(|py| {
     ///     let invalid_utf8 = b"fo\xd8o";
     /// #   #[allow(invalid_from_utf8)]
     ///     let err = std::str::from_utf8(invalid_utf8).expect_err("should be invalid utf8");
@@ -753,7 +753,7 @@ macro_rules! test_exception {
         fn $exc_ty () {
             use super::$exc_ty;
 
-            $crate::Python::with_gil(|py| {
+            $crate::Python::attach(|py| {
                 use $crate::types::PyAnyMethods;
                 let err: $crate::PyErr = {
                     None
@@ -827,7 +827,7 @@ mod tests {
 
     #[test]
     fn test_check_exception() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let err: PyErr = gaierror::new_err(());
             let socket = py
                 .import("socket")
@@ -855,7 +855,7 @@ mod tests {
 
     #[test]
     fn test_check_exception_nested() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let err: PyErr = MessageError::new_err(());
             let email = py
                 .import("email")
@@ -884,7 +884,7 @@ mod tests {
     fn custom_exception() {
         create_exception!(mymodule, CustomError, PyException);
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let error_type = py.get_type::();
             let ctx = [("CustomError", error_type)].into_py_dict(py).unwrap();
             let type_description: String = py
@@ -911,7 +911,7 @@ mod tests {
     #[test]
     fn custom_exception_dotted_module() {
         create_exception!(mymodule.exceptions, CustomError, PyException);
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let error_type = py.get_type::();
             let ctx = [("CustomError", error_type)].into_py_dict(py).unwrap();
             let type_description: String = py
@@ -930,7 +930,7 @@ mod tests {
     fn custom_exception_doc() {
         create_exception!(mymodule, CustomError, PyException, "Some docs");
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let error_type = py.get_type::();
             let ctx = [("CustomError", error_type)].into_py_dict(py).unwrap();
             let type_description: String = py
@@ -963,7 +963,7 @@ mod tests {
             concat!("Some", " more ", stringify!(docs))
         );
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let error_type = py.get_type::();
             let ctx = [("CustomError", error_type)].into_py_dict(py).unwrap();
             let type_description: String = py
@@ -989,7 +989,7 @@ mod tests {
 
     #[test]
     fn native_exception_debug() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let exc = py
                 .run(ffi::c_str!("raise Exception('banana')"), None, None)
                 .expect_err("raising should have given us an error")
@@ -1004,7 +1004,7 @@ mod tests {
 
     #[test]
     fn native_exception_display() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let exc = py
                 .run(ffi::c_str!("raise Exception('banana')"), None, None)
                 .expect_err("raising should have given us an error")
@@ -1022,7 +1022,7 @@ mod tests {
         let invalid_utf8 = b"fo\xd8o";
         #[allow(invalid_from_utf8)]
         let err = std::str::from_utf8(invalid_utf8).expect_err("should be invalid utf8");
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let decode_err = PyUnicodeDecodeError::new_utf8(py, invalid_utf8, err).unwrap();
             assert_eq!(
                 format!("{decode_err:?}"),
diff --git a/src/ffi/tests.rs b/src/ffi/tests.rs
index 7de4e3b1875..9ae39b00ec6 100644
--- a/src/ffi/tests.rs
+++ b/src/ffi/tests.rs
@@ -15,7 +15,7 @@ use libc::wchar_t;
 #[test]
 fn test_datetime_fromtimestamp() {
     use crate::IntoPyObject;
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let args = (100,).into_pyobject(py).unwrap();
         let dt = unsafe {
             PyDateTime_IMPORT();
@@ -37,7 +37,7 @@ fn test_datetime_fromtimestamp() {
 #[test]
 fn test_date_fromtimestamp() {
     use crate::IntoPyObject;
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let args = (100,).into_pyobject(py).unwrap();
         let dt = unsafe {
             PyDateTime_IMPORT();
@@ -58,7 +58,7 @@ fn test_date_fromtimestamp() {
 #[cfg_attr(target_arch = "wasm32", ignore)] // DateTime import fails on wasm for mysterious reasons
 #[test]
 fn test_utc_timezone() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let utc_timezone: Bound<'_, PyAny> = unsafe {
             PyDateTime_IMPORT();
             Bound::from_borrowed_ptr(py, PyDateTime_TimeZone_UTC())
@@ -81,7 +81,7 @@ fn test_utc_timezone() {
 fn test_timezone_from_offset() {
     use crate::{ffi_ptr_ext::FfiPtrExt, types::PyDelta};
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let delta = PyDelta::new(py, 0, 100, 0, false).unwrap();
         let tz = unsafe { PyTimeZone_FromOffset(delta.as_ptr()).assume_owned(py) };
         crate::py_run!(
@@ -99,7 +99,7 @@ fn test_timezone_from_offset() {
 fn test_timezone_from_offset_and_name() {
     use crate::{ffi_ptr_ext::FfiPtrExt, types::PyDelta};
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let delta = PyDelta::new(py, 0, 100, 0, false).unwrap();
         let tzname = PyString::new(py, "testtz");
         let tz = unsafe {
@@ -171,7 +171,7 @@ fn ascii_object_bitfield() {
 #[test]
 #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))]
 fn ascii() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         // This test relies on implementation details of PyString.
         let s = PyString::new(py, "hello, world");
         let ptr = s.as_ptr();
@@ -216,7 +216,7 @@ fn ascii() {
 #[test]
 #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))]
 fn ucs4() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let s = "哈哈🐈";
         let py_string = PyString::new(py, s);
         let ptr = py_string.as_ptr();
@@ -266,7 +266,7 @@ fn ucs4() {
 fn test_get_tzinfo() {
     use crate::types::PyTzInfo;
 
-    crate::Python::with_gil(|py| {
+    crate::Python::attach(|py| {
         use crate::types::{PyDateTime, PyTime};
 
         let utc: &Bound<'_, _> = &PyTzInfo::utc(py).unwrap();
@@ -302,7 +302,7 @@ fn test_get_tzinfo() {
 
 #[test]
 fn test_inc_dec_ref() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let obj = py.eval(ffi::c_str!("object()"), None, None).unwrap();
 
         let ref_count = obj.get_refcnt();
@@ -321,7 +321,7 @@ fn test_inc_dec_ref() {
 #[test]
 #[cfg(Py_3_12)]
 fn test_inc_dec_ref_immortal() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let obj = py.None();
 
         let ref_count = obj.get_refcnt(py);
diff --git a/src/gil.rs b/src/gil.rs
index f62e13762f0..e4cea3ff2a0 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(pyo3::ffi::c_str!("print('Hello World')"), None, None))
+/// Python::attach(|py| py.run(pyo3::ffi::c_str!("print('Hello World')"), None, None))
 /// # }
 /// ```
 #[cfg(not(any(PyPy, GraalPy)))]
@@ -144,7 +144,7 @@ pub(crate) enum GILGuard {
 }
 
 impl GILGuard {
-    /// PyO3 internal API for acquiring the GIL. The public API is Python::with_gil.
+    /// PyO3 internal API for acquiring the GIL. The public API is Python::attach.
     ///
     /// If the GIL was already acquired via PyO3, this returns
     /// `GILGuard::Assumed`. Otherwise, the GIL will be acquired and
@@ -466,7 +466,7 @@ mod tests {
 
     #[test]
     fn test_pyobject_drop_with_gil_decreases_refcnt() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let obj = get_object(py);
 
             // Create a reference to drop with the GIL.
@@ -488,7 +488,7 @@ mod tests {
     #[test]
     #[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 = Python::attach(|py| {
             let obj = get_object(py);
             // Create a reference to drop without the GIL.
             let reference = obj.clone_ref(py);
@@ -509,7 +509,7 @@ mod tests {
 
         // Next time the GIL is acquired, the reference is released
         #[allow(unused)]
-        Python::with_gil(|py| {
+        Python::attach(|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
@@ -522,11 +522,11 @@ mod tests {
     #[test]
     #[allow(deprecated)]
     fn test_gil_counts() {
-        // Check with_gil and GILGuard both increase counts correctly
+        // Check `attach` 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(|_| {
+        Python::attach(|_| {
             assert_eq!(get_gil_count(), 1);
 
             let pool = unsafe { GILGuard::assume() };
@@ -538,8 +538,8 @@ mod tests {
             drop(pool);
             assert_eq!(get_gil_count(), 2);
 
-            Python::with_gil(|_| {
-                // nested with_gil updates gil count
+            Python::attach(|_| {
+                // nested `attach` updates gil count
                 assert_eq!(get_gil_count(), 3);
             });
             assert_eq!(get_gil_count(), 2);
@@ -554,13 +554,13 @@ mod tests {
     fn test_allow_threads() {
         assert!(!gil_is_acquired());
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             assert!(gil_is_acquired());
 
             py.allow_threads(move || {
                 assert!(!gil_is_acquired());
 
-                Python::with_gil(|_| assert!(gil_is_acquired()));
+                Python::attach(|_| assert!(gil_is_acquired()));
 
                 assert!(!gil_is_acquired());
             });
@@ -575,7 +575,7 @@ mod tests {
     #[test]
     #[should_panic]
     fn test_allow_threads_updates_refcounts() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             // Make a simple object with 1 reference
             let obj = get_object(py);
             assert!(obj.get_refcnt(py) == 1);
@@ -587,8 +587,8 @@ mod tests {
     #[test]
     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(ffi::c_str!("object()"), None, None).unwrap());
+        Python::attach(|py| {
+            let obj = Python::attach(|_| 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);
@@ -598,7 +598,7 @@ mod tests {
     #[cfg(feature = "py-clone")]
     #[test]
     fn test_clone_with_gil() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let obj = get_object(py);
             let count = obj.get_refcnt(py);
 
@@ -618,7 +618,7 @@ mod tests {
         use crate::ffi;
         use crate::gil::GILGuard;
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let obj = get_object(py);
 
             unsafe extern "C" fn capsule_drop(capsule: *mut ffi::PyObject) {
@@ -652,7 +652,7 @@ mod tests {
     fn test_gil_guard_update_counts() {
         use crate::gil::GILGuard;
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let obj = get_object(py);
 
             // For GILGuard::acquire
diff --git a/src/impl_/coroutine.rs b/src/impl_/coroutine.rs
index 1d065c2f346..e624b4ee6a7 100644
--- a/src/impl_/coroutine.rs
+++ b/src/impl_/coroutine.rs
@@ -50,7 +50,7 @@ impl Deref for RefGuard {
 
 impl Drop for RefGuard {
     fn drop(&mut self) {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             self.0
                 .bind(py)
                 .get_class_object()
@@ -87,7 +87,7 @@ impl> DerefMut for RefMutGuard {
 
 impl> Drop for RefMutGuard {
     fn drop(&mut self) {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             self.0
                 .bind(py)
                 .get_class_object()
diff --git a/src/impl_/extract_argument.rs b/src/impl_/extract_argument.rs
index 5dc67677cfb..65848117d24 100644
--- a/src/impl_/extract_argument.rs
+++ b/src/impl_/extract_argument.rs
@@ -822,7 +822,7 @@ mod tests {
             keyword_only_parameters: &[],
         };
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let args = PyTuple::empty(py);
             let kwargs = [("foo", 0u8)].into_py_dict(py).unwrap();
             let err = unsafe {
@@ -853,7 +853,7 @@ mod tests {
             keyword_only_parameters: &[],
         };
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let args = PyTuple::empty(py);
             let kwargs = [(1u8, 1u8)].into_py_dict(py).unwrap();
             let err = unsafe {
@@ -884,7 +884,7 @@ mod tests {
             keyword_only_parameters: &[],
         };
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let args = PyTuple::empty(py);
             let mut output = [None, None];
             let err = unsafe {
diff --git a/src/impl_/pymethods.rs b/src/impl_/pymethods.rs
index 13039912762..fd4f6bbb3e0 100644
--- a/src/impl_/pymethods.rs
+++ b/src/impl_/pymethods.rs
@@ -720,7 +720,7 @@ mod tests {
         use crate::types::{PyAnyMethods, PyCFunction};
         use crate::{ffi, Python};
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             unsafe extern "C" fn accepts_no_arguments(
                 _slf: *mut ffi::PyObject,
                 _args: *const *mut ffi::PyObject,
diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs
index 08b1ead7584..a9736672bc7 100644
--- a/src/impl_/pymodule.rs
+++ b/src/impl_/pymodule.rs
@@ -245,7 +245,7 @@ mod tests {
                 }),
             )
         };
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let module = MODULE_DEF.make_module(py, false).unwrap().into_bound(py);
             assert_eq!(
                 module
@@ -294,7 +294,7 @@ mod tests {
             assert_eq!((*module_def.ffi_def.get()).m_name, NAME.as_ptr() as _);
             assert_eq!((*module_def.ffi_def.get()).m_doc, DOC.as_ptr() as _);
 
-            Python::with_gil(|py| {
+            Python::attach(|py| {
                 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 71638903b77..2fe67677dc9 100644
--- a/src/instance.rs
+++ b/src/instance.rs
@@ -81,11 +81,11 @@ where
     /// struct Foo {/* fields omitted */}
     ///
     /// # fn main() -> PyResult<()> {
-    /// let foo: Py = Python::with_gil(|py| -> PyResult<_> {
+    /// let foo: Py = Python::attach(|py| -> PyResult<_> {
     ///     let foo: Bound<'_, Foo> = Bound::new(py, Foo {})?;
     ///     Ok(foo.into())
     /// })?;
-    /// # Python::with_gil(move |_py| drop(foo));
+    /// # Python::attach(move |_py| drop(foo));
     /// # Ok(())
     /// # }
     /// ```
@@ -248,7 +248,7 @@ where
     /// }
     ///
     /// # fn main() -> PyResult<()> {
-    /// Python::with_gil(|py| -> PyResult<()> {
+    /// Python::attach(|py| -> PyResult<()> {
     ///     let foo: Bound<'_, Foo> = Bound::new(py, Foo { inner: 73 })?;
     ///     let inner: &u8 = &foo.borrow().inner;
     ///
@@ -284,7 +284,7 @@ where
     /// }
     ///
     /// # fn main() -> PyResult<()> {
-    /// Python::with_gil(|py| -> PyResult<()> {
+    /// Python::attach(|py| -> PyResult<()> {
     ///     let foo: Bound<'_, Foo> = Bound::new(py, Foo { inner: 73 })?;
     ///     foo.borrow_mut().inner = 35;
     ///
@@ -347,7 +347,7 @@ where
     ///     value: AtomicUsize,
     /// }
     ///
-    /// Python::with_gil(|py| {
+    /// Python::attach(|py| {
     ///     let counter = FrozenCounter { value: AtomicUsize::new(0) };
     ///
     ///     let py_counter = Bound::new(py, counter).unwrap();
@@ -399,7 +399,7 @@ where
     /// #[pyclass(extends = BaseClass)]
     /// struct SubClass;
     ///
-    /// Python::with_gil(|py| {
+    /// Python::attach(|py| {
     ///     let obj = Bound::new(py, (SubClass, BaseClass)).unwrap();
     ///     assert!(obj.as_super().pyrepr().is_ok());
     /// })
@@ -451,7 +451,7 @@ where
     /// #[pyclass(extends = BaseClass)]
     /// struct SubClass;
     ///
-    /// Python::with_gil(|py| {
+    /// Python::attach(|py| {
     ///     let obj = Bound::new(py, (SubClass, BaseClass)).unwrap();
     ///     assert!(obj.into_super().pyrepr().is_ok());
     /// })
@@ -666,7 +666,7 @@ impl<'a, 'py, T> Borrowed<'a, 'py, T> {
     /// use pyo3::{prelude::*, types::PyTuple};
     ///
     /// # fn main() -> PyResult<()> {
-    /// Python::with_gil(|py| -> PyResult<()> {
+    /// Python::attach(|py| -> PyResult<()> {
     ///     let tuple = PyTuple::new(py, [1, 2, 3])?;
     ///
     ///     // borrows from `tuple`, so can only be
@@ -899,7 +899,7 @@ impl<'a, 'py, T> BoundObject<'py, T> for Borrowed<'a, 'py, T> {
 ///
 /// impl Foo {
 ///     fn new() -> Foo {
-///         let foo = Python::with_gil(|py| {
+///         let foo = Python::attach(|py| {
 ///             // `py` will only last for this scope.
 ///
 ///             // `Bound<'py, PyDict>` inherits the GIL lifetime from `py` and
@@ -932,7 +932,7 @@ impl<'a, 'py, T> BoundObject<'py, T> for Borrowed<'a, 'py, T> {
 /// impl Foo {
 ///     #[new]
 ///     fn __new__() -> Foo {
-///         Python::with_gil(|py| {
+///         Python::attach(|py| {
 ///             let dict: Py = PyDict::new(py).unbind();
 ///             Foo { inner: dict }
 ///         })
@@ -940,7 +940,7 @@ impl<'a, 'py, T> BoundObject<'py, T> for Borrowed<'a, 'py, T> {
 /// }
 /// #
 /// # fn main() -> PyResult<()> {
-/// #     Python::with_gil(|py| {
+/// #     Python::attach(|py| {
 /// #         let m = pyo3::types::PyModule::new(py, "test")?;
 /// #         m.add_class::()?;
 /// #
@@ -969,7 +969,7 @@ impl<'a, 'py, T> BoundObject<'py, T> for Borrowed<'a, 'py, T> {
 /// impl Foo {
 ///     #[new]
 ///     fn __new__() -> PyResult {
-///         Python::with_gil(|py| {
+///         Python::attach(|py| {
 ///             let bar: Py = Py::new(py, Bar {})?;
 ///             Ok(Foo { inner: bar })
 ///         })
@@ -977,7 +977,7 @@ impl<'a, 'py, T> BoundObject<'py, T> for Borrowed<'a, 'py, T> {
 /// }
 /// #
 /// # fn main() -> PyResult<()> {
-/// #     Python::with_gil(|py| {
+/// #     Python::attach(|py| {
 /// #         let m = pyo3::types::PyModule::new(py, "test")?;
 /// #         m.add_class::()?;
 /// #
@@ -1004,7 +1004,7 @@ impl<'a, 'py, T> BoundObject<'py, T> for Borrowed<'a, 'py, T> {
 /// use pyo3::types::PyDict;
 ///
 /// # fn main() {
-/// Python::with_gil(|py| {
+/// Python::attach(|py| {
 ///     let first: Py = PyDict::new(py).unbind();
 ///
 ///     // All of these are valid syntax
@@ -1084,11 +1084,11 @@ where
     /// struct Foo {/* fields omitted */}
     ///
     /// # fn main() -> PyResult<()> {
-    /// let foo = Python::with_gil(|py| -> PyResult<_> {
+    /// let foo = Python::attach(|py| -> PyResult<_> {
     ///     let foo: Py = Py::new(py, Foo {})?;
     ///     Ok(foo)
     /// })?;
-    /// # Python::with_gil(move |_py| drop(foo));
+    /// # Python::attach(move |_py| drop(foo));
     /// # Ok(())
     /// # }
     /// ```
@@ -1162,7 +1162,7 @@ where
     /// }
     ///
     /// # fn main() -> PyResult<()> {
-    /// Python::with_gil(|py| -> PyResult<()> {
+    /// Python::attach(|py| -> PyResult<()> {
     ///     let foo: Py = Py::new(py, Foo { inner: 73 })?;
     ///     let inner: &u8 = &foo.borrow(py).inner;
     ///
@@ -1200,7 +1200,7 @@ where
     /// }
     ///
     /// # fn main() -> PyResult<()> {
-    /// Python::with_gil(|py| -> PyResult<()> {
+    /// Python::attach(|py| -> PyResult<()> {
     ///     let foo: Py = Py::new(py, Foo { inner: 73 })?;
     ///     foo.borrow_mut(py).inner = 35;
     ///
@@ -1270,14 +1270,14 @@ where
     ///     value: AtomicUsize,
     /// }
     ///
-    /// let cell  = Python::with_gil(|py| {
+    /// let cell  = Python::attach(|py| {
     ///     let counter = FrozenCounter { value: AtomicUsize::new(0) };
     ///
     ///     Py::new(py, counter).unwrap()
     /// });
     ///
     /// cell.get().value.fetch_add(1, Ordering::Relaxed);
-    /// # Python::with_gil(move |_py| drop(cell));
+    /// # Python::attach(move |_py| drop(cell));
     /// ```
     #[inline]
     pub fn get(&self) -> &T
@@ -1346,7 +1346,7 @@ impl Py {
     /// use pyo3::types::PyDict;
     ///
     /// # fn main() {
-    /// Python::with_gil(|py| {
+    /// Python::attach(|py| {
     ///     let first: Py = PyDict::new(py).unbind();
     ///     let second = Py::clone_ref(&first, py);
     ///
@@ -1378,7 +1378,7 @@ impl Py {
     /// use pyo3::types::PyDict;
     ///
     /// # fn main() {
-    /// Python::with_gil(|py| {
+    /// Python::attach(|py| {
     ///     let object: Py = PyDict::new(py).unbind();
     ///
     ///     // some usage of object
@@ -1439,7 +1439,7 @@ impl Py {
     ///     sys.getattr(py, intern!(py, "version"))
     /// }
     /// #
-    /// # Python::with_gil(|py| {
+    /// # Python::attach(|py| {
     /// #    let sys = py.import("sys").unwrap().unbind();
     /// #    version(sys, py).unwrap();
     /// # });
@@ -1468,7 +1468,7 @@ impl Py {
     ///     ob.setattr(py, intern!(py, "answer"), 42)
     /// }
     /// #
-    /// # Python::with_gil(|py| {
+    /// # Python::attach(|py| {
     /// #    let ob = PyModule::new(py, "empty").unwrap().into_py_any(py).unwrap();
     /// #    set_answer(ob, py).unwrap();
     /// # });
@@ -1795,7 +1795,7 @@ where
     T: PyTypeInfo,
 {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        Python::with_gil(|py| std::fmt::Display::fmt(self.bind(py), f))
+        Python::attach(|py| std::fmt::Display::fmt(self.bind(py), f))
     }
 }
 
@@ -1828,7 +1828,7 @@ impl PyObject {
     /// use pyo3::prelude::*;
     /// use pyo3::types::{PyDict, PyList};
     ///
-    /// Python::with_gil(|py| {
+    /// Python::attach(|py| {
     ///     let any: PyObject = PyDict::new(py).into();
     ///
     ///     assert!(any.downcast_bound::(py).is_ok());
@@ -1850,7 +1850,7 @@ impl PyObject {
     ///     i: i32,
     /// }
     ///
-    /// Python::with_gil(|py| {
+    /// Python::attach(|py| {
     ///     let class: PyObject = Py::new(py, Class { i: 0 })?.into_any();
     ///
     ///     let class_bound = class.downcast_bound::(py)?;
@@ -1897,7 +1897,7 @@ mod tests {
 
     #[test]
     fn test_call() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let obj = py.get_type::().into_pyobject(py).unwrap();
 
             let assert_repr = |obj: Bound<'_, PyAny>, expected: &str| {
@@ -1946,7 +1946,7 @@ mod tests {
             };
         }
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             tuple!(py, "a" => 1);
             tuple!(py, "a" => 1, "b" => 2);
             tuple!(py, "a" => 1, "b" => 2, "c" => 3);
@@ -1963,7 +1963,7 @@ mod tests {
 
     #[test]
     fn test_call_for_non_existing_method() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let obj: PyObject = PyDict::new(py).into();
             assert!(obj.call_method0(py, "asdf").is_err());
             assert!(obj
@@ -1976,19 +1976,19 @@ mod tests {
 
     #[test]
     fn py_from_dict() {
-        let dict: Py = Python::with_gil(|py| {
+        let dict: Py = Python::attach(|py| {
             let native = PyDict::new(py);
             Py::from(native)
         });
 
-        Python::with_gil(move |py| {
+        Python::attach(move |py| {
             assert_eq!(dict.get_refcnt(py), 1);
         });
     }
 
     #[test]
     fn pyobject_from_py() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let dict: Py = PyDict::new(py).unbind();
             let cnt = dict.get_refcnt(py);
             let p: PyObject = dict.into();
@@ -2000,7 +2000,7 @@ mod tests {
     fn attr() -> PyResult<()> {
         use crate::types::PyModule;
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             const CODE: &CStr = c_str!(
                 r#"
 class A:
@@ -2030,7 +2030,7 @@ a = A()
     fn pystring_attr() -> PyResult<()> {
         use crate::types::PyModule;
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             const CODE: &CStr = c_str!(
                 r#"
 class A:
@@ -2054,7 +2054,7 @@ a = A()
 
     #[test]
     fn invalid_attr() -> PyResult<()> {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let instance: Py = py.eval(ffi::c_str!("object()"), None, None)?.into();
 
             instance.getattr(py, "foo").unwrap_err();
@@ -2067,7 +2067,7 @@ a = A()
 
     #[test]
     fn test_py2_from_py_object() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let instance = py.eval(ffi::c_str!("object()"), None, None).unwrap();
             let ptr = instance.as_ptr();
             let instance: Bound<'_, PyAny> = instance.extract().unwrap();
@@ -2077,7 +2077,7 @@ a = A()
 
     #[test]
     fn test_py2_into_py_object() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let instance = py.eval(ffi::c_str!("object()"), None, None).unwrap();
             let ptr = instance.as_ptr();
             let instance: PyObject = instance.clone().unbind();
@@ -2087,7 +2087,7 @@ a = A()
 
     #[test]
     fn test_debug_fmt() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let obj = "hello world".into_pyobject(py).unwrap();
             assert_eq!(format!("{obj:?}"), "'hello world'");
         });
@@ -2095,7 +2095,7 @@ a = A()
 
     #[test]
     fn test_display_fmt() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let obj = "hello world".into_pyobject(py).unwrap();
             assert_eq!(format!("{obj}"), "hello world");
         });
@@ -2103,7 +2103,7 @@ a = A()
 
     #[test]
     fn test_bound_as_any() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let obj = PyString::new(py, "hello world");
             let any = obj.as_any();
             assert_eq!(any.as_ptr(), obj.as_ptr());
@@ -2112,7 +2112,7 @@ a = A()
 
     #[test]
     fn test_bound_into_any() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let obj = PyString::new(py, "hello world");
             let any = obj.clone().into_any();
             assert_eq!(any.as_ptr(), obj.as_ptr());
@@ -2121,7 +2121,7 @@ a = A()
 
     #[test]
     fn test_bound_py_conversions() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let obj: Bound<'_, PyString> = PyString::new(py, "hello world");
             let obj_unbound: &Py = obj.as_unbound();
             let _: &Bound<'_, PyString> = obj_unbound.bind(py);
@@ -2135,7 +2135,7 @@ a = A()
 
     #[test]
     fn test_borrowed_identity() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let yes = true.into_pyobject(py).unwrap();
             let no = false.into_pyobject(py).unwrap();
 
@@ -2147,7 +2147,7 @@ a = A()
     #[test]
     fn bound_from_borrowed_ptr_constructors() {
         // More detailed tests of the underlying semantics in pycell.rs
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             fn check_drop<'py>(
                 py: Python<'py>,
                 method: impl FnOnce(*mut ffi::PyObject) -> Bound<'py, PyAny>,
@@ -2186,7 +2186,7 @@ a = A()
     #[test]
     fn borrowed_ptr_constructors() {
         // More detailed tests of the underlying semantics in pycell.rs
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             fn check_drop<'py>(
                 py: Python<'py>,
                 method: impl FnOnce(&*mut ffi::PyObject) -> Borrowed<'_, 'py, PyAny>,
@@ -2221,7 +2221,7 @@ a = A()
 
     #[test]
     fn explicit_drop_ref() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let object: Py = PyDict::new(py).unbind();
             let object2 = object.clone_ref(py);
 
@@ -2246,7 +2246,7 @@ a = A()
         #[test]
         fn py_borrow_methods() {
             // More detailed tests of the underlying semantics in pycell.rs
-            Python::with_gil(|py| {
+            Python::attach(|py| {
                 let instance = Py::new(py, SomeClass(0)).unwrap();
                 assert_eq!(instance.borrow(py).0, 0);
                 assert_eq!(instance.try_borrow(py).unwrap().0, 0);
@@ -2265,7 +2265,7 @@ a = A()
         #[test]
         fn bound_borrow_methods() {
             // More detailed tests of the underlying semantics in pycell.rs
-            Python::with_gil(|py| {
+            Python::attach(|py| {
                 let instance = Bound::new(py, SomeClass(0)).unwrap();
                 assert_eq!(instance.borrow().0, 0);
                 assert_eq!(instance.try_borrow().unwrap().0, 0);
@@ -2286,7 +2286,7 @@ a = A()
 
         #[test]
         fn test_frozen_get() {
-            Python::with_gil(|py| {
+            Python::attach(|py| {
                 for i in 0..10 {
                     let instance = Py::new(py, FrozenClass(i)).unwrap();
                     assert_eq!(instance.get().0, i);
@@ -2316,7 +2316,7 @@ a = A()
 
         #[test]
         fn test_as_super() {
-            Python::with_gil(|py| {
+            Python::attach(|py| {
                 let obj = Bound::new(py, (SubClass, BaseClass)).unwrap();
                 let _: &Bound<'_, BaseClass> = obj.as_super();
                 let _: &Bound<'_, PyAny> = obj.as_super().as_super();
@@ -2326,7 +2326,7 @@ a = A()
 
         #[test]
         fn test_into_super() {
-            Python::with_gil(|py| {
+            Python::attach(|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();
diff --git a/src/lib.rs b/src/lib.rs
index ea160ed0966..370eee4fa7e 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -91,7 +91,7 @@
 //!
 //! - `abi3`: Restricts PyO3's API to a subset of the full Python API which is guaranteed by
 //! [PEP 384] to be forward-compatible with future Python versions.
-//! - `auto-initialize`: Changes [`Python::with_gil`] to automatically initialize the Python
+//! - `auto-initialize`: Changes [`Python::attach`] to automatically initialize the Python
 //! interpreter if needed.
 //! - `extension-module`: This will tell the linker to keep the Python symbols unresolved, so that
 //! your module can also be used with statically linked Python interpreters. Use this feature when
@@ -255,7 +255,7 @@
 //! use pyo3::ffi::c_str;
 //!
 //! fn main() -> PyResult<()> {
-//!     Python::with_gil(|py| {
+//!     Python::attach(|py| {
 //!         let sys = py.import("sys")?;
 //!         let version: String = sys.getattr("version")?.extract()?;
 //!
diff --git a/src/macros.rs b/src/macros.rs
index 01d035f85ac..7727a461d2a 100644
--- a/src/macros.rs
+++ b/src/macros.rs
@@ -12,7 +12,7 @@
 /// use pyo3::{prelude::*, py_run, types::PyList};
 ///
 /// # fn main() -> PyResult<()> {
-/// Python::with_gil(|py| {
+/// Python::attach(|py| {
 ///     let list = PyList::new(py, &[1, 2, 3])?;
 ///     py_run!(py, list, "assert list == [1, 2, 3]");
 /// # Ok(())
@@ -47,7 +47,7 @@
 ///     }
 /// }
 ///
-/// Python::with_gil(|py| {
+/// Python::attach(|py| {
 ///     let time = Py::new(py, Time {hour: 8, minute: 43, second: 16}).unwrap();
 ///     let time_as_tuple = (8, 43, 16);
 ///     py_run!(py, time time_as_tuple, r#"
@@ -76,7 +76,7 @@
 /// }
 ///
 /// # fn main() -> PyResult<()> {
-/// Python::with_gil(|py| {
+/// Python::attach(|py| {
 ///     let locals = [("C", py.get_type::())].into_py_dict(py)?;
 ///     pyo3::py_run!(py, *locals, "c = C()");
 /// #   Ok(())
diff --git a/src/marker.rs b/src/marker.rs
index fdc7b32ec00..bcf5d2fcb48 100644
--- a/src/marker.rs
+++ b/src/marker.rs
@@ -44,7 +44,7 @@
 //! use std::rc::Rc;
 //!
 //! fn main() {
-//!     Python::with_gil(|py| {
+//!     Python::attach(|py| {
 //!         let rc = Rc::new(5);
 //!
 //!         py.allow_threads(|| {
@@ -72,7 +72,7 @@
 //! use pyo3::types::PyString;
 //! use send_wrapper::SendWrapper;
 //!
-//! Python::with_gil(|py| {
+//! Python::attach(|py| {
 //!     let string = PyString::new(py, "foo");
 //!
 //!     let wrapped = SendWrapper::new(string);
@@ -151,7 +151,7 @@ use std::os::raw::c_int;
 /// # use pyo3::prelude::*;
 /// use std::rc::Rc;
 ///
-/// Python::with_gil(|py| {
+/// Python::attach(|py| {
 ///     let rc = Rc::new(42);
 ///
 ///     py.allow_threads(|| {
@@ -160,7 +160,7 @@ use std::os::raw::c_int;
 /// });
 /// ```
 ///
-/// This also implies that the interplay between `with_gil` and `allow_threads` is unsound, for example
+/// This also implies that the interplay between `attach` and `allow_threads` is unsound, for example
 /// one can circumvent this protection using the [`send_wrapper`](https://docs.rs/send_wrapper/) crate:
 ///
 /// ```no_run
@@ -168,7 +168,7 @@ use std::os::raw::c_int;
 /// # use pyo3::types::PyString;
 /// use send_wrapper::SendWrapper;
 ///
-/// Python::with_gil(|py| {
+/// Python::attach(|py| {
 ///     let string = PyString::new(py, "foo");
 ///
 ///     let wrapped = SendWrapper::new(string);
@@ -214,7 +214,7 @@ mod nightly {
         /// ```compile_fail
         /// # use pyo3::prelude::*;
         /// # use pyo3::types::PyString;
-        /// Python::with_gil(|py| {
+        /// Python::attach(|py| {
         ///     let string = PyString::new(py, "foo");
         ///
         ///     py.allow_threads(|| {
@@ -227,7 +227,7 @@ mod nightly {
         ///
         /// ```compile_fail
         /// # use pyo3::prelude::*;
-        /// Python::with_gil(|py| {
+        /// Python::attach(|py| {
         ///     py.allow_threads(|| {
         ///         drop(py);
         ///     });
@@ -242,7 +242,7 @@ mod nightly {
         /// # use pyo3::types::PyString;
         /// use send_wrapper::SendWrapper;
         ///
-        /// Python::with_gil(|py| {
+        /// Python::attach(|py| {
         ///     let string = PyString::new(py, "foo");
         ///
         ///     let wrapped = SendWrapper::new(string);
@@ -262,7 +262,7 @@ mod nightly {
         /// # use pyo3::prelude::*;
         /// use std::rc::Rc;
         ///
-        /// Python::with_gil(|py| {
+        /// Python::attach(|py| {
         ///     let rc = Rc::new(42);
         ///
         ///     py.allow_threads(|| {
@@ -320,9 +320,9 @@ 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.
 /// - 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::attach`] 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
+/// The first two options are zero-cost; [`Python::attach`] requires runtime checking and may need to block
 /// to acquire the GIL.
 ///
 /// # Deadlocks
@@ -357,12 +357,23 @@ pub use nightly::Ungil;
 pub struct Python<'py>(PhantomData<(&'py GILGuard, NotSend)>);
 
 impl Python<'_> {
+    /// See [Python::attach]
+    #[inline]
+    #[track_caller]
+    #[deprecated(note = "use `Python::attach` instead", since = "0.26.0")]
+    pub fn with_gil(f: F) -> R
+    where
+        F: for<'py> FnOnce(Python<'py>) -> R,
+    {
+        Self::attach(f)
+    }
+
     /// Acquires the global interpreter lock, allowing access to the Python interpreter. The
     /// provided closure `F` will be executed with the acquired `Python` marker token.
     ///
     /// If implementing [`#[pymethods]`](crate::pymethods) or [`#[pyfunction]`](crate::pyfunction),
     /// declare `py: Python` as an argument. PyO3 will pass in the token to grant access to the GIL
-    /// context in which the function is running, avoiding the need to call `with_gil`.
+    /// context in which the function is running, avoiding the need to call `attach`.
     ///
     /// If the [`auto-initialize`] feature is enabled and the Python runtime is not already
     /// initialized, this function will initialize it. See
@@ -389,7 +400,7 @@ impl Python<'_> {
     /// use pyo3::ffi::c_str;
     ///
     /// # fn main() -> PyResult<()> {
-    /// Python::with_gil(|py| -> PyResult<()> {
+    /// Python::attach(|py| -> PyResult<()> {
     ///     let x: i32 = py.eval(c_str!("5"), None, None)?.extract()?;
     ///     assert_eq!(x, 5);
     ///     Ok(())
@@ -400,7 +411,7 @@ impl Python<'_> {
     /// [`auto-initialize`]: https://pyo3.rs/main/features.html#auto-initialize
     #[inline]
     #[track_caller]
-    pub fn with_gil(f: F) -> R
+    pub fn attach(f: F) -> R
     where
         F: for<'py> FnOnce(Python<'py>) -> R,
     {
@@ -410,7 +421,7 @@ impl Python<'_> {
         f(guard.python())
     }
 
-    /// Like [`Python::with_gil`] except Python interpreter state checking is skipped.
+    /// Like [`Python::attach`] except Python interpreter state checking is skipped.
     ///
     /// Normally when the GIL is acquired, we check that the Python interpreter is an
     /// appropriate state (e.g. it is fully initialized). This function skips those
@@ -418,12 +429,12 @@ impl Python<'_> {
     ///
     /// # Safety
     ///
-    /// If [`Python::with_gil`] would succeed, it is safe to call this function.
+    /// If [`Python::attach`] would succeed, it is safe to call this function.
     ///
-    /// In most cases, you should use [`Python::with_gil`].
+    /// In most cases, you should use [`Python::attach`].
     ///
     /// A justified scenario for calling this function is during multi-phase interpreter
-    /// initialization when [`Python::with_gil`] would fail before
+    /// initialization when [`Python::attach`] would fail before
     // this link is only valid on 3.8+not pypy and up.
     #[cfg_attr(
         all(Py_3_8, not(PyPy)),
@@ -477,7 +488,7 @@ impl<'py> Python<'py> {
     /// }
     /// #
     /// # fn main() -> PyResult<()> {
-    /// #     Python::with_gil(|py| -> PyResult<()> {
+    /// #     Python::attach(|py| -> PyResult<()> {
     /// #         let fun = pyo3::wrap_pyfunction!(sum_numbers, py)?;
     /// #         let res = fun.call1((vec![1_u32, 2, 3],))?;
     /// #         assert_eq!(res.extract::()?, 6_u32);
@@ -533,7 +544,7 @@ impl<'py> Python<'py> {
     /// ```
     /// # use pyo3::prelude::*;
     /// # use pyo3::ffi::c_str;
-    /// # Python::with_gil(|py| {
+    /// # Python::attach(|py| {
     /// 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])
@@ -563,7 +574,7 @@ impl<'py> Python<'py> {
     ///     types::{PyBytes, PyDict},
     ///     ffi::c_str,
     /// };
-    /// Python::with_gil(|py| {
+    /// Python::attach(|py| {
     ///     let locals = PyDict::new(py);
     ///     py.run(c_str!(
     ///         r#"
@@ -705,7 +716,7 @@ impl<'py> Python<'py> {
     /// # Examples
     /// ```rust
     /// # use pyo3::Python;
-    /// Python::with_gil(|py| {
+    /// Python::attach(|py| {
     ///     // The full string could be, for example:
     ///     // "3.10.0 (tags/v3.10.0:b494f59, Oct  4 2021, 19:00:18) [MSC v.1929 64 bit (AMD64)]"
     ///     assert!(py.version().starts_with("3."));
@@ -725,7 +736,7 @@ impl<'py> Python<'py> {
     /// # Examples
     /// ```rust
     /// # use pyo3::Python;
-    /// Python::with_gil(|py| {
+    /// Python::attach(|py| {
     ///     // PyO3 supports Python 3.7 and up.
     ///     assert!(py.version_info() >= (3, 7));
     ///     assert!(py.version_info() >= (3, 7, 0));
@@ -818,7 +829,7 @@ mod tests {
 
     #[test]
     fn test_eval() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             // Make sure builtin names are accessible
             let v: i32 = py
                 .eval(ffi::c_str!("min(1, 2)"), None, None)
@@ -859,11 +870,11 @@ mod tests {
     #[test]
     #[cfg(not(target_arch = "wasm32"))] // We are building wasm Python with pthreads disabled
     fn test_allow_threads_releases_and_acquires_gil() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let b = std::sync::Arc::new(std::sync::Barrier::new(2));
 
             let b2 = b.clone();
-            std::thread::spawn(move || Python::with_gil(|_| b2.wait()));
+            std::thread::spawn(move || Python::attach(|_| b2.wait()));
 
             py.allow_threads(|| {
                 // If allow_threads does not release the GIL, this will deadlock because
@@ -882,7 +893,7 @@ mod tests {
 
     #[test]
     fn test_allow_threads_panics_safely() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let result = std::panic::catch_unwind(|| unsafe {
                 let py = Python::assume_gil_acquired();
                 py.allow_threads(|| {
@@ -903,11 +914,11 @@ 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"]).unwrap().unbind());
+        let list = Python::attach(|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"));
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             py.allow_threads(|| {
                 drop((list, &mut v, a));
             });
@@ -928,7 +939,7 @@ mod tests {
         let state = unsafe { crate::ffi::PyGILState_Check() };
         assert_eq!(state, GIL_NOT_HELD);
 
-        Python::with_gil(|_| {
+        Python::attach(|_| {
             let state = unsafe { crate::ffi::PyGILState_Check() };
             assert_eq!(state, GIL_HELD);
         });
@@ -939,7 +950,7 @@ mod tests {
 
     #[test]
     fn test_ellipsis() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             assert_eq!(py.Ellipsis().to_string(), "Ellipsis");
 
             let v = py
@@ -955,7 +966,7 @@ mod tests {
     fn test_py_run_inserts_globals() {
         use crate::types::dict::PyDictMethods;
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let namespace = PyDict::new(py);
             py.run(
                 ffi::c_str!("class Foo: pass\na = int(3)"),
@@ -1010,7 +1021,7 @@ cls.func()
             .unwrap(),
         };
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             runner.reproducer(py).unwrap();
         });
     }
diff --git a/src/marshal.rs b/src/marshal.rs
index cecaa16f53b..0cb82573b2d 100644
--- a/src/marshal.rs
+++ b/src/marshal.rs
@@ -23,7 +23,7 @@ pub const VERSION: i32 = 4;
 /// # Examples
 /// ```
 /// # use pyo3::{marshal, types::PyDict, prelude::PyDictMethods};
-/// # pyo3::Python::with_gil(|py| {
+/// # pyo3::Python::attach(|py| {
 /// let dict = PyDict::new(py);
 /// dict.set_item("aap", "noot").unwrap();
 /// dict.set_item("mies", "wim").unwrap();
@@ -59,7 +59,7 @@ mod tests {
 
     #[test]
     fn marshal_roundtrip() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let dict = PyDict::new(py);
             dict.set_item("aap", "noot").unwrap();
             dict.set_item("mies", "wim").unwrap();
diff --git a/src/pybacked.rs b/src/pybacked.rs
index 2d67786c802..b1ee692003d 100644
--- a/src/pybacked.rs
+++ b/src/pybacked.rs
@@ -321,7 +321,7 @@ mod test {
 
     #[test]
     fn py_backed_str_empty() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let s = PyString::new(py, "");
             let py_backed_str = s.extract::().unwrap();
             assert_eq!(&*py_backed_str, "");
@@ -330,7 +330,7 @@ mod test {
 
     #[test]
     fn py_backed_str() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let s = PyString::new(py, "hello");
             let py_backed_str = s.extract::().unwrap();
             assert_eq!(&*py_backed_str, "hello");
@@ -339,7 +339,7 @@ mod test {
 
     #[test]
     fn py_backed_str_try_from() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let s = PyString::new(py, "hello");
             let py_backed_str = PyBackedStr::try_from(s).unwrap();
             assert_eq!(&*py_backed_str, "hello");
@@ -348,7 +348,7 @@ mod test {
 
     #[test]
     fn py_backed_str_into_pyobject() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let orig_str = PyString::new(py, "hello");
             let py_backed_str = orig_str.extract::().unwrap();
             let new_str = py_backed_str.into_pyobject(py).unwrap();
@@ -360,7 +360,7 @@ mod test {
 
     #[test]
     fn py_backed_bytes_empty() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let b = PyBytes::new(py, b"");
             let py_backed_bytes = b.extract::().unwrap();
             assert_eq!(&*py_backed_bytes, b"");
@@ -369,7 +369,7 @@ mod test {
 
     #[test]
     fn py_backed_bytes() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let b = PyBytes::new(py, b"abcde");
             let py_backed_bytes = b.extract::().unwrap();
             assert_eq!(&*py_backed_bytes, b"abcde");
@@ -378,7 +378,7 @@ mod test {
 
     #[test]
     fn py_backed_bytes_from_bytes() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let b = PyBytes::new(py, b"abcde");
             let py_backed_bytes = PyBackedBytes::from(b);
             assert_eq!(&*py_backed_bytes, b"abcde");
@@ -387,7 +387,7 @@ mod test {
 
     #[test]
     fn py_backed_bytes_from_bytearray() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let b = PyByteArray::new(py, b"abcde");
             let py_backed_bytes = PyBackedBytes::from(b);
             assert_eq!(&*py_backed_bytes, b"abcde");
@@ -396,7 +396,7 @@ mod test {
 
     #[test]
     fn py_backed_bytes_into_pyobject() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let orig_bytes = PyBytes::new(py, b"abcde");
             let py_backed_bytes = PyBackedBytes::from(orig_bytes.clone());
             assert!((&py_backed_bytes)
@@ -408,7 +408,7 @@ mod test {
 
     #[test]
     fn rust_backed_bytes_into_pyobject() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let orig_bytes = PyByteArray::new(py, b"abcde");
             let rust_backed_bytes = PyBackedBytes::from(orig_bytes);
             assert!(matches!(
@@ -436,7 +436,7 @@ mod test {
     #[cfg(feature = "py-clone")]
     #[test]
     fn test_backed_str_clone() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let s1: PyBackedStr = PyString::new(py, "hello").try_into().unwrap();
             let s2 = s1.clone();
             assert_eq!(s1, s2);
@@ -448,7 +448,7 @@ mod test {
 
     #[test]
     fn test_backed_str_eq() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let s1: PyBackedStr = PyString::new(py, "hello").try_into().unwrap();
             let s2: PyBackedStr = PyString::new(py, "hello").try_into().unwrap();
             assert_eq!(s1, "hello");
@@ -462,7 +462,7 @@ mod test {
 
     #[test]
     fn test_backed_str_hash() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let h = {
                 let mut hasher = DefaultHasher::new();
                 "abcde".hash(&mut hasher);
@@ -482,7 +482,7 @@ mod test {
 
     #[test]
     fn test_backed_str_ord() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut a = vec!["a", "c", "d", "b", "f", "g", "e"];
             let mut b = a
                 .iter()
@@ -499,7 +499,7 @@ mod test {
     #[cfg(feature = "py-clone")]
     #[test]
     fn test_backed_bytes_from_bytes_clone() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let b1: PyBackedBytes = PyBytes::new(py, b"abcde").into();
             let b2 = b1.clone();
             assert_eq!(b1, b2);
@@ -512,7 +512,7 @@ mod test {
     #[cfg(feature = "py-clone")]
     #[test]
     fn test_backed_bytes_from_bytearray_clone() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let b1: PyBackedBytes = PyByteArray::new(py, b"abcde").into();
             let b2 = b1.clone();
             assert_eq!(b1, b2);
@@ -524,7 +524,7 @@ mod test {
 
     #[test]
     fn test_backed_bytes_eq() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let b1: PyBackedBytes = PyBytes::new(py, b"abcde").into();
             let b2: PyBackedBytes = PyByteArray::new(py, b"abcde").into();
 
@@ -539,7 +539,7 @@ mod test {
 
     #[test]
     fn test_backed_bytes_hash() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let h = {
                 let mut hasher = DefaultHasher::new();
                 b"abcde".hash(&mut hasher);
@@ -567,7 +567,7 @@ mod test {
 
     #[test]
     fn test_backed_bytes_ord() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut a = vec![b"a", b"c", b"d", b"b", b"f", b"g", b"e"];
             let mut b = a
                 .iter()
diff --git a/src/pycell.rs b/src/pycell.rs
index ff279000daf..215bfeedc8c 100644
--- a/src/pycell.rs
+++ b/src/pycell.rs
@@ -92,7 +92,7 @@
 //! #     }
 //! # }
 //! # fn main() -> PyResult<()> {
-//! Python::with_gil(|py| {
+//! Python::attach(|py| {
 //!     let n = Py::new(py, Number { inner: 0 })?;
 //!
 //!     // We borrow the guard and then dereference
@@ -128,7 +128,7 @@
 //!     std::mem::swap(&mut a.inner, &mut b.inner);
 //! }
 //! # fn main() {
-//! #     Python::with_gil(|py| {
+//! #     Python::attach(|py| {
 //! #         let n = Py::new(py, Number{inner: 35}).unwrap();
 //! #         let n2 = n.clone_ref(py);
 //! #         assert!(n.is(&n2));
@@ -166,7 +166,7 @@
 //! }
 //! # fn main() {
 //! #     // With duplicate numbers
-//! #     Python::with_gil(|py| {
+//! #     Python::attach(|py| {
 //! #         let n = Py::new(py, Number{inner: 35}).unwrap();
 //! #         let n2 = n.clone_ref(py);
 //! #         assert!(n.is(&n2));
@@ -175,7 +175,7 @@
 //! #     });
 //! #
 //! #     // With two different numbers
-//! #     Python::with_gil(|py| {
+//! #     Python::attach(|py| {
 //! #         let n = Py::new(py, Number{inner: 35}).unwrap();
 //! #         let n2 = Py::new(py, Number{inner: 42}).unwrap();
 //! #         assert!(!n.is(&n2));
@@ -244,7 +244,7 @@ use impl_::{PyClassBorrowChecker, PyClassObjectLayout};
 ///         format!("{}(base: {}, cnt: {})", slf.name, basename, refcnt)
 ///     }
 /// }
-/// # Python::with_gil(|py| {
+/// # Python::attach(|py| {
 /// #     let sub = Py::new(py, Child::new()).unwrap();
 /// #     pyo3::py_run!(py, sub, "assert sub.format() == 'Caterpillar(base: Butterfly, cnt: 4)', sub.format()");
 /// # });
@@ -359,7 +359,7 @@ where
     ///         format!("{} {} {}", super_.as_ref().name1, super_.name2, subname)
     ///     }
     /// }
-    /// # Python::with_gil(|py| {
+    /// # Python::attach(|py| {
     /// #     let sub = Py::new(py, Sub::new()).unwrap();
     /// #     pyo3::py_run!(py, sub, "assert sub.name() == 'base1 base2 sub'")
     /// # });
@@ -414,7 +414,7 @@ where
     ///         format!("{} {}", slf.as_super().base_name_len(), slf.sub_name_len())
     ///     }
     /// }
-    /// # Python::with_gil(|py| {
+    /// # Python::attach(|py| {
     /// #     let sub = Py::new(py, Sub::new()).unwrap();
     /// #     pyo3::py_run!(py, sub, "assert sub.format_name_lengths() == '9 8'")
     /// # });
@@ -707,7 +707,7 @@ mod tests {
 
     #[test]
     fn test_as_ptr() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let cell = Bound::new(py, SomeClass(0)).unwrap();
             let ptr = cell.as_ptr();
 
@@ -718,7 +718,7 @@ mod tests {
 
     #[test]
     fn test_into_ptr() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let cell = Bound::new(py, SomeClass(0)).unwrap();
             let ptr = cell.as_ptr();
 
@@ -774,7 +774,7 @@ mod tests {
 
     #[test]
     fn test_pyref_as_super() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let obj = SubSubClass::new(py).into_bound(py);
             let pyref = obj.borrow();
             assert_eq!(pyref.as_super().as_super().val1, 10);
@@ -787,7 +787,7 @@ mod tests {
 
     #[test]
     fn test_pyrefmut_as_super() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let obj = SubSubClass::new(py).into_bound(py);
             assert_eq!(SubSubClass::get_values(obj.borrow()), (10, 15, 20));
             {
@@ -806,7 +806,7 @@ mod tests {
 
     #[test]
     fn test_pyrefs_in_python() {
-        Python::with_gil(|py| {
+        Python::attach(|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");
diff --git a/src/pycell/impl_.rs b/src/pycell/impl_.rs
index b59314bc54a..49f4bf32f72 100644
--- a/src/pycell/impl_.rs
+++ b/src/pycell/impl_.rs
@@ -449,7 +449,7 @@ mod tests {
 
     #[test]
     fn test_mutable_borrow_prevents_further_borrows() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mmm = Py::new(
                 py,
                 PyClassInitializer::from(MutableBase)
@@ -500,7 +500,7 @@ mod tests {
 
     #[test]
     fn test_immutable_borrows_prevent_mutable_borrows() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mmm = Py::new(
                 py,
                 PyClassInitializer::from(MutableBase)
@@ -552,7 +552,7 @@ mod tests {
             x: u64,
         }
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let inst = Py::new(py, MyClass { x: 0 }).unwrap();
 
             let total_modifications = py.allow_threads(|| {
@@ -562,7 +562,7 @@ mod tests {
                     let threads = (0..10)
                         .map(|_| {
                             s.spawn(|| {
-                                Python::with_gil(|py| {
+                                Python::attach(|py| {
                                     // Each thread records its own view of how many writes it made
                                     let mut local_modifications = 0;
                                     for _ in 0..100 {
diff --git a/src/pyclass_init.rs b/src/pyclass_init.rs
index a4094487bc9..310fa77fb9c 100644
--- a/src/pyclass_init.rs
+++ b/src/pyclass_init.rs
@@ -51,7 +51,7 @@ use std::{
 ///             })
 ///     }
 /// }
-/// Python::with_gil(|py| {
+/// Python::attach(|py| {
 ///     let typeobj = py.get_type::();
 ///     let sub_sub_class = typeobj.call((), None).unwrap();
 ///     py_run!(
@@ -120,7 +120,7 @@ impl PyClassInitializer {
     /// }
     ///
     /// fn main() -> PyResult<()> {
-    ///     Python::with_gil(|py| {
+    ///     Python::attach(|py| {
     ///         let m = PyModule::new(py, "example")?;
     ///         m.add_class::()?;
     ///         m.add_class::()?;
@@ -287,7 +287,7 @@ mod tests {
     #[test]
     #[should_panic]
     fn add_subclass_to_py_is_unsound() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let base = Py::new(py, BaseClass {}).unwrap();
             let _subclass = PyClassInitializer::from(base).add_subclass(SubClass { _data: 42 });
         });
diff --git a/src/sync.rs b/src/sync.rs
index ba8f3da3310..276db6968ef 100644
--- a/src/sync.rs
+++ b/src/sync.rs
@@ -39,7 +39,7 @@ use crate::PyVisit;
 ///
 /// static NUMBERS: GILProtected>> = GILProtected::new(RefCell::new(Vec::new()));
 ///
-/// Python::with_gil(|py| {
+/// Python::attach(|py| {
 ///     NUMBERS.get(py).borrow_mut().push(42);
 /// });
 /// ```
@@ -117,7 +117,7 @@ unsafe impl Sync for GILProtected where T: Send {}
 ///         .get_or_init(py, || PyList::empty(py).unbind())
 ///         .bind(py)
 /// }
-/// # Python::with_gil(|py| assert_eq!(get_shared_list(py).len(), 0));
+/// # Python::attach(|py| assert_eq!(get_shared_list(py).len(), 0));
 /// ```
 pub struct GILOnceCell {
     once: Once,
@@ -140,7 +140,7 @@ pub struct GILOnceCell {
     /// let cell = GILOnceCell::new();
     /// {
     ///     let s = String::new();
-    ///     let _ = Python::with_gil(|py| cell.set(py,A(&s)));
+    ///     let _ = Python::attach(|py| cell.set(py,A(&s)));
     /// }
     /// ```
     _marker: PhantomData,
@@ -335,7 +335,7 @@ where
     ///         .call1((dict,))
     /// }
     ///
-    /// # Python::with_gil(|py| {
+    /// # Python::attach(|py| {
     /// #     let dict = PyDict::new(py);
     /// #     dict.set_item(intern!(py, "foo"), 42).unwrap();
     /// #     let fun = wrap_pyfunction!(create_ordered_dict, py).unwrap();
@@ -397,7 +397,7 @@ impl Drop for GILOnceCell {
 ///     Ok(dict)
 /// }
 /// #
-/// # Python::with_gil(|py| {
+/// # Python::attach(|py| {
 /// #     let fun_slow = wrap_pyfunction!(create_dict, py).unwrap();
 /// #     let dict = fun_slow.call0().unwrap();
 /// #     assert!(dict.contains("foo").unwrap());
@@ -794,7 +794,7 @@ mod tests {
 
     #[test]
     fn test_intern() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let foo1 = "foo";
             let foo2 = intern!(py, "foo");
             let foo3 = intern!(py, stringify!(foo));
@@ -815,7 +815,7 @@ mod tests {
 
     #[test]
     fn test_once_cell() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut cell = GILOnceCell::new();
 
             assert!(cell.get(py).is_none());
@@ -849,7 +849,7 @@ mod tests {
             }
         }
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut dropped = false;
             let cell = GILOnceCell::new();
             cell.set(py, RecordDrop(&mut dropped)).unwrap();
@@ -867,13 +867,13 @@ mod tests {
     fn test_critical_section() {
         let barrier = Barrier::new(2);
 
-        let bool_wrapper = Python::with_gil(|py| -> Py {
+        let bool_wrapper = Python::attach(|py| -> Py {
             Py::new(py, BoolWrapper(AtomicBool::new(false))).unwrap()
         });
 
         std::thread::scope(|s| {
             s.spawn(|| {
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let b = bool_wrapper.bind(py);
                     with_critical_section(b, || {
                         barrier.wait();
@@ -884,7 +884,7 @@ mod tests {
             });
             s.spawn(|| {
                 barrier.wait();
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let b = bool_wrapper.bind(py);
                     // this blocks until the other thread's critical section finishes
                     with_critical_section(b, || {
@@ -901,7 +901,7 @@ mod tests {
     fn test_critical_section2() {
         let barrier = Barrier::new(3);
 
-        let (bool_wrapper1, bool_wrapper2) = Python::with_gil(|py| {
+        let (bool_wrapper1, bool_wrapper2) = Python::attach(|py| {
             (
                 Py::new(py, BoolWrapper(AtomicBool::new(false))).unwrap(),
                 Py::new(py, BoolWrapper(AtomicBool::new(false))).unwrap(),
@@ -910,7 +910,7 @@ mod tests {
 
         std::thread::scope(|s| {
             s.spawn(|| {
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let b1 = bool_wrapper1.bind(py);
                     let b2 = bool_wrapper2.bind(py);
                     with_critical_section2(b1, b2, || {
@@ -923,7 +923,7 @@ mod tests {
             });
             s.spawn(|| {
                 barrier.wait();
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let b1 = bool_wrapper1.bind(py);
                     // this blocks until the other thread's critical section finishes
                     with_critical_section(b1, || {
@@ -933,7 +933,7 @@ mod tests {
             });
             s.spawn(|| {
                 barrier.wait();
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let b2 = bool_wrapper2.bind(py);
                     // this blocks until the other thread's critical section finishes
                     with_critical_section(b2, || {
@@ -950,13 +950,13 @@ mod tests {
     fn test_critical_section2_same_object_no_deadlock() {
         let barrier = Barrier::new(2);
 
-        let bool_wrapper = Python::with_gil(|py| -> Py {
+        let bool_wrapper = Python::attach(|py| -> Py {
             Py::new(py, BoolWrapper(AtomicBool::new(false))).unwrap()
         });
 
         std::thread::scope(|s| {
             s.spawn(|| {
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let b = bool_wrapper.bind(py);
                     with_critical_section2(b, b, || {
                         barrier.wait();
@@ -967,7 +967,7 @@ mod tests {
             });
             s.spawn(|| {
                 barrier.wait();
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let b = bool_wrapper.bind(py);
                     // this blocks until the other thread's critical section finishes
                     with_critical_section(b, || {
@@ -982,7 +982,7 @@ mod tests {
     #[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| {
+        let (vec1, vec2) = Python::attach(|py| {
             (
                 Py::new(py, VecWrapper(vec![1, 2, 3])).unwrap(),
                 Py::new(py, VecWrapper(vec![4, 5])).unwrap(),
@@ -991,7 +991,7 @@ mod tests {
 
         std::thread::scope(|s| {
             s.spawn(|| {
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let v1 = vec1.bind(py);
                     let v2 = vec2.bind(py);
                     with_critical_section2(v1, v2, || {
@@ -1001,7 +1001,7 @@ mod tests {
                 });
             });
             s.spawn(|| {
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let v1 = vec1.bind(py);
                     let v2 = vec2.bind(py);
                     with_critical_section2(v1, v2, || {
@@ -1012,7 +1012,7 @@ mod tests {
             });
         });
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v1 = vec1.bind(py);
             let v2 = vec2.bind(py);
             // execution order is not guaranteed, so we need to check both
@@ -1043,7 +1043,7 @@ mod tests {
                 std::thread::scope(|s| {
                     // poison the once
                     let handle = s.spawn(|| {
-                        Python::with_gil(|py| {
+                        Python::attach(|py| {
                             init.call_once_py_attached(py, || panic!());
                         })
                     });
@@ -1051,7 +1051,7 @@ mod tests {
 
                     // poisoning propagates
                     let handle = s.spawn(|| {
-                        Python::with_gil(|py| {
+                        Python::attach(|py| {
                             init.call_once_py_attached(py, || {});
                         });
                     });
@@ -1059,7 +1059,7 @@ mod tests {
                     assert!(handle.join().is_err());
 
                     // call_once_force will still run and reset the poisoned state
-                    Python::with_gil(|py| {
+                    Python::attach(|py| {
                         init.call_once_force_py_attached(py, |state| {
                             assert!($is_poisoned(state.clone()));
                         });
@@ -1069,7 +1069,7 @@ mod tests {
                     });
 
                     // calling call_once_force should return immediately without calling the closure
-                    Python::with_gil(|py| init.call_once_force_py_attached(py, |_| panic!()));
+                    Python::attach(|py| init.call_once_force_py_attached(py, |_| panic!()));
                 });
             }};
         }
@@ -1087,7 +1087,7 @@ mod tests {
             assert!(cell.get().is_none());
 
             s.spawn(|| {
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     assert_eq!(*cell.get_or_init_py_attached(py, || 12345), 12345);
                 });
             });
@@ -1101,13 +1101,13 @@ mod tests {
     fn test_mutex_ext() {
         let barrier = Barrier::new(2);
 
-        let mutex = Python::with_gil(|py| -> Mutex> {
+        let mutex = Python::attach(|py| -> Mutex> {
             Mutex::new(Py::new(py, BoolWrapper(AtomicBool::new(false))).unwrap())
         });
 
         std::thread::scope(|s| {
             s.spawn(|| {
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let b = mutex.lock_py_attached(py).unwrap();
                     barrier.wait();
                     // sleep to ensure the other thread actually blocks
@@ -1118,7 +1118,7 @@ mod tests {
             });
             s.spawn(|| {
                 barrier.wait();
-                Python::with_gil(|py| {
+                Python::attach(|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));
@@ -1138,11 +1138,11 @@ mod tests {
             ($guard:ty ,$mutex:stmt) => {{
                 let barrier = Barrier::new(2);
 
-                let mutex = Python::with_gil({ $mutex });
+                let mutex = Python::attach({ $mutex });
 
                 std::thread::scope(|s| {
                     s.spawn(|| {
-                        Python::with_gil(|py| {
+                        Python::attach(|py| {
                             let b: $guard = mutex.lock_py_attached(py);
                             barrier.wait();
                             // sleep to ensure the other thread actually blocks
@@ -1153,7 +1153,7 @@ mod tests {
                     });
                     s.spawn(|| {
                         barrier.wait();
-                        Python::with_gil(|py| {
+                        Python::attach(|py| {
                             // blocks until the other thread releases the lock
                             let b: $guard = mutex.lock_py_attached(py);
                             assert!((*b).bind(py).borrow().0.load(Ordering::Acquire));
@@ -1182,7 +1182,7 @@ mod tests {
 
         std::thread::scope(|s| {
             let lock_result = s.spawn(|| {
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let _unused = mutex.lock_py_attached(py);
                     panic!();
                 });
@@ -1190,7 +1190,7 @@ mod tests {
             assert!(lock_result.join().is_err());
             assert!(mutex.is_poisoned());
         });
-        let guard = Python::with_gil(|py| {
+        let guard = Python::attach(|py| {
             // recover from the poisoning
             match mutex.lock_py_attached(py) {
                 Ok(guard) => guard,
diff --git a/src/tests/common.rs b/src/tests/common.rs
index 8dbd7427ab3..74da5376be9 100644
--- a/src/tests/common.rs
+++ b/src/tests/common.rs
@@ -107,7 +107,7 @@ with warnings.catch_warnings(record=True) as warning_record:
     #[macro_export]
     macro_rules! py_expect_warning_for_fn {
         ($fn:ident, $($val:ident)+, [$(($warning_msg:literal, $warning_category:path)),+] $(,)?) => {
-            pyo3::Python::with_gil(|py| {
+            pyo3::Python::attach(|py| {
                 let f = wrap_pyfunction!($fn)(py).unwrap();
                 py_expect_warning!(
                     py,
diff --git a/src/tests/hygiene/pyfunction.rs b/src/tests/hygiene/pyfunction.rs
index 44784aefe1c..a692b0acd3f 100644
--- a/src/tests/hygiene/pyfunction.rs
+++ b/src/tests/hygiene/pyfunction.rs
@@ -26,7 +26,7 @@ fn multiple_warning_function() {}
 
 #[test]
 fn invoke_wrap_pyfunction() {
-    crate::Python::with_gil(|py| {
+    crate::Python::attach(|py| {
         let func = crate::wrap_pyfunction!(do_something, py).unwrap();
         crate::py_run!(py, func, r#"func(5)"#);
     });
diff --git a/src/types/any.rs b/src/types/any.rs
index c0e91341594..58ff89b56b8 100644
--- a/src/types/any.rs
+++ b/src/types/any.rs
@@ -84,7 +84,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed {
     ///     sys.hasattr(intern!(sys.py(), "version"))
     /// }
     /// #
-    /// # Python::with_gil(|py| {
+    /// # Python::attach(|py| {
     /// #    let sys = py.import("sys").unwrap();
     /// #    has_version(&sys).unwrap();
     /// # });
@@ -110,7 +110,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed {
     ///     sys.getattr(intern!(sys.py(), "version"))
     /// }
     /// #
-    /// # Python::with_gil(|py| {
+    /// # Python::attach(|py| {
     /// #    let sys = py.import("sys").unwrap();
     /// #    version(&sys).unwrap();
     /// # });
@@ -141,7 +141,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed {
     ///     sys.getattr_opt(intern!(sys.py(), "version"))
     /// }
     /// #
-    /// # Python::with_gil(|py| {
+    /// # Python::attach(|py| {
     /// #    let sys = py.import("sys").unwrap();
     /// #    let version = get_version_if_exists(&sys).unwrap();
     /// #    assert!(version.is_some());
@@ -168,7 +168,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed {
     ///     ob.setattr(intern!(ob.py(), "answer"), 42)
     /// }
     /// #
-    /// # Python::with_gil(|py| {
+    /// # Python::attach(|py| {
     /// #    let ob = PyModule::new(py, "empty").unwrap();
     /// #    set_answer(&ob).unwrap();
     /// # });
@@ -210,7 +210,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed {
     /// use std::cmp::Ordering;
     ///
     /// # fn main() -> PyResult<()> {
-    /// Python::with_gil(|py| -> PyResult<()> {
+    /// Python::attach(|py| -> PyResult<()> {
     ///     let a = PyFloat::new(py, 0_f64);
     ///     let b = PyFloat::new(py, 42_f64);
     ///     assert_eq!(a.compare(b)?, Ordering::Less);
@@ -226,7 +226,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed {
     /// use pyo3::types::{PyFloat, PyString};
     ///
     /// # fn main() -> PyResult<()> {
-    /// Python::with_gil(|py| -> PyResult<()> {
+    /// Python::attach(|py| -> PyResult<()> {
     ///     let a = PyFloat::new(py, 0_f64);
     ///     let b = PyString::new(py, "zero");
     ///     assert!(a.compare(b).is_err());
@@ -263,7 +263,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed {
     /// use pyo3::prelude::*;
     ///
     /// # fn main() -> PyResult<()> {
-    /// Python::with_gil(|py| -> PyResult<()> {
+    /// Python::attach(|py| -> PyResult<()> {
     ///     let a = 0_u8.into_pyobject(py)?;
     ///     let b = 42_u8.into_pyobject(py)?;
     ///     assert!(a.rich_compare(b, CompareOp::Le)?.is_truthy()?);
@@ -417,7 +417,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed {
     /// use pyo3::prelude::*;
     ///
     /// # fn main() -> PyResult<()> {
-    /// Python::with_gil(|py| -> PyResult<()> {
+    /// Python::attach(|py| -> PyResult<()> {
     ///     let builtins = PyModule::import(py, "builtins")?;
     ///     let print = builtins.getattr("print")?;
     ///     assert!(print.is_callable());
@@ -456,7 +456,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed {
     /// "#);
     ///
     /// # fn main() -> PyResult<()> {
-    /// Python::with_gil(|py| {
+    /// Python::attach(|py| {
     ///     let module = PyModule::from_code(py, CODE, c_str!("func.py"), c_str!(""))?;
     ///     let fun = module.getattr("function")?;
     ///     let args = ("hello",);
@@ -482,7 +482,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed {
     /// use pyo3::prelude::*;
     ///
     /// # fn main() -> PyResult<()> {
-    /// Python::with_gil(|py| -> PyResult<()> {
+    /// Python::attach(|py| -> PyResult<()> {
     ///     let module = PyModule::import(py, "builtins")?;
     ///     let help = module.getattr("help")?;
     ///     help.call0()?;
@@ -513,7 +513,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed {
     /// "#);
     ///
     /// # fn main() -> PyResult<()> {
-    /// Python::with_gil(|py| {
+    /// Python::attach(|py| {
     ///     let module = PyModule::from_code(py, CODE, c_str!("func.py"), c_str!(""))?;
     ///     let fun = module.getattr("function")?;
     ///     let args = ("hello",);
@@ -552,7 +552,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed {
     /// "#);
     ///
     /// # fn main() -> PyResult<()> {
-    /// Python::with_gil(|py| {
+    /// Python::attach(|py| {
     ///     let module = PyModule::from_code(py, CODE, c_str!("a.py"), c_str!(""))?;
     ///     let instance = module.getattr("a")?;
     ///     let args = ("hello",);
@@ -598,7 +598,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed {
     /// "#);
     ///
     /// # fn main() -> PyResult<()> {
-    /// Python::with_gil(|py| {
+    /// Python::attach(|py| {
     ///     let module = PyModule::from_code(py, CODE, c_str!("a.py"), c_str!(""))?;
     ///     let instance = module.getattr("a")?;
     ///     let result = instance.call_method0("method")?;
@@ -635,7 +635,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed {
     /// "#);
     ///
     /// # fn main() -> PyResult<()> {
-    /// Python::with_gil(|py| {
+    /// Python::attach(|py| {
     ///     let module = PyModule::from_code(py, CODE, c_str!("a.py"), c_str!(""))?;
     ///     let instance = module.getattr("a")?;
     ///     let args = ("hello",);
@@ -706,7 +706,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed {
     ///     }
     /// }
     ///
-    /// Python::with_gil(|py| {
+    /// Python::attach(|py| {
     ///     assert!(is_iterable(&vec![1, 2, 3].into_pyobject(py).unwrap()));
     ///     assert!(!is_iterable(&PyNone::get(py)));
     /// });
@@ -733,7 +733,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed {
     /// use pyo3::prelude::*;
     /// use pyo3::types::{PyDict, PyList};
     ///
-    /// Python::with_gil(|py| {
+    /// Python::attach(|py| {
     ///     let dict = PyDict::new(py);
     ///     assert!(dict.is_instance_of::());
     ///     let any = dict.as_any();
@@ -757,7 +757,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed {
     ///     i: i32,
     /// }
     ///
-    /// Python::with_gil(|py| {
+    /// Python::attach(|py| {
     ///     let class = Py::new(py, Class { i: 0 }).unwrap().into_bound(py).into_any();
     ///
     ///     let class_bound: &Bound<'_, Class> = class.downcast()?;
@@ -785,7 +785,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed {
     /// use pyo3::prelude::*;
     /// use pyo3::types::{PyDict, PyList};
     ///
-    /// Python::with_gil(|py| {
+    /// Python::attach(|py| {
     ///     let obj: Bound<'_, PyAny> = PyDict::new(py).into_any();
     ///
     ///     let obj: Bound<'_, PyAny> = match obj.downcast_into::() {
@@ -818,7 +818,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed {
     /// use pyo3::prelude::*;
     /// use pyo3::types::{PyBool, PyInt};
     ///
-    /// Python::with_gil(|py| {
+    /// Python::attach(|py| {
     ///     let b = PyBool::new(py, true);
     ///     assert!(b.is_instance_of::());
     ///     let any: &Bound<'_, PyAny> = b.as_any();
@@ -1644,7 +1644,7 @@ mod tests {
 
     #[test]
     fn test_lookup_special() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let module = PyModule::from_code(
                 py,
                 c_str!(
@@ -1718,7 +1718,7 @@ class NonHeapNonDescriptorInt:
 
     #[test]
     fn test_getattr_opt() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let module = PyModule::from_code(
                 py,
                 c_str!(
@@ -1763,7 +1763,7 @@ class Test:
 
     #[test]
     fn test_call_for_non_existing_method() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             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());
@@ -1774,7 +1774,7 @@ class Test:
 
     #[test]
     fn test_call_with_kwargs() {
-        Python::with_gil(|py| {
+        Python::attach(|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("sort", (), Some(&dict)).unwrap();
@@ -1784,7 +1784,7 @@ class Test:
 
     #[test]
     fn test_call_method0() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let module = PyModule::from_code(
                 py,
                 c_str!(
@@ -1813,7 +1813,7 @@ class SimpleClass:
 
     #[test]
     fn test_type() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let obj = py.eval(ffi::c_str!("42"), None, None).unwrap();
             assert_eq!(obj.get_type().as_type_ptr(), obj.get_type_ptr());
         });
@@ -1821,7 +1821,7 @@ class SimpleClass:
 
     #[test]
     fn test_dir() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let obj = py.eval(ffi::c_str!("42"), None, None).unwrap();
             let dir = py
                 .eval(ffi::c_str!("dir(42)"), None, None)
@@ -1840,7 +1840,7 @@ class SimpleClass:
 
     #[test]
     fn test_hasattr() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let x = 5i32.into_pyobject(py).unwrap();
             assert!(x.is_instance_of::());
 
@@ -1866,7 +1866,7 @@ class SimpleClass:
             }
         }
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let obj = Py::new(py, GetattrFail).unwrap();
             let obj = obj.bind(py).as_any();
 
@@ -1879,7 +1879,7 @@ class SimpleClass:
 
     #[test]
     fn test_nan_eq() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let nan = py.eval(ffi::c_str!("float('nan')"), None, None).unwrap();
             assert!(nan.compare(&nan).is_err());
         });
@@ -1887,7 +1887,7 @@ class SimpleClass:
 
     #[test]
     fn test_any_is_instance_of() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let x = 5i32.into_pyobject(py).unwrap();
             assert!(x.is_instance_of::());
 
@@ -1898,7 +1898,7 @@ class SimpleClass:
 
     #[test]
     fn test_any_is_instance() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let l = vec![1i8, 2].into_pyobject(py).unwrap();
             assert!(l.is_instance(&py.get_type::()).unwrap());
         });
@@ -1906,7 +1906,7 @@ class SimpleClass:
 
     #[test]
     fn test_any_is_exact_instance_of() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let x = 5i32.into_pyobject(py).unwrap();
             assert!(x.is_exact_instance_of::());
 
@@ -1922,7 +1922,7 @@ class SimpleClass:
 
     #[test]
     fn test_any_is_exact_instance() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let t = PyBool::new(py, true);
             assert!(t.is_instance(&py.get_type::()).unwrap());
             assert!(!t.is_exact_instance(&py.get_type::()));
@@ -1932,7 +1932,7 @@ class SimpleClass:
 
     #[test]
     fn test_any_contains() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v: Vec = vec![1, 1, 2, 3, 5, 8];
             let ob = v.into_pyobject(py).unwrap();
 
@@ -1959,7 +1959,7 @@ class SimpleClass:
         for<'py> &'a T: IntoPyObject<'py>,
         for<'py> <&'a T as IntoPyObject<'py>>::Error: Debug,
     {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             for a in list {
                 for b in list {
                     let a_py = a.into_pyobject(py).unwrap().into_any().into_bound();
@@ -2053,7 +2053,7 @@ class SimpleClass:
 
     #[test]
     fn test_rich_compare_type_error() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let py_int = 1i32.into_pyobject(py).unwrap();
             let py_str = "1".into_pyobject(py).unwrap();
 
@@ -2068,7 +2068,7 @@ class SimpleClass:
 
     #[test]
     fn test_is_callable() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             assert!(PyList::type_object(py).is_callable());
 
             let not_callable = 5i32.into_pyobject(py).unwrap();
@@ -2078,7 +2078,7 @@ class SimpleClass:
 
     #[test]
     fn test_is_empty() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let empty_list = PyList::empty(py).into_any();
             assert!(empty_list.is_empty().unwrap());
 
@@ -2107,7 +2107,7 @@ class SimpleClass:
             }
         }
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let obj = Bound::new(py, DirFail).unwrap();
             assert!(obj.dir().unwrap_err().is_instance_of::(py));
         })
diff --git a/src/types/boolobject.rs b/src/types/boolobject.rs
index 09f1c14d59f..a3538c60cc1 100644
--- a/src/types/boolobject.rs
+++ b/src/types/boolobject.rs
@@ -244,7 +244,7 @@ mod tests {
 
     #[test]
     fn test_true() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             assert!(PyBool::new(py, true).is_true());
             let t = PyBool::new(py, true);
             assert!(t.extract::().unwrap());
@@ -254,7 +254,7 @@ mod tests {
 
     #[test]
     fn test_false() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             assert!(!PyBool::new(py, false).is_true());
             let t = PyBool::new(py, false);
             assert!(!t.extract::().unwrap());
@@ -267,7 +267,7 @@ mod tests {
 
     #[test]
     fn test_pybool_comparisons() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let py_bool = PyBool::new(py, true);
             let py_bool_false = PyBool::new(py, false);
             let rust_bool = true;
diff --git a/src/types/bytearray.rs b/src/types/bytearray.rs
index c6a0683f635..12634b0ac2d 100644
--- a/src/types/bytearray.rs
+++ b/src/types/bytearray.rs
@@ -45,7 +45,7 @@ impl PyByteArray {
     /// use pyo3::{prelude::*, types::PyByteArray};
     ///
     /// # fn main() -> PyResult<()> {
-    /// Python::with_gil(|py| -> PyResult<()> {
+    /// Python::attach(|py| -> PyResult<()> {
     ///     let py_bytearray = PyByteArray::new_with(py, 10, |bytes: &mut [u8]| {
     ///         bytes.copy_from_slice(b"Hello Rust");
     ///         Ok(())
@@ -155,7 +155,7 @@ pub trait PyByteArrayMethods<'py>: crate::sealed::Sealed {
     ///     Ok(())
     /// }
     /// # fn main() -> PyResult<()> {
-    /// #     Python::with_gil(|py| -> PyResult<()> {
+    /// #     Python::attach(|py| -> PyResult<()> {
     /// #         let fun = wrap_pyfunction!(a_valid_function, py)?;
     /// #         let locals = pyo3::types::PyDict::new(py);
     /// #         locals.set_item("a_valid_function", fun)?;
@@ -222,7 +222,7 @@ pub trait PyByteArrayMethods<'py>: crate::sealed::Sealed {
     /// ```
     /// # use pyo3::prelude::*;
     /// # use pyo3::types::PyByteArray;
-    /// # Python::with_gil(|py| {
+    /// # Python::attach(|py| {
     /// let bytearray = PyByteArray::new(py, b"Hello World.");
     /// let mut copied_message = bytearray.to_vec();
     /// assert_eq!(b"Hello World.", copied_message.as_slice());
@@ -315,7 +315,7 @@ mod tests {
 
     #[test]
     fn test_len() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let src = b"Hello Python";
             let bytearray = PyByteArray::new(py, src);
             assert_eq!(src.len(), bytearray.len());
@@ -324,7 +324,7 @@ mod tests {
 
     #[test]
     fn test_as_bytes() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let src = b"Hello Python";
             let bytearray = PyByteArray::new(py, src);
 
@@ -336,7 +336,7 @@ mod tests {
 
     #[test]
     fn test_as_bytes_mut() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let src = b"Hello Python";
             let bytearray = PyByteArray::new(py, src);
 
@@ -352,7 +352,7 @@ mod tests {
 
     #[test]
     fn test_to_vec() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let src = b"Hello Python";
             let bytearray = PyByteArray::new(py, src);
 
@@ -363,7 +363,7 @@ mod tests {
 
     #[test]
     fn test_from() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let src = b"Hello Python";
             let bytearray = PyByteArray::new(py, src);
 
@@ -376,7 +376,7 @@ mod tests {
 
     #[test]
     fn test_from_err() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             if let Err(err) = PyByteArray::from(py.None().bind(py)) {
                 assert!(err.is_instance_of::(py));
             } else {
@@ -387,7 +387,7 @@ mod tests {
 
     #[test]
     fn test_try_from() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let src = b"Hello Python";
             let bytearray: &Bound<'_, PyAny> = &PyByteArray::new(py, src);
             let bytearray: Bound<'_, PyByteArray> = TryInto::try_into(bytearray).unwrap();
@@ -398,7 +398,7 @@ mod tests {
 
     #[test]
     fn test_resize() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let src = b"Hello Python";
             let bytearray = PyByteArray::new(py, src);
 
@@ -409,7 +409,7 @@ mod tests {
 
     #[test]
     fn test_byte_array_new_with() -> super::PyResult<()> {
-        Python::with_gil(|py| -> super::PyResult<()> {
+        Python::attach(|py| -> super::PyResult<()> {
             let py_bytearray = PyByteArray::new_with(py, 10, |b: &mut [u8]| {
                 b.copy_from_slice(b"Hello Rust");
                 Ok(())
@@ -422,7 +422,7 @@ mod tests {
 
     #[test]
     fn test_byte_array_new_with_zero_initialised() -> super::PyResult<()> {
-        Python::with_gil(|py| -> super::PyResult<()> {
+        Python::attach(|py| -> super::PyResult<()> {
             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]);
@@ -433,7 +433,7 @@ mod tests {
     #[test]
     fn test_byte_array_new_with_error() {
         use crate::exceptions::PyValueError;
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let py_bytearray_result = PyByteArray::new_with(py, 10, |_b: &mut [u8]| {
                 Err(PyValueError::new_err("Hello Crustaceans!"))
             });
diff --git a/src/types/bytes.rs b/src/types/bytes.rs
index 16e0c42f8b1..8e89e007487 100644
--- a/src/types/bytes.rs
+++ b/src/types/bytes.rs
@@ -29,7 +29,7 @@ use std::str;
 /// # use pyo3::prelude::*;
 /// use pyo3::types::PyBytes;
 ///
-/// # Python::with_gil(|py| {
+/// # Python::attach(|py| {
 /// let py_bytes = PyBytes::new(py, b"foo".as_slice());
 /// // via PartialEq<[u8]>
 /// assert_eq!(py_bytes, b"foo".as_slice());
@@ -77,7 +77,7 @@ impl PyBytes {
     /// use pyo3::{prelude::*, types::PyBytes};
     ///
     /// # fn main() -> PyResult<()> {
-    /// Python::with_gil(|py| -> PyResult<()> {
+    /// Python::attach(|py| -> PyResult<()> {
     ///     let py_bytes = PyBytes::new_with(py, 10, |bytes: &mut [u8]| {
     ///         bytes.copy_from_slice(b"Hello Rust");
     ///         Ok(())
@@ -281,7 +281,7 @@ mod tests {
 
     #[test]
     fn test_bytes_index() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let bytes = PyBytes::new(py, b"Hello World");
             assert_eq!(bytes[1], b'e');
         });
@@ -289,7 +289,7 @@ mod tests {
 
     #[test]
     fn test_bound_bytes_index() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let bytes = PyBytes::new(py, b"Hello World");
             assert_eq!(bytes[1], b'e');
 
@@ -300,7 +300,7 @@ mod tests {
 
     #[test]
     fn test_bytes_new_with() -> super::PyResult<()> {
-        Python::with_gil(|py| -> super::PyResult<()> {
+        Python::attach(|py| -> super::PyResult<()> {
             let py_bytes = PyBytes::new_with(py, 10, |b: &mut [u8]| {
                 b.copy_from_slice(b"Hello Rust");
                 Ok(())
@@ -313,7 +313,7 @@ mod tests {
 
     #[test]
     fn test_bytes_new_with_zero_initialised() -> super::PyResult<()> {
-        Python::with_gil(|py| -> super::PyResult<()> {
+        Python::attach(|py| -> super::PyResult<()> {
             let py_bytes = PyBytes::new_with(py, 10, |_b: &mut [u8]| Ok(()))?;
             let bytes: &[u8] = py_bytes.extract()?;
             assert_eq!(bytes, &[0; 10]);
@@ -324,7 +324,7 @@ mod tests {
     #[test]
     fn test_bytes_new_with_error() {
         use crate::exceptions::PyValueError;
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let py_bytes_result = PyBytes::new_with(py, 10, |_b: &mut [u8]| {
                 Err(PyValueError::new_err("Hello Crustaceans!"))
             });
@@ -338,7 +338,7 @@ mod tests {
 
     #[test]
     fn test_comparisons() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let b = b"hello, world".as_slice();
             let py_bytes = PyBytes::new(py, b);
 
@@ -369,7 +369,7 @@ mod tests {
     #[test]
     #[cfg(not(Py_LIMITED_API))]
     fn test_as_string() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let b = b"hello, world".as_slice();
             let py_bytes = PyBytes::new(py, b);
             unsafe {
diff --git a/src/types/capsule.rs b/src/types/capsule.rs
index 19dde8a1cba..1cc05e799fb 100644
--- a/src/types/capsule.rs
+++ b/src/types/capsule.rs
@@ -29,7 +29,7 @@ use std::os::raw::{c_char, c_int, c_void};
 ///     pub val: u32,
 /// }
 ///
-/// let r = Python::with_gil(|py| -> PyResult<()> {
+/// let r = Python::attach(|py| -> PyResult<()> {
 ///     let foo = Foo { val: 123 };
 ///     let name = CString::new("builtins.capsule").unwrap();
 ///
@@ -63,7 +63,7 @@ impl PyCapsule {
     /// use pyo3::{prelude::*, types::PyCapsule};
     /// use std::ffi::CString;
     ///
-    /// Python::with_gil(|py| {
+    /// Python::attach(|py| {
     ///     let name = CString::new("foo").unwrap();
     ///     let capsule = PyCapsule::new(py, 123_u32, Some(name)).unwrap();
     ///     let val = unsafe { capsule.reference::() };
@@ -77,7 +77,7 @@ impl PyCapsule {
     /// use pyo3::{prelude::*, types::PyCapsule};
     /// use std::ffi::CString;
     ///
-    /// Python::with_gil(|py| {
+    /// Python::attach(|py| {
     ///     let capsule = PyCapsule::new(py, (), None).unwrap();  // Oops! `()` is zero sized!
     /// });
     /// ```
@@ -179,7 +179,7 @@ pub trait PyCapsuleMethods<'py>: crate::sealed::Sealed {
     ///     ctx.send("Destructor called!".to_string()).unwrap();
     /// }
     ///
-    /// Python::with_gil(|py| {
+    /// Python::attach(|py| {
     ///     let capsule =
     ///         PyCapsule::new_with_destructor(py, 123, None, destructor as fn(u32, *mut c_void))
     ///             .unwrap();
@@ -354,7 +354,7 @@ mod tests {
             }
         }
 
-        Python::with_gil(|py| -> PyResult<()> {
+        Python::attach(|py| -> PyResult<()> {
             let foo = Foo { val: 123 };
             let name = CString::new("foo").unwrap();
 
@@ -375,13 +375,13 @@ mod tests {
             x
         }
 
-        let cap: Py = Python::with_gil(|py| {
+        let cap: Py = Python::attach(|py| {
             let name = CString::new("foo").unwrap();
             let cap = PyCapsule::new(py, foo as fn(u32) -> u32, Some(name)).unwrap();
             cap.into()
         });
 
-        Python::with_gil(move |py| {
+        Python::attach(move |py| {
             let f = unsafe { cap.bind(py).reference:: u32>() };
             assert_eq!(f(123), 123);
         });
@@ -389,7 +389,7 @@ mod tests {
 
     #[test]
     fn test_pycapsule_context() -> PyResult<()> {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let name = CString::new("foo").unwrap();
             let cap = PyCapsule::new(py, 0, Some(name))?;
 
@@ -413,7 +413,7 @@ mod tests {
             pub val: u32,
         }
 
-        Python::with_gil(|py| -> PyResult<()> {
+        Python::attach(|py| -> PyResult<()> {
             let foo = Foo { val: 123 };
             let name = CString::new("builtins.capsule").unwrap();
 
@@ -436,7 +436,7 @@ mod tests {
 
     #[test]
     fn test_vec_storage() {
-        let cap: Py = Python::with_gil(|py| {
+        let cap: Py = Python::attach(|py| {
             let name = CString::new("foo").unwrap();
 
             let stuff: Vec = vec![1, 2, 3, 4];
@@ -445,7 +445,7 @@ mod tests {
             cap.into()
         });
 
-        Python::with_gil(move |py| {
+        Python::attach(move |py| {
             let ctx: &Vec = unsafe { cap.bind(py).reference() };
             assert_eq!(ctx, &[1, 2, 3, 4]);
         })
@@ -455,7 +455,7 @@ mod tests {
     fn test_vec_context() {
         let context: Vec = vec![1, 2, 3, 4];
 
-        let cap: Py = Python::with_gil(|py| {
+        let cap: Py = Python::attach(|py| {
             let name = CString::new("foo").unwrap();
             let cap = PyCapsule::new(py, 0, Some(name)).unwrap();
             cap.set_context(Box::into_raw(Box::new(&context)).cast())
@@ -464,7 +464,7 @@ mod tests {
             cap.into()
         });
 
-        Python::with_gil(move |py| {
+        Python::attach(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]);
@@ -481,7 +481,7 @@ mod tests {
             context.send(true).unwrap();
         }
 
-        Python::with_gil(move |py| {
+        Python::attach(move |py| {
             let name = CString::new("foo").unwrap();
             let cap = PyCapsule::new_with_destructor(py, 0, Some(name), destructor).unwrap();
             cap.set_context(Box::into_raw(Box::new(tx)).cast()).unwrap();
@@ -493,7 +493,7 @@ mod tests {
 
     #[test]
     fn test_pycapsule_no_name() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let cap = PyCapsule::new(py, 0usize, None).unwrap();
 
             assert_eq!(unsafe { cap.reference::() }, &0usize);
diff --git a/src/types/code.rs b/src/types/code.rs
index 0c1683c75be..593a1b88317 100644
--- a/src/types/code.rs
+++ b/src/types/code.rs
@@ -22,7 +22,7 @@ mod tests {
 
     #[test]
     fn test_type_object() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             assert_eq!(PyCode::type_object(py).name().unwrap(), "code");
         })
     }
diff --git a/src/types/complex.rs b/src/types/complex.rs
index 00c6c58b853..0da7546c247 100644
--- a/src/types/complex.rs
+++ b/src/types/complex.rs
@@ -127,7 +127,7 @@ mod not_limited_impls {
 
         #[test]
         fn test_add() {
-            Python::with_gil(|py| {
+            Python::attach(|py| {
                 let l = PyComplex::from_doubles(py, 3.0, 1.2);
                 let r = PyComplex::from_doubles(py, 1.0, 2.6);
                 let res = l + r;
@@ -138,7 +138,7 @@ mod not_limited_impls {
 
         #[test]
         fn test_sub() {
-            Python::with_gil(|py| {
+            Python::attach(|py| {
                 let l = PyComplex::from_doubles(py, 3.0, 1.2);
                 let r = PyComplex::from_doubles(py, 1.0, 2.6);
                 let res = l - r;
@@ -149,7 +149,7 @@ mod not_limited_impls {
 
         #[test]
         fn test_mul() {
-            Python::with_gil(|py| {
+            Python::attach(|py| {
                 let l = PyComplex::from_doubles(py, 3.0, 1.2);
                 let r = PyComplex::from_doubles(py, 1.0, 2.6);
                 let res = l * r;
@@ -160,7 +160,7 @@ mod not_limited_impls {
 
         #[test]
         fn test_div() {
-            Python::with_gil(|py| {
+            Python::attach(|py| {
                 let l = PyComplex::from_doubles(py, 3.0, 1.2);
                 let r = PyComplex::from_doubles(py, 1.0, 2.6);
                 let res = l / r;
@@ -171,7 +171,7 @@ mod not_limited_impls {
 
         #[test]
         fn test_neg() {
-            Python::with_gil(|py| {
+            Python::attach(|py| {
                 let val = PyComplex::from_doubles(py, 3.0, 1.2);
                 let res = -val;
                 assert_approx_eq!(res.real(), -3.0);
@@ -181,7 +181,7 @@ mod not_limited_impls {
 
         #[test]
         fn test_abs() {
-            Python::with_gil(|py| {
+            Python::attach(|py| {
                 let val = PyComplex::from_doubles(py, 3.0, 1.2);
                 assert_approx_eq!(val.abs(), 3.231_098_884_280_702_2);
             });
@@ -189,7 +189,7 @@ mod not_limited_impls {
 
         #[test]
         fn test_pow() {
-            Python::with_gil(|py| {
+            Python::attach(|py| {
                 let l = PyComplex::from_doubles(py, 3.0, 1.2);
                 let r = PyComplex::from_doubles(py, 1.2, 2.6);
                 let val = l.pow(&r);
@@ -239,7 +239,7 @@ impl<'py> PyComplexMethods<'py> for Bound<'py, PyComplex> {
 
     #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))]
     fn pow(&self, other: &Bound<'py, PyComplex>) -> Bound<'py, PyComplex> {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             PyAnyMethods::pow(self.as_any(), other, py.None())
                 .downcast_into()
                 .expect("Complex method __pow__ failed.")
@@ -257,7 +257,7 @@ mod tests {
     fn test_from_double() {
         use assert_approx_eq::assert_approx_eq;
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             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/datetime.rs b/src/types/datetime.rs
index 8a9c4166aa6..c45a7e64098 100644
--- a/src/types/datetime.rs
+++ b/src/types/datetime.rs
@@ -976,7 +976,7 @@ mod tests {
     #[cfg(feature = "macros")]
     #[cfg_attr(target_arch = "wasm32", ignore)] // DateTime import fails on wasm for mysterious reasons
     fn test_datetime_fromtimestamp() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let dt = PyDateTime::from_timestamp(py, 100.0, None).unwrap();
             py_run!(
                 py,
@@ -998,7 +998,7 @@ mod tests {
     #[cfg(feature = "macros")]
     #[cfg_attr(target_arch = "wasm32", ignore)] // DateTime import fails on wasm for mysterious reasons
     fn test_date_fromtimestamp() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let dt = PyDate::from_timestamp(py, 100).unwrap();
             py_run!(
                 py,
@@ -1012,7 +1012,7 @@ mod tests {
     #[cfg(not(Py_LIMITED_API))]
     #[cfg_attr(target_arch = "wasm32", ignore)] // DateTime import fails on wasm for mysterious reasons
     fn test_new_with_fold() {
-        Python::with_gil(|py| {
+        Python::attach(|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);
 
@@ -1024,7 +1024,7 @@ mod tests {
     #[test]
     #[cfg_attr(target_arch = "wasm32", ignore)] // DateTime import fails on wasm for mysterious reasons
     fn test_get_tzinfo() {
-        crate::Python::with_gil(|py| {
+        crate::Python::attach(|py| {
             let utc = PyTzInfo::utc(py).unwrap();
 
             let dt = PyDateTime::new(py, 2018, 1, 1, 0, 0, 0, 0, Some(&utc)).unwrap();
@@ -1051,7 +1051,7 @@ mod tests {
     fn test_timezone_from_offset() {
         use crate::types::PyNone;
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             assert!(
                 PyTzInfo::fixed_offset(py, PyDelta::new(py, 0, -3600, 0, true).unwrap())
                     .unwrap()
diff --git a/src/types/dict.rs b/src/types/dict.rs
index c38fe638d10..46db32c65e6 100644
--- a/src/types/dict.rs
+++ b/src/types/dict.rs
@@ -819,7 +819,7 @@ mod tests {
 
     #[test]
     fn test_new() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let dict = [(7, 32)].into_py_dict(py).unwrap();
             assert_eq!(
                 32,
@@ -840,7 +840,7 @@ mod tests {
     #[test]
     #[cfg(not(any(PyPy, GraalPy)))]
     fn test_from_sequence() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let items = PyList::new(py, vec![("a", 1), ("b", 2)]).unwrap();
             let dict = PyDict::from_sequence(&items).unwrap();
             assert_eq!(
@@ -871,7 +871,7 @@ mod tests {
     #[test]
     #[cfg(not(any(PyPy, GraalPy)))]
     fn test_from_sequence_err() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let items = PyList::new(py, vec!["a", "b"]).unwrap();
             assert!(PyDict::from_sequence(&items).is_err());
         });
@@ -879,7 +879,7 @@ mod tests {
 
     #[test]
     fn test_copy() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let dict = [(7, 32)].into_py_dict(py).unwrap();
 
             let ndict = dict.copy().unwrap();
@@ -898,7 +898,7 @@ mod tests {
 
     #[test]
     fn test_len() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut v = HashMap::::new();
             let dict = (&v).into_pyobject(py).unwrap();
             assert_eq!(0, dict.len());
@@ -910,7 +910,7 @@ mod tests {
 
     #[test]
     fn test_contains() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut v = HashMap::new();
             v.insert(7, 32);
             let dict = v.into_pyobject(py).unwrap();
@@ -921,7 +921,7 @@ mod tests {
 
     #[test]
     fn test_get_item() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut v = HashMap::new();
             v.insert(7, 32);
             let dict = v.into_pyobject(py).unwrap();
@@ -957,7 +957,7 @@ mod tests {
             }
         }
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let class = py.get_type::();
             let instance = class.call0().unwrap();
             let d = PyDict::new(py);
@@ -975,7 +975,7 @@ mod tests {
 
     #[test]
     fn test_set_item() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut v = HashMap::new();
             v.insert(7, 32);
             let dict = v.into_pyobject(py).unwrap();
@@ -1002,7 +1002,7 @@ mod tests {
 
     #[test]
     fn test_set_item_refcnt() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let cnt;
             let obj = py.eval(ffi::c_str!("object()"), None, None).unwrap();
             {
@@ -1017,7 +1017,7 @@ mod tests {
 
     #[test]
     fn test_set_item_does_not_update_original_object() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut v = HashMap::new();
             v.insert(7, 32);
             let dict = (&v).into_pyobject(py).unwrap();
@@ -1030,7 +1030,7 @@ mod tests {
 
     #[test]
     fn test_del_item() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut v = HashMap::new();
             v.insert(7, 32);
             let dict = v.into_pyobject(py).unwrap();
@@ -1042,7 +1042,7 @@ mod tests {
 
     #[test]
     fn test_del_item_does_not_update_original_object() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut v = HashMap::new();
             v.insert(7, 32);
             let dict = (&v).into_pyobject(py).unwrap();
@@ -1053,7 +1053,7 @@ mod tests {
 
     #[test]
     fn test_items() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut v = HashMap::new();
             v.insert(7, 32);
             v.insert(8, 42);
@@ -1074,7 +1074,7 @@ mod tests {
 
     #[test]
     fn test_keys() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut v = HashMap::new();
             v.insert(7, 32);
             v.insert(8, 42);
@@ -1091,7 +1091,7 @@ mod tests {
 
     #[test]
     fn test_values() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut v = HashMap::new();
             v.insert(7, 32);
             v.insert(8, 42);
@@ -1108,7 +1108,7 @@ mod tests {
 
     #[test]
     fn test_iter() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut v = HashMap::new();
             v.insert(7, 32);
             v.insert(8, 42);
@@ -1127,7 +1127,7 @@ mod tests {
 
     #[test]
     fn test_iter_bound() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut v = HashMap::new();
             v.insert(7, 32);
             v.insert(8, 42);
@@ -1146,7 +1146,7 @@ mod tests {
 
     #[test]
     fn test_iter_value_mutated() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut v = HashMap::new();
             v.insert(7, 32);
             v.insert(8, 42);
@@ -1164,7 +1164,7 @@ mod tests {
     #[test]
     #[should_panic]
     fn test_iter_key_mutated() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut v = HashMap::new();
             for i in 0..10 {
                 v.insert(i * 2, i * 2);
@@ -1188,7 +1188,7 @@ mod tests {
     #[test]
     #[should_panic]
     fn test_iter_key_mutated_constant_len() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut v = HashMap::new();
             for i in 0..10 {
                 v.insert(i * 2, i * 2);
@@ -1211,7 +1211,7 @@ mod tests {
 
     #[test]
     fn test_iter_size_hint() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut v = HashMap::new();
             v.insert(7, 32);
             v.insert(8, 42);
@@ -1236,7 +1236,7 @@ mod tests {
 
     #[test]
     fn test_into_iter() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut v = HashMap::new();
             v.insert(7, 32);
             v.insert(8, 42);
@@ -1255,7 +1255,7 @@ mod tests {
 
     #[test]
     fn test_hashmap_into_dict() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut map = HashMap::::new();
             map.insert(1, 1);
 
@@ -1276,7 +1276,7 @@ mod tests {
 
     #[test]
     fn test_btreemap_into_dict() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut map = BTreeMap::::new();
             map.insert(1, 1);
 
@@ -1297,7 +1297,7 @@ mod tests {
 
     #[test]
     fn test_vec_into_dict() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let vec = vec![("a", 1), ("b", 2), ("c", 3)];
             let py_map = vec.into_py_dict(py).unwrap();
 
@@ -1316,7 +1316,7 @@ mod tests {
 
     #[test]
     fn test_slice_into_dict() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let arr = [("a", 1), ("b", 2), ("c", 3)];
             let py_map = arr.into_py_dict(py).unwrap();
 
@@ -1335,7 +1335,7 @@ mod tests {
 
     #[test]
     fn dict_as_mapping() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut map = HashMap::::new();
             map.insert(1, 1);
 
@@ -1356,7 +1356,7 @@ mod tests {
 
     #[test]
     fn dict_into_mapping() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut map = HashMap::::new();
             map.insert(1, 1);
 
@@ -1380,7 +1380,7 @@ mod tests {
     #[test]
     #[cfg(not(any(PyPy, GraalPy)))]
     fn dict_keys_view() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let dict = abc_dict(py);
             let keys = dict.call_method0("keys").unwrap();
             assert!(keys.is_instance(&py.get_type::()).unwrap());
@@ -1390,7 +1390,7 @@ mod tests {
     #[test]
     #[cfg(not(any(PyPy, GraalPy)))]
     fn dict_values_view() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let dict = abc_dict(py);
             let values = dict.call_method0("values").unwrap();
             assert!(values.is_instance(&py.get_type::()).unwrap());
@@ -1400,7 +1400,7 @@ mod tests {
     #[test]
     #[cfg(not(any(PyPy, GraalPy)))]
     fn dict_items_view() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let dict = abc_dict(py);
             let items = dict.call_method0("items").unwrap();
             assert!(items.is_instance(&py.get_type::()).unwrap());
@@ -1409,7 +1409,7 @@ mod tests {
 
     #[test]
     fn dict_update() {
-        Python::with_gil(|py| {
+        Python::attach(|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();
@@ -1480,7 +1480,7 @@ mod tests {
 
     #[test]
     fn dict_update_if_missing() {
-        Python::with_gil(|py| {
+        Python::attach(|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();
@@ -1551,7 +1551,7 @@ mod tests {
 
     #[test]
     fn test_iter_all() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let dict = [(1, true), (2, true), (3, true)].into_py_dict(py).unwrap();
             assert!(dict.iter().all(|(_, v)| v.extract::().unwrap()));
 
@@ -1562,7 +1562,7 @@ mod tests {
 
     #[test]
     fn test_iter_any() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let dict = [(1, true), (2, false), (3, false)]
                 .into_py_dict(py)
                 .unwrap();
@@ -1578,7 +1578,7 @@ mod tests {
     #[test]
     #[allow(clippy::search_is_some)]
     fn test_iter_find() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let dict = [(1, false), (2, true), (3, false)]
                 .into_py_dict(py)
                 .unwrap();
@@ -1604,7 +1604,7 @@ mod tests {
     #[test]
     #[allow(clippy::search_is_some)]
     fn test_iter_position() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let dict = [(1, false), (2, false), (3, true)]
                 .into_py_dict(py)
                 .unwrap();
@@ -1625,7 +1625,7 @@ mod tests {
 
     #[test]
     fn test_iter_fold() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let dict = [(1, 1), (2, 2), (3, 3)].into_py_dict(py).unwrap();
             let sum = dict
                 .iter()
@@ -1636,7 +1636,7 @@ mod tests {
 
     #[test]
     fn test_iter_try_fold() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let dict = [(1, 1), (2, 2), (3, 3)].into_py_dict(py).unwrap();
             let sum = dict
                 .iter()
@@ -1654,7 +1654,7 @@ mod tests {
 
     #[test]
     fn test_iter_count() {
-        Python::with_gil(|py| {
+        Python::attach(|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/ellipsis.rs b/src/types/ellipsis.rs
index e9c1e777463..86403be358a 100644
--- a/src/types/ellipsis.rs
+++ b/src/types/ellipsis.rs
@@ -49,7 +49,7 @@ mod tests {
 
     #[test]
     fn test_ellipsis_is_itself() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             assert!(PyEllipsis::get(py).is_instance_of::());
             assert!(PyEllipsis::get(py).is_exact_instance_of::());
         })
@@ -57,7 +57,7 @@ mod tests {
 
     #[test]
     fn test_ellipsis_type_object_consistent() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             assert!(PyEllipsis::get(py)
                 .get_type()
                 .is(PyEllipsis::type_object(py)));
@@ -66,7 +66,7 @@ mod tests {
 
     #[test]
     fn test_dict_is_not_ellipsis() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             assert!(PyDict::new(py).downcast::().is_err());
         })
     }
diff --git a/src/types/float.rs b/src/types/float.rs
index 2cc95246996..04b2dc158b2 100644
--- a/src/types/float.rs
+++ b/src/types/float.rs
@@ -271,7 +271,7 @@ mod tests {
             fn $func_name() {
                 use assert_approx_eq::assert_approx_eq;
 
-                Python::with_gil(|py| {
+                Python::attach(|py| {
 
                 let val = 123 as $t1;
                 let obj = val.into_pyobject(py).unwrap();
@@ -289,7 +289,7 @@ mod tests {
     fn test_float_value() {
         use assert_approx_eq::assert_approx_eq;
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v = 1.23f64;
             let obj = PyFloat::new(py, 1.23);
             assert_approx_eq!(v, obj.value());
@@ -298,7 +298,7 @@ mod tests {
 
     #[test]
     fn test_pyfloat_comparisons() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let f_64 = 1.01f64;
             let py_f64 = PyFloat::new(py, 1.01);
             let py_f64_ref = &py_f64;
diff --git a/src/types/frozenset.rs b/src/types/frozenset.rs
index edde7ca146e..69528c10725 100644
--- a/src/types/frozenset.rs
+++ b/src/types/frozenset.rs
@@ -258,7 +258,7 @@ mod tests {
 
     #[test]
     fn test_frozenset_new_and_len() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let set = PyFrozenSet::new(py, [1]).unwrap();
             assert_eq!(1, set.len());
 
@@ -269,7 +269,7 @@ mod tests {
 
     #[test]
     fn test_frozenset_empty() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let set = PyFrozenSet::empty(py).unwrap();
             assert_eq!(0, set.len());
             assert!(set.is_empty());
@@ -278,7 +278,7 @@ mod tests {
 
     #[test]
     fn test_frozenset_contains() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let set = PyFrozenSet::new(py, [1]).unwrap();
             assert!(set.contains(1).unwrap());
         });
@@ -286,7 +286,7 @@ mod tests {
 
     #[test]
     fn test_frozenset_iter() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let set = PyFrozenSet::new(py, [1]).unwrap();
 
             for el in set {
@@ -297,7 +297,7 @@ mod tests {
 
     #[test]
     fn test_frozenset_iter_bound() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let set = PyFrozenSet::new(py, [1]).unwrap();
 
             for el in &set {
@@ -308,7 +308,7 @@ mod tests {
 
     #[test]
     fn test_frozenset_iter_size_hint() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let set = PyFrozenSet::new(py, [1]).unwrap();
             let mut iter = set.iter();
 
@@ -325,7 +325,7 @@ mod tests {
     fn test_frozenset_builder() {
         use super::PyFrozenSetBuilder;
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut builder = PyFrozenSetBuilder::new(py).unwrap();
 
             // add an item
@@ -344,7 +344,7 @@ mod tests {
 
     #[test]
     fn test_iter_count() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let set = PyFrozenSet::new(py, vec![1, 2, 3]).unwrap();
             assert_eq!(set.iter().count(), 3);
         })
diff --git a/src/types/function.rs b/src/types/function.rs
index 1747c87038e..9df21203743 100644
--- a/src/types/function.rs
+++ b/src/types/function.rs
@@ -61,7 +61,7 @@ impl PyCFunction {
     /// # use pyo3::prelude::*;
     /// # use pyo3::{py_run, types::{PyCFunction, PyDict, PyTuple}};
     ///
-    /// Python::with_gil(|py| {
+    /// Python::attach(|py| {
     ///     let add_one = |args: &Bound<'_, PyTuple>, _kwargs: Option<&Bound<'_, PyDict>>| -> PyResult<_> {
     ///         let i = args.extract::<(i64,)>()?.0;
     ///         Ok(i+1)
diff --git a/src/types/genericalias.rs b/src/types/genericalias.rs
index 48cdcc17154..227b50aa9e8 100644
--- a/src/types/genericalias.rs
+++ b/src/types/genericalias.rs
@@ -50,7 +50,7 @@ mod tests {
     // created from Python.
     #[test]
     fn equivalency_test() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let list_int = py
                 .eval(ffi::c_str!("list[int]"), None, None)
                 .unwrap()
diff --git a/src/types/iterator.rs b/src/types/iterator.rs
index 6b7cf722337..8a7a74751af 100644
--- a/src/types/iterator.rs
+++ b/src/types/iterator.rs
@@ -15,7 +15,7 @@ use crate::{ffi, Bound, PyAny, PyErr, PyResult, PyTypeCheck};
 /// use pyo3::ffi::c_str;
 ///
 /// # fn main() -> PyResult<()> {
-/// Python::with_gil(|py| -> PyResult<()> {
+/// Python::attach(|py| -> PyResult<()> {
 ///     let list = py.eval(c_str!("iter([1, 2, 3, 4])"), None, None)?;
 ///     let numbers: PyResult> = list
 ///         .try_iter()?
@@ -140,7 +140,7 @@ mod tests {
 
     #[test]
     fn vec_iter() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let inst = vec![10, 20].into_pyobject(py).unwrap();
             let mut it = inst.try_iter().unwrap();
             assert_eq!(
@@ -157,13 +157,13 @@ mod tests {
 
     #[test]
     fn iter_refcnt() {
-        let (obj, count) = Python::with_gil(|py| {
+        let (obj, count) = Python::attach(|py| {
             let obj = vec![10, 20].into_pyobject(py).unwrap();
             let count = obj.get_refcnt();
             (obj.unbind(), count)
         });
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let inst = obj.bind(py);
             let mut it = inst.try_iter().unwrap();
 
@@ -173,14 +173,14 @@ mod tests {
             );
         });
 
-        Python::with_gil(move |py| {
+        Python::attach(move |py| {
             assert_eq!(count, obj.get_refcnt(py));
         });
     }
 
     #[test]
     fn iter_item_refcnt() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let count;
             let obj = py.eval(ffi::c_str!("object()"), None, None).unwrap();
             let list = {
@@ -215,7 +215,7 @@ def fibonacci(target):
 "#
         );
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let context = PyDict::new(py);
             py.run(fibonacci_generator, None, Some(&context)).unwrap();
 
@@ -243,7 +243,7 @@ def gen():
 "#
         );
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let context = PyDict::new(py);
             py.run(generator, None, Some(&context)).unwrap();
 
@@ -281,7 +281,7 @@ def fibonacci(target):
 "#
         );
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let context = PyDict::new(py);
             py.run(fibonacci_generator, None, Some(&context)).unwrap();
 
@@ -301,7 +301,7 @@ def fibonacci(target):
 
     #[test]
     fn int_not_iterable() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let x = 5i32.into_pyobject(py).unwrap();
             let err = PyIterator::from_object(&x).unwrap_err();
 
@@ -327,7 +327,7 @@ def fibonacci(target):
         }
 
         // Regression test for 2913
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let downcaster = crate::Py::new(py, Downcaster { failed: None }).unwrap();
             crate::py_run!(
                 py,
@@ -365,7 +365,7 @@ def fibonacci(target):
         }
 
         // Regression test for 2913
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let assert_iterator = crate::wrap_pyfunction!(assert_iterator, py).unwrap();
             crate::py_run!(
                 py,
@@ -384,7 +384,7 @@ def fibonacci(target):
     #[test]
     #[cfg(not(Py_LIMITED_API))]
     fn length_hint_becomes_size_hint_lower_bound() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let list = py.eval(ffi::c_str!("[1, 2, 3]"), None, None).unwrap();
             let iter = list.try_iter().unwrap();
             let hint = iter.size_hint();
diff --git a/src/types/list.rs b/src/types/list.rs
index c0edc39cbce..c41457a1a9a 100644
--- a/src/types/list.rs
+++ b/src/types/list.rs
@@ -72,7 +72,7 @@ impl PyList {
     /// use pyo3::types::PyList;
     ///
     /// # fn main() -> PyResult<()> {
-    /// Python::with_gil(|py| {
+    /// Python::attach(|py| {
     ///     let elements: Vec = vec![0, 1, 2, 3, 4, 5];
     ///     let list = PyList::new(py, elements)?;
     ///     assert_eq!(format!("{:?}", list), "[0, 1, 2, 3, 4, 5]");
@@ -132,7 +132,7 @@ pub trait PyListMethods<'py>: crate::sealed::Sealed {
     /// # Example
     /// ```
     /// use pyo3::{prelude::*, types::PyList};
-    /// Python::with_gil(|py| {
+    /// Python::attach(|py| {
     ///     let list = PyList::new(py, [2, 3, 5, 7]).unwrap();
     ///     let obj = list.get_item(0);
     ///     assert_eq!(obj.unwrap().extract::().unwrap(), 2);
@@ -263,7 +263,7 @@ impl<'py> PyListMethods<'py> for Bound<'py, PyList> {
     /// # Example
     /// ```
     /// use pyo3::{prelude::*, types::PyList};
-    /// Python::with_gil(|py| {
+    /// Python::attach(|py| {
     ///     let list = PyList::new(py, [2, 3, 5, 7]).unwrap();
     ///     let obj = list.get_item(0);
     ///     assert_eq!(obj.unwrap().extract::().unwrap(), 2);
@@ -950,7 +950,7 @@ mod tests {
 
     #[test]
     fn test_new() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             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());
@@ -961,7 +961,7 @@ mod tests {
 
     #[test]
     fn test_len() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let list = PyList::new(py, [1, 2, 3, 4]).unwrap();
             assert_eq!(4, list.len());
         });
@@ -969,7 +969,7 @@ mod tests {
 
     #[test]
     fn test_get_item() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             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());
@@ -980,7 +980,7 @@ mod tests {
 
     #[test]
     fn test_get_slice() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let list = PyList::new(py, [2, 3, 5, 7]).unwrap();
             let slice = list.get_slice(1, 3);
             assert_eq!(2, slice.len());
@@ -991,7 +991,7 @@ mod tests {
 
     #[test]
     fn test_set_item() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let list = PyList::new(py, [2, 3, 5, 7]).unwrap();
             let val = 42i32.into_pyobject(py).unwrap();
             let val2 = 42i32.into_pyobject(py).unwrap();
@@ -1004,7 +1004,7 @@ mod tests {
 
     #[test]
     fn test_set_item_refcnt() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let obj = py.eval(ffi::c_str!("object()"), None, None).unwrap();
             let cnt;
             {
@@ -1021,7 +1021,7 @@ mod tests {
 
     #[test]
     fn test_insert() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let list = PyList::new(py, [2, 3, 5, 7]).unwrap();
             let val = 42i32.into_pyobject(py).unwrap();
             let val2 = 43i32.into_pyobject(py).unwrap();
@@ -1038,7 +1038,7 @@ mod tests {
 
     #[test]
     fn test_insert_refcnt() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let cnt;
             let obj = py.eval(ffi::c_str!("object()"), None, None).unwrap();
             {
@@ -1053,7 +1053,7 @@ mod tests {
 
     #[test]
     fn test_append() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let list = PyList::new(py, [2]).unwrap();
             list.append(3).unwrap();
             assert_eq!(2, list.get_item(0).unwrap().extract::().unwrap());
@@ -1063,7 +1063,7 @@ mod tests {
 
     #[test]
     fn test_append_refcnt() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let cnt;
             let obj = py.eval(ffi::c_str!("object()"), None, None).unwrap();
             {
@@ -1077,7 +1077,7 @@ mod tests {
 
     #[test]
     fn test_iter() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v = vec![2, 3, 5, 7];
             let list = PyList::new(py, &v).unwrap();
             let mut idx = 0;
@@ -1091,7 +1091,7 @@ mod tests {
 
     #[test]
     fn test_iter_size_hint() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v = vec![2, 3, 5, 7];
             let ob = (&v).into_pyobject(py).unwrap();
             let list = ob.downcast::().unwrap();
@@ -1110,7 +1110,7 @@ mod tests {
 
     #[test]
     fn test_iter_rev() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v = vec![2, 3, 5, 7];
             let ob = v.into_pyobject(py).unwrap();
             let list = ob.downcast::().unwrap();
@@ -1138,7 +1138,7 @@ mod tests {
 
     #[test]
     fn test_iter_all() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let list = PyList::new(py, [true, true, true]).unwrap();
             assert!(list.iter().all(|x| x.extract::().unwrap()));
 
@@ -1149,7 +1149,7 @@ mod tests {
 
     #[test]
     fn test_iter_any() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let list = PyList::new(py, [true, true, true]).unwrap();
             assert!(list.iter().any(|x| x.extract::().unwrap()));
 
@@ -1163,7 +1163,7 @@ mod tests {
 
     #[test]
     fn test_iter_find() {
-        Python::with_gil(|py: Python<'_>| {
+        Python::attach(|py: Python<'_>| {
             let list = PyList::new(py, ["hello", "world"]).unwrap();
             assert_eq!(
                 Some("world".to_string()),
@@ -1182,7 +1182,7 @@ mod tests {
 
     #[test]
     fn test_iter_position() {
-        Python::with_gil(|py: Python<'_>| {
+        Python::attach(|py: Python<'_>| {
             let list = PyList::new(py, ["hello", "world"]).unwrap();
             assert_eq!(
                 Some(1),
@@ -1199,7 +1199,7 @@ mod tests {
 
     #[test]
     fn test_iter_fold() {
-        Python::with_gil(|py: Python<'_>| {
+        Python::attach(|py: Python<'_>| {
             let list = PyList::new(py, [1, 2, 3]).unwrap();
             let sum = list
                 .iter()
@@ -1210,7 +1210,7 @@ mod tests {
 
     #[test]
     fn test_iter_fold_out_of_bounds() {
-        Python::with_gil(|py: Python<'_>| {
+        Python::attach(|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
@@ -1227,7 +1227,7 @@ mod tests {
 
     #[test]
     fn test_iter_rfold() {
-        Python::with_gil(|py: Python<'_>| {
+        Python::attach(|py: Python<'_>| {
             let list = PyList::new(py, [1, 2, 3]).unwrap();
             let sum = list
                 .iter()
@@ -1238,7 +1238,7 @@ mod tests {
 
     #[test]
     fn test_iter_try_fold() {
-        Python::with_gil(|py: Python<'_>| {
+        Python::attach(|py: Python<'_>| {
             let list = PyList::new(py, [1, 2, 3]).unwrap();
             let sum = list
                 .iter()
@@ -1256,7 +1256,7 @@ mod tests {
 
     #[test]
     fn test_iter_try_rfold() {
-        Python::with_gil(|py: Python<'_>| {
+        Python::attach(|py: Python<'_>| {
             let list = PyList::new(py, [1, 2, 3]).unwrap();
             let sum = list
                 .iter()
@@ -1274,7 +1274,7 @@ mod tests {
 
     #[test]
     fn test_into_iter() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             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());
@@ -1286,7 +1286,7 @@ mod tests {
     fn test_into_iter_bound() {
         use crate::types::any::PyAnyMethods;
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let list = PyList::new(py, [1, 2, 3, 4]).unwrap();
             let mut items = vec![];
             for item in &list {
@@ -1298,7 +1298,7 @@ mod tests {
 
     #[test]
     fn test_as_sequence() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let list = PyList::new(py, [1, 2, 3, 4]).unwrap();
 
             assert_eq!(list.as_sequence().len().unwrap(), 4);
@@ -1315,7 +1315,7 @@ mod tests {
 
     #[test]
     fn test_into_sequence() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let list = PyList::new(py, [1, 2, 3, 4]).unwrap();
 
             let sequence = list.into_sequence();
@@ -1327,7 +1327,7 @@ mod tests {
 
     #[test]
     fn test_extract() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v = vec![2, 3, 5, 7];
             let list = PyList::new(py, &v).unwrap();
             let v2 = list.as_any().extract::>().unwrap();
@@ -1337,7 +1337,7 @@ mod tests {
 
     #[test]
     fn test_sort() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v = vec![7, 3, 2, 5];
             let list = PyList::new(py, &v).unwrap();
             assert_eq!(7, list.get_item(0).unwrap().extract::().unwrap());
@@ -1354,7 +1354,7 @@ mod tests {
 
     #[test]
     fn test_reverse() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v = vec![2, 3, 5, 7];
             let list = PyList::new(py, &v).unwrap();
             assert_eq!(2, list.get_item(0).unwrap().extract::().unwrap());
@@ -1371,7 +1371,7 @@ mod tests {
 
     #[test]
     fn test_array_into_pyobject() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let array = [1, 2].into_pyobject(py).unwrap();
             let list = array.downcast::().unwrap();
             assert_eq!(1, list.get_item(0).unwrap().extract::().unwrap());
@@ -1381,7 +1381,7 @@ mod tests {
 
     #[test]
     fn test_list_get_item_invalid_index() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let list = PyList::new(py, [2, 3, 5, 7]).unwrap();
             let obj = list.get_item(5);
             assert!(obj.is_err());
@@ -1394,7 +1394,7 @@ mod tests {
 
     #[test]
     fn test_list_get_item_sanity() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let list = PyList::new(py, [2, 3, 5, 7]).unwrap();
             let obj = list.get_item(0);
             assert_eq!(obj.unwrap().extract::().unwrap(), 2);
@@ -1404,7 +1404,7 @@ mod tests {
     #[cfg(not(Py_LIMITED_API))]
     #[test]
     fn test_list_get_item_unchecked_sanity() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let list = PyList::new(py, [2, 3, 5, 7]).unwrap();
             let obj = unsafe { list.get_item_unchecked(0) };
             assert_eq!(obj.extract::().unwrap(), 2);
@@ -1413,7 +1413,7 @@ mod tests {
 
     #[test]
     fn test_list_del_item() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             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());
@@ -1435,7 +1435,7 @@ mod tests {
 
     #[test]
     fn test_list_set_slice() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             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();
@@ -1447,7 +1447,7 @@ mod tests {
 
     #[test]
     fn test_list_del_slice() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             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());
@@ -1458,7 +1458,7 @@ mod tests {
 
     #[test]
     fn test_list_contains() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let list = PyList::new(py, [1, 1, 2, 3, 5, 8]).unwrap();
             assert_eq!(6, list.len());
 
@@ -1475,7 +1475,7 @@ mod tests {
 
     #[test]
     fn test_list_index() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             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());
@@ -1511,7 +1511,7 @@ mod tests {
         expected = "Attempted to create PyList but `elements` was larger than reported by its `ExactSizeIterator` implementation."
     )]
     fn too_long_iterator() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let iter = FaultyIter(0..usize::MAX, 73);
             let _list = PyList::new(py, iter).unwrap();
         })
@@ -1522,7 +1522,7 @@ mod tests {
         expected = "Attempted to create PyList but `elements` was smaller than reported by its `ExactSizeIterator` implementation."
     )]
     fn too_short_iterator() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let iter = FaultyIter(0..35, 73);
             let _list = PyList::new(py, iter).unwrap();
         })
@@ -1533,7 +1533,7 @@ mod tests {
         expected = "out of range integral type conversion attempted on `elements.len()`"
     )]
     fn overflowing_size() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let iter = FaultyIter(0..0, usize::MAX);
 
             let _list = PyList::new(py, iter).unwrap();
@@ -1586,7 +1586,7 @@ mod tests {
             }
         }
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             std::panic::catch_unwind(|| {
                 let iter = FaultyIter(0..50, 50);
                 let _list = PyList::new(py, iter).unwrap();
@@ -1603,7 +1603,7 @@ mod tests {
 
     #[test]
     fn test_list_to_tuple() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             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]).unwrap();
@@ -1613,7 +1613,7 @@ mod tests {
 
     #[test]
     fn test_iter_nth() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v = vec![6, 7, 8, 9, 10];
             let ob = (&v).into_pyobject(py).unwrap();
             let list = ob.downcast::().unwrap();
@@ -1656,7 +1656,7 @@ mod tests {
 
     #[test]
     fn test_iter_nth_back() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v = vec![1, 2, 3, 4, 5];
             let ob = (&v).into_pyobject(py).unwrap();
             let list = ob.downcast::().unwrap();
@@ -1714,7 +1714,7 @@ mod tests {
     #[cfg(feature = "nightly")]
     #[test]
     fn test_iter_advance_by() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v = vec![1, 2, 3, 4, 5];
             let ob = (&v).into_pyobject(py).unwrap();
             let list = ob.downcast::().unwrap();
@@ -1740,7 +1740,7 @@ mod tests {
     #[cfg(feature = "nightly")]
     #[test]
     fn test_iter_advance_back_by() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v = vec![1, 2, 3, 4, 5];
             let ob = (&v).into_pyobject(py).unwrap();
             let list = ob.downcast::().unwrap();
@@ -1765,7 +1765,7 @@ mod tests {
 
     #[test]
     fn test_iter_last() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let list = PyList::new(py, vec![1, 2, 3]).unwrap();
             let last = list.iter().last();
             assert_eq!(last.unwrap().extract::().unwrap(), 3);
@@ -1774,7 +1774,7 @@ mod tests {
 
     #[test]
     fn test_iter_count() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let list = PyList::new(py, vec![1, 2, 3]).unwrap();
             assert_eq!(list.iter().count(), 3);
         })
diff --git a/src/types/mapping.rs b/src/types/mapping.rs
index 5950db72743..6ecda738d4a 100644
--- a/src/types/mapping.rs
+++ b/src/types/mapping.rs
@@ -196,7 +196,7 @@ mod tests {
 
     #[test]
     fn test_len() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut v = HashMap::::new();
             let ob = (&v).into_pyobject(py).unwrap();
             let mapping = ob.downcast::().unwrap();
@@ -213,7 +213,7 @@ mod tests {
 
     #[test]
     fn test_contains() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut v = HashMap::new();
             v.insert("key0", 1234);
             let ob = v.into_pyobject(py).unwrap();
@@ -228,7 +228,7 @@ mod tests {
 
     #[test]
     fn test_get_item() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut v = HashMap::new();
             v.insert(7, 32);
             let ob = v.into_pyobject(py).unwrap();
@@ -246,7 +246,7 @@ mod tests {
 
     #[test]
     fn test_set_item() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut v = HashMap::new();
             v.insert(7, 32);
             let ob = v.into_pyobject(py).unwrap();
@@ -266,7 +266,7 @@ mod tests {
 
     #[test]
     fn test_del_item() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut v = HashMap::new();
             v.insert(7, 32);
             let ob = v.into_pyobject(py).unwrap();
@@ -282,7 +282,7 @@ mod tests {
 
     #[test]
     fn test_items() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut v = HashMap::new();
             v.insert(7, 32);
             v.insert(8, 42);
@@ -304,7 +304,7 @@ mod tests {
 
     #[test]
     fn test_keys() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut v = HashMap::new();
             v.insert(7, 32);
             v.insert(8, 42);
@@ -322,7 +322,7 @@ mod tests {
 
     #[test]
     fn test_values() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut v = HashMap::new();
             v.insert(7, 32);
             v.insert(8, 42);
diff --git a/src/types/mappingproxy.rs b/src/types/mappingproxy.rs
index db2a52cecbf..0de1f4b246d 100644
--- a/src/types/mappingproxy.rs
+++ b/src/types/mappingproxy.rs
@@ -144,7 +144,7 @@ mod tests {
 
     #[test]
     fn test_new() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let pydict = [(7, 32)].into_py_dict(py).unwrap();
             let mappingproxy = PyMappingProxy::new(py, pydict.as_mapping());
             mappingproxy.get_item(7i32).unwrap();
@@ -165,7 +165,7 @@ mod tests {
 
     #[test]
     fn test_len() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut v = HashMap::new();
             let dict = v.clone().into_py_dict(py).unwrap();
             let mappingproxy = PyMappingProxy::new(py, dict.as_mapping());
@@ -179,7 +179,7 @@ mod tests {
 
     #[test]
     fn test_contains() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut v = HashMap::new();
             v.insert(7, 32);
             let dict = v.clone().into_py_dict(py).unwrap();
@@ -191,7 +191,7 @@ mod tests {
 
     #[test]
     fn test_get_item() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut v = HashMap::new();
             v.insert(7, 32);
             let dict = v.clone().into_py_dict(py).unwrap();
@@ -213,7 +213,7 @@ mod tests {
 
     #[test]
     fn test_set_item_refcnt() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let cnt;
             {
                 let none = py.None();
@@ -229,7 +229,7 @@ mod tests {
 
     #[test]
     fn test_isempty() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let map: HashMap = HashMap::new();
             let dict = map.into_py_dict(py).unwrap();
             let mappingproxy = PyMappingProxy::new(py, dict.as_mapping());
@@ -239,7 +239,7 @@ mod tests {
 
     #[test]
     fn test_keys() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut v = HashMap::new();
             v.insert(7, 32);
             v.insert(8, 42);
@@ -257,7 +257,7 @@ mod tests {
 
     #[test]
     fn test_values() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut v: HashMap = HashMap::new();
             v.insert(7, 32);
             v.insert(8, 42);
@@ -275,7 +275,7 @@ mod tests {
 
     #[test]
     fn test_items() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut v = HashMap::new();
             v.insert(7, 32);
             v.insert(8, 42);
@@ -298,7 +298,7 @@ mod tests {
 
     #[test]
     fn test_iter() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut v = HashMap::new();
             v.insert(7, 32);
             v.insert(8, 42);
@@ -319,7 +319,7 @@ mod tests {
 
     #[test]
     fn test_hashmap_into_python() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut map = HashMap::::new();
             map.insert(1, 1);
 
@@ -333,7 +333,7 @@ mod tests {
 
     #[test]
     fn test_hashmap_into_mappingproxy() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut map = HashMap::::new();
             map.insert(1, 1);
 
@@ -347,7 +347,7 @@ mod tests {
 
     #[test]
     fn test_btreemap_into_py() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut map = BTreeMap::::new();
             map.insert(1, 1);
 
@@ -361,7 +361,7 @@ mod tests {
 
     #[test]
     fn test_btreemap_into_mappingproxy() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut map = BTreeMap::::new();
             map.insert(1, 1);
 
@@ -375,7 +375,7 @@ mod tests {
 
     #[test]
     fn test_vec_into_mappingproxy() {
-        Python::with_gil(|py| {
+        Python::attach(|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());
@@ -387,7 +387,7 @@ mod tests {
 
     #[test]
     fn test_slice_into_mappingproxy() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let arr = [("a", 1), ("b", 2), ("c", 3)];
 
             let dict = arr.into_py_dict(py).unwrap();
@@ -400,7 +400,7 @@ mod tests {
 
     #[test]
     fn mappingproxy_as_mapping() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut map = HashMap::::new();
             map.insert(1, 1);
 
@@ -433,7 +433,7 @@ mod tests {
     #[test]
     #[cfg(not(any(PyPy, GraalPy)))]
     fn mappingproxy_keys_view() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mappingproxy = abc_mappingproxy(py);
             let keys = mappingproxy.call_method0("keys").unwrap();
             assert!(keys.is_instance(&py.get_type::()).unwrap());
@@ -443,7 +443,7 @@ mod tests {
     #[test]
     #[cfg(not(any(PyPy, GraalPy)))]
     fn mappingproxy_values_view() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mappingproxy = abc_mappingproxy(py);
             let values = mappingproxy.call_method0("values").unwrap();
             assert!(values.is_instance(&py.get_type::()).unwrap());
@@ -453,7 +453,7 @@ mod tests {
     #[test]
     #[cfg(not(any(PyPy, GraalPy)))]
     fn mappingproxy_items_view() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mappingproxy = abc_mappingproxy(py);
             let items = mappingproxy.call_method0("items").unwrap();
             assert!(items.is_instance(&py.get_type::()).unwrap());
@@ -462,7 +462,7 @@ mod tests {
 
     #[test]
     fn get_value_from_mappingproxy_of_strings() {
-        Python::with_gil(|py: Python<'_>| {
+        Python::attach(|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());
@@ -490,7 +490,7 @@ mod tests {
 
     #[test]
     fn get_value_from_mappingproxy_of_integers() {
-        Python::with_gil(|py: Python<'_>| {
+        Python::attach(|py: Python<'_>| {
             const LEN: usize = 10_000;
             let items: Vec<(usize, usize)> = (1..LEN).map(|i| (i, i - 1)).collect();
 
@@ -538,7 +538,7 @@ mod tests {
 
     #[test]
     fn iter_mappingproxy_nosegv() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             const LEN: usize = 1_000;
             let items = (0..LEN as u64).map(|i| (i, i * 2));
 
diff --git a/src/types/mod.rs b/src/types/mod.rs
index a7101d65ced..19058d33193 100644
--- a/src/types/mod.rs
+++ b/src/types/mod.rs
@@ -64,7 +64,7 @@ pub use self::weakref::{PyWeakref, PyWeakrefMethods, PyWeakrefProxy, PyWeakrefRe
 /// use pyo3::ffi::c_str;
 ///
 /// # pub fn main() -> PyResult<()> {
-/// Python::with_gil(|py| {
+/// Python::attach(|py| {
 ///     let dict = py.eval(c_str!("{'a':'b', 'c':'d'}"), None, None)?.downcast_into::()?;
 ///
 ///     for (key, value) in &dict {
diff --git a/src/types/module.rs b/src/types/module.rs
index ef081d12c3e..e5bd408a47d 100644
--- a/src/types/module.rs
+++ b/src/types/module.rs
@@ -43,7 +43,7 @@ impl PyModule {
     /// use pyo3::prelude::*;
     ///
     /// # fn main() -> PyResult<()> {
-    /// Python::with_gil(|py| -> PyResult<()> {
+    /// Python::attach(|py| -> PyResult<()> {
     ///     let module = PyModule::new(py, "my_module")?;
     ///
     ///     assert_eq!(module.name()?, "my_module");
@@ -68,7 +68,7 @@ impl PyModule {
     /// # fn main() {
     /// use pyo3::prelude::*;
     ///
-    /// Python::with_gil(|py| {
+    /// Python::attach(|py| {
     ///     let module = PyModule::import(py, "antigravity").expect("No flying for you.");
     /// });
     /// # }
@@ -124,7 +124,7 @@ impl PyModule {
     /// // This path is resolved relative to this file.
     /// let code = c_str!(include_str!("../../assets/script.py"));
     ///
-    /// Python::with_gil(|py| -> PyResult<()> {
+    /// Python::attach(|py| -> PyResult<()> {
     ///     PyModule::from_code(py, code, c_str!("example.py"), c_str!("example"))?;
     ///     Ok(())
     /// })?;
@@ -145,7 +145,7 @@ impl PyModule {
     /// // if you just want to bundle a script with your module.
     /// let code = std::fs::read_to_string("assets/script.py")?;
     ///
-    /// Python::with_gil(|py| -> PyResult<()> {
+    /// Python::attach(|py| -> PyResult<()> {
     ///     PyModule::from_code(py, CString::new(code)?.as_c_str(), c_str!("example.py"), c_str!("example"))?;
     ///     Ok(())
     /// })?;
@@ -557,7 +557,7 @@ mod tests {
 
     #[test]
     fn module_import_and_name() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let builtins = PyModule::import(py, "builtins").unwrap();
             assert_eq!(builtins.name().unwrap(), "builtins");
         })
@@ -566,7 +566,7 @@ mod tests {
     #[test]
     fn module_filename() {
         use crate::types::string::PyStringMethods;
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let site = PyModule::import(py, "site").unwrap();
             assert!(site
                 .filename()
@@ -579,7 +579,7 @@ mod tests {
 
     #[test]
     fn module_from_code_empty_file() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let builtins = PyModule::from_code(py, c_str!(""), c_str!(""), c_str!("")).unwrap();
             assert_eq!(builtins.filename().unwrap(), "");
         })
diff --git a/src/types/none.rs b/src/types/none.rs
index 71af995ad43..4b864237958 100644
--- a/src/types/none.rs
+++ b/src/types/none.rs
@@ -47,7 +47,7 @@ mod tests {
 
     #[test]
     fn test_none_is_itself() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             assert!(PyNone::get(py).is_instance_of::());
             assert!(PyNone::get(py).is_exact_instance_of::());
         })
@@ -55,21 +55,21 @@ mod tests {
 
     #[test]
     fn test_none_type_object_consistent() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             assert!(PyNone::get(py).get_type().is(PyNone::type_object(py)));
         })
     }
 
     #[test]
     fn test_none_is_none() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             assert!(PyNone::get(py).downcast::().unwrap().is_none());
         })
     }
 
     #[test]
     fn test_dict_is_not_none() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             assert!(PyDict::new(py).downcast::().is_err());
         })
     }
diff --git a/src/types/notimplemented.rs b/src/types/notimplemented.rs
index 9e9bcb79759..ebdf3aced6d 100644
--- a/src/types/notimplemented.rs
+++ b/src/types/notimplemented.rs
@@ -52,7 +52,7 @@ mod tests {
 
     #[test]
     fn test_notimplemented_is_itself() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             assert!(PyNotImplemented::get(py).is_instance_of::());
             assert!(PyNotImplemented::get(py).is_exact_instance_of::());
         })
@@ -60,7 +60,7 @@ mod tests {
 
     #[test]
     fn test_notimplemented_type_object_consistent() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             assert!(PyNotImplemented::get(py)
                 .get_type()
                 .is(PyNotImplemented::type_object(py)));
@@ -69,7 +69,7 @@ mod tests {
 
     #[test]
     fn test_dict_is_not_notimplemented() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             assert!(PyDict::new(py).downcast::().is_err());
         })
     }
diff --git a/src/types/num.rs b/src/types/num.rs
index 2ba92d0c742..41ecb5ea527 100644
--- a/src/types/num.rs
+++ b/src/types/num.rs
@@ -75,7 +75,7 @@ mod tests {
 
     #[test]
     fn test_partial_eq() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v_i8 = 123i8;
             let v_u8 = 123i8;
             let v_i16 = 123i16;
@@ -137,7 +137,7 @@ mod tests {
 
     #[test]
     fn test_display_int() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let s = PyInt::new(py, 42u8);
             assert_eq!(format!("{s}"), "42");
 
diff --git a/src/types/range.rs b/src/types/range.rs
index e499eac49f0..fcf90c2cc7a 100644
--- a/src/types/range.rs
+++ b/src/types/range.rs
@@ -72,7 +72,7 @@ mod tests {
 
     #[test]
     fn test_py_range_new() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let range = PyRange::new(py, isize::MIN, isize::MAX).unwrap();
             assert_eq!(range.start().unwrap(), isize::MIN);
             assert_eq!(range.stop().unwrap(), isize::MAX);
@@ -82,7 +82,7 @@ mod tests {
 
     #[test]
     fn test_py_range_new_with_step() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let range = PyRange::new_with_step(py, 1, 10, 2).unwrap();
             assert_eq!(range.start().unwrap(), 1);
             assert_eq!(range.stop().unwrap(), 10);
diff --git a/src/types/sequence.rs b/src/types/sequence.rs
index 2546b483baf..dfdfd8e8856 100644
--- a/src/types/sequence.rs
+++ b/src/types/sequence.rs
@@ -403,7 +403,7 @@ mod tests {
 
     fn get_object() -> PyObject {
         // Convenience function for getting a single unique object
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let obj = py.eval(ffi::c_str!("object()"), None, None).unwrap();
 
             obj.into_pyobject(py).unwrap().unbind()
@@ -412,7 +412,7 @@ mod tests {
 
     #[test]
     fn test_numbers_are_not_sequences() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v = 42i32;
             assert!(v
                 .into_pyobject(py)
@@ -424,7 +424,7 @@ mod tests {
 
     #[test]
     fn test_strings_are_sequences() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v = "London Calling";
             assert!(v
                 .into_pyobject(py)
@@ -436,7 +436,7 @@ mod tests {
 
     #[test]
     fn test_strings_cannot_be_extracted_to_vec() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v = "London Calling";
             let ob = v.into_pyobject(py).unwrap();
 
@@ -447,7 +447,7 @@ mod tests {
 
     #[test]
     fn test_seq_empty() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v: Vec = vec![];
             let ob = v.into_pyobject(py).unwrap();
             let seq = ob.downcast::().unwrap();
@@ -460,7 +460,7 @@ mod tests {
 
     #[test]
     fn test_seq_is_empty() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let list = vec![1].into_pyobject(py).unwrap();
             let seq = list.downcast::().unwrap();
             assert!(!seq.is_empty().unwrap());
@@ -473,7 +473,7 @@ mod tests {
 
     #[test]
     fn test_seq_contains() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v: Vec = vec![1, 1, 2, 3, 5, 8];
             let ob = v.into_pyobject(py).unwrap();
             let seq = ob.downcast::().unwrap();
@@ -492,7 +492,7 @@ mod tests {
 
     #[test]
     fn test_seq_get_item() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v: Vec = vec![1, 1, 2, 3, 5, 8];
             let ob = v.into_pyobject(py).unwrap();
             let seq = ob.downcast::().unwrap();
@@ -508,7 +508,7 @@ mod tests {
 
     #[test]
     fn test_seq_del_item() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v: Vec = vec![1, 1, 2, 3, 5, 8];
             let ob = v.into_pyobject(py).unwrap();
             let seq = ob.downcast::().unwrap();
@@ -532,7 +532,7 @@ mod tests {
 
     #[test]
     fn test_seq_set_item() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v: Vec = vec![1, 2];
             let ob = v.into_pyobject(py).unwrap();
             let seq = ob.downcast::().unwrap();
@@ -546,7 +546,7 @@ mod tests {
     fn test_seq_set_item_refcnt() {
         let obj = get_object();
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v: Vec = vec![1, 2];
             let ob = v.into_pyobject(py).unwrap();
             let seq = ob.downcast::().unwrap();
@@ -554,14 +554,14 @@ mod tests {
             assert!(ptr::eq(seq.get_item(1).unwrap().as_ptr(), obj.as_ptr()));
         });
 
-        Python::with_gil(move |py| {
+        Python::attach(move |py| {
             assert_eq!(1, obj.get_refcnt(py));
         });
     }
 
     #[test]
     fn test_seq_get_slice() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v: Vec = vec![1, 1, 2, 3, 5, 8];
             let ob = v.into_pyobject(py).unwrap();
             let seq = ob.downcast::().unwrap();
@@ -581,7 +581,7 @@ mod tests {
 
     #[test]
     fn test_set_slice() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v: Vec = vec![1, 1, 2, 3, 5, 8];
             let w: Vec = vec![7, 4];
             let ob = v.into_pyobject(py).unwrap();
@@ -596,7 +596,7 @@ mod tests {
 
     #[test]
     fn test_del_slice() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v: Vec = vec![1, 1, 2, 3, 5, 8];
             let ob = v.into_pyobject(py).unwrap();
             let seq = ob.downcast::().unwrap();
@@ -609,7 +609,7 @@ mod tests {
 
     #[test]
     fn test_seq_index() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v: Vec = vec![1, 1, 2, 3, 5, 8];
             let ob = v.into_pyobject(py).unwrap();
             let seq = ob.downcast::().unwrap();
@@ -625,7 +625,7 @@ mod tests {
     #[test]
     #[cfg(not(any(PyPy, GraalPy)))]
     fn test_seq_count() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v: Vec = vec![1, 1, 2, 3, 5, 8];
             let ob = v.into_pyobject(py).unwrap();
             let seq = ob.downcast::().unwrap();
@@ -640,7 +640,7 @@ mod tests {
 
     #[test]
     fn test_seq_iter() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v: Vec = vec![1, 1, 2, 3, 5, 8];
             let ob = (&v).into_pyobject(py).unwrap();
             let seq = ob.downcast::().unwrap();
@@ -655,7 +655,7 @@ mod tests {
 
     #[test]
     fn test_seq_strings() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v = vec!["It", "was", "the", "worst", "of", "times"];
             let ob = v.into_pyobject(py).unwrap();
             let seq = ob.downcast::().unwrap();
@@ -670,7 +670,7 @@ mod tests {
 
     #[test]
     fn test_seq_concat() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v: Vec = vec![1, 2, 3];
             let ob = v.into_pyobject(py).unwrap();
             let seq = ob.downcast::().unwrap();
@@ -685,7 +685,7 @@ mod tests {
 
     #[test]
     fn test_seq_concat_string() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v = "string";
             let ob = v.into_pyobject(py).unwrap();
             let seq = ob.downcast::().unwrap();
@@ -700,7 +700,7 @@ mod tests {
 
     #[test]
     fn test_seq_repeat() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v = vec!["foo", "bar"];
             let ob = v.into_pyobject(py).unwrap();
             let seq = ob.downcast::().unwrap();
@@ -715,7 +715,7 @@ mod tests {
 
     #[test]
     fn test_seq_inplace() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v = vec!["foo", "bar"];
             let ob = v.into_pyobject(py).unwrap();
             let seq = ob.downcast::().unwrap();
@@ -731,7 +731,7 @@ mod tests {
 
     #[test]
     fn test_list_coercion() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v = vec!["foo", "bar"];
             let ob = (&v).into_pyobject(py).unwrap();
             let seq = ob.downcast::().unwrap();
@@ -745,7 +745,7 @@ mod tests {
 
     #[test]
     fn test_strings_coerce_to_lists() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v = "foo";
             let ob = v.into_pyobject(py).unwrap();
             let seq = ob.downcast::().unwrap();
@@ -759,7 +759,7 @@ mod tests {
 
     #[test]
     fn test_tuple_coercion() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v = ("foo", "bar");
             let ob = v.into_pyobject(py).unwrap();
             let seq = ob.downcast::().unwrap();
@@ -773,7 +773,7 @@ mod tests {
 
     #[test]
     fn test_lists_coerce_to_tuples() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v = vec!["foo", "bar"];
             let ob = (&v).into_pyobject(py).unwrap();
             let seq = ob.downcast::().unwrap();
@@ -787,7 +787,7 @@ mod tests {
 
     #[test]
     fn test_extract_tuple_to_vec() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v: Vec = py
                 .eval(ffi::c_str!("(1, 2)"), None, None)
                 .unwrap()
@@ -799,7 +799,7 @@ mod tests {
 
     #[test]
     fn test_extract_range_to_vec() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v: Vec = py
                 .eval(ffi::c_str!("range(1, 5)"), None, None)
                 .unwrap()
@@ -811,7 +811,7 @@ mod tests {
 
     #[test]
     fn test_extract_bytearray_to_vec() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v: Vec = py
                 .eval(ffi::c_str!("bytearray(b'abc')"), None, None)
                 .unwrap()
@@ -823,7 +823,7 @@ mod tests {
 
     #[test]
     fn test_seq_downcast_unchecked() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let v = vec!["foo", "bar"];
             let ob = v.into_pyobject(py).unwrap();
             let seq = ob.downcast::().unwrap();
diff --git a/src/types/set.rs b/src/types/set.rs
index 8db32c56c1e..acc41340ae1 100644
--- a/src/types/set.rs
+++ b/src/types/set.rs
@@ -301,7 +301,7 @@ mod tests {
 
     #[test]
     fn test_set_new() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let set = PySet::new(py, [1]).unwrap();
             assert_eq!(1, set.len());
 
@@ -312,7 +312,7 @@ mod tests {
 
     #[test]
     fn test_set_empty() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let set = PySet::empty(py).unwrap();
             assert_eq!(0, set.len());
             assert!(set.is_empty());
@@ -321,7 +321,7 @@ mod tests {
 
     #[test]
     fn test_set_len() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let mut v = HashSet::::new();
             let ob = (&v).into_pyobject(py).unwrap();
             let set = ob.downcast::().unwrap();
@@ -335,7 +335,7 @@ mod tests {
 
     #[test]
     fn test_set_clear() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let set = PySet::new(py, [1]).unwrap();
             assert_eq!(1, set.len());
             set.clear();
@@ -345,7 +345,7 @@ mod tests {
 
     #[test]
     fn test_set_contains() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let set = PySet::new(py, [1]).unwrap();
             assert!(set.contains(1).unwrap());
         });
@@ -353,7 +353,7 @@ mod tests {
 
     #[test]
     fn test_set_discard() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let set = PySet::new(py, [1]).unwrap();
             assert!(!set.discard(2).unwrap());
             assert_eq!(1, set.len());
@@ -368,7 +368,7 @@ mod tests {
 
     #[test]
     fn test_set_add() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let set = PySet::new(py, [1, 2]).unwrap();
             set.add(1).unwrap(); // Add a dupliated element
             assert!(set.contains(1).unwrap());
@@ -377,7 +377,7 @@ mod tests {
 
     #[test]
     fn test_set_pop() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let set = PySet::new(py, [1]).unwrap();
             let val = set.pop();
             assert!(val.is_some());
@@ -395,7 +395,7 @@ mod tests {
 
     #[test]
     fn test_set_iter() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let set = PySet::new(py, [1]).unwrap();
 
             for el in set {
@@ -408,7 +408,7 @@ mod tests {
     fn test_set_iter_bound() {
         use crate::types::any::PyAnyMethods;
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let set = PySet::new(py, [1]).unwrap();
 
             for el in &set {
@@ -420,7 +420,7 @@ mod tests {
     #[test]
     #[should_panic]
     fn test_set_iter_mutation() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let set = PySet::new(py, [1, 2, 3, 4, 5]).unwrap();
 
             for _ in &set {
@@ -432,7 +432,7 @@ mod tests {
     #[test]
     #[should_panic]
     fn test_set_iter_mutation_same_len() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let set = PySet::new(py, [1, 2, 3, 4, 5]).unwrap();
 
             for item in &set {
@@ -445,7 +445,7 @@ mod tests {
 
     #[test]
     fn test_set_iter_size_hint() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let set = PySet::new(py, [1]).unwrap();
             let mut iter = set.iter();
 
@@ -460,7 +460,7 @@ mod tests {
 
     #[test]
     fn test_iter_count() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let set = PySet::new(py, vec![1, 2, 3]).unwrap();
             assert_eq!(set.iter().count(), 3);
         })
diff --git a/src/types/slice.rs b/src/types/slice.rs
index 9b743156606..f6b9c378af0 100644
--- a/src/types/slice.rs
+++ b/src/types/slice.rs
@@ -160,7 +160,7 @@ mod tests {
 
     #[test]
     fn test_py_slice_new() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let slice = PySlice::new(py, isize::MIN, isize::MAX, 1);
             assert_eq!(
                 slice.getattr("start").unwrap().extract::().unwrap(),
@@ -179,7 +179,7 @@ mod tests {
 
     #[test]
     fn test_py_slice_full() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let slice = PySlice::full(py);
             assert!(slice.getattr("start").unwrap().is_none(),);
             assert!(slice.getattr("stop").unwrap().is_none(),);
diff --git a/src/types/string.rs b/src/types/string.rs
index 59300440231..a525177c1a3 100644
--- a/src/types/string.rs
+++ b/src/types/string.rs
@@ -141,7 +141,7 @@ impl<'a> PyStringData<'a> {
 /// # use pyo3::prelude::*;
 /// use pyo3::types::PyString;
 ///
-/// # Python::with_gil(|py| {
+/// # Python::attach(|py| {
 /// let py_string = PyString::new(py, "foo");
 /// // via PartialEq
 /// assert_eq!(py_string, "foo");
@@ -539,7 +539,7 @@ mod tests {
 
     #[test]
     fn test_to_cow_utf8() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let s = "ascii 🐈";
             let py_string = PyString::new(py, s);
             assert_eq!(s, py_string.to_cow().unwrap());
@@ -548,7 +548,7 @@ mod tests {
 
     #[test]
     fn test_to_cow_surrogate() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let py_string = py
                 .eval(ffi::c_str!(r"'\ud800'"), None, None)
                 .unwrap()
@@ -560,7 +560,7 @@ mod tests {
 
     #[test]
     fn test_to_cow_unicode() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let s = "哈哈🐈";
             let py_string = PyString::new(py, s);
             assert_eq!(s, py_string.to_cow().unwrap());
@@ -569,7 +569,7 @@ mod tests {
 
     #[test]
     fn test_encode_utf8_unicode() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let s = "哈哈🐈";
             let obj = PyString::new(py, s);
             assert_eq!(s.as_bytes(), obj.encode_utf8().unwrap().as_bytes());
@@ -578,7 +578,7 @@ mod tests {
 
     #[test]
     fn test_encode_utf8_surrogate() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let obj: PyObject = py
                 .eval(ffi::c_str!(r"'\ud800'"), None, None)
                 .unwrap()
@@ -594,7 +594,7 @@ mod tests {
 
     #[test]
     fn test_to_string_lossy() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let py_string = py
                 .eval(ffi::c_str!(r"'🐈 Hello \ud800World'"), None, None)
                 .unwrap()
@@ -607,7 +607,7 @@ mod tests {
 
     #[test]
     fn test_debug_string() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let s = "Hello\n".into_pyobject(py).unwrap();
             assert_eq!(format!("{s:?}"), "'Hello\\n'");
         })
@@ -615,7 +615,7 @@ mod tests {
 
     #[test]
     fn test_display_string() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let s = "Hello\n".into_pyobject(py).unwrap();
             assert_eq!(format!("{s}"), "Hello\n");
         })
@@ -623,7 +623,7 @@ mod tests {
 
     #[test]
     fn test_string_from_object() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let py_bytes = PyBytes::new(py, b"ab\xFFcd");
 
             let py_string = PyString::from_object(&py_bytes, "utf-8", "ignore").unwrap();
@@ -635,7 +635,7 @@ mod tests {
 
     #[test]
     fn test_string_from_obect_with_invalid_encoding_errors() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let py_bytes = PyBytes::new(py, b"abcd");
 
             let result = PyString::from_object(&py_bytes, "utf\0-8", "ignore");
@@ -649,7 +649,7 @@ mod tests {
     #[test]
     #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))]
     fn test_string_data_ucs1() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let s = PyString::new(py, "hello, world");
             let data = unsafe { s.data().unwrap() };
 
@@ -662,7 +662,7 @@ mod tests {
     #[test]
     #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))]
     fn test_string_data_ucs1_invalid() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             // 0xfe is not allowed in UTF-8.
             let buffer = b"f\xfe\0";
             let ptr = unsafe {
@@ -688,7 +688,7 @@ mod tests {
     #[test]
     #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))]
     fn test_string_data_ucs2() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             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() };
@@ -704,7 +704,7 @@ mod tests {
     #[test]
     #[cfg(all(not(any(Py_LIMITED_API, PyPy, GraalPy)), target_endian = "little"))]
     fn test_string_data_ucs2_invalid() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             // U+FF22 (valid) & U+d800 (never valid)
             let buffer = b"\x22\xff\x00\xd8\x00\x00";
             let ptr = unsafe {
@@ -730,7 +730,7 @@ mod tests {
     #[test]
     #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))]
     fn test_string_data_ucs4() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let s = "哈哈🐈";
             let py_string = PyString::new(py, s);
             let data = unsafe { py_string.data().unwrap() };
@@ -743,7 +743,7 @@ mod tests {
     #[test]
     #[cfg(all(not(any(Py_LIMITED_API, PyPy, GraalPy)), target_endian = "little"))]
     fn test_string_data_ucs4_invalid() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             // U+20000 (valid) & U+d800 (never valid)
             let buffer = b"\x00\x00\x02\x00\x00\xd8\x00\x00\x00\x00\x00\x00";
             let ptr = unsafe {
@@ -768,7 +768,7 @@ mod tests {
 
     #[test]
     fn test_intern_string() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let py_string1 = PyString::intern(py, "foo");
             assert_eq!(py_string1, "foo");
 
@@ -786,7 +786,7 @@ mod tests {
 
     #[test]
     fn test_py_to_str_utf8() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let s = "ascii 🐈";
             let py_string = PyString::new(py, s).unbind();
 
@@ -799,7 +799,7 @@ mod tests {
 
     #[test]
     fn test_py_to_str_surrogate() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let py_string: Py = py
                 .eval(ffi::c_str!(r"'\ud800'"), None, None)
                 .unwrap()
@@ -815,7 +815,7 @@ mod tests {
 
     #[test]
     fn test_py_to_string_lossy() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let py_string: Py = py
                 .eval(ffi::c_str!(r"'🐈 Hello \ud800World'"), None, None)
                 .unwrap()
@@ -827,7 +827,7 @@ mod tests {
 
     #[test]
     fn test_comparisons() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let s = "hello, world";
             let py_string = PyString::new(py, s);
 
diff --git a/src/types/traceback.rs b/src/types/traceback.rs
index 885c0f67031..c37b943e51e 100644
--- a/src/types/traceback.rs
+++ b/src/types/traceback.rs
@@ -37,7 +37,7 @@ pub trait PyTracebackMethods<'py>: crate::sealed::Sealed {
     /// ```rust
     /// # use pyo3::{Python, PyResult, prelude::PyTracebackMethods, ffi::c_str};
     /// # let result: PyResult<()> =
-    /// Python::with_gil(|py| {
+    /// Python::attach(|py| {
     ///     let err = py
     ///         .run(c_str!("raise Exception('banana')"), None, None)
     ///         .expect_err("raise will create a Python error");
@@ -89,7 +89,7 @@ mod tests {
 
     #[test]
     fn format_traceback() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let err = py
                 .run(ffi::c_str!("raise Exception('banana')"), None, None)
                 .expect_err("raising should have given us an error");
@@ -103,7 +103,7 @@ mod tests {
 
     #[test]
     fn test_err_from_value() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let locals = PyDict::new(py);
             // Produce an error from python so that it has a traceback
             py.run(
@@ -127,7 +127,7 @@ except Exception as e:
 
     #[test]
     fn test_err_into_py() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let locals = PyDict::new(py);
             // Produce an error from python so that it has a traceback
             py.run(
diff --git a/src/types/tuple.rs b/src/types/tuple.rs
index 0a315a42e00..6b7deadb6c9 100644
--- a/src/types/tuple.rs
+++ b/src/types/tuple.rs
@@ -74,7 +74,7 @@ impl PyTuple {
     /// use pyo3::types::PyTuple;
     ///
     /// # fn main() -> PyResult<()> {
-    /// Python::with_gil(|py| {
+    /// Python::attach(|py| {
     ///     let elements: Vec = vec![0, 1, 2, 3, 4, 5];
     ///     let tuple = PyTuple::new(py, elements)?;
     ///     assert_eq!(format!("{:?}", tuple), "(0, 1, 2, 3, 4, 5)");
@@ -142,7 +142,7 @@ pub trait PyTupleMethods<'py>: crate::sealed::Sealed {
     /// use pyo3::prelude::*;
     ///
     /// # fn main() -> PyResult<()> {
-    /// Python::with_gil(|py| -> PyResult<()> {
+    /// Python::attach(|py| -> PyResult<()> {
     ///     let tuple = (1, 2, 3).into_pyobject(py)?;
     ///     let obj = tuple.get_item(0);
     ///     assert_eq!(obj?.extract::()?, 1);
@@ -1038,7 +1038,7 @@ mod tests {
     use std::ops::Range;
     #[test]
     fn test_new() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let ob = PyTuple::new(py, [1, 2, 3]).unwrap();
             assert_eq!(3, ob.len());
             let ob = ob.as_any();
@@ -1053,7 +1053,7 @@ mod tests {
 
     #[test]
     fn test_len() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let ob = (1, 2, 3).into_pyobject(py).unwrap();
             let tuple = ob.downcast::().unwrap();
             assert_eq!(3, tuple.len());
@@ -1065,7 +1065,7 @@ mod tests {
 
     #[test]
     fn test_empty() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let tuple = PyTuple::empty(py);
             assert!(tuple.is_empty());
             assert_eq!(0, tuple.len());
@@ -1074,7 +1074,7 @@ mod tests {
 
     #[test]
     fn test_slice() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let tup = PyTuple::new(py, [2, 3, 5, 7]).unwrap();
             let slice = tup.get_slice(1, 3);
             assert_eq!(2, slice.len());
@@ -1085,7 +1085,7 @@ mod tests {
 
     #[test]
     fn test_iter() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let ob = (1, 2, 3).into_pyobject(py).unwrap();
             let tuple = ob.downcast::().unwrap();
             assert_eq!(3, tuple.len());
@@ -1109,7 +1109,7 @@ mod tests {
 
     #[test]
     fn test_iter_rev() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let ob = (1, 2, 3).into_pyobject(py).unwrap();
             let tuple = ob.downcast::().unwrap();
             assert_eq!(3, tuple.len());
@@ -1133,7 +1133,7 @@ mod tests {
 
     #[test]
     fn test_bound_iter() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let tuple = PyTuple::new(py, [1, 2, 3]).unwrap();
             assert_eq!(3, tuple.len());
             let mut iter = tuple.iter();
@@ -1156,7 +1156,7 @@ mod tests {
 
     #[test]
     fn test_bound_iter_rev() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let tuple = PyTuple::new(py, [1, 2, 3]).unwrap();
             assert_eq!(3, tuple.len());
             let mut iter = tuple.iter().rev();
@@ -1179,7 +1179,7 @@ mod tests {
 
     #[test]
     fn test_into_iter() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let ob = (1, 2, 3).into_pyobject(py).unwrap();
             let tuple = ob.downcast::().unwrap();
             assert_eq!(3, tuple.len());
@@ -1192,7 +1192,7 @@ mod tests {
 
     #[test]
     fn test_into_iter_bound() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let tuple = (1, 2, 3).into_pyobject(py).unwrap();
             assert_eq!(3, tuple.len());
 
@@ -1207,7 +1207,7 @@ mod tests {
     #[test]
     #[cfg(not(any(Py_LIMITED_API, GraalPy)))]
     fn test_as_slice() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let ob = (1, 2, 3).into_pyobject(py).unwrap();
             let tuple = ob.downcast::().unwrap();
 
@@ -1221,7 +1221,7 @@ mod tests {
 
     #[test]
     fn test_tuple_lengths_up_to_12() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let t0 = (0,).into_pyobject(py).unwrap();
             let t1 = (0, 1).into_pyobject(py).unwrap();
             let t2 = (0, 1, 2).into_pyobject(py).unwrap();
@@ -1289,7 +1289,7 @@ mod tests {
 
     #[test]
     fn test_tuple_get_item_invalid_index() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let ob = (1, 2, 3).into_pyobject(py).unwrap();
             let tuple = ob.downcast::().unwrap();
             let obj = tuple.get_item(5);
@@ -1303,7 +1303,7 @@ mod tests {
 
     #[test]
     fn test_tuple_get_item_sanity() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let ob = (1, 2, 3).into_pyobject(py).unwrap();
             let tuple = ob.downcast::().unwrap();
             let obj = tuple.get_item(0);
@@ -1314,7 +1314,7 @@ mod tests {
     #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))]
     #[test]
     fn test_tuple_get_item_unchecked_sanity() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let ob = (1, 2, 3).into_pyobject(py).unwrap();
             let tuple = ob.downcast::().unwrap();
             let obj = unsafe { tuple.get_item_unchecked(0) };
@@ -1324,7 +1324,7 @@ mod tests {
 
     #[test]
     fn test_tuple_contains() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let ob = (1, 1, 2, 3, 5, 8).into_pyobject(py).unwrap();
             let tuple = ob.downcast::().unwrap();
             assert_eq!(6, tuple.len());
@@ -1342,7 +1342,7 @@ mod tests {
 
     #[test]
     fn test_tuple_index() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let ob = (1, 1, 2, 3, 5, 8).into_pyobject(py).unwrap();
             let tuple = ob.downcast::().unwrap();
             assert_eq!(0, tuple.index(1i32).unwrap());
@@ -1377,7 +1377,7 @@ mod tests {
         expected = "Attempted to create PyTuple but `elements` was larger than reported by its `ExactSizeIterator` implementation."
     )]
     fn too_long_iterator() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let iter = FaultyIter(0..usize::MAX, 73);
             let _tuple = PyTuple::new(py, iter);
         })
@@ -1388,7 +1388,7 @@ mod tests {
         expected = "Attempted to create PyTuple but `elements` was smaller than reported by its `ExactSizeIterator` implementation."
     )]
     fn too_short_iterator() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let iter = FaultyIter(0..35, 73);
             let _tuple = PyTuple::new(py, iter);
         })
@@ -1399,7 +1399,7 @@ mod tests {
         expected = "out of range integral type conversion attempted on `elements.len()`"
     )]
     fn overflowing_size() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let iter = FaultyIter(0..0, usize::MAX);
 
             let _tuple = PyTuple::new(py, iter);
@@ -1453,7 +1453,7 @@ mod tests {
             }
         }
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             std::panic::catch_unwind(|| {
                 let iter = FaultyIter(0..50, 50);
                 let _tuple = PyTuple::new(py, iter);
@@ -1498,7 +1498,7 @@ mod tests {
 
         let s = (Bad(1), Bad(2), Bad(3), Bad(4));
         NEEDS_DESTRUCTING_COUNT.store(4, SeqCst);
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             std::panic::catch_unwind(|| {
                 let _tuple = (&s).into_pyobject(py).unwrap();
             })
@@ -1515,7 +1515,7 @@ mod tests {
 
     #[test]
     fn test_tuple_to_list() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             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]).unwrap();
@@ -1525,7 +1525,7 @@ mod tests {
 
     #[test]
     fn test_tuple_as_sequence() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let tuple = PyTuple::new(py, vec![1, 2, 3]).unwrap();
             let sequence = tuple.as_sequence();
             assert!(tuple.get_item(0).unwrap().eq(1).unwrap());
@@ -1538,7 +1538,7 @@ mod tests {
 
     #[test]
     fn test_tuple_into_sequence() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let tuple = PyTuple::new(py, vec![1, 2, 3]).unwrap();
             let sequence = tuple.into_sequence();
             assert!(sequence.get_item(0).unwrap().eq(1).unwrap());
@@ -1548,7 +1548,7 @@ mod tests {
 
     #[test]
     fn test_bound_tuple_get_item() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let tuple = PyTuple::new(py, vec![1, 2, 3, 4]).unwrap();
 
             assert_eq!(tuple.len(), 4);
@@ -1581,7 +1581,7 @@ mod tests {
 
     #[test]
     fn test_bound_tuple_nth() {
-        Python::with_gil(|py| {
+        Python::attach(|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);
@@ -1612,7 +1612,7 @@ mod tests {
 
     #[test]
     fn test_bound_tuple_nth_back() {
-        Python::with_gil(|py| {
+        Python::attach(|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);
@@ -1655,7 +1655,7 @@ mod tests {
     #[cfg(feature = "nightly")]
     #[test]
     fn test_bound_tuple_advance_by() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let tuple = PyTuple::new(py, vec![1, 2, 3, 4, 5]).unwrap();
             let mut iter = tuple.iter();
 
@@ -1680,7 +1680,7 @@ mod tests {
     #[cfg(feature = "nightly")]
     #[test]
     fn test_bound_tuple_advance_back_by() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let tuple = PyTuple::new(py, vec![1, 2, 3, 4, 5]).unwrap();
             let mut iter = tuple.iter();
 
@@ -1704,7 +1704,7 @@ mod tests {
 
     #[test]
     fn test_iter_last() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let tuple = PyTuple::new(py, vec![1, 2, 3]).unwrap();
             let last = tuple.iter().last();
             assert_eq!(last.unwrap().extract::().unwrap(), 3);
@@ -1713,7 +1713,7 @@ mod tests {
 
     #[test]
     fn test_iter_count() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let tuple = PyTuple::new(py, vec![1, 2, 3]).unwrap();
             assert_eq!(tuple.iter().count(), 3);
         })
diff --git a/src/types/typeobject.rs b/src/types/typeobject.rs
index 2efe26bff49..142a4d1f0be 100644
--- a/src/types/typeobject.rs
+++ b/src/types/typeobject.rs
@@ -253,7 +253,7 @@ mod tests {
 
     #[test]
     fn test_type_is_subclass() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let bool_type = py.get_type::();
             let long_type = py.get_type::();
             assert!(bool_type.is_subclass(&long_type).unwrap());
@@ -262,14 +262,14 @@ mod tests {
 
     #[test]
     fn test_type_is_subclass_of() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             assert!(py.get_type::().is_subclass_of::().unwrap());
         });
     }
 
     #[test]
     fn test_mro() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             assert!(py
                 .get_type::()
                 .mro()
@@ -288,7 +288,7 @@ mod tests {
 
     #[test]
     fn test_bases_bool() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             assert!(py
                 .get_type::()
                 .bases()
@@ -299,7 +299,7 @@ mod tests {
 
     #[test]
     fn test_bases_object() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             assert!(py
                 .get_type::()
                 .bases()
@@ -310,7 +310,7 @@ mod tests {
 
     #[test]
     fn test_type_names_standard() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let module_name = generate_unique_module_name("test_module");
             let module = PyModule::from_code(
                 py,
@@ -341,7 +341,7 @@ class MyClass:
 
     #[test]
     fn test_type_names_builtin() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let bool_type = py.get_type::();
             assert_eq!(bool_type.name().unwrap(), "bool");
             assert_eq!(bool_type.qualname().unwrap(), "bool");
@@ -352,7 +352,7 @@ class MyClass:
 
     #[test]
     fn test_type_names_nested() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let module_name = generate_unique_module_name("test_module");
             let module = PyModule::from_code(
                 py,
diff --git a/src/types/weakref/anyref.rs b/src/types/weakref/anyref.rs
index b74ec53a939..04220e0dc59 100644
--- a/src/types/weakref/anyref.rs
+++ b/src/types/weakref/anyref.rs
@@ -71,7 +71,7 @@ pub trait PyWeakrefMethods<'py>: crate::sealed::Sealed {
     /// }
     ///
     /// # fn main() -> PyResult<()> {
-    /// Python::with_gil(|py| {
+    /// Python::attach(|py| {
     ///     let data = Bound::new(py, Foo{})?;
     ///     let reference = PyWeakrefReference::new(&data)?;
     ///
@@ -151,7 +151,7 @@ pub trait PyWeakrefMethods<'py>: crate::sealed::Sealed {
     /// }
     ///
     /// # fn main() -> PyResult<()> {
-    /// Python::with_gil(|py| {
+    /// Python::attach(|py| {
     ///     let data = Bound::new(py, Foo{})?;
     ///     let reference = PyWeakrefReference::new(&data)?;
     ///
@@ -221,7 +221,7 @@ pub trait PyWeakrefMethods<'py>: crate::sealed::Sealed {
     /// }
     ///
     /// # fn main() -> PyResult<()> {
-    /// Python::with_gil(|py| {
+    /// Python::attach(|py| {
     ///     let data = Bound::new(py, Foo{})?;
     ///     let reference = PyWeakrefReference::new(&data)?;
     ///
@@ -291,7 +291,7 @@ pub trait PyWeakrefMethods<'py>: crate::sealed::Sealed {
     /// }
     ///
     /// # fn main() -> PyResult<()> {
-    /// Python::with_gil(|py| {
+    /// Python::attach(|py| {
     ///     let data = Bound::new(py, Foo{})?;
     ///     let reference = PyWeakrefReference::new(&data)?;
     ///
@@ -369,7 +369,7 @@ mod tests {
                 )
                     -> PyResult>,
             ) -> PyResult<()> {
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let class = get_type(py)?;
                     let object = class.call0()?;
                     let reference = create_reference(&object)?;
@@ -414,7 +414,7 @@ mod tests {
                 )
                     -> PyResult>,
             ) -> PyResult<()> {
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let class = get_type(py)?;
                     let object = class.call0()?;
                     let reference = create_reference(&object)?;
@@ -456,7 +456,7 @@ mod tests {
             ) -> PyResult<()> {
                 let not_call_retrievable = !call_retrievable;
 
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let class = get_type(py)?;
                     let object = class.call0()?;
                     let reference = create_reference(&object)?;
@@ -497,7 +497,7 @@ mod tests {
                 )
                     -> PyResult>,
             ) -> PyResult<()> {
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let object = Py::new(py, WeakrefablePyClass {})?;
                     let reference = create_reference(object.bind(py))?;
 
@@ -538,7 +538,7 @@ mod tests {
                 )
                     -> PyResult>,
             ) -> PyResult<()> {
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let object = Py::new(py, WeakrefablePyClass {})?;
                     let reference = create_reference(object.bind(py))?;
 
@@ -576,7 +576,7 @@ mod tests {
             ) -> PyResult<()> {
                 let not_call_retrievable = !call_retrievable;
 
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let object = Py::new(py, WeakrefablePyClass {})?;
                     let reference = create_reference(object.bind(py))?;
 
diff --git a/src/types/weakref/proxy.rs b/src/types/weakref/proxy.rs
index e577ae229c0..b8dedd99ac6 100644
--- a/src/types/weakref/proxy.rs
+++ b/src/types/weakref/proxy.rs
@@ -52,7 +52,7 @@ impl PyWeakrefProxy {
     /// struct Foo { /* fields omitted */ }
     ///
     /// # fn main() -> PyResult<()> {
-    /// Python::with_gil(|py| {
+    /// Python::attach(|py| {
     ///     let foo = Bound::new(py, Foo {})?;
     ///     let weakref = PyWeakrefProxy::new(&foo)?;
     ///     assert!(
@@ -109,7 +109,7 @@ impl PyWeakrefProxy {
     /// }
     ///
     /// # fn main() -> PyResult<()> {
-    /// Python::with_gil(|py| {
+    /// Python::attach(|py| {
     ///     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{})?;
@@ -254,7 +254,7 @@ mod tests {
 
             #[test]
             fn test_weakref_proxy_behavior() -> PyResult<()> {
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let class = get_type(py)?;
                     let object = class.call0()?;
                     let reference = PyWeakrefProxy::new(&object)?;
@@ -316,7 +316,7 @@ mod tests {
 
             #[test]
             fn test_weakref_upgrade_as() -> PyResult<()> {
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let class = get_type(py)?;
                     let object = class.call0()?;
                     let reference = PyWeakrefProxy::new(&object)?;
@@ -351,7 +351,7 @@ mod tests {
 
             #[test]
             fn test_weakref_upgrade_as_unchecked() -> PyResult<()> {
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let class = get_type(py)?;
                     let object = class.call0()?;
                     let reference = PyWeakrefProxy::new(&object)?;
@@ -380,7 +380,7 @@ mod tests {
 
             #[test]
             fn test_weakref_upgrade() -> PyResult<()> {
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let class = get_type(py)?;
                     let object = class.call0()?;
                     let reference = PyWeakrefProxy::new(&object)?;
@@ -398,7 +398,7 @@ mod tests {
 
             #[test]
             fn test_weakref_get_object() -> PyResult<()> {
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let class = get_type(py)?;
                     let object = class.call0()?;
                     let reference = PyWeakrefProxy::new(&object)?;
@@ -426,7 +426,7 @@ mod tests {
 
             #[test]
             fn test_weakref_proxy_behavior() -> PyResult<()> {
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let object: Bound<'_, WeakrefablePyClass> =
                         Bound::new(py, WeakrefablePyClass {})?;
                     let reference = PyWeakrefProxy::new(&object)?;
@@ -490,7 +490,7 @@ mod tests {
 
             #[test]
             fn test_weakref_upgrade_as() -> PyResult<()> {
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let object = Py::new(py, WeakrefablePyClass {})?;
                     let reference = PyWeakrefProxy::new(object.bind(py))?;
 
@@ -521,7 +521,7 @@ mod tests {
 
             #[test]
             fn test_weakref_upgrade_as_unchecked() -> PyResult<()> {
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let object = Py::new(py, WeakrefablePyClass {})?;
                     let reference = PyWeakrefProxy::new(object.bind(py))?;
 
@@ -546,7 +546,7 @@ mod tests {
 
             #[test]
             fn test_weakref_upgrade() -> PyResult<()> {
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let object = Py::new(py, WeakrefablePyClass {})?;
                     let reference = PyWeakrefProxy::new(object.bind(py))?;
 
@@ -590,7 +590,7 @@ mod tests {
 
             #[test]
             fn test_weakref_proxy_behavior() -> PyResult<()> {
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let class = get_type(py)?;
                     let object = class.call0()?;
                     let reference = PyWeakrefProxy::new(&object)?;
@@ -639,7 +639,7 @@ mod tests {
 
             #[test]
             fn test_weakref_upgrade_as() -> PyResult<()> {
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let class = get_type(py)?;
                     let object = class.call0()?;
                     let reference = PyWeakrefProxy::new(&object)?;
@@ -674,7 +674,7 @@ mod tests {
 
             #[test]
             fn test_weakref_upgrade_as_unchecked() -> PyResult<()> {
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let class = get_type(py)?;
                     let object = class.call0()?;
                     let reference = PyWeakrefProxy::new(&object)?;
@@ -703,7 +703,7 @@ mod tests {
 
             #[test]
             fn test_weakref_upgrade() -> PyResult<()> {
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let class = get_type(py)?;
                     let object = class.call0()?;
                     let reference = PyWeakrefProxy::new(&object)?;
@@ -739,7 +739,7 @@ mod tests {
 
             #[test]
             fn test_weakref_proxy_behavior() -> PyResult<()> {
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let object: Bound<'_, WeakrefablePyClass> =
                         Bound::new(py, WeakrefablePyClass {})?;
                     let reference = PyWeakrefProxy::new(&object)?;
@@ -791,7 +791,7 @@ mod tests {
 
             #[test]
             fn test_weakref_upgrade_as() -> PyResult<()> {
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let object = Py::new(py, WeakrefablePyClass {})?;
                     let reference = PyWeakrefProxy::new(object.bind(py))?;
 
@@ -822,7 +822,7 @@ mod tests {
 
             #[test]
             fn test_weakref_upgrade_as_unchecked() -> PyResult<()> {
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let object = Py::new(py, WeakrefablePyClass {})?;
                     let reference = PyWeakrefProxy::new(object.bind(py))?;
 
@@ -847,7 +847,7 @@ mod tests {
 
             #[test]
             fn test_weakref_upgrade() -> PyResult<()> {
-                Python::with_gil(|py| {
+                Python::attach(|py| {
                     let object = Py::new(py, WeakrefablePyClass {})?;
                     let reference = PyWeakrefProxy::new(object.bind(py))?;
 
diff --git a/src/types/weakref/reference.rs b/src/types/weakref/reference.rs
index 8a6dd94790d..9fbd275d01b 100644
--- a/src/types/weakref/reference.rs
+++ b/src/types/weakref/reference.rs
@@ -64,7 +64,7 @@ impl PyWeakrefReference {
     /// struct Foo { /* fields omitted */ }
     ///
     /// # fn main() -> PyResult<()> {
-    /// Python::with_gil(|py| {
+    /// Python::attach(|py| {
     ///     let foo = Bound::new(py, Foo {})?;
     ///     let weakref = PyWeakrefReference::new(&foo)?;
     ///     assert!(
@@ -120,7 +120,7 @@ impl PyWeakrefReference {
     /// }
     ///
     /// # fn main() -> PyResult<()> {
-    /// Python::with_gil(|py| {
+    /// Python::attach(|py| {
     ///     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{})?;
@@ -250,7 +250,7 @@ mod tests {
 
         #[test]
         fn test_weakref_reference_behavior() -> PyResult<()> {
-            Python::with_gil(|py| {
+            Python::attach(|py| {
                 let class = get_type(py)?;
                 let object = class.call0()?;
                 let reference = PyWeakrefReference::new(&object)?;
@@ -292,7 +292,7 @@ mod tests {
 
         #[test]
         fn test_weakref_upgrade_as() -> PyResult<()> {
-            Python::with_gil(|py| {
+            Python::attach(|py| {
                 let class = get_type(py)?;
                 let object = class.call0()?;
                 let reference = PyWeakrefReference::new(&object)?;
@@ -327,7 +327,7 @@ mod tests {
 
         #[test]
         fn test_weakref_upgrade_as_unchecked() -> PyResult<()> {
-            Python::with_gil(|py| {
+            Python::attach(|py| {
                 let class = get_type(py)?;
                 let object = class.call0()?;
                 let reference = PyWeakrefReference::new(&object)?;
@@ -356,7 +356,7 @@ mod tests {
 
         #[test]
         fn test_weakref_upgrade() -> PyResult<()> {
-            Python::with_gil(|py| {
+            Python::attach(|py| {
                 let class = get_type(py)?;
                 let object = class.call0()?;
                 let reference = PyWeakrefReference::new(&object)?;
@@ -387,7 +387,7 @@ mod tests {
 
         #[test]
         fn test_weakref_reference_behavior() -> PyResult<()> {
-            Python::with_gil(|py| {
+            Python::attach(|py| {
                 let object: Bound<'_, WeakrefablePyClass> = Bound::new(py, WeakrefablePyClass {})?;
                 let reference = PyWeakrefReference::new(&object)?;
 
@@ -426,7 +426,7 @@ mod tests {
 
         #[test]
         fn test_weakref_upgrade_as() -> PyResult<()> {
-            Python::with_gil(|py| {
+            Python::attach(|py| {
                 let object = Py::new(py, WeakrefablePyClass {})?;
                 let reference = PyWeakrefReference::new(object.bind(py))?;
 
@@ -457,7 +457,7 @@ mod tests {
 
         #[test]
         fn test_weakref_upgrade_as_unchecked() -> PyResult<()> {
-            Python::with_gil(|py| {
+            Python::attach(|py| {
                 let object = Py::new(py, WeakrefablePyClass {})?;
                 let reference = PyWeakrefReference::new(object.bind(py))?;
 
@@ -482,7 +482,7 @@ mod tests {
 
         #[test]
         fn test_weakref_upgrade() -> PyResult<()> {
-            Python::with_gil(|py| {
+            Python::attach(|py| {
                 let object = Py::new(py, WeakrefablePyClass {})?;
                 let reference = PyWeakrefReference::new(object.bind(py))?;
 
diff --git a/src/version.rs b/src/version.rs
index 5c060d79daf..32cb1b6860c 100644
--- a/src/version.rs
+++ b/src/version.rs
@@ -6,7 +6,7 @@
 ///
 /// ```rust
 /// # use pyo3::Python;
-/// Python::with_gil(|py| {
+/// Python::attach(|py| {
 ///     // PyO3 supports Python 3.7 and up.
 ///     assert!(py.version_info() >= (3, 7));
 ///     assert!(py.version_info() >= (3, 7, 0));
@@ -99,7 +99,7 @@ mod test {
     use crate::Python;
     #[test]
     fn test_python_version_info() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let version = py.version_info();
             #[cfg(Py_3_7)]
             assert!(version >= (3, 7));
diff --git a/tests/test_anyhow.rs b/tests/test_anyhow.rs
index faa8a70f9c0..82c586d778d 100644
--- a/tests/test_anyhow.rs
+++ b/tests/test_anyhow.rs
@@ -12,7 +12,7 @@ fn test_anyhow_py_function_ok_result() {
         Ok(String::from("OK buddy"))
     }
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let func = wrap_pyfunction!(produce_ok_result)(py).unwrap();
 
         py_run!(
@@ -35,7 +35,7 @@ fn test_anyhow_py_function_err_result() {
         anyhow::bail!("error time")
     }
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let func = wrap_pyfunction!(produce_err_result)(py).unwrap();
         let locals = PyDict::new(py);
         locals.set_item("func", func).unwrap();
diff --git a/tests/test_append_to_inittab.rs b/tests/test_append_to_inittab.rs
index 3c042bd2bb2..783573af281 100644
--- a/tests/test_append_to_inittab.rs
+++ b/tests/test_append_to_inittab.rs
@@ -28,7 +28,7 @@ fn test_module_append_to_inittab() {
 
     append_to_inittab!(module_mod_with_functions);
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         py.run(
             ffi::c_str!(
                 r#"
@@ -43,7 +43,7 @@ assert module_fn_with_functions.foo() == 123
         .unwrap();
     });
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         py.run(
             ffi::c_str!(
                 r#"
diff --git a/tests/test_arithmetics.rs b/tests/test_arithmetics.rs
index 2e5220eadd8..a73be443a2e 100644
--- a/tests/test_arithmetics.rs
+++ b/tests/test_arithmetics.rs
@@ -47,7 +47,7 @@ impl UnaryArithmetic {
 
 #[test]
 fn unary_arithmetic() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let c = Py::new(py, UnaryArithmetic::new(2.7)).unwrap();
         py_run!(py, c, "assert repr(-c) == 'UA(-2.7)'");
         py_run!(py, c, "assert repr(+c) == 'UA(2.7)'");
@@ -91,7 +91,7 @@ impl Indexable {
 
 #[test]
 fn indexable() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let i = Py::new(py, Indexable(5)).unwrap();
         py_run!(py, i, "assert int(i) == 5");
         py_run!(py, i, "assert [0, 1, 2, 3, 4, 5][i] == 5");
@@ -150,7 +150,7 @@ impl InPlaceOperations {
 
 #[test]
 fn inplace_operations() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let init = |value, code| {
             let c = Py::new(py, InPlaceOperations { value }).unwrap();
             py_run!(py, c, code);
@@ -240,7 +240,7 @@ impl BinaryArithmetic {
 
 #[test]
 fn binary_arithmetic() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let c = Py::new(py, BinaryArithmetic {}).unwrap();
         py_run!(py, c, "assert c + c == 'BA + BA'");
         py_run!(py, c, "assert c.__add__(c) == 'BA + BA'");
@@ -341,7 +341,7 @@ impl RhsArithmetic {
 
 #[test]
 fn rhs_arithmetic() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let c = Py::new(py, RhsArithmetic {}).unwrap();
         py_run!(py, c, "assert c.__radd__(1) == '1 + RA'");
         py_run!(py, c, "assert 1 + c == '1 + RA'");
@@ -470,7 +470,7 @@ impl LhsAndRhs {
 
 #[test]
 fn lhs_fellback_to_rhs() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let c = Py::new(py, LhsAndRhs {}).unwrap();
         // If the light hand value is `LhsAndRhs`, LHS is used.
         py_run!(py, c, "assert c + 1 == 'LR + 1'");
@@ -546,7 +546,7 @@ impl RichComparisons2 {
 
 #[test]
 fn rich_comparisons() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let c = Py::new(py, RichComparisons {}).unwrap();
         py_run!(py, c, "assert (c < c) == 'RC < RC'");
         py_run!(py, c, "assert (c < 1) == 'RC < 1'");
@@ -571,7 +571,7 @@ fn rich_comparisons() {
 
 #[test]
 fn rich_comparisons_python_3_type_error() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let c2 = Py::new(py, RichComparisons2 {}).unwrap();
         py_expect_exception!(py, c2, "c2 < c2", PyTypeError);
         py_expect_exception!(py, c2, "c2 < 1", PyTypeError);
@@ -672,7 +672,7 @@ mod return_not_implemented {
     }
 
     fn _test_binary_dunder(dunder: &str) {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let c2 = Py::new(py, RichComparisonToSelf {}).unwrap();
             py_run!(
                 py,
@@ -685,7 +685,7 @@ mod return_not_implemented {
     fn _test_binary_operator(operator: &str, dunder: &str) {
         _test_binary_dunder(dunder);
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let c2 = Py::new(py, RichComparisonToSelf {}).unwrap();
             py_expect_exception!(
                 py,
diff --git a/tests/test_buffer.rs b/tests/test_buffer.rs
index 40bccf889b1..4d975f36479 100644
--- a/tests/test_buffer.rs
+++ b/tests/test_buffer.rs
@@ -88,7 +88,7 @@ impl TestBufferErrors {
 
 #[test]
 fn test_get_buffer_errors() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let instance = Py::new(
             py,
             TestBufferErrors {
diff --git a/tests/test_buffer_protocol.rs b/tests/test_buffer_protocol.rs
index c68b7785450..2243a04f0f2 100644
--- a/tests/test_buffer_protocol.rs
+++ b/tests/test_buffer_protocol.rs
@@ -49,7 +49,7 @@ impl Drop for TestBufferClass {
 fn test_buffer() {
     let drop_called = Arc::new(AtomicBool::new(false));
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let instance = Py::new(
             py,
             TestBufferClass {
@@ -71,7 +71,7 @@ fn test_buffer_referenced() {
 
     let buf = {
         let input = vec![b' ', b'2', b'3'];
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let instance = TestBufferClass {
                 vec: input.clone(),
                 drop_called: drop_called.clone(),
@@ -88,7 +88,7 @@ fn test_buffer_referenced() {
 
     assert!(!drop_called.load(Ordering::Relaxed));
 
-    Python::with_gil(|_| {
+    Python::attach(|_| {
         drop(buf);
     });
 
@@ -120,7 +120,7 @@ fn test_releasebuffer_unraisable_error() {
         }
     }
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let capture = UnraisableCapture::install(py);
 
         let instance = Py::new(py, ReleaseBufferError {}).unwrap();
diff --git a/tests/test_bytes.rs b/tests/test_bytes.rs
index 4e58cdc08e0..7df25c2fe58 100644
--- a/tests/test_bytes.rs
+++ b/tests/test_bytes.rs
@@ -13,7 +13,7 @@ fn bytes_pybytes_conversion(bytes: &[u8]) -> &[u8] {
 
 #[test]
 fn test_pybytes_bytes_conversion() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let f = wrap_pyfunction!(bytes_pybytes_conversion)(py).unwrap();
         py_assert!(py, f, "f(b'Hello World') == b'Hello World'");
     });
@@ -26,7 +26,7 @@ fn bytes_vec_conversion(py: Python<'_>, bytes: Vec) -> Bound<'_, PyBytes> {
 
 #[test]
 fn test_pybytes_vec_conversion() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let f = wrap_pyfunction!(bytes_vec_conversion)(py).unwrap();
         py_assert!(py, f, "f(b'Hello World') == b'Hello World'");
     });
@@ -34,7 +34,7 @@ fn test_pybytes_vec_conversion() {
 
 #[test]
 fn test_bytearray_vec_conversion() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let f = wrap_pyfunction!(bytes_vec_conversion)(py).unwrap();
         py_assert!(py, f, "f(bytearray(b'Hello World')) == b'Hello World'");
     });
@@ -43,11 +43,11 @@ fn test_bytearray_vec_conversion() {
 #[test]
 fn test_py_as_bytes() {
     let pyobj: pyo3::Py =
-        Python::with_gil(|py| pyo3::types::PyBytes::new(py, b"abc").unbind());
+        Python::attach(|py| pyo3::types::PyBytes::new(py, b"abc").unbind());
 
-    let data = Python::with_gil(|py| pyobj.as_bytes(py));
+    let data = Python::attach(|py| pyobj.as_bytes(py));
 
     assert_eq!(data, b"abc");
 
-    Python::with_gil(move |_py| drop(pyobj));
+    Python::attach(move |_py| drop(pyobj));
 }
diff --git a/tests/test_class_attributes.rs b/tests/test_class_attributes.rs
index 5c4398e7a56..97a3cc75dd9 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| {
+    Python::attach(|py| {
         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'");
@@ -83,7 +83,7 @@ fn class_attributes_mutable() {
         }
     }
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let obj = py.get_type::();
         py_run!(py, obj, "obj.MY_CONST = 'BAZ'");
         py_run!(py, obj, "obj.a = 42");
@@ -119,7 +119,7 @@ fn immutable_type_object() {
         Variant(u32),
     }
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let obj = py.get_type::();
         py_expect_exception!(py, obj, "obj.MY_CONST = 'FOOBAR'", PyTypeError);
         py_expect_exception!(py, obj, "obj.a = 6", PyTypeError);
@@ -142,7 +142,7 @@ impl Bar {
 
 #[test]
 fn recursive_class_attributes() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let foo_obj = py.get_type::();
         let bar_obj = py.get_type::();
         py_assert!(py, foo_obj, "foo_obj.a_foo.x == 1");
@@ -198,7 +198,7 @@ fn test_fallible_class_attribute() {
         }
     }
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let stderr = CaptureStdErr::new(py).unwrap();
         assert!(std::panic::catch_unwind(|| py.get_type::()).is_err());
         assert_eq!(
@@ -241,7 +241,7 @@ impl StructWithRenamedFields {
 fn test_renaming_all_struct_fields() {
     use pyo3::types::PyBool;
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let struct_class = py.get_type::();
         let struct_obj = struct_class.call0().unwrap();
         assert!(struct_obj
@@ -274,7 +274,7 @@ macro_rules! test_case {
         fn $test_name() {
             //use pyo3::types::PyInt;
 
-            Python::with_gil(|py| {
+            Python::attach(|py| {
                 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());
diff --git a/tests/test_class_basics.rs b/tests/test_class_basics.rs
index 1c579b7307a..8c3efe07aa5 100644
--- a/tests/test_class_basics.rs
+++ b/tests/test_class_basics.rs
@@ -12,7 +12,7 @@ struct EmptyClass {}
 
 #[test]
 fn empty_class() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let typeobj = py.get_type::();
         // By default, don't allow creating instances from python.
         assert!(typeobj.call((), None).is_err());
@@ -26,7 +26,7 @@ struct UnitClass;
 
 #[test]
 fn unit_class() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let typeobj = py.get_type::();
         // By default, don't allow creating instances from python.
         assert!(typeobj.call((), None).is_err());
@@ -57,7 +57,7 @@ struct ClassWithDocs {
 
 #[test]
 fn class_with_docstr() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let typeobj = py.get_type::();
         py_run!(
             py,
@@ -103,7 +103,7 @@ impl EmptyClass2 {
 
 #[test]
 fn custom_names() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let typeobj = py.get_type::();
         py_assert!(py, typeobj, "typeobj.__name__ == 'CustomName'");
         py_assert!(py, typeobj, "typeobj.custom_fn.__name__ == 'custom_fn'");
@@ -141,7 +141,7 @@ impl ClassRustKeywords {
 
 #[test]
 fn keyword_names() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let typeobj = py.get_type::();
         py_assert!(py, typeobj, "typeobj.__name__ == 'loop'");
         py_assert!(py, typeobj, "typeobj.struct.__name__ == 'struct'");
@@ -166,7 +166,7 @@ impl RawIdents {
 
 #[test]
 fn test_raw_idents() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let typeobj = py.get_type::();
         py_assert!(py, typeobj, "not hasattr(typeobj, 'r#fn')");
         py_assert!(py, typeobj, "hasattr(typeobj, 'fn')");
@@ -183,7 +183,7 @@ struct EmptyClassInModule {}
 #[test]
 #[ignore]
 fn empty_class_in_module() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let module = PyModule::new(py, "test_module.nested").unwrap();
         module.add_class::().unwrap();
 
@@ -220,7 +220,7 @@ impl ClassWithObjectField {
 
 #[test]
 fn class_with_object_field() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let ty = py.get_type::();
         py_assert!(py, ty, "ty(5).value == 5");
         py_assert!(py, ty, "ty(None).value == None");
@@ -235,7 +235,7 @@ struct ClassWithHash {
 
 #[test]
 fn class_with_hash() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         use pyo3::types::IntoPyDict;
         let class = ClassWithHash { value: 42 };
         let hash = {
@@ -288,7 +288,7 @@ impl UnsendableChild {
 }
 
 fn test_unsendable() -> PyResult<()> {
-    let (keep_obj_here, obj) = Python::with_gil(|py| -> PyResult<_> {
+    let (keep_obj_here, obj) = Python::attach(|py| -> PyResult<_> {
         let obj: Py = PyType::new::(py).call1((5,))?.extract()?;
 
         // Accessing the value inside this thread should not panic
@@ -306,13 +306,13 @@ fn test_unsendable() -> PyResult<()> {
 
     let caught_panic = std::thread::spawn(move || {
         // This access must panic
-        Python::with_gil(move |py| {
+        Python::attach(move |py| {
             obj.borrow(py);
         });
     })
     .join();
 
-    Python::with_gil(|_py| drop(keep_obj_here));
+    Python::attach(|_py| drop(keep_obj_here));
 
     if let Err(err) = caught_panic {
         if let Some(msg) = err.downcast_ref::() {
@@ -380,7 +380,7 @@ impl ClassWithFromPyWithMethods {
 
 #[test]
 fn test_pymethods_from_py_with() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let instance = Py::new(py, ClassWithFromPyWithMethods {}).unwrap();
 
         py_run!(
@@ -405,7 +405,7 @@ struct TupleClass(#[pyo3(get, set, name = "value")] i32);
 
 #[test]
 fn test_tuple_struct_class() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let typeobj = py.get_type::();
         assert!(typeobj.call((), None).is_err());
 
@@ -436,7 +436,7 @@ struct DunderDictSupport {
 #[test]
 #[cfg(any(Py_3_9, not(Py_LIMITED_API)))]
 fn dunder_dict_support() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let inst = Py::new(
             py,
             DunderDictSupport {
@@ -458,7 +458,7 @@ fn dunder_dict_support() {
 #[test]
 #[cfg(any(Py_3_9, not(Py_LIMITED_API)))]
 fn access_dunder_dict() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let inst = Py::new(
             py,
             DunderDictSupport {
@@ -487,7 +487,7 @@ struct InheritDict {
 #[test]
 #[cfg(any(Py_3_9, not(Py_LIMITED_API)))]
 fn inherited_dict() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let inst = Py::new(
             py,
             (
@@ -519,7 +519,7 @@ struct WeakRefDunderDictSupport {
 #[test]
 #[cfg(any(Py_3_9, not(Py_LIMITED_API)))]
 fn weakref_dunder_dict_support() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let inst = Py::new(
             py,
             WeakRefDunderDictSupport {
@@ -544,7 +544,7 @@ struct WeakRefSupport {
 #[test]
 #[cfg(any(Py_3_9, not(Py_LIMITED_API)))]
 fn weakref_support() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let inst = Py::new(
             py,
             WeakRefSupport {
@@ -570,7 +570,7 @@ struct InheritWeakRef {
 #[test]
 #[cfg(any(Py_3_9, not(Py_LIMITED_API)))]
 fn inherited_weakref() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let inst = Py::new(
             py,
             (
@@ -598,7 +598,7 @@ fn access_frozen_class_without_gil() {
         value: AtomicUsize,
     }
 
-    let py_counter: Py = Python::with_gil(|py| {
+    let py_counter: Py = Python::attach(|py| {
         let counter = FrozenCounter {
             value: AtomicUsize::new(0),
         };
@@ -612,7 +612,7 @@ fn access_frozen_class_without_gil() {
 
     assert_eq!(py_counter.get().value.load(Ordering::Relaxed), 1);
 
-    Python::with_gil(move |_py| drop(py_counter));
+    Python::attach(move |_py| drop(py_counter));
 }
 
 #[test]
@@ -637,7 +637,7 @@ fn drop_unsendable_elsewhere() {
         }
     }
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let capture = UnraisableCapture::install(py);
 
         let dropped = Arc::new(AtomicBool::new(false));
@@ -652,7 +652,7 @@ fn drop_unsendable_elsewhere() {
 
         py.allow_threads(|| {
             spawn(move || {
-                Python::with_gil(move |_py| {
+                Python::attach(move |_py| {
                     drop(unsendable);
                 });
             })
@@ -684,7 +684,7 @@ fn test_unsendable_dict() {
         }
     }
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let inst = Py::new(py, UnsendableDictClass {}).unwrap();
         py_run!(py, inst, "assert inst.__dict__ == {}");
     });
@@ -704,7 +704,7 @@ fn test_unsendable_dict_with_weakref() {
         }
     }
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let inst = Py::new(py, UnsendableDictClassWithWeakRef {}).unwrap();
         py_run!(py, inst, "assert inst.__dict__ == {}");
         py_run!(
@@ -734,7 +734,7 @@ impl ClassWithRuntimeParametrization {
 #[test]
 #[cfg(Py_3_9)]
 fn test_runtime_parametrization() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let ty = py.get_type::();
         py_assert!(py, ty, "ty[int] == ty.__class_getitem__((int,))");
         py_run!(
diff --git a/tests/test_class_comparisons.rs b/tests/test_class_comparisons.rs
index 75bd6251aac..2f716ca431a 100644
--- a/tests/test_class_comparisons.rs
+++ b/tests/test_class_comparisons.rs
@@ -21,7 +21,7 @@ pub enum MyEnumOrd {
 
 #[test]
 fn test_enum_eq_enum() {
-    Python::with_gil(|py| {
+    Python::attach(|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();
@@ -33,7 +33,7 @@ fn test_enum_eq_enum() {
 
 #[test]
 fn test_enum_eq_incomparable() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let var1 = Py::new(py, MyEnum::Variant).unwrap();
         py_assert!(py, var1, "(var1 == 'foo') == False");
         py_assert!(py, var1, "(var1 != 'foo') == True");
@@ -42,7 +42,7 @@ fn test_enum_eq_incomparable() {
 
 #[test]
 fn test_enum_ord_comparable_opt_in_only() {
-    Python::with_gil(|py| {
+    Python::attach(|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
@@ -52,7 +52,7 @@ fn test_enum_ord_comparable_opt_in_only() {
 
 #[test]
 fn test_simple_enum_ord_comparable() {
-    Python::with_gil(|py| {
+    Python::attach(|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();
@@ -80,7 +80,7 @@ pub enum MyComplexEnumOrd2 {
 
 #[test]
 fn test_complex_enum_ord_comparable() {
-    Python::with_gil(|py| {
+    Python::attach(|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();
@@ -156,7 +156,7 @@ pub struct Point {
 
 #[test]
 fn test_struct_numeric_ord_comparable() {
-    Python::with_gil(|py| {
+    Python::attach(|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();
@@ -180,7 +180,7 @@ pub struct Person {
 
 #[test]
 fn test_struct_string_ord_comparable() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let var1 = Py::new(
             py,
             Person {
@@ -239,7 +239,7 @@ impl PartialOrd for Record {
 
 #[test]
 fn test_struct_custom_ord_comparable() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let var1 = Py::new(
             py,
             Record {
diff --git a/tests/test_class_conversion.rs b/tests/test_class_conversion.rs
index a1e2188e83a..c451a37ac35 100644
--- a/tests/test_class_conversion.rs
+++ b/tests/test_class_conversion.rs
@@ -16,7 +16,7 @@ struct Cloneable {
 fn test_cloneable_pyclass() {
     let c = Cloneable { x: 10 };
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let py_c = Py::new(py, c.clone()).unwrap();
 
         let c2: Cloneable = py_c.extract(py).unwrap();
@@ -62,7 +62,7 @@ struct PolymorphicContainer {
 
 #[test]
 fn test_polymorphic_container_stores_base_class() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let p = Py::new(
             py,
             PolymorphicContainer {
@@ -77,7 +77,7 @@ fn test_polymorphic_container_stores_base_class() {
 
 #[test]
 fn test_polymorphic_container_stores_sub_class() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let p = Py::new(
             py,
             PolymorphicContainer {
@@ -103,7 +103,7 @@ fn test_polymorphic_container_stores_sub_class() {
 
 #[test]
 fn test_polymorphic_container_does_not_accept_other_types() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let p = Py::new(
             py,
             PolymorphicContainer {
@@ -122,7 +122,7 @@ fn test_polymorphic_container_does_not_accept_other_types() {
 
 #[test]
 fn test_pyref_as_base() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let cell = Bound::new(py, (SubClass {}, BaseClass { value: 120 })).unwrap();
 
         // First try PyRefMut
@@ -142,7 +142,7 @@ fn test_pyref_as_base() {
 
 #[test]
 fn test_pycell_deref() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let obj = Bound::new(py, (SubClass {}, BaseClass { value: 120 })).unwrap();
 
         // Should be able to deref as PyAny
diff --git a/tests/test_class_formatting.rs b/tests/test_class_formatting.rs
index 353f2abf7ed..976e6da4e5e 100644
--- a/tests/test_class_formatting.rs
+++ b/tests/test_class_formatting.rs
@@ -40,7 +40,7 @@ impl Display for MyEnum3 {
 
 #[test]
 fn test_enum_class_fmt() {
-    Python::with_gil(|py| {
+    Python::attach(|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();
@@ -60,7 +60,7 @@ pub struct Point {
 
 #[test]
 fn test_custom_struct_custom_str() {
-    Python::with_gil(|py| {
+    Python::attach(|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'");
     })
@@ -82,7 +82,7 @@ impl Display for Point2 {
 
 #[test]
 fn test_struct_str() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let var1 = Py::new(py, Point2 { x: 1, y: 2, z: 3 }).unwrap();
         py_assert!(py, var1, "str(var1) == '(1, 2, 3)'");
     })
@@ -103,7 +103,7 @@ impl Display for ComplexEnumWithStr {
 
 #[test]
 fn test_custom_complex_enum_str() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let var1 = Py::new(py, ComplexEnumWithStr::A(45)).unwrap();
         let var2 = Py::new(
             py,
@@ -127,7 +127,7 @@ struct Coord2(u32, u32, u32);
 
 #[test]
 fn test_str_representation_by_position() {
-    Python::with_gil(|py| {
+    Python::attach(|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'");
@@ -145,7 +145,7 @@ struct Point4 {
 
 #[test]
 fn test_mixed_and_repeated_str_formats() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let var1 = Py::new(
             py,
             Point4 {
@@ -172,7 +172,7 @@ struct Foo {
 
 #[test]
 fn test_raw_identifier_struct_custom_str() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let var1 = Py::new(py, Foo { r#type: 3 }).unwrap();
         py_assert!(py, var1, "str(var1) == 'type: 3'");
     })
diff --git a/tests/test_class_new.rs b/tests/test_class_new.rs
index 75070bd274b..37a1044b8c5 100644
--- a/tests/test_class_new.rs
+++ b/tests/test_class_new.rs
@@ -18,7 +18,7 @@ impl EmptyClassWithNew {
 
 #[test]
 fn empty_class_with_new() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let typeobj = py.get_type::();
         assert!(typeobj
             .call((), None)
@@ -47,7 +47,7 @@ impl UnitClassWithNew {
 
 #[test]
 fn unit_class_with_new() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let typeobj = py.get_type::();
         assert!(typeobj
             .call((), None)
@@ -70,7 +70,7 @@ impl TupleClassWithNew {
 
 #[test]
 fn tuple_class_with_new() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let typeobj = py.get_type::();
         let wrp = typeobj.call((42,), None).unwrap();
         let obj = wrp.downcast::().unwrap();
@@ -95,7 +95,7 @@ impl NewWithOneArg {
 
 #[test]
 fn new_with_one_arg() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let typeobj = py.get_type::();
         let wrp = typeobj.call((42,), None).unwrap();
         let obj = wrp.downcast::().unwrap();
@@ -123,7 +123,7 @@ impl NewWithTwoArgs {
 
 #[test]
 fn new_with_two_args() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let typeobj = py.get_type::();
         let wrp = typeobj
             .call((10, 20), None)
@@ -154,7 +154,7 @@ impl SuperClass {
 /// See https://github.com/PyO3/pyo3/issues/947 for the corresponding bug.
 #[test]
 fn subclass_new() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let super_cls = py.get_type::();
         let source = pyo3_ffi::c_str!(pyo3::indoc::indoc!(
             r#"
@@ -199,7 +199,7 @@ impl NewWithCustomError {
 
 #[test]
 fn new_with_custom_error() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let typeobj = py.get_type::();
         let err = typeobj.call0().unwrap_err();
         assert_eq!(err.to_string(), "ValueError: custom error");
@@ -234,7 +234,7 @@ impl NewExisting {
 
 #[test]
 fn test_new_existing() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let typeobj = py.get_type::();
 
         let obj1 = typeobj.call1((0,)).unwrap();
@@ -272,7 +272,7 @@ impl NewReturnsPy {
 
 #[test]
 fn test_new_returns_py() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let type_ = py.get_type::();
         let obj = type_.call0().unwrap();
         assert!(obj.is_exact_instance_of::());
@@ -292,7 +292,7 @@ impl NewReturnsBound {
 
 #[test]
 fn test_new_returns_bound() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let type_ = py.get_type::();
         let obj = type_.call0().unwrap();
         assert!(obj.is_exact_instance_of::());
@@ -318,7 +318,7 @@ impl NewClassMethod {
 
 #[test]
 fn test_new_class_method() {
-    pyo3::Python::with_gil(|py| {
+    pyo3::Python::attach(|py| {
         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 6c9c17bd69c..868caf7e69b 100644
--- a/tests/test_coroutine.rs
+++ b/tests/test_coroutine.rs
@@ -32,7 +32,7 @@ fn noop_coroutine() {
     async fn noop() -> usize {
         42
     }
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let noop = wrap_pyfunction!(noop, py).unwrap();
         let test = "import asyncio; assert asyncio.run(noop()) == 42";
         py_run!(py, noop, &handle_windows(test));
@@ -58,7 +58,7 @@ fn test_coroutine_qualname() {
         #[staticmethod]
         async fn my_staticmethod() {}
     }
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let test = r#"
         for coro, name, qualname in [
             (my_fn(), "my_fn", "my_fn"),
@@ -93,7 +93,7 @@ fn sleep_0_like_coroutine() {
         })
         .await
     }
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let sleep_0 = wrap_pyfunction!(sleep_0, py).unwrap();
         let test = "import asyncio; assert asyncio.run(sleep_0()) == 42";
         py_run!(py, sleep_0, &handle_windows(test));
@@ -112,7 +112,7 @@ async fn sleep(seconds: f64) -> usize {
 
 #[test]
 fn sleep_coroutine() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let sleep = wrap_pyfunction!(sleep, py).unwrap();
         let test = r#"import asyncio; assert asyncio.run(sleep(0.1)) == 42"#;
         py_run!(py, sleep, &handle_windows(test));
@@ -126,7 +126,7 @@ async fn return_tuple() -> (usize, usize) {
 
 #[test]
 fn tuple_coroutine() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let func = wrap_pyfunction!(return_tuple, py).unwrap();
         let test = r#"import asyncio; assert asyncio.run(func()) == (42, 43)"#;
         py_run!(py, func, &handle_windows(test));
@@ -135,7 +135,7 @@ fn tuple_coroutine() {
 
 #[test]
 fn cancelled_coroutine() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let sleep = wrap_pyfunction!(sleep, py).unwrap();
         let test = r#"
         import asyncio
@@ -174,7 +174,7 @@ fn coroutine_cancel_handle() {
             _ = cancel.cancelled().fuse() => 0,
         }
     }
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let cancellable_sleep = wrap_pyfunction!(cancellable_sleep, py).unwrap();
         let test = r#"
         import asyncio;
@@ -206,7 +206,7 @@ fn coroutine_is_cancelled() {
             sleep(0.001).await;
         }
     }
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let sleep_loop = wrap_pyfunction!(sleep_loop, py).unwrap();
         let test = r#"
         import asyncio;
@@ -234,7 +234,7 @@ fn coroutine_panic() {
     async fn panic() {
         panic!("test panic");
     }
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let panic = wrap_pyfunction!(panic, py).unwrap();
         let test = r#"
         import asyncio
@@ -284,7 +284,7 @@ fn test_async_method_receiver() {
         }
     }
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let test = r#"
         import asyncio
 
@@ -342,7 +342,7 @@ fn test_async_method_receiver_with_other_args() {
         }
     }
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let test = r#"
         import asyncio
 
diff --git a/tests/test_datetime.rs b/tests/test_datetime.rs
index 55b4d1bfdfb..b24b94397d6 100644
--- a/tests/test_datetime.rs
+++ b/tests/test_datetime.rs
@@ -70,7 +70,7 @@ macro_rules! assert_check_only {
 
 #[test]
 fn test_date_check() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let (obj, sub_obj, sub_sub_obj) = _get_subclasses(py, "date", "2018, 1, 1").unwrap();
         unsafe { PyDateTime_IMPORT() }
         assert_check_exact!(PyDate_Check, PyDate_CheckExact, obj);
@@ -84,7 +84,7 @@ fn test_date_check() {
 
 #[test]
 fn test_time_check() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let (obj, sub_obj, sub_sub_obj) = _get_subclasses(py, "time", "12, 30, 15").unwrap();
         unsafe { PyDateTime_IMPORT() }
 
@@ -99,7 +99,7 @@ fn test_time_check() {
 
 #[test]
 fn test_datetime_check() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let (obj, sub_obj, sub_sub_obj) = _get_subclasses(py, "datetime", "2018, 1, 1, 13, 30, 15")
             .map_err(|e| e.display(py))
             .unwrap();
@@ -117,7 +117,7 @@ fn test_datetime_check() {
 
 #[test]
 fn test_delta_check() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let (obj, sub_obj, sub_sub_obj) = _get_subclasses(py, "timedelta", "1, -3").unwrap();
         unsafe { PyDateTime_IMPORT() }
 
@@ -132,7 +132,7 @@ fn test_datetime_utc() {
     use assert_approx_eq::assert_approx_eq;
     use pyo3::types::PyDateTime;
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let utc = PyTzInfo::utc(py).unwrap();
 
         let dt = PyDateTime::new(py, 2018, 1, 1, 0, 0, 0, 0, Some(&utc)).unwrap();
@@ -171,7 +171,7 @@ static INVALID_TIMES: &[(u8, u8, u8, u32)] =
 fn test_pydate_out_of_bounds() {
     use pyo3::types::PyDate;
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         for val in INVALID_DATES {
             let (year, month, day) = val;
             let dt = PyDate::new(py, *year, *month, *day);
@@ -184,7 +184,7 @@ fn test_pydate_out_of_bounds() {
 fn test_pytime_out_of_bounds() {
     use pyo3::types::PyTime;
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         for val in INVALID_TIMES {
             let (hour, minute, second, microsecond) = val;
             let dt = PyTime::new(py, *hour, *minute, *second, *microsecond, None);
@@ -198,7 +198,7 @@ fn test_pydatetime_out_of_bounds() {
     use pyo3::types::PyDateTime;
     use std::iter;
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let valid_time = (0, 0, 0, 0);
         let valid_date = (2018, 1, 1);
 
diff --git a/tests/test_datetime_import.rs b/tests/test_datetime_import.rs
index 295f7142c9d..92298504fa3 100644
--- a/tests/test_datetime_import.rs
+++ b/tests/test_datetime_import.rs
@@ -14,7 +14,7 @@ fn test_bad_datetime_module_panic() {
         .unwrap();
     std::fs::File::create(tmpdir.path().join("datetime.py")).unwrap();
 
-    Python::with_gil(|py: Python<'_>| {
+    Python::attach(|py: Python<'_>| {
         let sys = py.import("sys").unwrap();
         sys.getattr("path")
             .unwrap()
diff --git a/tests/test_declarative_module.rs b/tests/test_declarative_module.rs
index 1f2ff1f29b6..f3a01001952 100644
--- a/tests/test_declarative_module.rs
+++ b/tests/test_declarative_module.rs
@@ -173,7 +173,7 @@ fn declarative_module(py: Python<'_>) -> &Bound<'_, PyModule> {
 
 #[test]
 fn test_declarative_module() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let m = declarative_module(py);
         py_assert!(
             py,
@@ -216,7 +216,7 @@ mod r#type {
 
 #[test]
 fn test_raw_ident_module() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let m = pyo3::wrap_pymodule!(r#type)(py).into_bound(py);
         py_assert!(py, m, "m.double(2) == 4");
     })
@@ -224,7 +224,7 @@ fn test_raw_ident_module() {
 
 #[test]
 fn test_module_names() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let m = declarative_module(py);
         py_assert!(
             py,
@@ -248,7 +248,7 @@ fn test_module_names() {
 
 #[test]
 fn test_inner_module_full_path() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let m = declarative_module(py);
         py_assert!(py, m, "m.full_path_inner");
     })
diff --git a/tests/test_default_impls.rs b/tests/test_default_impls.rs
index 670772369f8..e5735f5dd2a 100644
--- a/tests/test_default_impls.rs
+++ b/tests/test_default_impls.rs
@@ -14,7 +14,7 @@ enum TestDefaultRepr {
 
 #[test]
 fn test_default_slot_exists() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let test_object = Py::new(py, TestDefaultRepr::Var).unwrap();
         py_assert!(
             py,
@@ -39,7 +39,7 @@ impl OverrideSlot {
 
 #[test]
 fn test_override_slot() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let test_object = Py::new(py, OverrideSlot::Var).unwrap();
         py_assert!(py, test_object, "repr(test_object) == 'overridden'");
     })
diff --git a/tests/test_enum.rs b/tests/test_enum.rs
index 1421856e63d..2e588f9266a 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| {
+    Python::attach(|py| {
         let my_enum = py.get_type::();
         let var = Py::new(py, MyEnum::Variant).unwrap();
         py_assert!(py, my_enum var, "my_enum.Variant == var");
@@ -25,7 +25,7 @@ fn test_enum_class_attr() {
 
 #[test]
 fn test_enum_eq_enum() {
-    Python::with_gil(|py| {
+    Python::attach(|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();
@@ -37,7 +37,7 @@ fn test_enum_eq_enum() {
 
 #[test]
 fn test_enum_eq_incomparable() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let var1 = Py::new(py, MyEnum::Variant).unwrap();
         py_assert!(py, var1, "(var1 == 'foo') == False");
         py_assert!(py, var1, "(var1 != 'foo') == True");
@@ -51,7 +51,7 @@ fn return_enum() -> MyEnum {
 
 #[test]
 fn test_return_enum() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let f = wrap_pyfunction!(return_enum)(py).unwrap();
         let mynum = py.get_type::();
 
@@ -66,7 +66,7 @@ fn enum_arg(e: MyEnum) {
 
 #[test]
 fn test_enum_arg() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let f = wrap_pyfunction!(enum_arg)(py).unwrap();
         let mynum = py.get_type::();
 
@@ -83,7 +83,7 @@ enum CustomDiscriminant {
 
 #[test]
 fn test_custom_discriminant() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         #[allow(non_snake_case)]
         let CustomDiscriminant = py.get_type::();
         let one = Py::new(py, CustomDiscriminant::One).unwrap();
@@ -102,7 +102,7 @@ fn test_custom_discriminant() {
 
 #[test]
 fn test_enum_to_int() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let one = Py::new(py, CustomDiscriminant::One).unwrap();
         py_assert!(py, one, "int(one) == 1");
         let v = Py::new(py, MyEnum::Variant).unwrap();
@@ -113,7 +113,7 @@ fn test_enum_to_int() {
 
 #[test]
 fn test_enum_compare_int() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let one = Py::new(py, CustomDiscriminant::One).unwrap();
         py_run!(
             py,
@@ -136,7 +136,7 @@ enum SmallEnum {
 
 #[test]
 fn test_enum_compare_int_no_throw_when_overflow() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let v = Py::new(py, SmallEnum::V).unwrap();
         py_assert!(py, v, "v != 1<<30")
     })
@@ -152,7 +152,7 @@ enum BigEnum {
 
 #[test]
 fn test_big_enum_no_overflow() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let usize_max = usize::MAX;
         let v = Py::new(py, BigEnum::V).unwrap();
 
@@ -181,7 +181,7 @@ pub enum RenameEnum {
 
 #[test]
 fn test_rename_enum_repr_correct() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let var1 = Py::new(py, RenameEnum::Variant).unwrap();
         py_assert!(py, var1, "repr(var1) == 'MyEnum.Variant'");
     })
@@ -196,7 +196,7 @@ pub enum RenameVariantEnum {
 
 #[test]
 fn test_rename_variant_repr_correct() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let var1 = Py::new(py, RenameVariantEnum::Variant).unwrap();
         py_assert!(py, var1, "repr(var1) == 'RenameVariantEnum.VARIANT'");
     })
@@ -214,7 +214,7 @@ enum RenameAllVariantsEnum {
 
 #[test]
 fn test_renaming_all_enum_variants() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         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");
@@ -235,7 +235,7 @@ enum CustomModuleComplexEnum {
 
 #[test]
 fn test_custom_module() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let enum_obj = py.get_type::();
         py_assert!(
             py,
@@ -254,7 +254,7 @@ pub enum EqOnly {
 
 #[test]
 fn test_simple_enum_eq_only() {
-    Python::with_gil(|py| {
+    Python::attach(|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();
@@ -272,7 +272,7 @@ enum SimpleEnumWithHash {
 
 #[test]
 fn test_simple_enum_with_hash() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         use pyo3::types::IntoPyDict;
         let class = SimpleEnumWithHash::A;
         let hash = {
@@ -302,7 +302,7 @@ enum ComplexEnumWithHash {
 
 #[test]
 fn test_complex_enum_with_hash() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         use pyo3::types::IntoPyDict;
         let class = ComplexEnumWithHash::B {
             msg: String::from("Hello"),
@@ -354,7 +354,7 @@ fn custom_eq() {
         }
     }
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let a = Bound::new(py, CustomPyEq::A).unwrap();
         let b = Bound::new(py, CustomPyEq::B).unwrap();
 
@@ -379,7 +379,7 @@ pub enum ComplexEnumWithRaw {
 // Cover simple field lookups with raw identifiers
 #[test]
 fn complex_enum_with_raw() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let complex = ComplexEnumWithRaw::Raw { r#type: 314159 };
 
         py_assert!(py, complex, "complex.type == 314159");
@@ -390,7 +390,7 @@ fn complex_enum_with_raw() {
 #[test]
 #[cfg(Py_3_10)]
 fn complex_enum_with_raw_pattern_match() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let complex = ComplexEnumWithRaw::Raw { r#type: 314159 };
         let cls = py.get_type::();
 
diff --git a/tests/test_exceptions.rs b/tests/test_exceptions.rs
index af21b2c33a8..081041ed12d 100644
--- a/tests/test_exceptions.rs
+++ b/tests/test_exceptions.rs
@@ -21,7 +21,7 @@ fn fail_to_open_file() -> PyResult<()> {
 #[cfg_attr(target_arch = "wasm32", ignore)] // Not sure why this fails.
 #[cfg(not(target_os = "windows"))]
 fn test_filenotfounderror() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let fail_to_open_file = wrap_pyfunction!(fail_to_open_file)(py).unwrap();
 
         py_run!(
@@ -66,7 +66,7 @@ fn call_fail_with_custom_error() -> PyResult<()> {
 
 #[test]
 fn test_custom_error() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let call_fail_with_custom_error =
             wrap_pyfunction!(call_fail_with_custom_error)(py).unwrap();
 
@@ -105,7 +105,7 @@ fn test_write_unraisable() {
     use pyo3::{exceptions::PyRuntimeError, ffi, types::PyNotImplemented};
     use std::ptr;
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let capture = UnraisableCapture::install(py);
 
         assert!(capture.borrow(py).capture.is_none());
diff --git a/tests/test_field_cfg.rs b/tests/test_field_cfg.rs
index 1766cadb3d5..a684772fb1e 100644
--- a/tests/test_field_cfg.rs
+++ b/tests/test_field_cfg.rs
@@ -28,7 +28,7 @@ enum CfgSimpleEnum {
 
 #[test]
 fn test_cfg() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let cfg = CfgClass { b: 3 };
         let py_cfg = Py::new(py, cfg).unwrap();
         assert!(py_cfg.bind(py).getattr("a").is_err());
@@ -39,7 +39,7 @@ fn test_cfg() {
 
 #[test]
 fn test_cfg_simple_enum() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let simple = py.get_type::();
         pyo3::py_run!(
             py,
diff --git a/tests/test_frompy_intopy_roundtrip.rs b/tests/test_frompy_intopy_roundtrip.rs
index 58d4053e472..35501447422 100644
--- a/tests/test_frompy_intopy_roundtrip.rs
+++ b/tests/test_frompy_intopy_roundtrip.rs
@@ -21,7 +21,7 @@ pub struct A<'py> {
 
 #[test]
 fn test_named_fields_struct() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let a = A {
             s: "Hello".into(),
             t: PyString::new(py, "World"),
@@ -57,7 +57,7 @@ pub struct B {
 
 #[test]
 fn test_transparent_named_field_struct() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let b = B {
             test: "test".into(),
         };
@@ -79,7 +79,7 @@ pub struct D {
 
 #[test]
 fn test_generic_transparent_named_field_struct() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let d = D {
             test: String::from("test"),
         };
@@ -111,7 +111,7 @@ pub struct GenericWithBound(HashMap);
 
 #[test]
 fn test_generic_with_bound() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let mut hash_map = HashMap::::new();
         hash_map.insert("1".into(), 1);
         hash_map.insert("2".into(), 2);
@@ -167,7 +167,7 @@ pub struct Tuple(String, usize);
 
 #[test]
 fn test_tuple_struct() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let tup = Tuple(String::from("test"), 1);
         let tuple = (&tup).into_pyobject(py).unwrap();
         let new_tup = tuple.extract::().unwrap();
@@ -184,7 +184,7 @@ pub struct TransparentTuple(String);
 
 #[test]
 fn test_transparent_tuple_struct() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let tup = TransparentTuple(String::from("test"));
         let tuple = (&tup).into_pyobject(py).unwrap();
         let new_tup = tuple.extract::().unwrap();
@@ -234,7 +234,7 @@ pub enum Foo {
 
 #[test]
 fn test_enum() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let tuple_var = Foo::TupleVar(1, "test".into());
         let foo = (&tuple_var).into_pyobject(py).unwrap();
         assert_eq!(tuple_var, foo.extract::().unwrap());
diff --git a/tests/test_frompyobject.rs b/tests/test_frompyobject.rs
index 7e5110d7e5a..35cc813a603 100644
--- a/tests/test_frompyobject.rs
+++ b/tests/test_frompyobject.rs
@@ -52,7 +52,7 @@ impl PyA {
 
 #[test]
 fn test_named_fields_struct() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let pya = PyA {
             s: "foo".into(),
             foo: None,
@@ -75,7 +75,7 @@ pub struct B {
 
 #[test]
 fn test_transparent_named_field_struct() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let test = "test".into_pyobject(py).unwrap();
         let b = test
             .extract::()
@@ -95,7 +95,7 @@ pub struct D {
 
 #[test]
 fn test_generic_transparent_named_field_struct() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let test = "test".into_pyobject(py).unwrap();
         let d = test
             .extract::>()
@@ -114,7 +114,7 @@ pub struct GenericWithBound(std::collections::HashMa
 
 #[test]
 fn test_generic_with_bound() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let dict = [("1", 1), ("2", 2)].into_py_dict(py).unwrap();
         let map = dict.extract::>().unwrap().0;
         assert_eq!(map.len(), 2);
@@ -141,7 +141,7 @@ pub struct PyE {
 
 #[test]
 fn test_generic_named_fields_struct() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let pye = PyE {
             test: "test".into(),
             test2: 2,
@@ -167,7 +167,7 @@ pub struct C {
 
 #[test]
 fn test_named_field_with_ext_fn() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let pyc = PyE {
             test: "foo".into(),
             test2: 0,
@@ -184,7 +184,7 @@ pub struct Tuple(String, usize);
 
 #[test]
 fn test_tuple_struct() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let tup = PyTuple::new(
             py,
             &[
@@ -216,7 +216,7 @@ pub struct TransparentTuple(String);
 
 #[test]
 fn test_transparent_tuple_struct() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let tup = 1i32.into_pyobject(py).unwrap();
         let tup = tup.extract::();
         assert!(tup.is_err());
@@ -245,7 +245,7 @@ struct Baz {
 
 #[test]
 fn test_struct_nested_type_errors() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let pybaz = PyBaz {
             tup: ("test".into(), "test".into()),
             e: PyE {
@@ -268,7 +268,7 @@ fn test_struct_nested_type_errors() {
 
 #[test]
 fn test_struct_nested_type_errors_with_generics() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let pybaz = PyBaz {
             tup: ("test".into(), "test".into()),
             e: PyE {
@@ -291,7 +291,7 @@ fn test_struct_nested_type_errors_with_generics() {
 
 #[test]
 fn test_transparent_struct_error_message() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let tup = 1i32.into_pyobject(py).unwrap();
         let tup = tup.extract::();
         assert!(tup.is_err());
@@ -305,7 +305,7 @@ fn test_transparent_struct_error_message() {
 
 #[test]
 fn test_tuple_struct_error_message() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let tup = (1, "test").into_pyobject(py).unwrap();
         let tup = tup.extract::();
         assert!(tup.is_err());
@@ -319,7 +319,7 @@ fn test_tuple_struct_error_message() {
 
 #[test]
 fn test_transparent_tuple_error_message() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let tup = 1i32.into_pyobject(py).unwrap();
         let tup = tup.extract::();
         assert!(tup.is_err());
@@ -368,7 +368,7 @@ fn test_struct_rename_all() {
         custom_name: i32,
     }
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let RenameAll {
             some_field,
             other_field,
@@ -399,7 +399,7 @@ fn test_enum_rename_all() {
         },
     }
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let RenameAll::Foo {
             some_field,
             other_field,
@@ -450,7 +450,7 @@ pub struct PyBool {
 
 #[test]
 fn test_enum() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let tup = PyTuple::new(
             py,
             &[
@@ -534,7 +534,7 @@ fn test_enum() {
 
 #[test]
 fn test_enum_error() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let dict = PyDict::new(py);
         let err = dict.extract::>().unwrap_err();
         assert_eq!(
@@ -578,7 +578,7 @@ enum EnumWithCatchAll<'py> {
 
 #[test]
 fn test_enum_catch_all() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let dict = PyDict::new(py);
         let f = dict
             .extract::>()
@@ -605,7 +605,7 @@ pub enum Bar {
 
 #[test]
 fn test_err_rename() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let dict = PyDict::new(py);
         let f = dict.extract::();
         assert!(f.is_err());
@@ -631,7 +631,7 @@ pub struct Zap {
 
 #[test]
 fn test_from_py_with() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let py_zap = py
             .eval(
                 pyo3_ffi::c_str!(r#"{"name": "whatever", "my_object": [1, 2, 3]}"#),
@@ -655,7 +655,7 @@ pub struct ZapTuple(
 
 #[test]
 fn test_from_py_with_tuple_struct() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let py_zap = py
             .eval(pyo3_ffi::c_str!(r#"("whatever", [1, 2, 3])"#), None, None)
             .expect("failed to create tuple");
@@ -669,7 +669,7 @@ fn test_from_py_with_tuple_struct() {
 
 #[test]
 fn test_from_py_with_tuple_struct_error() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let py_zap = py
             .eval(
                 pyo3_ffi::c_str!(r#"("whatever", [1, 2, 3], "third")"#),
@@ -699,7 +699,7 @@ pub enum ZapEnum {
 
 #[test]
 fn test_from_py_with_enum() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let py_zap = py
             .eval(pyo3_ffi::c_str!(r#"("whatever", [1, 2, 3])"#), None, None)
             .expect("failed to create tuple");
@@ -720,7 +720,7 @@ pub struct TransparentFromPyWith {
 
 #[test]
 fn test_transparent_from_py_with() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let result = PyList::new(py, [1, 2, 3])
             .unwrap()
             .extract::()
@@ -744,7 +744,7 @@ pub struct WithKeywordAttrC {
 
 #[test]
 fn test_with_keyword_attr() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let cls = WithKeywordAttrC { r#box: 3 }.into_pyobject(py).unwrap();
         let result = cls.extract::().unwrap();
         let expected = WithKeywordAttr { r#box: 3 };
@@ -760,7 +760,7 @@ pub struct WithKeywordItem {
 
 #[test]
 fn test_with_keyword_item() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let dict = PyDict::new(py);
         dict.set_item("box", 3).unwrap();
         let result = dict.extract::().unwrap();
@@ -779,7 +779,7 @@ pub struct WithDefaultItem {
 
 #[test]
 fn test_with_default_item() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let dict = PyDict::new(py);
         dict.set_item("value", 3).unwrap();
         let result = dict.extract::().unwrap();
@@ -801,7 +801,7 @@ pub struct WithExplicitDefaultItem {
 
 #[test]
 fn test_with_explicit_default_item() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let dict = PyDict::new(py);
         dict.set_item("value", 3).unwrap();
         let result = dict.extract::().unwrap();
@@ -820,7 +820,7 @@ pub struct WithDefaultItemAndConversionFunction {
 
 #[test]
 fn test_with_default_item_and_conversion_function() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         // Filled case
         let dict = PyDict::new(py);
         dict.set_item("opt", (1,)).unwrap();
@@ -865,7 +865,7 @@ pub enum WithDefaultItemEnum {
 
 #[test]
 fn test_with_default_item_enum() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         // A and B filled
         let dict = PyDict::new(py);
         dict.set_item("a", 1).unwrap();
diff --git a/tests/test_gc.rs b/tests/test_gc.rs
index 76901fd1530..e8a608ff00c 100644
--- a/tests/test_gc.rs
+++ b/tests/test_gc.rs
@@ -21,7 +21,7 @@ struct ClassWithFreelist {}
 
 #[test]
 fn class_with_freelist() {
-    let ptr = Python::with_gil(|py| {
+    let ptr = Python::attach(|py| {
         let inst = Py::new(py, ClassWithFreelist {}).unwrap();
         let _inst2 = Py::new(py, ClassWithFreelist {}).unwrap();
         let ptr = inst.as_ptr();
@@ -29,7 +29,7 @@ fn class_with_freelist() {
         ptr
     });
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let inst3 = Py::new(py, ClassWithFreelist {}).unwrap();
         assert_eq!(ptr, inst3.as_ptr());
 
@@ -59,10 +59,10 @@ fn spin_freelist(py: Python<'_>, data: usize) {
 fn multithreaded_class_with_freelist() {
     std::thread::scope(|s| {
         s.spawn(|| {
-            Python::with_gil(|py| spin_freelist(py, 12));
+            Python::attach(|py| spin_freelist(py, 12));
         });
         s.spawn(|| {
-            Python::with_gil(|py| spin_freelist(py, 0x4d3d3d3));
+            Python::attach(|py| spin_freelist(py, 0x4d3d3d3));
         });
     });
 }
@@ -103,7 +103,7 @@ impl DropCheck {
                 return;
             }
 
-            Python::with_gil(|py| {
+            Python::attach(|py| {
                 py.run(ffi::c_str!("import gc; gc.collect()"), None, None)
                     .unwrap();
             });
@@ -134,7 +134,7 @@ fn data_is_dropped() {
     let (guard1, check1) = drop_check();
     let (guard2, check2) = drop_check();
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let data_is_dropped = DataIsDropped {
             _guard1: guard1,
             _guard2: guard2,
@@ -172,7 +172,7 @@ impl CycleWithClear {
 fn test_cycle_clear() {
     let (guard, check) = drop_check();
 
-    let ptr = Python::with_gil(|py| {
+    let ptr = Python::attach(|py| {
         let inst = Bound::new(
             py,
             CycleWithClear {
@@ -219,7 +219,7 @@ fn gc_null_traversal() {
         }
     }
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let obj = Py::new(
             py,
             GcNullTraversal {
@@ -270,7 +270,7 @@ fn inheritance_with_new_methods_with_drop() {
     let (guard_base, check_base) = drop_check();
     let (guard_sub, check_sub) = drop_check();
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let typeobj = py.get_type::();
         let inst = typeobj
             .call((), None)
@@ -315,7 +315,7 @@ fn gc_during_borrow() {
         }
     }
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         // get the traverse function
         let ty = py.get_type::();
         let traverse = unsafe { get_type_traverse(ty.as_type_ptr()).unwrap() };
@@ -360,7 +360,7 @@ fn traverse_partial() {
         }
     }
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         // get the traverse function
         let ty = py.get_type::();
         let traverse = unsafe { get_type_traverse(ty.as_type_ptr()).unwrap() };
@@ -395,7 +395,7 @@ fn traverse_panic() {
         }
     }
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         // get the traverse function
         let ty = py.get_type::();
         let traverse = unsafe { get_type_traverse(ty.as_type_ptr()).unwrap() };
@@ -417,11 +417,11 @@ fn tries_gil_in_traverse() {
     #[pymethods]
     impl TriesGILInTraverse {
         fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
-            Python::with_gil(|_py| Ok(()))
+            Python::attach(|_py| Ok(()))
         }
     }
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         // get the traverse function
         let ty = py.get_type::();
         let traverse = unsafe { get_type_traverse(ty.as_type_ptr()).unwrap() };
@@ -480,7 +480,7 @@ fn traverse_cannot_be_hijacked() {
         }
     }
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         // get the traverse function
         let ty = py.get_type::();
         let traverse = unsafe { get_type_traverse(ty.as_type_ptr()).unwrap() };
@@ -513,7 +513,7 @@ impl DropDuringTraversal {
 fn drop_during_traversal_with_gil() {
     let (guard, check) = drop_check();
 
-    let ptr = Python::with_gil(|py| {
+    let ptr = Python::attach(|py| {
         let cycle = Mutex::new(None);
         let inst = Py::new(
             py,
@@ -547,7 +547,7 @@ fn drop_during_traversal_with_gil() {
 fn drop_during_traversal_without_gil() {
     let (guard, check) = drop_check();
 
-    let inst = Python::with_gil(|py| {
+    let inst = Python::attach(|py| {
         let cycle = Mutex::new(None);
         let inst = Py::new(
             py,
@@ -594,7 +594,7 @@ fn unsendable_are_not_traversed_on_foreign_thread() {
 
     unsafe impl Send for SendablePtr {}
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let ty = py.get_type::();
         let traverse = unsafe { get_type_traverse(ty.as_type_ptr()).unwrap() };
 
@@ -646,7 +646,7 @@ fn test_traverse_subclass() {
 
     let (guard, check) = drop_check();
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let base = CycleWithClear {
             cycle: None,
             _guard: guard,
@@ -693,7 +693,7 @@ fn test_traverse_subclass_override_clear() {
 
     let (guard, check) = drop_check();
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let base = CycleWithClear {
             cycle: None,
             _guard: guard,
diff --git a/tests/test_getter_setter.rs b/tests/test_getter_setter.rs
index 82a50442ec5..4d15627ea51 100644
--- a/tests/test_getter_setter.rs
+++ b/tests/test_getter_setter.rs
@@ -65,7 +65,7 @@ fn extract_len(any: &Bound<'_, PyAny>) -> PyResult {
 
 #[test]
 fn class_with_properties() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let inst = Py::new(py, ClassWithProperties { num: 10 }).unwrap();
 
         py_run!(py, inst, "assert inst.get_num() == 10");
@@ -110,7 +110,7 @@ impl GetterSetter {
 
 #[test]
 fn getter_setter_autogen() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let inst = Py::new(
             py,
             GetterSetter {
@@ -151,7 +151,7 @@ impl RefGetterSetter {
 #[test]
 fn ref_getter_setter() {
     // Regression test for #837
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let inst = Py::new(py, RefGetterSetter { num: 10 }).unwrap();
 
         py_run!(py, inst, "assert inst.num == 10");
@@ -177,7 +177,7 @@ impl TupleClassGetterSetter {
 
 #[test]
 fn tuple_struct_getter_setter() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let inst = Py::new(py, TupleClassGetterSetter(10)).unwrap();
 
         py_assert!(py, inst, "inst.num == 10");
@@ -193,7 +193,7 @@ struct All {
 
 #[test]
 fn get_set_all() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let inst = Py::new(py, All { num: 10 }).unwrap();
 
         py_run!(py, inst, "assert inst.num == 10");
@@ -209,7 +209,7 @@ struct All2 {
 
 #[test]
 fn get_all_and_set() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let inst = Py::new(py, All2 { num: 10 }).unwrap();
 
         py_run!(py, inst, "assert inst.num == 10");
@@ -228,7 +228,7 @@ fn cell_getter_setter() {
     let c = CellGetterSetter {
         cell_inner: Cell::new(10),
     };
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let inst = Py::new(py, c).unwrap();
         let cell = Cell::new(20i32).into_pyobject(py).unwrap();
 
@@ -255,7 +255,7 @@ fn borrowed_value_with_lifetime_of_self() {
         }
     }
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let inst = Py::new(py, BorrowedValue {}).unwrap();
 
         py_run!(py, inst, "assert inst.value == 'value'");
@@ -270,7 +270,7 @@ fn frozen_py_field_get() {
         value: Py,
     }
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let inst = Py::new(
             py,
             FrozenPyField {
@@ -303,7 +303,7 @@ fn test_optional_setter() {
         }
     }
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let instance = Py::new(py, SimpleClass { field: None }).unwrap();
         py_run!(py, instance, "assert instance.field is None");
         py_run!(
diff --git a/tests/test_inheritance.rs b/tests/test_inheritance.rs
index bc2e7e24890..0500c0c2f83 100644
--- a/tests/test_inheritance.rs
+++ b/tests/test_inheritance.rs
@@ -20,7 +20,7 @@ struct SubclassAble {}
 
 #[test]
 fn subclass() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let d = [("SubclassAble", py.get_type::())]
             .into_py_dict(py)
             .unwrap();
@@ -74,7 +74,7 @@ impl SubClass {
 
 #[test]
 fn inheritance_with_new_methods() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let typeobj = py.get_type::();
         let inst = typeobj.call((), None).unwrap();
         py_run!(py, inst, "assert inst.val1 == 10; assert inst.val2 == 5");
@@ -83,7 +83,7 @@ fn inheritance_with_new_methods() {
 
 #[test]
 fn call_base_and_sub_methods() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let obj = Py::new(py, SubClass::new()).unwrap();
         py_run!(
             py,
@@ -98,7 +98,7 @@ fn call_base_and_sub_methods() {
 
 #[test]
 fn mutation_fails() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let obj = Py::new(py, SubClass::new()).unwrap();
         let global = [("obj", obj)].into_py_dict(py).unwrap();
         let e = py
@@ -114,7 +114,7 @@ fn mutation_fails() {
 
 #[test]
 fn is_subclass_and_is_instance() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let sub_ty = py.get_type::();
         let base_ty = py.get_type::();
         assert!(sub_ty.is_subclass_of::().unwrap());
@@ -157,7 +157,7 @@ impl SubClass2 {
 
 #[test]
 fn handle_result_in_new() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let subclass = py.get_type::();
         py_run!(
             py,
@@ -202,7 +202,7 @@ mod inheriting_native_type {
             }
         }
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let set_sub = pyo3::Py::new(py, SetWithName::new()).unwrap();
             py_run!(
                 py,
@@ -229,7 +229,7 @@ mod inheriting_native_type {
 
     #[test]
     fn inherit_dict() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let dict_sub = pyo3::Py::new(py, DictWithName::new()).unwrap();
             py_run!(
                 py,
@@ -241,7 +241,7 @@ mod inheriting_native_type {
 
     #[test]
     fn inherit_dict_drop() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let dict_sub = pyo3::Py::new(py, DictWithName::new()).unwrap();
             assert_eq!(dict_sub.get_refcnt(py), 1);
 
@@ -274,7 +274,7 @@ mod inheriting_native_type {
 
     #[test]
     fn custom_exception() {
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             let cls = py.get_type::();
             let dict = [("cls", &cls)].into_py_dict(py).unwrap();
             let res = py.run(
@@ -314,7 +314,7 @@ impl SimpleClass {
 #[test]
 fn test_subclass_ref_counts() {
     // regression test for issue #1363
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         #[allow(non_snake_case)]
         let SimpleClass = py.get_type::();
         py_run!(
diff --git a/tests/test_intopyobject.rs b/tests/test_intopyobject.rs
index 2fbddddb8c1..2e824739f59 100644
--- a/tests/test_intopyobject.rs
+++ b/tests/test_intopyobject.rs
@@ -18,7 +18,7 @@ pub struct A<'py> {
 
 #[test]
 fn test_named_fields_struct() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let a = A {
             s: "Hello".into(),
             t: PyString::new(py, "World"),
@@ -60,7 +60,7 @@ pub struct B<'a> {
 
 #[test]
 fn test_transparent_named_field_struct() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let pyb = B { test: "test" }.into_pyobject(py).unwrap();
         let b = pyb.extract::().unwrap();
         assert_eq!(b, "test");
@@ -75,7 +75,7 @@ pub struct D {
 
 #[test]
 fn test_generic_transparent_named_field_struct() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let pyd = D {
             test: String::from("test"),
         }
@@ -95,7 +95,7 @@ pub struct GenericWithBound(HashMap);
 
 #[test]
 fn test_generic_with_bound() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let mut hash_map = HashMap::::new();
         hash_map.insert("1".into(), 1);
         hash_map.insert("2".into(), 2);
@@ -126,7 +126,7 @@ pub struct Tuple(String, usize);
 
 #[test]
 fn test_tuple_struct() {
-    Python::with_gil(|py| {
+    Python::attach(|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();
@@ -140,7 +140,7 @@ pub struct TransparentTuple(String);
 
 #[test]
 fn test_transparent_tuple_struct() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let tup = TransparentTuple(String::from("test"))
             .into_pyobject(py)
             .unwrap();
@@ -177,7 +177,7 @@ pub enum Foo<'py> {
 
 #[test]
 fn test_enum() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let foo = Foo::TupleVar(1, "test".into(), std::marker::PhantomData)
             .into_pyobject(py)
             .unwrap();
@@ -231,7 +231,7 @@ fn zap_into_py<'py>(
 
 #[test]
 fn test_into_py_with() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let zap = Zap {
             name: "whatever".into(),
             some_object_length: 3,
@@ -271,7 +271,7 @@ fn test_struct_into_py_rename_all() {
         long_field_name: 0.0,
     };
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let py_foo_ref = (&foo).into_pyobject(py).unwrap();
         let py_foo = foo.into_pyobject(py).unwrap();
 
diff --git a/tests/test_macro_docs.rs b/tests/test_macro_docs.rs
index 42159be9d3c..fec11e51904 100644
--- a/tests/test_macro_docs.rs
+++ b/tests/test_macro_docs.rs
@@ -22,7 +22,7 @@ impl MacroDocs {
 
 #[test]
 fn meth_doc() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let d = [("C", py.get_type::())]
             .into_py_dict(py)
             .unwrap();
diff --git a/tests/test_macros.rs b/tests/test_macros.rs
index 4b5feddf3f9..72b5b45ef7f 100644
--- a/tests/test_macros.rs
+++ b/tests/test_macros.rs
@@ -73,7 +73,7 @@ property_rename_via_macro!(my_new_property_name);
 
 #[test]
 fn test_macro_rules_interactions() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let my_base = py.get_type::();
         py_assert!(py, my_base, "my_base.__name__ == 'MyClass'");
 
diff --git a/tests/test_mapping.rs b/tests/test_mapping.rs
index ba65c647222..cb6e9f49f10 100644
--- a/tests/test_mapping.rs
+++ b/tests/test_mapping.rs
@@ -85,7 +85,7 @@ fn map_dict(py: Python<'_>) -> Bound<'_, pyo3::types::PyDict> {
 
 #[test]
 fn test_getitem() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let d = map_dict(py);
 
         py_assert!(py, *d, "m['1'] == 0");
@@ -97,7 +97,7 @@ fn test_getitem() {
 
 #[test]
 fn test_setitem() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let d = map_dict(py);
 
         py_run!(py, *d, "m['1'] = 4; assert m['1'] == 4");
@@ -110,7 +110,7 @@ fn test_setitem() {
 
 #[test]
 fn test_delitem() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let d = map_dict(py);
         py_run!(
             py,
@@ -124,7 +124,7 @@ fn test_delitem() {
 
 #[test]
 fn mapping_is_not_sequence() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let mut index = HashMap::new();
         index.insert("Foo".into(), 1);
         index.insert("Bar".into(), 2);
diff --git a/tests/test_methods.rs b/tests/test_methods.rs
index 5f8398f6681..5c96aa2afc6 100644
--- a/tests/test_methods.rs
+++ b/tests/test_methods.rs
@@ -34,7 +34,7 @@ impl InstanceMethod {
 
 #[test]
 fn instance_method() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let obj = Bound::new(py, InstanceMethod { member: 42 }).unwrap();
         let obj_ref = obj.borrow();
         assert_eq!(obj_ref.method(), 42);
@@ -58,7 +58,7 @@ impl InstanceMethodWithArgs {
 
 #[test]
 fn instance_method_with_args() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let obj = Bound::new(py, InstanceMethodWithArgs { member: 7 }).unwrap();
         let obj_ref = obj.borrow();
         assert_eq!(obj_ref.method(6), 42);
@@ -91,7 +91,7 @@ impl ClassMethod {
 
 #[test]
 fn class_method() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let d = [("C", py.get_type::())]
             .into_py_dict(py)
             .unwrap();
@@ -120,7 +120,7 @@ impl ClassMethodWithArgs {
 
 #[test]
 fn class_method_with_args() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let d = [("C", py.get_type::())]
             .into_py_dict(py)
             .unwrap();
@@ -151,7 +151,7 @@ impl StaticMethod {
 
 #[test]
 fn static_method() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         assert_eq!(StaticMethod::method(py), "StaticMethod.method()!");
 
         let d = [("C", py.get_type::())]
@@ -177,7 +177,7 @@ impl StaticMethodWithArgs {
 
 #[test]
 fn static_method_with_args() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         assert_eq!(StaticMethodWithArgs::method(py, 1234), "0x4d2");
 
         let d = [("C", py.get_type::())]
@@ -375,7 +375,7 @@ impl MethSignature {
 
 #[test]
 fn meth_signature() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let inst = Py::new(py, MethSignature {}).unwrap();
 
         py_run!(py, inst, "assert inst.get_optional() == 10");
@@ -722,7 +722,7 @@ impl MethDocs {
 
 #[test]
 fn meth_doc() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let d = [("C", py.get_type::())].into_py_dict(py).unwrap();
         py_assert!(py, *d, "C.__doc__ == 'A class with \"documentation\".'");
         py_assert!(
@@ -757,7 +757,7 @@ impl MethodWithLifeTime {
 
 #[test]
 fn method_with_lifetime() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let obj = Py::new(py, MethodWithLifeTime {}).unwrap();
         py_run!(
             py,
@@ -807,7 +807,7 @@ impl MethodWithPyClassArg {
 
 #[test]
 fn method_with_pyclassarg() {
-    Python::with_gil(|py| {
+    Python::attach(|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).unwrap();
@@ -870,7 +870,7 @@ impl CfgStruct {
 
 #[test]
 fn test_cfg_attrs() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let inst = Py::new(py, CfgStruct {}).unwrap();
 
         #[cfg(unix)]
@@ -913,7 +913,7 @@ impl FromSequence {
 
 #[test]
 fn test_from_sequence() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let typeobj = py.get_type::();
         py_assert!(py, typeobj, "typeobj(range(0, 4)).numbers == [0, 1, 2, 3]");
     });
@@ -993,7 +993,7 @@ impl r#RawIdents {
 
 #[test]
 fn test_raw_idents() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let raw_idents_type = py.get_type::();
         assert_eq!(raw_idents_type.qualname().unwrap(), "RawIdents");
         py_run!(
@@ -1180,7 +1180,7 @@ fn test_option_pyclass_arg() {
         arg.map(|_| SomePyClass {})
     }
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let f = wrap_pyfunction!(option_class_arg, py).unwrap();
         assert!(f.call0().unwrap().is_none());
         let obj = Py::new(py, SomePyClass {}).unwrap();
@@ -1289,7 +1289,7 @@ fn test_pymethods_warn() {
         }
     }
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let typeobj = py.get_type::();
         let obj = typeobj.call0().unwrap();
 
@@ -1407,7 +1407,7 @@ fn test_pymethods_warn() {
         }
     }
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let typeobj = py.get_type::();
 
         // #[new], #[classmethod], FnType::FnNewClass
@@ -1446,7 +1446,7 @@ fn test_py_methods_multiple_warn() {
         fn multiple_warn_custom_category_method(&self) {}
     }
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let typeobj = py.get_type::();
         let obj = typeobj.call0().unwrap();
 
diff --git a/tests/test_module.rs b/tests/test_module.rs
index 35b7d354332..9b316134752 100644
--- a/tests/test_module.rs
+++ b/tests/test_module.rs
@@ -72,7 +72,7 @@ fn module_with_functions(m: &Bound<'_, PyModule>) -> PyResult<()> {
 fn test_module_with_functions() {
     use pyo3::wrap_pymodule;
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let d = [(
             "module_with_functions",
             wrap_pymodule!(module_with_functions)(py),
@@ -130,7 +130,7 @@ fn module_with_explicit_py_arg(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyRe
 fn test_module_with_explicit_py_arg() {
     use pyo3::wrap_pymodule;
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let d = [(
             "module_with_explicit_py_arg",
             wrap_pymodule!(module_with_explicit_py_arg)(py),
@@ -152,7 +152,7 @@ fn some_name(m: &Bound<'_, PyModule>) -> PyResult<()> {
 fn test_module_renaming() {
     use pyo3::wrap_pymodule;
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let d = [("different_name", wrap_pymodule!(some_name)(py))]
             .into_py_dict(py)
             .unwrap();
@@ -163,7 +163,7 @@ fn test_module_renaming() {
 
 #[test]
 fn test_module_from_code_bound() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let adder_mod = PyModule::from_code(
             py,
             c_str!("def add(a,b):\n\treturn a+b"),
@@ -202,7 +202,7 @@ fn raw_ident_module(module: &Bound<'_, PyModule>) -> PyResult<()> {
 fn test_raw_idents() {
     use pyo3::wrap_pymodule;
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let module = wrap_pymodule!(raw_ident_module)(py);
 
         py_assert!(py, module, "module.move() == 42");
@@ -223,7 +223,7 @@ fn test_custom_names() {
         Ok(())
     }
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let module = pyo3::wrap_pymodule!(custom_names)(py);
 
         py_assert!(py, module, "not hasattr(module, 'custom_named_fn')");
@@ -239,7 +239,7 @@ fn test_module_dict() {
         Ok(())
     }
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let module = pyo3::wrap_pymodule!(module_dict)(py);
 
         py_assert!(py, module, "module.yay == 'me'");
@@ -248,7 +248,7 @@ fn test_module_dict() {
 
 #[test]
 fn test_module_dunder_all() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         #[pymodule]
         fn dunder_all(m: &Bound<'_, PyModule>) -> PyResult<()> {
             m.dict().set_item("yay", "me")?;
@@ -299,7 +299,7 @@ fn supermodule(module: &Bound<'_, PyModule>) -> PyResult<()> {
 fn test_module_nesting() {
     use pyo3::wrap_pymodule;
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let supermodule = wrap_pymodule!(supermodule)(py);
 
         py_assert!(
@@ -345,7 +345,7 @@ fn vararg_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
 
 #[test]
 fn test_vararg_module() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let m = pyo3::wrap_pymodule!(vararg_module)(py);
 
         py_assert!(py, m, "m.ext_vararg_fn() == [5, ()]");
@@ -370,7 +370,7 @@ fn test_module_with_constant() {
         Ok(())
     }
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let m = pyo3::wrap_pymodule!(module_with_constant)(py);
         py_assert!(py, m, "isinstance(m.ANON, m.AnonClass)");
     });
@@ -444,7 +444,7 @@ fn module_with_functions_with_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
 
 #[test]
 fn test_module_functions_with_module() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let m = pyo3::wrap_pymodule!(module_with_functions_with_module)(py);
         py_assert!(
             py,
@@ -485,7 +485,7 @@ fn test_module_doc_hidden() {
         Ok(())
     }
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let m = pyo3::wrap_pymodule!(my_module)(py);
         py_assert!(py, m, "m.__doc__ == ''");
     })
diff --git a/tests/test_multiple_pymethods.rs b/tests/test_multiple_pymethods.rs
index 13baeed3815..ccbda42b865 100644
--- a/tests/test_multiple_pymethods.rs
+++ b/tests/test_multiple_pymethods.rs
@@ -64,7 +64,7 @@ impl PyClassWithMultiplePyMethods {
 
 #[test]
 fn test_class_with_multiple_pymethods() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let cls = py.get_type::();
         py_assert!(py, cls, "cls()() == 'call'");
         py_assert!(py, cls, "cls().method() == 'method'");
diff --git a/tests/test_proto_methods.rs b/tests/test_proto_methods.rs
index 56039502f24..f1af2b64f18 100644
--- a/tests/test_proto_methods.rs
+++ b/tests/test_proto_methods.rs
@@ -78,7 +78,7 @@ fn make_example(py: Python<'_>) -> Bound<'_, ExampleClass> {
 
 #[test]
 fn test_getattr() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let example_py = make_example(py);
         assert_eq!(
             example_py
@@ -105,7 +105,7 @@ fn test_getattr() {
 
 #[test]
 fn test_setattr() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let example_py = make_example(py);
         example_py.setattr("special_custom_attr", 15).unwrap();
         assert_eq!(
@@ -121,7 +121,7 @@ fn test_setattr() {
 
 #[test]
 fn test_delattr() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let example_py = make_example(py);
         example_py.delattr("special_custom_attr").unwrap();
         assert!(example_py.getattr("special_custom_attr").unwrap().is_none());
@@ -130,7 +130,7 @@ fn test_delattr() {
 
 #[test]
 fn test_str() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let example_py = make_example(py);
         assert_eq!(example_py.str().unwrap(), "5");
     })
@@ -138,7 +138,7 @@ fn test_str() {
 
 #[test]
 fn test_repr() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let example_py = make_example(py);
         assert_eq!(example_py.repr().unwrap(), "ExampleClass(value=5)");
     })
@@ -146,7 +146,7 @@ fn test_repr() {
 
 #[test]
 fn test_hash() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let example_py = make_example(py);
         assert_eq!(example_py.hash().unwrap(), 5);
     })
@@ -154,7 +154,7 @@ fn test_hash() {
 
 #[test]
 fn test_bool() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let example_py = make_example(py);
         assert!(example_py.is_truthy().unwrap());
         example_py.borrow_mut().value = 0;
@@ -174,7 +174,7 @@ impl LenOverflow {
 
 #[test]
 fn len_overflow() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let inst = Py::new(py, LenOverflow).unwrap();
         py_expect_exception!(py, inst, "len(inst)", PyOverflowError);
     });
@@ -207,7 +207,7 @@ impl Mapping {
 
 #[test]
 fn mapping() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         PyMapping::register::(py).unwrap();
 
         let inst = Py::new(
@@ -315,7 +315,7 @@ impl Sequence {
 
 #[test]
 fn sequence() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         PySequence::register::(py).unwrap();
 
         let inst = Py::new(py, Sequence { values: vec![] }).unwrap();
@@ -378,7 +378,7 @@ impl Iterator {
 
 #[test]
 fn iterator() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let inst = Py::new(
             py,
             Iterator {
@@ -406,7 +406,7 @@ struct NotCallable;
 
 #[test]
 fn callable() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let c = Py::new(py, Callable).unwrap();
         py_assert!(py, c, "callable(c)");
         py_assert!(py, c, "c(7) == 42");
@@ -433,7 +433,7 @@ impl SetItem {
 
 #[test]
 fn setitem() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let c = Bound::new(py, SetItem { key: 0, val: 0 }).unwrap();
         py_run!(py, c, "c[1] = 2");
         {
@@ -459,7 +459,7 @@ impl DelItem {
 
 #[test]
 fn delitem() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let c = Bound::new(py, DelItem { key: 0 }).unwrap();
         py_run!(py, c, "del c[1]");
         {
@@ -488,7 +488,7 @@ impl SetDelItem {
 
 #[test]
 fn setdelitem() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let c = Bound::new(py, SetDelItem { val: None }).unwrap();
         py_run!(py, c, "c[1] = 2");
         {
@@ -513,7 +513,7 @@ impl Contains {
 
 #[test]
 fn contains() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let c = Py::new(py, Contains {}).unwrap();
         py_run!(py, c, "assert 1 in c");
         py_run!(py, c, "assert -1 not in c");
@@ -543,7 +543,7 @@ impl GetItem {
 
 #[test]
 fn test_getitem() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let ob = Py::new(py, GetItem {}).unwrap();
 
         py_assert!(py, ob, "ob[1] == 'int'");
@@ -566,7 +566,7 @@ impl ClassWithGetAttr {
 
 #[test]
 fn getattr_doesnt_override_member() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let inst = Py::new(py, ClassWithGetAttr { data: 4 }).unwrap();
         py_assert!(py, inst, "inst.data == 4");
         py_assert!(py, inst, "inst.a == 8");
@@ -588,7 +588,7 @@ impl ClassWithGetAttribute {
 
 #[test]
 fn getattribute_overrides_member() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let inst = Py::new(py, ClassWithGetAttribute { data: 4 }).unwrap();
         py_assert!(py, inst, "inst.data == 8");
         py_assert!(py, inst, "inst.y == 8");
@@ -621,7 +621,7 @@ impl ClassWithGetAttrAndGetAttribute {
 
 #[test]
 fn getattr_and_getattribute() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let inst = Py::new(py, ClassWithGetAttrAndGetAttribute).unwrap();
         py_assert!(py, inst, "inst.exists == 42");
         py_assert!(py, inst, "inst.lucky == 57");
@@ -668,7 +668,7 @@ impl OnceFuture {
 #[test]
 #[cfg(not(target_arch = "wasm32"))] // Won't work without wasm32 event loop (e.g., Pyodide has WebLoop)
 fn test_await() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let once = py.get_type::();
         let source = pyo3_ffi::c_str!(
             r#"
@@ -720,7 +720,7 @@ impl AsyncIterator {
 #[test]
 #[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| {
+    Python::attach(|py| {
         let once = py.get_type::();
         let source = pyo3_ffi::c_str!(
             r#"
@@ -787,7 +787,7 @@ impl DescrCounter {
 
 #[test]
 fn descr_getset() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let counter = py.get_type::();
         let source = pyo3_ffi::c_str!(pyo3::indoc::indoc!(
             r#"
@@ -835,7 +835,7 @@ impl NotHashable {
 fn test_hash_opt_out() {
     // By default Python provides a hash implementation, which can be disabled by setting __hash__
     // to None.
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let empty = Py::new(py, EmptyClass).unwrap();
         py_assert!(py, empty, "hash(empty) is not None");
 
@@ -884,7 +884,7 @@ impl NoContains {
 
 #[test]
 fn test_contains_opt_out() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let defaulted_contains = Py::new(py, DefaultedContains).unwrap();
         py_assert!(py, defaulted_contains, "'a' in defaulted_contains");
 
diff --git a/tests/test_pyerr_debug_unformattable.rs b/tests/test_pyerr_debug_unformattable.rs
index 6c4755087c5..18c5ac8687c 100644
--- a/tests/test_pyerr_debug_unformattable.rs
+++ b/tests/test_pyerr_debug_unformattable.rs
@@ -14,7 +14,7 @@ fn err_debug_unformattable() {
     //     traceback: Some(\">\")
     // }
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         // PyTracebackMethods::format uses io.StringIO. Mock it out to trigger a
         // formatting failure:
         // TypeError: 'Mock' object cannot be converted to 'PyString'
diff --git a/tests/test_pyfunction.rs b/tests/test_pyfunction.rs
index b817850d727..02e184ead06 100644
--- a/tests/test_pyfunction.rs
+++ b/tests/test_pyfunction.rs
@@ -26,7 +26,7 @@ fn struct_function() {}
 
 #[test]
 fn test_rust_keyword_name() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let f = wrap_pyfunction!(struct_function)(py).unwrap();
 
         py_assert!(py, f, "f.__name__ == 'struct'");
@@ -41,7 +41,7 @@ fn optional_bool(arg: Option) -> String {
 #[test]
 fn test_optional_bool() {
     // Regression test for issue #932
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let f = wrap_pyfunction!(optional_bool)(py).unwrap();
 
         py_assert!(py, f, "f() == 'Some(true)'");
@@ -60,7 +60,7 @@ fn required_optional_str(arg: Option<&str>) -> &str {
 #[test]
 fn test_optional_str() {
     // Regression test for issue #4965
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let f = wrap_pyfunction!(required_optional_str)(py).unwrap();
 
         py_assert!(py, f, "f('') == ''");
@@ -81,7 +81,7 @@ fn required_optional_class(arg: Option<&MyClass>) {
 #[test]
 fn test_required_optional_class() {
     // Regression test for issue #4965
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let f = wrap_pyfunction!(required_optional_class)(py).unwrap();
         let val = Bound::new(py, MyClass()).unwrap();
 
@@ -104,7 +104,7 @@ fn buffer_inplace_add(py: Python<'_>, x: PyBuffer, y: PyBuffer) {
 #[cfg(not(Py_LIMITED_API))]
 #[test]
 fn test_buffer_add() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let f = wrap_pyfunction!(buffer_inplace_add)(py).unwrap();
 
         py_expect_exception!(
@@ -148,7 +148,7 @@ fn function_with_pycfunction_arg<'py>(
 
 #[test]
 fn test_functions_with_function_args() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let py_cfunc_arg = wrap_pyfunction!(function_with_pycfunction_arg)(py).unwrap();
         let bool_to_string = wrap_pyfunction!(optional_bool)(py).unwrap();
 
@@ -196,7 +196,7 @@ fn function_with_custom_conversion(
 #[cfg(not(Py_LIMITED_API))]
 #[test]
 fn test_function_with_custom_conversion() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let custom_conv_func = wrap_pyfunction!(function_with_custom_conversion)(py).unwrap();
 
         pyo3::py_run!(
@@ -215,7 +215,7 @@ fn test_function_with_custom_conversion() {
 #[cfg(not(Py_LIMITED_API))]
 #[test]
 fn test_function_with_custom_conversion_error() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let custom_conv_func = wrap_pyfunction!(function_with_custom_conversion)(py).unwrap();
 
         py_expect_exception!(
@@ -252,7 +252,7 @@ fn test_from_py_with_defaults() {
         len
     }
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let f = wrap_pyfunction!(from_py_with_option)(py).unwrap();
 
         assert_eq!(f.call0().unwrap().extract::().unwrap(), 0);
@@ -288,7 +288,7 @@ fn conversion_error(
 
 #[test]
 fn test_conversion_error() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let conversion_error = wrap_pyfunction!(conversion_error)(py).unwrap();
         py_expect_exception!(
             py,
@@ -376,7 +376,7 @@ fn extract_traceback(py: Python<'_>, mut error: PyErr) -> String {
 fn test_pycfunction_new() {
     use pyo3::ffi;
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         unsafe extern "C" fn c_fn(
             _self: *mut ffi::PyObject,
             _args: *mut ffi::PyObject,
@@ -408,7 +408,7 @@ fn test_pycfunction_new_with_keywords() {
     use std::os::raw::c_long;
     use std::ptr;
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         unsafe extern "C" fn c_fn(
             _self: *mut ffi::PyObject,
             args: *mut ffi::PyObject,
@@ -474,11 +474,11 @@ fn test_pycfunction_new_with_keywords() {
 
 #[test]
 fn test_closure() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let f = |args: &Bound<'_, types::PyTuple>,
                  _kwargs: Option<&Bound<'_, types::PyDict>>|
          -> PyResult<_> {
-            Python::with_gil(|py| {
+            Python::attach(|py| {
                 let res: PyResult> = args
                     .iter()
                     .map(|elem| {
@@ -514,7 +514,7 @@ fn test_closure() {
 
 #[test]
 fn test_closure_counter() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let counter = std::cell::RefCell::new(0);
         let counter_fn = move |_args: &Bound<'_, types::PyTuple>,
                                _kwargs: Option<&Bound<'_, types::PyDict>>|
@@ -542,7 +542,7 @@ fn use_pyfunction() {
         }
     }
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         use function_in_module::foo;
 
         // check imported name can be wrapped
@@ -578,7 +578,7 @@ fn return_value_borrows_from_arguments<'py>(
 
 #[test]
 fn test_return_value_borrows_from_arguments() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let function = wrap_pyfunction!(return_value_borrows_from_arguments, py).unwrap();
 
         let key = Py::new(py, Key("key".to_owned())).unwrap();
@@ -602,7 +602,7 @@ fn test_some_wrap_arguments() {
         [a, b, c, d]
     }
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let function = wrap_pyfunction!(some_wrap_arguments, py).unwrap();
         py_assert!(py, function, "function() == [1, 2, None, None]");
     })
@@ -619,7 +619,7 @@ fn test_reference_to_bound_arguments() {
         y.map_or_else(|| Ok(x.clone()), |y| y.add(x))
     }
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let function = wrap_pyfunction!(reference_args, py).unwrap();
         py_assert!(py, function, "function(1) == 1");
         py_assert!(py, function, "function(1, 2) == 3");
@@ -646,7 +646,7 @@ fn test_pyfunction_raw_ident() {
         Ok(())
     }
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let m = pyo3::wrap_pymodule!(m)(py);
         py_assert!(py, m, "m.struct()");
         py_assert!(py, m, "m.enum()");
diff --git a/tests/test_pyself.rs b/tests/test_pyself.rs
index c4f3e6d6fa6..4eff50b4770 100644
--- a/tests/test_pyself.rs
+++ b/tests/test_pyself.rs
@@ -92,7 +92,7 @@ fn reader() -> Reader {
 
 #[test]
 fn test_nested_iter() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let reader = reader().into_pyobject(py).unwrap();
         py_assert!(
             py,
@@ -104,7 +104,7 @@ fn test_nested_iter() {
 
 #[test]
 fn test_clone_ref() {
-    Python::with_gil(|py| {
+    Python::attach(|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()");
@@ -113,7 +113,7 @@ fn test_clone_ref() {
 
 #[test]
 fn test_nested_iter_reset() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let reader = Bound::new(py, reader()).unwrap();
         py_assert!(
             py,
diff --git a/tests/test_sequence.rs b/tests/test_sequence.rs
index 9405aa08487..3f8a6bd816f 100644
--- a/tests/test_sequence.rs
+++ b/tests/test_sequence.rs
@@ -117,7 +117,7 @@ fn seq_dict(py: Python<'_>) -> Bound<'_, pyo3::types::PyDict> {
 
 #[test]
 fn test_getitem() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let d = seq_dict(py);
 
         py_assert!(py, *d, "s[0] == 1");
@@ -130,7 +130,7 @@ fn test_getitem() {
 
 #[test]
 fn test_setitem() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let d = seq_dict(py);
 
         py_run!(py, *d, "s[0] = 4; assert list(s) == [4, 2, 3]");
@@ -140,7 +140,7 @@ fn test_setitem() {
 
 #[test]
 fn test_delitem() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let d = [("ByteSequence", py.get_type::())]
             .into_py_dict(py)
             .unwrap();
@@ -182,7 +182,7 @@ fn test_delitem() {
 
 #[test]
 fn test_contains() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let d = seq_dict(py);
 
         py_assert!(py, *d, "1 in s");
@@ -195,7 +195,7 @@ fn test_contains() {
 
 #[test]
 fn test_concat() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let d = seq_dict(py);
 
         py_run!(
@@ -214,7 +214,7 @@ fn test_concat() {
 
 #[test]
 fn test_inplace_concat() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let d = seq_dict(py);
 
         py_run!(
@@ -228,7 +228,7 @@ fn test_inplace_concat() {
 
 #[test]
 fn test_repeat() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let d = seq_dict(py);
 
         py_run!(py, *d, "s2 = s * 2; assert list(s2) == [1, 2, 3, 1, 2, 3]");
@@ -238,7 +238,7 @@ fn test_repeat() {
 
 #[test]
 fn test_inplace_repeat() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let d = [("ByteSequence", py.get_type::())]
             .into_py_dict(py)
             .unwrap();
@@ -262,7 +262,7 @@ struct AnyObjectList {
 
 #[test]
 fn test_any_object_list_get() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let list = AnyObjectList {
             items: [1i32, 2, 3]
                 .iter()
@@ -278,7 +278,7 @@ fn test_any_object_list_get() {
 
 #[test]
 fn test_any_object_list_set() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let list = Bound::new(py, AnyObjectList { items: vec![] }).unwrap();
 
         py_run!(py, list, "list.items = [1, 2, 3]");
@@ -314,7 +314,7 @@ impl OptionList {
 #[test]
 fn test_option_list_get() {
     // Regression test for #798
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let list = Py::new(
             py,
             OptionList {
@@ -331,7 +331,7 @@ fn test_option_list_get() {
 
 #[test]
 fn sequence_is_not_mapping() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let list = Bound::new(
             py,
             OptionList {
@@ -350,7 +350,7 @@ fn sequence_is_not_mapping() {
 
 #[test]
 fn sequence_length() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let list = Bound::new(
             py,
             OptionList {
@@ -384,7 +384,7 @@ impl GenericList {
 
     fn __getitem__(&self, idx: isize) -> PyResult {
         match self.items.get(idx as usize) {
-            Some(x) => pyo3::Python::with_gil(|py| Ok(x.clone_ref(py))),
+            Some(x) => pyo3::Python::attach(|py| Ok(x.clone_ref(py))),
             None => Err(PyIndexError::new_err("Index out of bounds")),
         }
     }
@@ -396,7 +396,7 @@ fn test_generic_both_subscriptions_types() {
     use pyo3::types::PyInt;
     use std::convert::Infallible;
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let l = Bound::new(
             py,
             GenericList {
diff --git a/tests/test_serde.rs b/tests/test_serde.rs
index 9e97946b5f2..ec4d859084e 100644
--- a/tests/test_serde.rs
+++ b/tests/test_serde.rs
@@ -31,7 +31,7 @@ mod test_serde {
             friends: vec![],
         };
 
-        let user = Python::with_gil(|py| {
+        let user = Python::attach(|py| {
             let py_friend1 = Py::new(py, friend1).expect("failed to create friend 1");
             let py_friend2 = Py::new(py, friend2).expect("failed to create friend 2");
 
@@ -69,7 +69,7 @@ mod test_serde {
         assert_eq!(user.friends.len(), 1usize);
         let friend = user.friends.first().unwrap();
 
-        Python::with_gil(|py| {
+        Python::attach(|py| {
             assert_eq!(friend.borrow(py).username, "friend");
             assert_eq!(
                 friend.borrow(py).group.as_ref().unwrap().borrow(py).name,
diff --git a/tests/test_static_slots.rs b/tests/test_static_slots.rs
index 48d80a43856..b11ecd08fb9 100644
--- a/tests/test_static_slots.rs
+++ b/tests/test_static_slots.rs
@@ -48,7 +48,7 @@ fn test_dict(py: Python<'_>) -> Bound<'_, pyo3::types::PyDict> {
 
 #[test]
 fn test_len() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let d = test_dict(py);
 
         py_assert!(py, *d, "len(s) == 5");
@@ -57,7 +57,7 @@ fn test_len() {
 
 #[test]
 fn test_getitem() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let d = test_dict(py);
 
         py_assert!(py, *d, "s[4] == 5.0");
@@ -66,7 +66,7 @@ fn test_getitem() {
 
 #[test]
 fn test_list() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let d = test_dict(py);
 
         py_assert!(py, *d, "list(s) == [1.0, 2.0, 3.0, 4.0, 5.0]");
diff --git a/tests/test_string.rs b/tests/test_string.rs
index 02bf2ecd4df..6ac6615d8a2 100644
--- a/tests/test_string.rs
+++ b/tests/test_string.rs
@@ -10,7 +10,7 @@ fn take_str(_s: &str) {}
 
 #[test]
 fn test_unicode_encode_error() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let take_str = wrap_pyfunction!(take_str)(py).unwrap();
         py_expect_exception!(
             py,
diff --git a/tests/test_super.rs b/tests/test_super.rs
index 5c17af3503d..644365e8829 100644
--- a/tests/test_super.rs
+++ b/tests/test_super.rs
@@ -42,7 +42,7 @@ impl SubClass {
 
 #[test]
 fn test_call_super_method() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let cls = py.get_type::();
         pyo3::py_run!(
             py,
diff --git a/tests/test_text_signature.rs b/tests/test_text_signature.rs
index 7aceedd44c0..d1decc9bf3b 100644
--- a/tests/test_text_signature.rs
+++ b/tests/test_text_signature.rs
@@ -12,7 +12,7 @@ fn class_without_docs_or_signature() {
     #[pyclass]
     struct MyClass {}
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let typeobj = py.get_type::();
 
         py_assert!(py, typeobj, "typeobj.__doc__ is None");
@@ -27,7 +27,7 @@ fn class_with_docs() {
     /// docs line2
     struct MyClass {}
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let typeobj = py.get_type::();
 
         py_assert!(py, typeobj, "typeobj.__doc__ == 'docs line1\\ndocs line2'");
@@ -51,7 +51,7 @@ fn class_with_signature_no_doc() {
         }
     }
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let typeobj = py.get_type::();
         py_assert!(py, typeobj, "typeobj.__doc__ == ''");
         py_assert!(
@@ -80,7 +80,7 @@ fn class_with_docs_and_signature() {
         }
     }
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let typeobj = py.get_type::();
 
         py_assert!(py, typeobj, "typeobj.__doc__ == 'docs line1\\ndocs line2'");
@@ -100,7 +100,7 @@ fn test_function() {
         let _ = (a, b, c);
     }
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let f = wrap_pyfunction!(my_function)(py).unwrap();
 
         py_assert!(py, f, "f.__text_signature__ == '(a, b=None, *, c=42)'");
@@ -147,7 +147,7 @@ fn test_auto_test_signature_function() {
         let _ = (a, b, c);
     }
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let f = wrap_pyfunction!(my_function)(py).unwrap();
         py_assert!(
             py,
@@ -238,7 +238,7 @@ fn test_auto_test_signature_method() {
         }
     }
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let cls = py.get_type::();
         #[cfg(any(not(Py_LIMITED_API), Py_3_10))]
         py_assert!(py, cls, "cls.__text_signature__ == '(a, b, c)'");
@@ -317,7 +317,7 @@ fn test_auto_test_signature_opt_out() {
         }
     }
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let f = wrap_pyfunction!(my_function)(py).unwrap();
         py_assert!(py, f, "f.__text_signature__ == None");
 
@@ -345,7 +345,7 @@ fn test_pyfn() {
         Ok(())
     }
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let m = wrap_pymodule!(my_module)(py);
 
         py_assert!(
@@ -383,7 +383,7 @@ fn test_methods() {
         }
     }
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let typeobj = py.get_type::();
 
         py_assert!(
@@ -424,7 +424,7 @@ fn test_raw_identifiers() {
         fn r#method(&self) {}
     }
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         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 d8d9c79e31a..2c7851914aa 100644
--- a/tests/test_variable_arguments.rs
+++ b/tests/test_variable_arguments.rs
@@ -26,7 +26,7 @@ impl MyClass {
 
 #[test]
 fn variable_args() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         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,)");
@@ -36,7 +36,7 @@ fn variable_args() {
 
 #[test]
 fn variable_kwargs() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         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}");
diff --git a/tests/test_various.rs b/tests/test_various.rs
index a8aa6b7a71f..f525fdb77a5 100644
--- a/tests/test_various.rs
+++ b/tests/test_various.rs
@@ -26,7 +26,7 @@ impl MutRefArg {
 
 #[test]
 fn mut_ref_arg() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let inst1 = Py::new(py, MutRefArg { n: 0 }).unwrap();
         let inst2 = Py::new(py, MutRefArg { n: 0 }).unwrap();
 
@@ -51,7 +51,7 @@ fn get_zero() -> PyUsize {
 /// Checks that we can use return a custom class in arbitrary function and use those functions
 /// both in rust and python
 fn return_custom_class() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         // Using from rust
         assert_eq!(get_zero().value, 0);
 
@@ -63,7 +63,7 @@ fn return_custom_class() {
 
 #[test]
 fn intopytuple_primitive() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let tup = (1, 2, "foo");
         py_assert!(py, tup, "tup == (1, 2, 'foo')");
         py_assert!(py, tup, "tup[0] == 1");
@@ -77,7 +77,7 @@ struct SimplePyClass {}
 
 #[test]
 fn intopytuple_pyclass() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let tup = (
             Py::new(py, SimplePyClass {}).unwrap(),
             Py::new(py, SimplePyClass {}).unwrap(),
@@ -90,7 +90,7 @@ fn intopytuple_pyclass() {
 
 #[test]
 fn pytuple_primitive_iter() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let tup = PyTuple::new(py, [1u32, 2, 3].iter()).unwrap();
         py_assert!(py, tup, "tup == (1, 2, 3)");
     });
@@ -98,7 +98,7 @@ fn pytuple_primitive_iter() {
 
 #[test]
 fn pytuple_pyclass_iter() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let tup = PyTuple::new(
             py,
             [
@@ -149,7 +149,7 @@ fn test_pickle() {
             .set_item(module.name()?, module)
     }
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let module = PyModule::new(py, "test_module").unwrap();
         module.add_class::().unwrap();
         add_module(module).unwrap();
@@ -203,7 +203,7 @@ fn result_conversion_function() -> Result<(), MyError> {
 
 #[test]
 fn test_result_conversion() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         wrap_pyfunction!(result_conversion_function)(py).unwrap();
     });
 }
diff --git a/tests/ui/invalid_closure.rs b/tests/ui/invalid_closure.rs
index cb302375934..6dd7d508703 100644
--- a/tests/ui/invalid_closure.rs
+++ b/tests/ui/invalid_closure.rs
@@ -2,7 +2,7 @@ use pyo3::prelude::*;
 use pyo3::types::{PyCFunction, PyDict, PyTuple};
 
 fn main() {
-    let fun: Py = Python::with_gil(|py| {
+    let fun: Py = Python::attach(|py| {
         let local_data = vec![0, 1, 2, 3, 4];
         let ref_: &[u8] = &local_data;
 
@@ -16,7 +16,7 @@ fn main() {
             .into()
     });
 
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         fun.call0(py).unwrap();
     });
 }
diff --git a/tests/ui/invalid_intern_arg.rs b/tests/ui/invalid_intern_arg.rs
index ac82fed97d8..a90dbdd351a 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(pyo3::intern!(py, _foo)).unwrap());
+    Python::attach(|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 935d8406e37..eabbc058948 100644
--- a/tests/ui/invalid_intern_arg.stderr
+++ b/tests/ui/invalid_intern_arg.stderr
@@ -1,8 +1,8 @@
 error[E0435]: attempt to use a non-constant value in a constant
- --> tests/ui/invalid_intern_arg.rs:5:55
+ --> tests/ui/invalid_intern_arg.rs:5:53
   |
-5 |     Python::with_gil(|py| py.import(pyo3::intern!(py, _foo)).unwrap());
-  |                                                       ^^^^ non-constant value
+5 |     Python::attach(|py| py.import(pyo3::intern!(py, _foo)).unwrap());
+  |                                                     ^^^^ non-constant value
   |
 help: consider using `let` instead of `static`
  --> src/sync.rs
@@ -12,10 +12,10 @@ help: consider using `let` instead of `static`
   |
 
 error: lifetime may not live long enough
- --> tests/ui/invalid_intern_arg.rs:5:27
+ --> tests/ui/invalid_intern_arg.rs:5:25
   |
-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>`
+5 |     Python::attach(|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>`
diff --git a/tests/ui/invalid_pycallargs.rs b/tests/ui/invalid_pycallargs.rs
index b77dbb20dcb..e3af53d29c2 100644
--- a/tests/ui/invalid_pycallargs.rs
+++ b/tests/ui/invalid_pycallargs.rs
@@ -1,7 +1,7 @@
 use pyo3::prelude::*;
 
 fn main() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let any = py.None().into_bound(py);
         any.call1("foo");
     })
diff --git a/tests/ui/invalid_result_conversion.rs b/tests/ui/invalid_result_conversion.rs
index 2679f3afda7..692ce71f6dd 100644
--- a/tests/ui/invalid_result_conversion.rs
+++ b/tests/ui/invalid_result_conversion.rs
@@ -26,7 +26,7 @@ fn should_not_work() -> Result<(), MyError> {
 }
 
 fn main() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         wrap_pyfunction!(should_not_work)(py);
     });
 }
diff --git a/tests/ui/not_send.rs b/tests/ui/not_send.rs
index 6566f2d7de5..291bd7470f8 100644
--- a/tests/ui/not_send.rs
+++ b/tests/ui/not_send.rs
@@ -5,7 +5,7 @@ fn test_not_send_allow_threads(py: Python<'_>) {
 }
 
 fn main() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         test_not_send_allow_threads(py);
     })
 }
diff --git a/tests/ui/not_send2.rs b/tests/ui/not_send2.rs
index 32d95c1a231..9b76bbe591a 100644
--- a/tests/ui/not_send2.rs
+++ b/tests/ui/not_send2.rs
@@ -2,7 +2,7 @@ use pyo3::prelude::*;
 use pyo3::types::PyString;
 
 fn main() {
-    Python::with_gil(|py| {
+    Python::attach(|py| {
         let string = PyString::new(py, "foo");
 
         py.allow_threads(|| {
diff --git a/tests/ui/traverse.stderr b/tests/ui/traverse.stderr
index 504a6dfa671..5cac370c3a9 100644
--- a/tests/ui/traverse.stderr
+++ b/tests/ui/traverse.stderr
@@ -1,28 +1,28 @@
-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.
+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::attach` will panic.
   --> tests/ui/traverse.rs:10:27
    |
 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.
+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::attach` will panic.
   --> tests/ui/traverse.rs:20:27
    |
 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.
+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::attach` will panic.
   --> tests/ui/traverse.rs:30:27
    |
 30 |     fn __traverse__(_slf: Bound<'_, Self>, _visit: PyVisit) -> Result<(), PyTraverseError> {
    |                           ^^^^^
 
-error: __traverse__ may not take a receiver other than `&self`. Usually, an implementation of `__traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError>` should do nothing but calls to `visit.call`. Most importantly, safe access to the GIL is prohibited inside implementations of `__traverse__`, i.e. `Python::with_gil` will panic.
+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::attach` will panic.
   --> tests/ui/traverse.rs:40:21
    |
 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.
+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::attach` will panic.
   --> tests/ui/traverse.rs:60:33
    |
 60 |     fn __traverse__(&self, _py: Python<'_>, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
diff --git a/tests/ui/wrong_aspyref_lifetimes.rs b/tests/ui/wrong_aspyref_lifetimes.rs
index d547e9e86fc..b5f050ebcf9 100644
--- a/tests/ui/wrong_aspyref_lifetimes.rs
+++ b/tests/ui/wrong_aspyref_lifetimes.rs
@@ -1,10 +1,10 @@
 use pyo3::{types::PyDict, Bound, Py, Python};
 
 fn main() {
-    let dict: Py = Python::with_gil(|py| PyDict::new(py).unbind());
+    let dict: Py = Python::attach(|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));
+    // Should not be able to get access to Py contents outside of `attach`.
+    let dict: &Bound<'_, PyDict> = Python::attach(|py| dict.bind(py));
 
     let _py: Python = dict.py(); // Obtain a Python<'p> without GIL.
 }
diff --git a/tests/ui/wrong_aspyref_lifetimes.stderr b/tests/ui/wrong_aspyref_lifetimes.stderr
index f2f43d99f25..8a9cc553349 100644
--- a/tests/ui/wrong_aspyref_lifetimes.stderr
+++ b/tests/ui/wrong_aspyref_lifetimes.stderr
@@ -1,8 +1,8 @@
 error: lifetime may not live long enough
- --> tests/ui/wrong_aspyref_lifetimes.rs:7:58
+ --> tests/ui/wrong_aspyref_lifetimes.rs:7:56
   |
-7 |     let dict: &Bound<'_, PyDict> = Python::with_gil(|py| dict.bind(py));
-  |                                                      --- ^^^^^^^^^^^^^ returning this value requires that `'1` must outlive `'2`
-  |                                                      | |
-  |                                                      | return type of closure is &'2 pyo3::Bound<'_, PyDict>
-  |                                                      has type `Python<'1>`
+7 |     let dict: &Bound<'_, PyDict> = Python::attach(|py| dict.bind(py));
+  |                                                    --- ^^^^^^^^^^^^^ returning this value requires that `'1` must outlive `'2`
+  |                                                    | |
+  |                                                    | return type of closure is &'2 pyo3::Bound<'_, PyDict>
+  |                                                    has type `Python<'1>`

From 4b3610b0c74e0ad202b0a04c93b37a7d8024e124 Mon Sep 17 00:00:00 2001
From: Nathan Goldbaum 
Date: Thu, 3 Jul 2025 17:24:50 -0600
Subject: [PATCH 729/936] fix: prevent GIL re-enabled RuntimeWarning in tests
 (#5222)

---
 tests/test_append_to_inittab.rs | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/tests/test_append_to_inittab.rs b/tests/test_append_to_inittab.rs
index 783573af281..541c63870d6 100644
--- a/tests/test_append_to_inittab.rs
+++ b/tests/test_append_to_inittab.rs
@@ -7,13 +7,13 @@ fn foo() -> usize {
     123
 }
 
-#[pymodule]
+#[pymodule(gil_used = false)]
 fn module_fn_with_functions(m: &Bound<'_, PyModule>) -> PyResult<()> {
     m.add_function(wrap_pyfunction!(foo, m)?)?;
     Ok(())
 }
 
-#[pymodule]
+#[pymodule(gil_used = false)]
 mod module_mod_with_functions {
     #[pymodule_export]
     use super::foo;

From 4be809959e1720a8fedf8fa8cdb954dacc337e9d Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 4 Jul 2025 08:33:08 +0100
Subject: [PATCH 730/936] build(deps): update codspeed-criterion-compat
 requirement (#5220)

Updates the requirements on [codspeed-criterion-compat](https://github.com/CodSpeedHQ/codspeed-rust) to permit the latest version.
- [Release notes](https://github.com/CodSpeedHQ/codspeed-rust/releases)
- [Commits](https://github.com/CodSpeedHQ/codspeed-rust/compare/v2.3.0...v3.0.1)

---
updated-dependencies:
- dependency-name: codspeed-criterion-compat
  dependency-version: 3.0.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] 
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
 pyo3-benches/Cargo.toml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pyo3-benches/Cargo.toml b/pyo3-benches/Cargo.toml
index 9f0c4c67b08..7eeae6a3581 100644
--- a/pyo3-benches/Cargo.toml
+++ b/pyo3-benches/Cargo.toml
@@ -13,7 +13,7 @@ pyo3 = { path = "../", features = ["auto-initialize", "full"] }
 pyo3-build-config = { path = "../pyo3-build-config" }
 
 [dev-dependencies]
-codspeed-criterion-compat = "2.3"
+codspeed-criterion-compat = "3.0"
 criterion = "0.6.0"
 num-bigint = "0.4.3"
 rust_decimal = { version = "1.0.0", default-features = false }

From d4176c578322506e881b42228890127498f6d88d Mon Sep 17 00:00:00 2001
From: Cheuk Ting Ho 
Date: Fri, 4 Jul 2025 13:12:16 +0100
Subject: [PATCH 731/936] ci: Move netlify build into GitHub action (#5153)

* Move netlify build into GitHub action

* update rust nightly

* update rust nightly

* add back lychee

* move deploy to separate workflow

* add deployment secrets

* add safety comment

* move netlify cli install

---------

Co-authored-by: David Hewitt 
---
 .github/workflows/netlify-build.yml  | 81 ++++++++++++++++++++++++++++
 .github/workflows/netlify-deploy.yml | 41 ++++++++++++++
 .netlify/redirect.sh                 | 49 +++++++++++++++++
 3 files changed, 171 insertions(+)
 create mode 100644 .github/workflows/netlify-build.yml
 create mode 100644 .github/workflows/netlify-deploy.yml
 create mode 100644 .netlify/redirect.sh

diff --git a/.github/workflows/netlify-build.yml b/.github/workflows/netlify-build.yml
new file mode 100644
index 00000000000..4f6c8198c9d
--- /dev/null
+++ b/.github/workflows/netlify-build.yml
@@ -0,0 +1,81 @@
+name: netlify-build
+
+on:
+  push:
+    branches:
+      - main
+  pull_request:
+  release:
+    types: [published]
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
+  cancel-in-progress: true
+
+env:
+  CARGO_TERM_COLOR: always
+
+jobs:
+  guide-build:
+    runs-on: ubuntu-latest
+    outputs:
+      tag_name: ${{ steps.prepare_tag.outputs.tag_name }}
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/setup-python@v5
+        with:
+          python-version: '3.12'
+      - uses: actions/setup-node@v4
+        with:
+          node-version: '20.x'
+          registry-url: 'https://registry.npmjs.org/'
+
+      - uses: dtolnay/rust-toolchain@nightly
+
+      - name: Use nightly
+        run: |
+          rustup update nightly
+          rustup default nightly
+
+      - name: Setup mdBook
+        uses: taiki-e/install-action@v2
+        with:
+          tool: mdbook, lychee, mdbook-linkcheck
+
+      - name: Prepare tag
+        id: prepare_tag
+        run: |
+          TAG_NAME="${GITHUB_REF##*/}"
+          echo "::set-output name=tag_name::${TAG_NAME}"
+
+      # Start with the current guides.
+      - name: Get the guide
+        run: |
+          wget -qc https://github.com/PyO3/pyo3/archive/gh-pages.tar.gz -O - | tar -xz
+          mv pyo3-gh-pages netlify_build
+
+      # Set redirects.
+      - name: Set redirects
+        run: |
+          source .netlify/redirect.sh
+
+      # This builds the book and docs in netlify_build/main
+      - name: Build the guide and public docs
+        run: |
+          python -m pip install --upgrade pip && pip install nox towncrier
+          towncrier build --yes --version Unreleased --date TBC
+          nox -s check-guide
+          mv target/guide/ netlify_build/main/
+          nox -s docs
+          mv target/doc netlify_build/main/doc/
+          echo "" > netlify_build/main/doc/index.html
+          nox -s docs -- nightly internal
+          mkdir -p netlify_build/internal
+          mv target/doc netlify_build/internal/
+
+      # Upload the built site as an artifact for deploy workflow to consume
+      - name: Upload Build Artifact
+        uses: actions/upload-artifact@v4
+        with:
+          name: site
+          path: ./netlify_build
diff --git a/.github/workflows/netlify-deploy.yml b/.github/workflows/netlify-deploy.yml
new file mode 100644
index 00000000000..04100b139ce
--- /dev/null
+++ b/.github/workflows/netlify-deploy.yml
@@ -0,0 +1,41 @@
+# This runs as a separate job because it needs to run on the `workflow_run` event
+# in order to access the netlify secrets.
+#
+# This is safe because this doesn't run arbitrary code from PRs.
+
+name: netlify-deploy
+
+on:
+  workflow_run:
+    workflows: ['netlify-build']
+    types:
+      - completed
+
+env:
+  CARGO_TERM_COLOR: always
+
+jobs:
+  deploy:
+    runs-on: ubuntu-latest
+    if: github.event.workflow_run.conclusion == 'success'
+    environment: netlify
+
+    steps:
+      - name: Download Build Artifact
+        uses: actions/download-artifact@v4
+        with:
+          name: site
+          github-token: ${{ secrets.GITHUB_TOKEN }}
+          run-id: ${{ github.event.workflow_run.id }}
+
+      - name: Install netlify-cli
+        run: |
+          npm install -g netlify-cli
+
+      - name: Deploy to Netlify
+        run: |
+          ls -l netlify_build/
+          DEBUG=* netlify deploy \
+              --site ${{ secrets.NETLIFY_SITE_ID }} \
+              --auth ${{ secrets.NETLIFY_TOKEN }} \
+              ${{ github.event.workflow_run.head_branch == 'main' && '--prod' || '' }} \
diff --git a/.netlify/redirect.sh b/.netlify/redirect.sh
new file mode 100644
index 00000000000..46d2dbeedef
--- /dev/null
+++ b/.netlify/redirect.sh
@@ -0,0 +1,49 @@
+# Add redirect for each documented version
+set +x  # these loops get very spammy and fill the deploy log
+
+for d in netlify_build/v*; do
+    version="${d/netlify_build\/v/}"
+    echo "/v$version/doc/* https://docs.rs/pyo3/$version/:splat" >> netlify_build/_redirects
+    if [ $version != $PYO3_VERSION ]; then
+        # for old versions, mark the files in the latest version as the canonical URL
+        for file in $(find $d -type f); do
+            file_path="${file/$d\//}"
+            # remove index.html and/or .html suffix to match the page URL on the
+            # final netlfiy site
+            url_path="$file_path"
+            if [[ $file_path == index.html ]]; then
+                url_path=""
+            elif [[ $file_path == *.html ]]; then
+                url_path="${file_path%.html}"
+            fi
+            echo "/v$version/$url_path" >> netlify_build/_headers
+            if test -f "netlify_build/v$PYO3_VERSION/$file_path"; then
+                echo "  Link: ; rel=\"canonical\"" >> netlify_build/_headers
+            else
+                # this file doesn't exist in the latest guide, don't index it
+                echo "  X-Robots-Tag: noindex" >> netlify_build/_headers
+            fi
+        done
+    fi
+done
+
+# Add latest redirect
+echo "/latest/* /v${PYO3_VERSION}/:splat 302" >> netlify_build/_redirects
+
+# some backwards compatbiility redirects
+echo "/latest/building_and_distribution/* /latest/building-and-distribution/:splat 302" >> netlify_build/_redirects
+echo "/latest/building-and-distribution/multiple_python_versions/* /latest/building-and-distribution/multiple-python-versions:splat 302" >> netlify_build/_redirects
+echo "/latest/function/error_handling/* /latest/function/error-handling/:splat 302" >> netlify_build/_redirects
+echo "/latest/getting_started/* /latest/getting-started/:splat 302" >> netlify_build/_redirects
+echo "/latest/python_from_rust/* /latest/python-from-rust/:splat 302" >> netlify_build/_redirects
+echo "/latest/python_typing_hints/* /latest/python-typing-hints/:splat 302" >> netlify_build/_redirects
+echo "/latest/trait_bounds/* /latest/trait-bounds/:splat 302" >> netlify_build/_redirects
+
+## Add landing page redirect
+if [ "${CONTEXT}" == "deploy-preview" ]; then
+    echo "/ /main/" >> netlify_build/_redirects
+else
+    echo "/ /v${PYO3_VERSION}/ 302" >> netlify_build/_redirects
+fi
+
+set -x
\ No newline at end of file

From 834ad10f96c950af299e9cbaa4c043134dbf4034 Mon Sep 17 00:00:00 2001
From: Bas Schoenmaeckers <7943856+bschoenmaeckers@users.noreply.github.com>
Date: Fri, 4 Jul 2025 15:29:12 +0200
Subject: [PATCH 732/936] Move `MutexExt` return type into associated type
 (#5201)

---
 newsfragments/5201.changed.md |  1 +
 src/sync.rs                   | 45 ++++++++++++++++++++++-------------
 2 files changed, 29 insertions(+), 17 deletions(-)
 create mode 100644 newsfragments/5201.changed.md

diff --git a/newsfragments/5201.changed.md b/newsfragments/5201.changed.md
new file mode 100644
index 00000000000..a61a8ca992b
--- /dev/null
+++ b/newsfragments/5201.changed.md
@@ -0,0 +1 @@
+Move `MutexExt` return type into associated type
diff --git a/src/sync.rs b/src/sync.rs
index 276db6968ef..2492fef7a15 100644
--- a/src/sync.rs
+++ b/src/sync.rs
@@ -566,10 +566,12 @@ pub trait OnceLockExt: once_lock_ext_sealed::Sealed {
 
 /// Extension trait for [`std::sync::Mutex`] which helps avoid deadlocks between
 /// the Python interpreter and acquiring the `Mutex`.
-pub trait MutexExt<'a, T, R>: Sealed
-where
-    Self: 'a,
-{
+pub trait MutexExt: Sealed {
+    /// The result type returned by the `lock_py_attached` method.
+    type LockResult<'a>
+    where
+        Self: 'a;
+
     /// Lock this `Mutex` in a manner that cannot deadlock with the Python interpreter.
     ///
     /// Before attempting to lock the mutex, this function detaches from the
@@ -577,7 +579,7 @@ where
     /// 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(&'a self, py: Python<'_>) -> R;
+    fn lock_py_attached(&self, py: Python<'_>) -> Self::LockResult<'_>;
 }
 
 impl OnceExt for Once {
@@ -646,13 +648,16 @@ impl OnceLockExt for std::sync::OnceLock {
     }
 }
 
-impl<'a, T> MutexExt<'a, T, std::sync::LockResult>>
-    for std::sync::Mutex
-{
+impl MutexExt for std::sync::Mutex {
+    type LockResult<'a>
+        = std::sync::LockResult>
+    where
+        Self: 'a;
+
     fn lock_py_attached(
-        &'a self,
+        &self,
         _py: Python<'_>,
-    ) -> std::sync::LockResult> {
+    ) -> 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
@@ -675,10 +680,13 @@ impl<'a, T> MutexExt<'a, T, std::sync::LockResult>>
 }
 
 #[cfg(feature = "lock_api")]
-impl<'a, R: lock_api::RawMutex, T> MutexExt<'a, T, lock_api::MutexGuard<'a, R, T>>
-    for lock_api::Mutex
-{
-    fn lock_py_attached(&'a self, _py: Python<'_>) -> lock_api::MutexGuard<'a, R, T> {
+impl MutexExt for lock_api::Mutex {
+    type LockResult<'a>
+        = lock_api::MutexGuard<'a, R, T>
+    where
+        Self: 'a;
+
+    fn lock_py_attached(&self, _py: Python<'_>) -> lock_api::MutexGuard<'_, R, T> {
         if let Some(guard) = self.try_lock() {
             return guard;
         }
@@ -691,12 +699,15 @@ impl<'a, R: lock_api::RawMutex, T> MutexExt<'a, T, lock_api::MutexGuard<'a, R, T
 }
 
 #[cfg(feature = "arc_lock")]
-impl<'a, R, T> MutexExt<'a, T, lock_api::ArcMutexGuard>
-    for std::sync::Arc>
+impl MutexExt for std::sync::Arc>
 where
     R: lock_api::RawMutex,
-    Self: 'a,
 {
+    type LockResult<'a>
+        = lock_api::ArcMutexGuard
+    where
+        Self: 'a;
+
     fn lock_py_attached(&self, _py: Python<'_>) -> lock_api::ArcMutexGuard {
         if let Some(guard) = self.try_lock_arc() {
             return guard;

From ebfc854dc5021865ff59b4ed904870ea3f751ed4 Mon Sep 17 00:00:00 2001
From: Icxolu <10486322+Icxolu@users.noreply.github.com>
Date: Fri, 4 Jul 2025 18:30:11 +0200
Subject: [PATCH 733/936] rename `Python::allow_threads` to `Python::detach`
 (#5221)

* rename `Python::allow_threads` to `Python::detach`

* Update guide/src/migration.md

Co-authored-by: David Hewitt 

---------

Co-authored-by: David Hewitt 
---
 examples/word-count/src/lib.rs               |  6 +-
 examples/word-count/tests/test_word_count.py |  8 +--
 examples/word-count/word_count/__init__.py   |  4 +-
 guide/src/async-await.md                     |  2 +-
 guide/src/conversions/tables.md              |  2 +-
 guide/src/features.md                        |  2 +-
 guide/src/free-threading.md                  |  6 +-
 guide/src/migration.md                       | 12 +++-
 guide/src/parallelism.md                     | 24 ++++----
 newsfragments/5221.changed.md                |  1 +
 pytests/src/pyclasses.rs                     |  2 +-
 src/err/err_state.rs                         |  6 +-
 src/gil.rs                                   |  8 +--
 src/instance.rs                              |  2 +-
 src/marker.rs                                | 63 ++++++++++++--------
 src/pycell/impl_.rs                          |  2 +-
 src/types/bytearray.rs                       |  2 +-
 tests/test_class_basics.rs                   |  2 +-
 tests/test_pyfunction.rs                     |  2 +-
 tests/ui/not_send.rs                         |  6 +-
 tests/ui/not_send.stderr                     | 22 +++----
 tests/ui/not_send2.rs                        |  2 +-
 tests/ui/not_send2.stderr                    | 22 +++----
 23 files changed, 113 insertions(+), 95 deletions(-)
 create mode 100644 newsfragments/5221.changed.md

diff --git a/examples/word-count/src/lib.rs b/examples/word-count/src/lib.rs
index 5bc73df97a4..baa9cf18e57 100644
--- a/examples/word-count/src/lib.rs
+++ b/examples/word-count/src/lib.rs
@@ -17,8 +17,8 @@ fn search_sequential(contents: &str, needle: &str) -> usize {
 }
 
 #[pyfunction]
-fn search_sequential_allow_threads(py: Python<'_>, contents: &str, needle: &str) -> usize {
-    py.allow_threads(|| search_sequential(contents, needle))
+fn search_sequential_detached(py: Python<'_>, contents: &str, needle: &str) -> usize {
+    py.detach(|| search_sequential(contents, needle))
 }
 
 /// Count the occurrences of needle in line, case insensitive
@@ -36,7 +36,7 @@ fn count_line(line: &str, needle: &str) -> usize {
 fn word_count(m: &Bound<'_, PyModule>) -> PyResult<()> {
     m.add_function(wrap_pyfunction!(search, m)?)?;
     m.add_function(wrap_pyfunction!(search_sequential, m)?)?;
-    m.add_function(wrap_pyfunction!(search_sequential_allow_threads, m)?)?;
+    m.add_function(wrap_pyfunction!(search_sequential_detached, m)?)?;
 
     Ok(())
 }
diff --git a/examples/word-count/tests/test_word_count.py b/examples/word-count/tests/test_word_count.py
index 5991e4ae1de..5b665d8e29c 100644
--- a/examples/word-count/tests/test_word_count.py
+++ b/examples/word-count/tests/test_word_count.py
@@ -50,12 +50,8 @@ def test_word_count_python_sequential(benchmark, contents):
 def run_rust_sequential_twice(
     executor: ThreadPoolExecutor, contents: str, needle: str
 ) -> int:
-    future_1 = executor.submit(
-        word_count.search_sequential_allow_threads, contents, needle
-    )
-    future_2 = executor.submit(
-        word_count.search_sequential_allow_threads, contents, needle
-    )
+    future_1 = executor.submit(word_count.search_sequential_detached, contents, needle)
+    future_2 = executor.submit(word_count.search_sequential_detached, contents, needle)
     result_1 = future_1.result()
     result_2 = future_2.result()
     return result_1 + result_2
diff --git a/examples/word-count/word_count/__init__.py b/examples/word-count/word_count/__init__.py
index 8ce7a175471..4a7f1b5ee5e 100644
--- a/examples/word-count/word_count/__init__.py
+++ b/examples/word-count/word_count/__init__.py
@@ -1,10 +1,10 @@
-from .word_count import search, search_sequential, search_sequential_allow_threads
+from .word_count import search, search_sequential, search_sequential_detached
 
 __all__ = [
     "search_py",
     "search",
     "search_sequential",
-    "search_sequential_allow_threads",
+    "search_sequential_detached",
 ]
 
 
diff --git a/guide/src/async-await.md b/guide/src/async-await.md
index 59ea99b6ef8..bf2dd84f5b3 100644
--- a/guide/src/async-await.md
+++ b/guide/src/async-await.md
@@ -67,7 +67,7 @@ where
     fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll {
         let waker = cx.waker();
         Python::attach(|py| {
-            py.allow_threads(|| pin!(&mut self.0).poll(&mut Context::from_waker(waker)))
+            py.detach(|| pin!(&mut self.0).poll(&mut Context::from_waker(waker)))
         })
     }
 }
diff --git a/guide/src/conversions/tables.md b/guide/src/conversions/tables.md
index 23d6942b177..cec255178ed 100644
--- a/guide/src/conversions/tables.md
+++ b/guide/src/conversions/tables.md
@@ -67,7 +67,7 @@ Using Rust library types as function arguments will incur a conversion cost comp
 However, once that conversion cost has been paid, the Rust standard library types offer a number of benefits:
 - You can write functionality in native-speed Rust code (free of Python's runtime costs).
 - You get better interoperability with the rest of the Rust ecosystem.
-- You can use `Python::allow_threads` to release the Python GIL and let other Python threads make progress while your Rust code is executing.
+- You can use `Python::detach` to release the Python GIL and let other Python threads make progress while your Rust code is executing.
 - You also benefit from stricter type checking. For example you can specify `Vec`, which will only accept a Python `list` containing integers. The Python-native equivalent, `&PyList`, would accept a Python `list` containing Python objects of any type.
 
 For most PyO3 usage the conversion cost is worth paying to get these benefits. As always, if you're not sure it's worth it in your case, benchmark it!
diff --git a/guide/src/features.md b/guide/src/features.md
index 8339816310c..13c2d2b8d8f 100644
--- a/guide/src/features.md
+++ b/guide/src/features.md
@@ -105,7 +105,7 @@ See [the `#[pyclass]` implementation details](class.md#implementation-details) f
 
 ### `nightly`
 
-The `nightly` feature needs the nightly Rust compiler. This allows PyO3 to use the `auto_traits` and `negative_impls` features to fix the `Python::allow_threads` function.
+The `nightly` feature needs the nightly Rust compiler. This allows PyO3 to use the `auto_traits` and `negative_impls` features to fix the `Python::detach` function.
 
 ### `resolve-config`
 
diff --git a/guide/src/free-threading.md b/guide/src/free-threading.md
index 96866d32f04..3af858029f3 100644
--- a/guide/src/free-threading.md
+++ b/guide/src/free-threading.md
@@ -180,7 +180,7 @@ 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
+[`Python::detach`] 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
@@ -197,7 +197,7 @@ 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 [`Python::allow_threads`] or calling a python
+code that releases the GIL with [`Python::detach`] 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
@@ -402,7 +402,7 @@ interpreter.
 [`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#method.get_or_init
-[`Python::allow_threads`]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.allow_threads
+[`Python::detach`]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.detach
 [`Python::attach`]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.attach
 [`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 91c00ce5017..6d7ba316185 100644
--- a/guide/src/migration.md
+++ b/guide/src/migration.md
@@ -3,9 +3,19 @@
 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.25.* to 0.26
+### Rename of `Python::with_gil` and `Python::allow_threads`
+
+Click to expand +The names for these APIs were created when the global interpreter lock (GIL) was mandatory. With the introduction of free-threading in Python 3.13 this is no longer the case, and the naming does not has no universal meaning anymore. +For this reason we chose to rename these to more modern terminology introduced in free-threading: +- `Python::with_gil` is now called `Python::attach`, it attaches a Python thread-state to the current thread. In GIL enabled builds there can only be 1 thread attached to the interpreter, in free-threading there can be more. +- `Python::allow_threads` is now called `Python::detach`, it detaches a previously attached thread-state. +
+ ## from 0.24.* to 0.25 ### `AsPyPointer` removal -
+
Click to expand The `AsPyPointer` trait is mostly a leftover from the now removed gil-refs API. The last remaining uses were the GC API, namely `PyVisit::call`, and identity comparison (`PyAnyMethods::is` and `Py::is`). diff --git a/guide/src/parallelism.md b/guide/src/parallelism.md index c215211dce4..c9c01d4f3b8 100644 --- a/guide/src/parallelism.md +++ b/guide/src/parallelism.md @@ -49,7 +49,7 @@ fn search_sequential(contents: &str, needle: &str) -> usize { } ``` -To enable parallel execution of this function, the [`Python::allow_threads`] method can be used to temporarily release the GIL, thus allowing other Python threads to run. We then have a function exposed to the Python runtime which calls `search_sequential` inside a closure passed to [`Python::allow_threads`] to enable true parallelism: +To enable parallel execution of this function, the [`Python::detach`] method can be used to temporarily release the GIL, thus allowing other Python threads to run. We then have a function exposed to the Python runtime which calls `search_sequential` inside a closure passed to [`Python::detach`] to enable true parallelism: ```rust,no_run # #![allow(dead_code)] # use pyo3::prelude::*; @@ -68,23 +68,23 @@ To enable parallel execution of this function, the [`Python::allow_threads`] met # contents.lines().map(|line| count_line(line, needle)).sum() # } #[pyfunction] -fn search_sequential_allow_threads(py: Python<'_>, contents: &str, needle: &str) -> usize { - py.allow_threads(|| search_sequential(contents, needle)) +fn search_sequential_detached(py: Python<'_>, contents: &str, needle: &str) -> usize { + py.detach(|| search_sequential(contents, needle)) } ``` Now Python threads can use more than one CPU core, resolving the limitation which usually makes multi-threading in Python only good for IO-bound tasks: ```Python from concurrent.futures import ThreadPoolExecutor -from word_count import search_sequential_allow_threads +from word_count import search_sequential_detached executor = ThreadPoolExecutor(max_workers=2) future_1 = executor.submit( - word_count.search_sequential_allow_threads, contents, needle + word_count.search_sequential_detached, contents, needle ) future_2 = executor.submit( - word_count.search_sequential_allow_threads, contents, needle + word_count.search_sequential_detached, contents, needle ) result_1 = future_1.result() result_2 = future_2.result() @@ -149,7 +149,7 @@ struct UserID { let allowed_ids: Vec = Python::attach(|outer_py| { let instances: Vec> = (0..10).map(|x| Py::new(outer_py, UserID { id: x }).unwrap()).collect(); - outer_py.allow_threads(|| { + outer_py.detach(|| { instances.par_iter().map(|instance| { Python::attach(|inner_py| { instance.borrow(inner_py).id > 5 @@ -165,13 +165,13 @@ 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 +It's also important to see that this example uses [`Python::detach`] 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 +`detach`, 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 +thread. Calling `detach` 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 +`detach` 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 +[`Python::detach`]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.detach diff --git a/newsfragments/5221.changed.md b/newsfragments/5221.changed.md new file mode 100644 index 00000000000..afc86a3dd12 --- /dev/null +++ b/newsfragments/5221.changed.md @@ -0,0 +1 @@ +rename `Python::allow_threads` to `Python::detach` \ No newline at end of file diff --git a/pytests/src/pyclasses.rs b/pytests/src/pyclasses.rs index 4e681b2c941..d9ec2547478 100644 --- a/pytests/src/pyclasses.rs +++ b/pytests/src/pyclasses.rs @@ -62,7 +62,7 @@ impl PyClassThreadIter { let current_count = self.count; self.count += 1; if current_count == 0 { - py.allow_threads(|| thread::sleep(time::Duration::from_millis(100))); + py.detach(|| thread::sleep(time::Duration::from_millis(100))); } self.count } diff --git a/src/err/err_state.rs b/src/err/err_state.rs index f9a64c96d25..1a8a54186cd 100644 --- a/src/err/err_state.rs +++ b/src/err/err_state.rs @@ -45,7 +45,7 @@ impl PyErrState { pub(crate) fn normalized(normalized: PyErrStateNormalized) -> Self { 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 + // reaching the `py.detach` 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(|| {}); @@ -98,7 +98,7 @@ impl PyErrState { } // avoid deadlock of `.call_once` with the GIL - py.allow_threads(|| { + py.detach(|| { self.normalized.call_once(|| { self.normalizing_thread .lock() @@ -406,7 +406,7 @@ mod tests { 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(|| { + py.detach(|| { std::thread::sleep(std::time::Duration::from_millis(10)); }); py.None() diff --git a/src/gil.rs b/src/gil.rs index e4cea3ff2a0..ade83e6c412 100644 --- a/src/gil.rs +++ b/src/gil.rs @@ -551,13 +551,13 @@ mod tests { } #[test] - fn test_allow_threads() { + fn test_detach() { assert!(!gil_is_acquired()); Python::attach(|py| { assert!(gil_is_acquired()); - py.allow_threads(move || { + py.detach(move || { assert!(!gil_is_acquired()); Python::attach(|_| assert!(gil_is_acquired())); @@ -574,13 +574,13 @@ mod tests { #[cfg(feature = "py-clone")] #[test] #[should_panic] - fn test_allow_threads_updates_refcounts() { + fn test_detach_updates_refcounts() { Python::attach(|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 which should panic - py.allow_threads(|| obj.clone()); + py.detach(|| obj.clone()); }); } diff --git a/src/instance.rs b/src/instance.rs index 2fe67677dc9..bc9c949516f 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -58,7 +58,7 @@ mod bound_object_sealed { /// /// 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. +/// released, such as [`Python::detach`](crate::Python::detach)'s closure. /// /// See #[doc = concat!("[the guide](https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/types.html#boundpy-t)")] diff --git a/src/marker.rs b/src/marker.rs index bcf5d2fcb48..860405ab0ac 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -14,7 +14,7 @@ //! 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 +//! That API is provided by [`Python::detach`] and enforced via the [`Ungil`] bound on the //! closure and the return type. This is done by relying on the [`Send`] auto trait. `Ungil` is //! defined as the following: //! @@ -34,7 +34,7 @@ //! ## Drawbacks //! //! There is no reason to prevent `!Send` types like [`Rc`] from crossing the closure. After all, -//! [`Python::allow_threads`] just lets other Python threads run - it does not itself launch a new +//! [`Python::detach`] just lets other Python threads run - it does not itself launch a new //! thread. //! //! ```rust, compile_fail @@ -47,7 +47,7 @@ //! Python::attach(|py| { //! let rc = Rc::new(5); //! -//! py.allow_threads(|| { +//! py.detach(|| { //! // This would actually be fine... //! println!("{:?}", *rc); //! }); @@ -77,7 +77,7 @@ //! //! let wrapped = SendWrapper::new(string); //! -//! py.allow_threads(|| { +//! py.detach(|| { //! # #[cfg(not(feature = "nightly"))] //! # { //! // 💥 Unsound! 💥 @@ -154,13 +154,13 @@ use std::os::raw::c_int; /// Python::attach(|py| { /// let rc = Rc::new(42); /// -/// py.allow_threads(|| { +/// py.detach(|| { /// println!("{:?}", rc); /// }); /// }); /// ``` /// -/// This also implies that the interplay between `attach` and `allow_threads` is unsound, for example +/// This also implies that the interplay between `attach` and `detach` is unsound, for example /// one can circumvent this protection using the [`send_wrapper`](https://docs.rs/send_wrapper/) crate: /// /// ```no_run @@ -173,7 +173,7 @@ use std::os::raw::c_int; /// /// let wrapped = SendWrapper::new(string); /// -/// py.allow_threads(|| { +/// py.detach(|| { /// let sneaky: &Bound<'_, PyString> = &*wrapped; /// /// println!("{:?}", sneaky); @@ -217,7 +217,7 @@ mod nightly { /// Python::attach(|py| { /// let string = PyString::new(py, "foo"); /// - /// py.allow_threads(|| { + /// py.detach(|| { /// println!("{:?}", string); /// }); /// }); @@ -228,7 +228,7 @@ mod nightly { /// ```compile_fail /// # use pyo3::prelude::*; /// Python::attach(|py| { - /// py.allow_threads(|| { + /// py.detach(|| { /// drop(py); /// }); /// }); @@ -247,7 +247,7 @@ mod nightly { /// /// let wrapped = SendWrapper::new(string); /// - /// py.allow_threads(|| { + /// py.detach(|| { /// let sneaky: &PyString = *wrapped; /// /// println!("{:?}", sneaky); @@ -255,7 +255,7 @@ mod nightly { /// }); /// ``` /// - /// This also enables using non-[`Send`] types in `allow_threads`, + /// This also enables using non-[`Send`] types in `detach`, /// at least if they are not also bound to the GIL: /// /// ```rust @@ -265,7 +265,7 @@ mod nightly { /// Python::attach(|py| { /// let rc = Rc::new(42); /// - /// py.allow_threads(|| { + /// py.detach(|| { /// println!("{:?}", rc); /// }); /// }); @@ -347,7 +347,7 @@ pub use nightly::Ungil; /// * Thread 1's Python interpreter call blocks trying to reacquire the GIL held by thread 2 /// /// To avoid deadlocking, you should release the GIL before trying to lock a mutex or `await`ing in -/// asynchronous code, e.g. with [`Python::allow_threads`]. +/// asynchronous code, e.g. with [`Python::detach`]. /// /// # Releasing and freeing memory /// @@ -457,6 +457,17 @@ impl Python<'_> { } impl<'py> Python<'py> { + /// See [Python::detach] + #[inline] + #[deprecated(note = "use `Python::detach` instead", since = "0.26.0")] + pub fn allow_threads(self, f: F) -> T + where + F: Ungil + FnOnce() -> T, + T: Ungil, + { + self.detach(f) + } + /// Temporarily releases the GIL, thus allowing other Python threads to run. The GIL will be /// reacquired when `F`'s scope ends. /// @@ -479,7 +490,7 @@ impl<'py> Python<'py> { /// #[pyfunction] /// fn sum_numbers(py: Python<'_>, numbers: Vec) -> PyResult { /// // We release the GIL here so any other Python threads get a chance to run. - /// py.allow_threads(move || { + /// py.detach(move || { /// // An example of an "expensive" Rust calculation /// let sum = numbers.iter().sum(); /// @@ -498,7 +509,7 @@ impl<'py> Python<'py> { /// ``` /// /// Please see the [Parallelism] chapter of the guide for a thorough discussion of using - /// [`Python::allow_threads`] in this manner. + /// [`Python::detach`] in this manner. /// /// # Example: Passing borrowed Python references into the closure is not allowed /// @@ -508,7 +519,7 @@ impl<'py> Python<'py> { /// /// fn parallel_print(py: Python<'_>) { /// let s = PyString::new(py, "This object cannot be accessed without holding the GIL >_<"); - /// py.allow_threads(move || { + /// py.detach(move || { /// println!("{:?}", s); // This causes a compile error. /// }); /// } @@ -518,7 +529,7 @@ impl<'py> Python<'py> { /// [`PyString`]: crate::types::PyString /// [auto-traits]: https://doc.rust-lang.org/nightly/unstable-book/language-features/auto-traits.html /// [Parallelism]: https://pyo3.rs/main/parallelism.html - pub fn allow_threads(self, f: F) -> T + pub fn detach(self, f: F) -> T where F: Ungil + FnOnce() -> T, T: Ungil, @@ -869,21 +880,21 @@ mod tests { #[test] #[cfg(not(target_arch = "wasm32"))] // We are building wasm Python with pthreads disabled - fn test_allow_threads_releases_and_acquires_gil() { + fn test_detach_releases_and_acquires_gil() { Python::attach(|py| { let b = std::sync::Arc::new(std::sync::Barrier::new(2)); let b2 = b.clone(); std::thread::spawn(move || Python::attach(|_| b2.wait())); - py.allow_threads(|| { - // If allow_threads does not release the GIL, this will deadlock because + py.detach(|| { + // If `detach` does not release the GIL, this will deadlock because // the thread spawned above will never be able to acquire the GIL. b.wait(); }); unsafe { - // If the GIL is not reacquired at the end of allow_threads, this call + // If the GIL is not reacquired at the end of `detach`, this call // will crash the Python interpreter. let tstate = ffi::PyEval_SaveThread(); ffi::PyEval_RestoreThread(tstate); @@ -892,11 +903,11 @@ mod tests { } #[test] - fn test_allow_threads_panics_safely() { + fn test_detach_panics_safely() { Python::attach(|py| { let result = std::panic::catch_unwind(|| unsafe { let py = Python::assume_gil_acquired(); - py.allow_threads(|| { + py.detach(|| { panic!("There was a panic!"); }); }); @@ -904,7 +915,7 @@ mod tests { // Check panic was caught assert!(result.is_err()); - // If allow_threads is implemented correctly, this thread still owns the GIL here + // If `detach` 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]).unwrap(); assert_eq!(list.extract::>().unwrap(), vec![1, 2, 3, 4]); @@ -913,13 +924,13 @@ mod tests { #[cfg(not(pyo3_disable_reference_pool))] #[test] - fn test_allow_threads_pass_stuff_in() { + fn test_detach_pass_stuff_in() { let list = Python::attach(|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")); Python::attach(|py| { - py.allow_threads(|| { + py.detach(|| { drop((list, &mut v, a)); }); }); diff --git a/src/pycell/impl_.rs b/src/pycell/impl_.rs index 49f4bf32f72..c3d697d56d0 100644 --- a/src/pycell/impl_.rs +++ b/src/pycell/impl_.rs @@ -555,7 +555,7 @@ mod tests { Python::attach(|py| { let inst = Py::new(py, MyClass { x: 0 }).unwrap(); - let total_modifications = py.allow_threads(|| { + let total_modifications = py.detach(|| { std::thread::scope(|s| { // Spawn a bunch of threads all racing to write to // the same instance of `MyClass`. diff --git a/src/types/bytearray.rs b/src/types/bytearray.rs index 12634b0ac2d..5aa7acceb86 100644 --- a/src/types/bytearray.rs +++ b/src/types/bytearray.rs @@ -192,7 +192,7 @@ pub trait PyByteArrayMethods<'py>: crate::sealed::Sealed { /// /// // This explicitly yields control back to the Python interpreter... /// // ...but it's not always this obvious. Many things do this implicitly. - /// py.allow_threads(|| { + /// py.detach(|| { /// // Python code could be mutating through its handle to `bytes`, /// // which makes reading it a data race, which is undefined behavior. /// println!("{:?}", slice[0]); diff --git a/tests/test_class_basics.rs b/tests/test_class_basics.rs index 8c3efe07aa5..d085db60351 100644 --- a/tests/test_class_basics.rs +++ b/tests/test_class_basics.rs @@ -650,7 +650,7 @@ fn drop_unsendable_elsewhere() { ) .unwrap(); - py.allow_threads(|| { + py.detach(|| { spawn(move || { Python::attach(move |_py| { drop(unsendable); diff --git a/tests/test_pyfunction.rs b/tests/test_pyfunction.rs index 02e184ead06..39790e34652 100644 --- a/tests/test_pyfunction.rs +++ b/tests/test_pyfunction.rs @@ -569,7 +569,7 @@ fn return_value_borrows_from_arguments<'py>( key: &'py Key, value: &'py Value, ) -> HashMap<&'py str, i32> { - py.allow_threads(move || { + py.detach(move || { let mut map = HashMap::new(); map.insert(key.0.as_str(), value.0); map diff --git a/tests/ui/not_send.rs b/tests/ui/not_send.rs index 291bd7470f8..926343538aa 100644 --- a/tests/ui/not_send.rs +++ b/tests/ui/not_send.rs @@ -1,11 +1,11 @@ use pyo3::prelude::*; -fn test_not_send_allow_threads(py: Python<'_>) { - py.allow_threads(|| { drop(py); }); +fn test_not_send_detach(py: Python<'_>) { + py.detach(|| { drop(py); }); } fn main() { Python::attach(|py| { - test_not_send_allow_threads(py); + test_not_send_detach(py); }) } diff --git a/tests/ui/not_send.stderr b/tests/ui/not_send.stderr index 5d05b3de059..7987c34c25a 100644 --- a/tests/ui/not_send.stderr +++ b/tests/ui/not_send.stderr @@ -1,8 +1,8 @@ error[E0277]: `*mut pyo3::Python<'static>` cannot be shared between threads safely - --> tests/ui/not_send.rs:4:22 + --> tests/ui/not_send.rs:4:15 | -4 | py.allow_threads(|| { drop(py); }); - | ------------- ^^^^^^^^^^^^^^^^ `*mut pyo3::Python<'static>` cannot be shared between threads safely +4 | py.detach(|| { drop(py); }); + | ------ ^^^^^^^^^^^^^^^^ `*mut pyo3::Python<'static>` cannot be shared between threads safely | | | required by a bound introduced by this call | @@ -30,16 +30,16 @@ note: required because it appears within the type `pyo3::Python<'_>` | ^^^^^^ = note: required for `&pyo3::Python<'_>` to implement `Send` note: required because it's used within this closure - --> tests/ui/not_send.rs:4:22 + --> tests/ui/not_send.rs:4:15 | -4 | py.allow_threads(|| { drop(py); }); - | ^^ - = note: required for `{closure@$DIR/tests/ui/not_send.rs:4:22: 4:24}` to implement `Ungil` -note: required by a bound in `pyo3::Python::<'py>::allow_threads` +4 | py.detach(|| { drop(py); }); + | ^^ + = note: required for `{closure@$DIR/tests/ui/not_send.rs:4:15: 4:17}` to implement `Ungil` +note: required by a bound in `pyo3::Python::<'py>::detach` --> src/marker.rs | - | pub fn allow_threads(self, f: F) -> T - | ------------- required by a bound in this associated function + | pub fn detach(self, f: F) -> T + | ------ required by a bound in this associated function | where | F: Ungil + FnOnce() -> T, - | ^^^^^ required by this bound in `Python::<'py>::allow_threads` + | ^^^^^ required by this bound in `Python::<'py>::detach` diff --git a/tests/ui/not_send2.rs b/tests/ui/not_send2.rs index 9b76bbe591a..78c73d5e9c8 100644 --- a/tests/ui/not_send2.rs +++ b/tests/ui/not_send2.rs @@ -5,7 +5,7 @@ fn main() { Python::attach(|py| { let string = PyString::new(py, "foo"); - py.allow_threads(|| { + py.detach(|| { println!("{:?}", string); }); }); diff --git a/tests/ui/not_send2.stderr b/tests/ui/not_send2.stderr index eec2b644e9a..f01bb9b5208 100644 --- a/tests/ui/not_send2.stderr +++ b/tests/ui/not_send2.stderr @@ -1,8 +1,8 @@ error[E0277]: `*mut pyo3::Python<'static>` cannot be shared between threads safely - --> tests/ui/not_send2.rs:8:26 + --> tests/ui/not_send2.rs:8:19 | -8 | py.allow_threads(|| { - | ____________-------------_^ +8 | py.detach(|| { + | ____________------_^ | | | | | required by a bound introduced by this call 9 | | println!("{:?}", string); @@ -38,16 +38,16 @@ note: required because it appears within the type `pyo3::Bound<'_, PyString>` | ^^^^^ = note: required for `&pyo3::Bound<'_, PyString>` to implement `Send` note: required because it's used within this closure - --> tests/ui/not_send2.rs:8:26 + --> tests/ui/not_send2.rs:8:19 | -8 | py.allow_threads(|| { - | ^^ - = note: required for `{closure@$DIR/tests/ui/not_send2.rs:8:26: 8:28}` to implement `Ungil` -note: required by a bound in `pyo3::Python::<'py>::allow_threads` +8 | py.detach(|| { + | ^^ + = note: required for `{closure@$DIR/tests/ui/not_send2.rs:8:19: 8:21}` to implement `Ungil` +note: required by a bound in `pyo3::Python::<'py>::detach` --> src/marker.rs | - | pub fn allow_threads(self, f: F) -> T - | ------------- required by a bound in this associated function + | pub fn detach(self, f: F) -> T + | ------ required by a bound in this associated function | where | F: Ungil + FnOnce() -> T, - | ^^^^^ required by this bound in `Python::<'py>::allow_threads` + | ^^^^^ required by this bound in `Python::<'py>::detach` From e85b5693d386f446135e6783b02af6c8bb883e2a Mon Sep 17 00:00:00 2001 From: Thomas Tanon Date: Fri, 4 Jul 2025 19:01:42 +0200 Subject: [PATCH 734/936] Introspection: implement output type (#5208) - Adds a new 'returns' field in the introspection data that contains the returned type annotation - Generate it using a new IntoPyObject::OUTPUT_TYPE associated constant (similar to INPUT_TYPE) - Add the internal PyReturnType trait to allow getting easily T::RETURN_TYPE when the function returns Result - Take care of the special case of `()` converted to `None` and not `()` - Adding `OUTPUT_TYPE` to the `IntoPyObject` will be a follow-up (to avoid a huge PR) --- newsfragments/5208.added.md | 1 + pyo3-introspection/src/introspection.rs | 13 ++++++-- pyo3-introspection/src/model.rs | 2 ++ pyo3-introspection/src/stubs.rs | 33 ++++++++++++------- pyo3-macros-backend/src/introspection.rs | 29 +++++++++++++++- pyo3-macros-backend/src/method.rs | 4 +++ pyo3-macros-backend/src/pyclass.rs | 6 ++++ pyo3-macros-backend/src/pyfunction.rs | 3 ++ pyo3-macros-backend/src/pyimpl.rs | 3 ++ pytests/stubs/pyclasses.pyi | 28 ++++++++-------- pytests/stubs/pyfunctions.pyi | 18 +++++----- src/conversion.rs | 10 ++++++ src/conversions/std/num.rs | 42 ++++++++++++++++++++++++ src/impl_.rs | 2 ++ src/impl_/introspection.rs | 17 ++++++++++ tests/test_compile_error.rs | 1 + 16 files changed, 176 insertions(+), 36 deletions(-) create mode 100644 newsfragments/5208.added.md create mode 100644 src/impl_/introspection.rs diff --git a/newsfragments/5208.added.md b/newsfragments/5208.added.md new file mode 100644 index 00000000000..13f44a20a19 --- /dev/null +++ b/newsfragments/5208.added.md @@ -0,0 +1 @@ +Introspection and sub generation: add basic return type support \ No newline at end of file diff --git a/pyo3-introspection/src/introspection.rs b/pyo3-introspection/src/introspection.rs index 41553059558..0d77532ab6f 100644 --- a/pyo3-introspection/src/introspection.rs +++ b/pyo3-introspection/src/introspection.rs @@ -121,7 +121,8 @@ fn convert_members( arguments, parent: _, decorators, - } => functions.push(convert_function(name, arguments, decorators)), + returns, + } => functions.push(convert_function(name, arguments, decorators, returns)), } } Ok((modules, classes, functions)) @@ -170,7 +171,12 @@ fn convert_class( }) } -fn convert_function(name: &str, arguments: &ChunkArguments, decorators: &[String]) -> Function { +fn convert_function( + name: &str, + arguments: &ChunkArguments, + decorators: &[String], + returns: &Option, +) -> Function { Function { name: name.into(), decorators: decorators.to_vec(), @@ -187,6 +193,7 @@ fn convert_function(name: &str, arguments: &ChunkArguments, decorators: &[String .as_ref() .map(convert_variable_length_argument), }, + returns: returns.clone(), } } @@ -382,6 +389,8 @@ enum Chunk { parent: Option, #[serde(default)] decorators: Vec, + #[serde(default)] + returns: Option, }, } diff --git a/pyo3-introspection/src/model.rs b/pyo3-introspection/src/model.rs index 85ed57901a1..0ca7cf50fc1 100644 --- a/pyo3-introspection/src/model.rs +++ b/pyo3-introspection/src/model.rs @@ -19,6 +19,8 @@ pub struct Function { /// decorator like 'property' or 'staticmethod' pub decorators: Vec, pub arguments: Arguments, + /// return type + pub returns: Option, } #[derive(Debug, Eq, PartialEq, Clone, Hash)] diff --git a/pyo3-introspection/src/stubs.rs b/pyo3-introspection/src/stubs.rs index e60223d27e7..5f419aa0b19 100644 --- a/pyo3-introspection/src/stubs.rs +++ b/pyo3-introspection/src/stubs.rs @@ -108,17 +108,22 @@ fn function_stubs(function: &Function, modules_to_import: &mut BTreeSet) if let Some(argument) = &function.arguments.kwarg { parameters.push(format!("**{}", variable_length_argument_stub(argument))); } - let output = format!("def {}({}): ...", function.name, parameters.join(", ")); - if function.decorators.is_empty() { - return output; - } let mut buffer = String::new(); for decorator in &function.decorators { buffer.push('@'); buffer.push_str(decorator); buffer.push('\n'); } - buffer.push_str(&output); + buffer.push_str("def "); + buffer.push_str(&function.name); + buffer.push('('); + buffer.push_str(¶meters.join(", ")); + buffer.push(')'); + if let Some(returns) = &function.returns { + buffer.push_str(" -> "); + buffer.push_str(annotation_stub(returns, modules_to_import)); + } + buffer.push_str(": ..."); buffer } @@ -132,11 +137,7 @@ fn argument_stub(argument: &Argument, modules_to_import: &mut BTreeSet) let mut output = argument.name.clone(); if let Some(annotation) = &argument.annotation { output.push_str(": "); - output.push_str(annotation); - if let Some((module, _)) = annotation.rsplit_once('.') { - // TODO: this is very naive - modules_to_import.insert(module.into()); - } + output.push_str(annotation_stub(annotation, modules_to_import)); } if let Some(default_value) = &argument.default_value { output.push_str(if argument.annotation.is_some() { @@ -153,6 +154,14 @@ fn variable_length_argument_stub(argument: &VariableLengthArgument) -> String { argument.name.clone() } +fn annotation_stub<'a>(annotation: &'a str, modules_to_import: &mut BTreeSet) -> &'a str { + if let Some((module, _)) = annotation.rsplit_once('.') { + // TODO: this is very naive + modules_to_import.insert(module.into()); + } + annotation +} + #[cfg(test)] mod tests { use super::*; @@ -186,9 +195,10 @@ mod tests { name: "kwarg".into(), }), }, + returns: Some("list[str]".into()), }; assert_eq!( - "def func(posonly, /, arg, *varargs, karg: str, **kwarg): ...", + "def func(posonly, /, arg, *varargs, karg: str, **kwarg) -> list[str]: ...", function_stubs(&function, &mut BTreeSet::new()) ) } @@ -217,6 +227,7 @@ mod tests { }], kwarg: None, }, + returns: None, }; assert_eq!( "def afunc(posonly=1, /, arg=True, *, karg: str = \"foo\"): ...", diff --git a/pyo3-macros-backend/src/introspection.rs b/pyo3-macros-backend/src/introspection.rs index bdd0eed6200..c0e9ab2ddd3 100644 --- a/pyo3-macros-backend/src/introspection.rs +++ b/pyo3-macros-backend/src/introspection.rs @@ -21,7 +21,7 @@ use std::mem::take; use std::sync::atomic::{AtomicUsize, Ordering}; use syn::ext::IdentExt; use syn::visit_mut::{visit_type_mut, VisitMut}; -use syn::{Attribute, Ident, Type, TypePath}; +use syn::{Attribute, Ident, ReturnType, Type, TypePath}; static GLOBAL_COUNTER_FOR_UNIQUE_NAMES: AtomicUsize = AtomicUsize::new(0); @@ -99,12 +99,14 @@ pub fn class_introspection_code( .emit(pyo3_crate_path) } +#[allow(clippy::too_many_arguments)] pub fn function_introspection_code( pyo3_crate_path: &PyO3CratePath, ident: Option<&Ident>, name: &str, signature: &FunctionSignature<'_>, first_argument: Option<&'static str>, + returns: ReturnType, decorators: impl IntoIterator, parent: Option<&Type>, ) -> TokenStream { @@ -115,6 +117,25 @@ pub fn function_introspection_code( "arguments", arguments_introspection_data(signature, first_argument, parent), ), + ( + "returns", + match returns { + ReturnType::Default => IntrospectionNode::String("None".into()), + ReturnType::Type(_, ty) => match *ty { + Type::Tuple(t) if t.elems.is_empty() => { + // () is converted to None in return types + IntrospectionNode::String("None".into()) + } + mut ty => { + if let Some(class_type) = parent { + replace_self(&mut ty, class_type); + } + ty = ty.elide_lifetimes(); + IntrospectionNode::OutputType { rust_type: ty } + } + }, + }, + ), ]); if let Some(ident) = ident { desc.insert( @@ -290,6 +311,7 @@ enum IntrospectionNode<'a> { String(Cow<'a, str>), IntrospectionId(Option>), InputType { rust_type: Type, nullable: bool }, + OutputType { rust_type: Type }, Map(HashMap<&'static str, IntrospectionNode<'a>>), List(Vec>), } @@ -340,6 +362,11 @@ impl IntrospectionNode<'_> { } content.push_str("\""); } + Self::OutputType { rust_type } => { + content.push_str("\""); + content.push_tokens(quote! { <#rust_type as #pyo3_crate_path::impl_::introspection::PyReturnType>::OUTPUT_TYPE }); + content.push_str("\""); + } Self::Map(map) => { content.push_str("{"); for (i, (key, value)) in map.into_iter().enumerate() { diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index 862dd3d89b6..df316c611ca 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -445,6 +445,8 @@ pub struct FnSpec<'a> { pub asyncness: Option, pub unsafety: Option, pub warnings: Vec, + #[cfg(feature = "experimental-inspect")] + pub output: syn::ReturnType, } pub fn parse_method_receiver(arg: &syn::FnArg) -> Result { @@ -526,6 +528,8 @@ impl<'a> FnSpec<'a> { asyncness: sig.asyncness, unsafety: sig.unsafety, warnings, + #[cfg(feature = "experimental-inspect")] + output: sig.output.clone(), }) } diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 449ede64832..4c402430597 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -1670,6 +1670,8 @@ fn complex_enum_struct_variant_new<'a>( asyncness: None, unsafety: None, warnings: vec![], + #[cfg(feature = "experimental-inspect")] + output: syn::ReturnType::Default, }; crate::pymethod::impl_py_method_def_new(&variant_cls_type, &spec, ctx) @@ -1725,6 +1727,8 @@ fn complex_enum_tuple_variant_new<'a>( asyncness: None, unsafety: None, warnings: vec![], + #[cfg(feature = "experimental-inspect")] + output: syn::ReturnType::Default, }; crate::pymethod::impl_py_method_def_new(&variant_cls_type, &spec, ctx) @@ -1750,6 +1754,8 @@ fn complex_enum_variant_field_getter<'a>( asyncness: None, unsafety: None, warnings: vec![], + #[cfg(feature = "experimental-inspect")] + output: syn::ReturnType::Type(Token![->](field_span), Box::new(variant_cls_type.clone())), }; let property_type = crate::pymethod::PropertyType::Function { diff --git a/pyo3-macros-backend/src/pyfunction.rs b/pyo3-macros-backend/src/pyfunction.rs index b24ea21289d..46f68f6ce28 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -390,6 +390,7 @@ pub fn impl_wrap_pyfunction( &name.to_string(), &signature, None, + func.sig.output.clone(), [] as [String; 0], None, ); @@ -410,6 +411,8 @@ pub fn impl_wrap_pyfunction( asyncness: func.sig.asyncness, unsafety: func.sig.unsafety, warnings, + #[cfg(feature = "experimental-inspect")] + output: func.sig.output.clone(), }; let wrapper_ident = format_ident!("__pyfunction_{}", spec.name); diff --git a/pyo3-macros-backend/src/pyimpl.rs b/pyo3-macros-backend/src/pyimpl.rs index 12f2271b360..575cd079ef4 100644 --- a/pyo3-macros-backend/src/pyimpl.rs +++ b/pyo3-macros-backend/src/pyimpl.rs @@ -367,6 +367,7 @@ fn method_introspection_code(spec: &FnSpec<'_>, parent: &syn::Type, ctx: &Ctx) - // We introduce self/cls argument and setup decorators let mut first_argument = None; + let mut output = spec.output.clone(); let mut decorators = Vec::new(); match &spec.tp { FnType::Getter(_) => { @@ -382,6 +383,7 @@ fn method_introspection_code(spec: &FnSpec<'_>, parent: &syn::Type, ctx: &Ctx) - } FnType::FnNew | FnType::FnNewClass(_) => { first_argument = Some("cls"); + output = syn::ReturnType::Default; // The __new__ Python function return type is None } FnType::FnClass(_) => { first_argument = Some("cls"); @@ -404,6 +406,7 @@ fn method_introspection_code(spec: &FnSpec<'_>, parent: &syn::Type, ctx: &Ctx) - &name, &spec.signature, first_argument, + output, decorators, Some(parent), ) diff --git a/pytests/stubs/pyclasses.pyi b/pytests/stubs/pyclasses.pyi index e1c1bacc38e..34486cada01 100644 --- a/pytests/stubs/pyclasses.pyi +++ b/pytests/stubs/pyclasses.pyi @@ -1,33 +1,33 @@ import typing class AssertingBaseClass: - def __new__(cls, /, expected_type: typing.Any): ... + def __new__(cls, /, expected_type: typing.Any) -> None: ... class ClassWithDecorators: - def __new__(cls, /): ... + def __new__(cls, /) -> None: ... @property - def attr(self, /): ... + def attr(self, /) -> int: ... @attr.setter - def attr(self, /, value: int): ... + def attr(self, /, value: int) -> None: ... @classmethod @property - def cls_attribute(cls, /): ... + def cls_attribute(cls, /) -> int: ... @classmethod - def cls_method(cls, /): ... + def cls_method(cls, /) -> int: ... @staticmethod - def static_method(): ... + def static_method() -> int: ... class ClassWithoutConstructor: ... class EmptyClass: - def __len__(self, /): ... - def __new__(cls, /): ... - def method(self, /): ... + def __len__(self, /) -> int: ... + def __new__(cls, /) -> None: ... + def method(self, /) -> None: ... class PyClassIter: - def __new__(cls, /): ... - def __next__(self, /): ... + def __new__(cls, /) -> None: ... + def __next__(self, /) -> int: ... class PyClassThreadIter: - def __new__(cls, /): ... - def __next__(self, /): ... + def __new__(cls, /) -> None: ... + def __next__(self, /) -> int: ... diff --git a/pytests/stubs/pyfunctions.pyi b/pytests/stubs/pyfunctions.pyi index b74b2f1fc61..8a9bbe4f501 100644 --- a/pytests/stubs/pyfunctions.pyi +++ b/pytests/stubs/pyfunctions.pyi @@ -1,22 +1,24 @@ import typing -def args_kwargs(*args, **kwargs): ... -def none(): ... -def positional_only(a: typing.Any, /, b: typing.Any): ... +def args_kwargs(*args, **kwargs) -> typing.Any: ... +def none() -> None: ... +def positional_only(a: typing.Any, /, b: typing.Any) -> typing.Any: ... def simple( a: typing.Any, b: typing.Any | None = None, *, c: typing.Any | None = None -): ... +) -> typing.Any: ... def simple_args( a: typing.Any, b: typing.Any | None = None, *args, c: typing.Any | None = None -): ... +) -> typing.Any: ... def simple_args_kwargs( a: typing.Any, b: typing.Any | None = None, *args, c: typing.Any | None = None, **kwargs, -): ... +) -> typing.Any: ... def simple_kwargs( a: typing.Any, b: typing.Any | None = None, c: typing.Any | None = None, **kwargs -): ... -def with_typed_args(a: bool = False, b: int = 0, c: float = 0.0, d: str = ""): ... +) -> typing.Any: ... +def with_typed_args( + a: bool = False, b: int = 0, c: float = 0.0, d: str = "" +) -> typing.Any: ... diff --git a/src/conversion.rs b/src/conversion.rs index 9c264145667..e5cf5bfd59d 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -47,6 +47,16 @@ pub trait IntoPyObject<'py>: Sized { /// The type returned in the event of a conversion error. type Error: Into; + /// 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::INPUT_TYPE`]. + /// 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")] + const OUTPUT_TYPE: &'static str = "typing.Any"; + /// Performs the conversion. fn into_pyobject(self, py: Python<'py>) -> Result; diff --git a/src/conversions/std/num.rs b/src/conversions/std/num.rs index 3ac8ac6d128..3366606b81b 100644 --- a/src/conversions/std/num.rs +++ b/src/conversions/std/num.rs @@ -20,6 +20,9 @@ macro_rules! int_fits_larger_int { type Output = Bound<'py, Self::Target>; type Error = Infallible; + #[cfg(feature = "experimental-inspect")] + const OUTPUT_TYPE: &'static str = <$larger_type>::OUTPUT_TYPE; + fn into_pyobject(self, py: Python<'py>) -> Result { (self as $larger_type).into_pyobject(py) } @@ -35,6 +38,9 @@ macro_rules! int_fits_larger_int { type Output = Bound<'py, Self::Target>; type Error = Infallible; + #[cfg(feature = "experimental-inspect")] + const OUTPUT_TYPE: &'static str = <$larger_type>::OUTPUT_TYPE; + fn into_pyobject(self, py: Python<'py>) -> Result { (*self).into_pyobject(py) } @@ -95,6 +101,9 @@ macro_rules! int_convert_u64_or_i64 { type Output = Bound<'py, Self::Target>; type Error = Infallible; + #[cfg(feature = "experimental-inspect")] + const OUTPUT_TYPE: &'static str = "int"; + fn into_pyobject(self, py: Python<'py>) -> Result { unsafe { Ok($pylong_from_ll_or_ull(self) @@ -113,6 +122,9 @@ macro_rules! int_convert_u64_or_i64 { type Output = Bound<'py, Self::Target>; type Error = Infallible; + #[cfg(feature = "experimental-inspect")] + const OUTPUT_TYPE: &'static str = "int"; + #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { (*self).into_pyobject(py) @@ -146,6 +158,9 @@ macro_rules! int_fits_c_long { type Output = Bound<'py, Self::Target>; type Error = Infallible; + #[cfg(feature = "experimental-inspect")] + const OUTPUT_TYPE: &'static str = "int"; + fn into_pyobject(self, py: Python<'py>) -> Result { unsafe { Ok(ffi::PyLong_FromLong(self as c_long) @@ -165,6 +180,9 @@ macro_rules! int_fits_c_long { type Output = Bound<'py, Self::Target>; type Error = Infallible; + #[cfg(feature = "experimental-inspect")] + const OUTPUT_TYPE: &'static str = "int"; + #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { (*self).into_pyobject(py) @@ -199,6 +217,9 @@ impl<'py> IntoPyObject<'py> for u8 { type Output = Bound<'py, Self::Target>; type Error = Infallible; + #[cfg(feature = "experimental-inspect")] + const OUTPUT_TYPE: &'static str = "int"; + fn into_pyobject(self, py: Python<'py>) -> Result { unsafe { Ok(ffi::PyLong_FromLong(self as c_long) @@ -230,6 +251,9 @@ impl<'py> IntoPyObject<'py> for &'_ u8 { type Output = Bound<'py, Self::Target>; type Error = Infallible; + #[cfg(feature = "experimental-inspect")] + const OUTPUT_TYPE: &'static str = "int"; + fn into_pyobject(self, py: Python<'py>) -> Result { u8::into_pyobject(*self, py) } @@ -313,6 +337,9 @@ mod fast_128bit_int_conversion { type Output = Bound<'py, Self::Target>; type Error = Infallible; + #[cfg(feature = "experimental-inspect")] + const OUTPUT_TYPE: &'static str = "int"; + fn into_pyobject(self, py: Python<'py>) -> Result { #[cfg(not(Py_3_13))] { @@ -367,6 +394,9 @@ mod fast_128bit_int_conversion { type Output = Bound<'py, Self::Target>; type Error = Infallible; + #[cfg(feature = "experimental-inspect")] + const OUTPUT_TYPE: &'static str = "int"; + #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { (*self).into_pyobject(py) @@ -454,6 +484,9 @@ mod slow_128bit_int_conversion { type Output = Bound<'py, Self::Target>; type Error = Infallible; + #[cfg(feature = "experimental-inspect")] + const OUTPUT_TYPE: &'static str = "int"; + 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)?; @@ -479,6 +512,9 @@ mod slow_128bit_int_conversion { type Output = Bound<'py, Self::Target>; type Error = Infallible; + #[cfg(feature = "experimental-inspect")] + const OUTPUT_TYPE: &'static str = "int"; + #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { (*self).into_pyobject(py) @@ -545,6 +581,9 @@ macro_rules! nonzero_int_impl { type Output = Bound<'py, Self::Target>; type Error = Infallible; + #[cfg(feature = "experimental-inspect")] + const OUTPUT_TYPE: &'static str = "int"; + #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { self.get().into_pyobject(py) @@ -561,6 +600,9 @@ macro_rules! nonzero_int_impl { type Output = Bound<'py, Self::Target>; type Error = Infallible; + #[cfg(feature = "experimental-inspect")] + const OUTPUT_TYPE: &'static str = "int"; + #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { (*self).into_pyobject(py) diff --git a/src/impl_.rs b/src/impl_.rs index 5cf2af4d664..de82c650f43 100644 --- a/src/impl_.rs +++ b/src/impl_.rs @@ -15,6 +15,8 @@ pub mod exceptions; pub mod extract_argument; pub mod freelist; pub mod frompyobject; +#[cfg(feature = "experimental-inspect")] +pub mod introspection; pub(crate) mod not_send; pub mod panic; pub mod pycell; diff --git a/src/impl_/introspection.rs b/src/impl_/introspection.rs new file mode 100644 index 00000000000..cd00a55162b --- /dev/null +++ b/src/impl_/introspection.rs @@ -0,0 +1,17 @@ +use crate::conversion::IntoPyObject; + +/// Trait to guess a function Python return type +/// +/// It is useful to properly get the return type `T` when the Rust implementation returns e.g. `PyResult` +pub trait PyReturnType { + /// The function return type + const OUTPUT_TYPE: &'static str; +} + +impl<'a, T: IntoPyObject<'a>> PyReturnType for T { + const OUTPUT_TYPE: &'static str = T::OUTPUT_TYPE; +} + +impl PyReturnType for Result { + const OUTPUT_TYPE: &'static str = T::OUTPUT_TYPE; +} diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index 0608a1f0df4..82a128f3bf5 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -47,6 +47,7 @@ fn test_compile_errors() { 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"); + #[cfg(not(feature = "experimental-inspect"))] 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)))] From cc1b9c57702a6276871a513b69cbb01893b3617d Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 4 Jul 2025 18:31:29 +0100 Subject: [PATCH 735/936] fix netlify deploy (#5224) --- .github/workflows/netlify-deploy.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/netlify-deploy.yml b/.github/workflows/netlify-deploy.yml index 04100b139ce..d9ff526c3d8 100644 --- a/.github/workflows/netlify-deploy.yml +++ b/.github/workflows/netlify-deploy.yml @@ -7,7 +7,7 @@ name: netlify-deploy on: workflow_run: - workflows: ['netlify-build'] + workflows: ["netlify-build"] types: - completed @@ -34,8 +34,8 @@ jobs: - name: Deploy to Netlify run: | - ls -l netlify_build/ + ls -la DEBUG=* netlify deploy \ --site ${{ secrets.NETLIFY_SITE_ID }} \ --auth ${{ secrets.NETLIFY_TOKEN }} \ - ${{ github.event.workflow_run.head_branch == 'main' && '--prod' || '' }} \ + ${{ github.event.workflow_run.head_branch == 'main' && '--prod' || '' }} From 2dc118e588edc2f1ac675724ca6cf2c019ac380b Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Fri, 4 Jul 2025 21:34:31 +0200 Subject: [PATCH 736/936] add `PyCode::compile` to create executable code objects (#5217) --- newsfragments/5217.added.md | 1 + pyo3-ffi/src/cpython/code.rs | 6 +- src/marker.rs | 95 +++++------------------ src/types/code.rs | 142 +++++++++++++++++++++++++++++++++-- src/types/mod.rs | 4 +- 5 files changed, 159 insertions(+), 89 deletions(-) create mode 100644 newsfragments/5217.added.md diff --git a/newsfragments/5217.added.md b/newsfragments/5217.added.md new file mode 100644 index 00000000000..2762b422a1b --- /dev/null +++ b/newsfragments/5217.added.md @@ -0,0 +1 @@ +add `PyCode::compile` and `PyCodeMethods::run` to create and execute code objects \ No newline at end of file diff --git a/pyo3-ffi/src/cpython/code.rs b/pyo3-ffi/src/cpython/code.rs index 3d47a1bc8c3..56ae0c5083d 100644 --- a/pyo3-ffi/src/cpython/code.rs +++ b/pyo3-ffi/src/cpython/code.rs @@ -4,7 +4,7 @@ use crate::pyport::Py_ssize_t; #[cfg(not(GraalPy))] use std::os::raw::c_char; use std::os::raw::{c_int, c_void}; -#[cfg(not(any(PyPy, GraalPy)))] +#[cfg(not(PyPy))] use std::ptr::addr_of_mut; // skipped private _PY_MONITORING_LOCAL_EVENTS @@ -71,14 +71,14 @@ pub const CO_FUTURE_GENERATOR_STOP: c_int = 0x8_0000; pub const CO_MAXBLOCKS: usize = 20; -#[cfg(not(any(PyPy, GraalPy)))] +#[cfg(not(PyPy))] #[cfg_attr(windows, link(name = "pythonXY"))] extern "C" { pub static mut PyCode_Type: PyTypeObject; } #[inline] -#[cfg(not(any(PyPy, GraalPy)))] +#[cfg(not(PyPy))] pub unsafe fn PyCode_Check(op: *mut PyObject) -> c_int { (Py_TYPE(op) == addr_of_mut!(PyCode_Type)) as c_int } diff --git a/src/marker.rs b/src/marker.rs index 860405ab0ac..acfcd22b7de 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -117,21 +117,18 @@ //! [`Rc`]: std::rc::Rc //! [`Py`]: crate::Py use crate::conversion::IntoPyObject; -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; -use crate::py_result_ext::PyResultExt; use crate::types::any::PyAnyMethods; use crate::types::{ - PyAny, PyDict, PyEllipsis, PyModule, PyNone, PyNotImplemented, PyString, PyType, + PyAny, PyCode, PyCodeMethods, PyDict, PyEllipsis, PyModule, PyNone, PyNotImplemented, PyString, + PyType, }; use crate::version::PythonVersionInfo; use crate::{ffi, Bound, PyObject, PyTypeInfo}; use std::ffi::CStr; use std::marker::PhantomData; -use std::os::raw::c_int; /// Types that are safe to access while the GIL is not held. /// @@ -567,7 +564,13 @@ impl<'py> Python<'py> { globals: Option<&Bound<'py, PyDict>>, locals: Option<&Bound<'py, PyDict>>, ) -> PyResult> { - self.run_code(code, ffi::Py_eval_input, globals, locals) + let code = PyCode::compile( + self, + code, + ffi::c_str!(""), + crate::types::PyCodeInput::Eval, + )?; + code.run(globals, locals) } /// Executes one or more Python statements in the given context. @@ -611,79 +614,17 @@ impl<'py> Python<'py> { globals: Option<&Bound<'py, PyDict>>, locals: Option<&Bound<'py, PyDict>>, ) -> PyResult<()> { - let res = self.run_code(code, ffi::Py_file_input, globals, locals); - res.map(|obj| { + let code = PyCode::compile( + self, + code, + ffi::c_str!(""), + crate::types::PyCodeInput::File, + )?; + code.run(globals, locals).map(|obj| { debug_assert!(obj.is_none()); }) } - /// Runs code in the given context. - /// - /// `start` indicates the type of input expected: one of `Py_single_input`, - /// `Py_file_input`, or `Py_eval_input`. - /// - /// If `globals` is `None`, it defaults to Python module `__main__`. - /// If `locals` is `None`, it defaults to the value of `globals`. - fn run_code( - self, - code: &CStr, - start: c_int, - globals: Option<&Bound<'py, PyDict>>, - locals: Option<&Bound<'py, PyDict>>, - ) -> PyResult> { - 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); - - // 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 { - ffi::Py_CompileString(code.as_ptr(), ffi::c_str!("").as_ptr(), start) - .assume_owned_or_err(self)? - }; - - unsafe { - ffi::PyEval_EvalCode(code_obj.as_ptr(), globals.as_ptr(), locals.as_ptr()) - .assume_owned_or_err(self) - .downcast_into_unchecked() - } - } - /// Gets the Python type object for type `T`. #[inline] pub fn get_type(self) -> Bound<'py, PyType> @@ -766,7 +707,7 @@ impl<'py> Python<'py> { /// Lets the Python interpreter check and handle any pending signals. This will invoke the /// corresponding signal handlers registered in Python (if any). /// - /// Returns `Err(`[`PyErr`]`)` if any signal handler raises an exception. + /// Returns `Err(`[`PyErr`](crate::PyErr)`)` if any signal handler raises an exception. /// /// These signals include `SIGINT` (normally raised by CTRL + C), which by default raises /// `KeyboardInterrupt`. For this reason it is good practice to call this function regularly @@ -939,6 +880,8 @@ mod tests { #[test] #[cfg(not(Py_LIMITED_API))] fn test_acquire_gil() { + use std::ffi::c_int; + const GIL_NOT_HELD: c_int = 0; const GIL_HELD: c_int = 1; diff --git a/src/types/code.rs b/src/types/code.rs index 593a1b88317..15fea7ef9a2 100644 --- a/src/types/code.rs +++ b/src/types/code.rs @@ -1,5 +1,9 @@ -use crate::ffi; -use crate::PyAny; +use super::PyAnyMethods as _; +use super::PyDict; +use crate::ffi_ptr_ext::FfiPtrExt; +use crate::py_result_ext::PyResultExt; +use crate::{ffi, Bound, PyAny, PyErr, PyResult, Python}; +use std::ffi::CStr; /// Represents a Python code object. /// @@ -8,22 +12,146 @@ use crate::PyAny; #[repr(transparent)] pub struct PyCode(PyAny); +#[cfg(not(any(Py_LIMITED_API, PyPy)))] pyobject_native_type_core!( PyCode, pyobject_native_static_type_object!(ffi::PyCode_Type), #checkfunction=ffi::PyCode_Check ); +#[cfg(any(Py_LIMITED_API, PyPy))] +pyobject_native_type_named!(PyCode); + +#[cfg(any(Py_LIMITED_API, PyPy))] +impl crate::PyTypeCheck for PyCode { + const NAME: &'static str = "PyCode"; + #[cfg(feature = "experimental-inspect")] + const PYTHON_TYPE: &'static str = "types.CodeType"; + + fn type_check(object: &Bound<'_, PyAny>) -> bool { + let py = object.py(); + static TYPE: crate::sync::GILOnceCell> = + crate::sync::GILOnceCell::new(); + + TYPE.import(py, "types", "CodeType") + .and_then(|ty| object.is_instance(ty)) + .unwrap_or_default() + } +} + +/// Compilation mode of [`PyCode::compile`] +pub enum PyCodeInput { + /// Python grammar for isolated expressions + Eval, + /// Python grammar for sequences of statements as read from a file + File, +} + +impl PyCode { + /// Compiles code in the given context. + /// + /// `input` decides whether `code` is treated as + /// - [`PyCodeInput::Eval`]: an isolated expression + /// - [`PyCodeInput::File`]: a sequence of statements + pub fn compile<'py>( + py: Python<'py>, + code: &CStr, + filename: &CStr, + input: PyCodeInput, + ) -> PyResult> { + let start = match input { + PyCodeInput::Eval => ffi::Py_eval_input, + PyCodeInput::File => ffi::Py_file_input, + }; + unsafe { + ffi::Py_CompileString(code.as_ptr(), filename.as_ptr(), start) + .assume_owned_or_err(py) + .downcast_into_unchecked() + } + } +} + +/// Implementation of functionality for [`PyCode`]. +/// +/// These methods are defined for the `Bound<'py, PyCode>` 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`. +pub trait PyCodeMethods<'py> { + /// Runs code object. + /// + /// If `globals` is `None`, it defaults to Python module `__main__`. + /// If `locals` is `None`, it defaults to the value of `globals`. + fn run( + &self, + globals: Option<&Bound<'py, PyDict>>, + locals: Option<&Bound<'py, PyDict>>, + ) -> PyResult>; +} + +impl<'py> PyCodeMethods<'py> for Bound<'py, PyCode> { + fn run( + &self, + globals: Option<&Bound<'py, PyDict>>, + locals: Option<&Bound<'py, PyDict>>, + ) -> PyResult> { + let mptr = unsafe { + ffi::compat::PyImport_AddModuleRef(ffi::c_str!("__main__").as_ptr()) + .assume_owned_or_err(self.py())? + }; + let attr = mptr.getattr(crate::intern!(self.py(), "__dict__"))?; + let globals = match globals { + Some(globals) => globals, + None => attr.downcast::()?, + }; + let locals = locals.unwrap_or(globals); + + // 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.py(), "__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.py())); + } + } + Ok(()) + })?; + } + + unsafe { + ffi::PyEval_EvalCode(self.as_ptr(), globals.as_ptr(), locals.as_ptr()) + .assume_owned_or_err(self.py()) + } + } +} + #[cfg(test)] mod tests { - use super::*; - use crate::types::PyTypeMethods; - use crate::{PyTypeInfo, Python}; - #[test] + #[cfg(not(any(Py_LIMITED_API, PyPy)))] fn test_type_object() { + use crate::types::PyTypeMethods; + use crate::{PyTypeInfo, Python}; + Python::attach(|py| { - assert_eq!(PyCode::type_object(py).name().unwrap(), "code"); + assert_eq!(super::PyCode::type_object(py).name().unwrap(), "code"); }) } } diff --git a/src/types/mod.rs b/src/types/mod.rs index 19058d33193..da02cda71fe 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -5,8 +5,7 @@ pub use self::boolobject::{PyBool, PyBoolMethods}; pub use self::bytearray::{PyByteArray, PyByteArrayMethods}; pub use self::bytes::{PyBytes, PyBytesMethods}; pub use self::capsule::{PyCapsule, PyCapsuleMethods}; -#[cfg(all(not(Py_LIMITED_API), not(PyPy), not(GraalPy)))] -pub use self::code::PyCode; +pub use self::code::{PyCode, PyCodeInput, PyCodeMethods}; pub use self::complex::{PyComplex, PyComplexMethods}; #[allow(deprecated)] pub use self::datetime::{ @@ -223,7 +222,6 @@ pub(crate) mod boolobject; pub(crate) mod bytearray; pub(crate) mod bytes; pub(crate) mod capsule; -#[cfg(all(not(Py_LIMITED_API), not(PyPy), not(GraalPy)))] mod code; pub(crate) mod complex; pub(crate) mod datetime; From 91fa545128629b78da4333543c8ba2ac208aa5e0 Mon Sep 17 00:00:00 2001 From: Keming Date: Sat, 12 Jul 2025 01:13:00 +0800 Subject: [PATCH 737/936] ci: fix the lychee fragments check for guide/src (#5230) * chore: fix the lychee fragments check for guide/src Signed-off-by: Keming * fix cmd escape Signed-off-by: Keming * ignore all the docs.rs fragments Co-authored-by: David Hewitt * try lychee-action Signed-off-by: Keming * reduce max-concurrency to 32 Signed-off-by: Keming --------- Signed-off-by: Keming Co-authored-by: David Hewitt --- .github/workflows/gh-pages.yml | 10 +++++++++- .github/workflows/netlify-build.yml | 10 +++++++++- guide/src/getting-started.md | 2 +- noxfile.py | 13 ++++++++++--- 4 files changed, 29 insertions(+), 6 deletions(-) diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index e5980dbec74..397b2f36a0d 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -30,7 +30,15 @@ jobs: - name: Setup mdBook uses: taiki-e/install-action@v2 with: - tool: mdbook,mdbook-tabs,lychee + tool: mdbook,mdbook-tabs + + - name: Link Checker + id: lychee + uses: lycheeverse/lychee-action@v2 + with: + # setup lychee but don't run it for now + args: --version + lycheeVersion: nightly - name: Prepare tag id: prepare_tag diff --git a/.github/workflows/netlify-build.yml b/.github/workflows/netlify-build.yml index 4f6c8198c9d..4858a6ff428 100644 --- a/.github/workflows/netlify-build.yml +++ b/.github/workflows/netlify-build.yml @@ -40,7 +40,15 @@ jobs: - name: Setup mdBook uses: taiki-e/install-action@v2 with: - tool: mdbook, lychee, mdbook-linkcheck + tool: mdbook, mdbook-linkcheck + + - name: Link Checker + id: lychee + uses: lycheeverse/lychee-action@v2 + with: + # setup lychee but don't run it yet + args: --version + lycheeVersion: nightly - name: Prepare tag id: prepare_tag diff --git a/guide/src/getting-started.md b/guide/src/getting-started.md index 99cffa25895..dfa55d8cb8d 100644 --- a/guide/src/getting-started.md +++ b/guide/src/getting-started.md @@ -16,7 +16,7 @@ To use PyO3, you need at least Python 3.7. While you can simply use the default ## Virtualenvs -While you can use any virtualenv manager you like, we recommend the use of `pyenv` in particular if you want to develop or test for multiple different Python versions, so that is what the examples in this book will use. The installation instructions for `pyenv` can be found [here](https://github.com/pyenv/pyenv#getting-pyenv). (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.) +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#a-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.) 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. diff --git a/noxfile.py b/noxfile.py index 98d19f085b6..77d2b2476ae 100644 --- a/noxfile.py +++ b/noxfile.py @@ -504,6 +504,13 @@ def check_guide(session: nox.Session): "https://pyo3.rs/main/": f"file://{PYO3_GUIDE_TARGET}/", "https://pyo3.rs/latest/": f"file://{PYO3_GUIDE_TARGET}/", "%7B%7B#PYO3_DOCS_VERSION}}": "latest", + # bypass fragments for edge cases + # blob links + "(https://github.com/[^/]+/[^/]+/blob/[^#]+)#[a-zA-Z0-9._-]*": "$1", + # issue comments + "(https://github.com/[^/]+/[^/]+/issues/[0-9]+)#issuecomment-[0-9]*": "$1", + # rust docs + "(https://docs.rs/[^#]+)#[a-zA-Z0-9._-]*": "$1", } remap_args = [] for key, value in remaps.items(): @@ -513,9 +520,7 @@ def check_guide(session: nox.Session): _run( session, "lychee", - # FIXME: would be nice to use `--include-fragments` here, but we've had - # a lot of flaky failures from it - see https://github.com/lycheeverse/lychee/issues/1746 - # "--include-fragments", + "--include-fragments", str(PYO3_GUIDE_SRC), *remap_args, "--accept=200,429", @@ -533,6 +538,8 @@ def check_guide(session: nox.Session): "--exclude=http://www.adobe.com/", "--exclude=http://www.nhncorp.com/", "--accept=200,429", + # reduce the concurrency to avoid rate-limit from `pyo3.rs` + "--max-concurrency=32", *session.posargs, ) From 799b3c8a0f8c3e8dea5ab4f3b54ab96723ed0a0c Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Fri, 11 Jul 2025 19:53:09 +0200 Subject: [PATCH 738/936] docs pass (#5225) --- README.md | 2 +- guide/src/async-await.md | 8 +-- guide/src/class.md | 25 ++++---- guide/src/class/numeric.md | 12 ++-- guide/src/class/object.md | 14 +++-- guide/src/class/protocols.md | 2 +- guide/src/conversions/tables.md | 2 +- guide/src/ecosystem/logging.md | 34 +++++------ guide/src/exception.md | 9 +-- guide/src/features.md | 6 -- guide/src/free-threading.md | 29 +++++---- guide/src/function.md | 61 +++++++++---------- guide/src/function/signature.md | 22 +++---- guide/src/getting-started.md | 22 +++---- guide/src/parallelism.md | 12 ++-- guide/src/performance.md | 10 +-- .../python-from-rust/calling-existing-code.md | 15 +++-- guide/src/trait-bounds.md | 27 ++++---- guide/src/types.md | 2 +- 19 files changed, 154 insertions(+), 160 deletions(-) diff --git a/README.md b/README.md index fdbc17ec432..318ddadebb5 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ ## Usage -Requires Rust 1.63 or greater. +Requires Rust 1.74 or greater. PyO3 supports the following Python distributions: - CPython 3.7 or greater diff --git a/guide/src/async-await.md b/guide/src/async-await.md index bf2dd84f5b3..9e53ca8dc7a 100644 --- a/guide/src/async-await.md +++ b/guide/src/async-await.md @@ -35,15 +35,15 @@ As a consequence, `async fn` parameters and return types must also be `Send + 's However, there is an exception for method receivers, so async methods can accept `&self`/`&mut self`. Note that this means that the class instance is borrowed for as long as the returned future is not completed, even across yield points and while waiting for I/O operations to complete. Hence, other methods cannot obtain exclusive borrows while the future is still being polled. This is the same as how async methods in Rust generally work but it is more problematic for Rust code interfacing with Python code due to pervasive shared mutability. This strongly suggests to prefer shared borrows `&self` over exclusive ones `&mut self` to avoid racy borrow check failures at runtime. -## Implicit GIL holding +## Implicitly attached to the interpreter -Even if it is not possible to pass a `py: Python<'py>` parameter to `async fn`, the GIL is still held during the execution of the future – it's also the case for regular `fn` without `Python<'py>`/`Bound<'py, PyAny>` parameter, yet the GIL is held. +Even if it is not possible to pass a `py: Python<'py>` token to an `async fn`, we're still attached to the interpreter during the execution of the future – the same as for a regular `fn` without `Python<'py>`/`Bound<'py, PyAny>` parameter It is still possible to get a `Python` marker using [`Python::attach`]({{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.attach); because `attach` is reentrant and optimized, the cost will be negligible. -## Release the GIL across `.await` +## Detaching from the interpreter across `.await` -There is currently no simple way to release the GIL when awaiting a future, *but solutions are currently in development*. +There is currently no simple way to detach from the interpreter when awaiting a future, *but solutions are currently in development*. Here is the advised workaround for now: diff --git a/guide/src/class.md b/guide/src/class.md index 1e522ce6143..73c6af4dadc 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -183,18 +183,19 @@ For arguments, see the [`Method arguments`](#method-arguments) section below. ## Adding the class to a module -The next step is to create the module initializer and add our class to it: +The next step is to create the Python module and add our class to it: ```rust # #![allow(dead_code)] # use pyo3::prelude::*; +# fn main() {} # #[pyclass] # struct Number(i32); # #[pymodule] -fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> { - m.add_class::()?; - Ok(()) +mod my_module { + #[pymodule_export] + use super::Number; } ``` @@ -242,7 +243,7 @@ Python::attach(|py| { }); ``` -A `Bound<'py, T>` is restricted to the GIL lifetime `'py`. To make the object longer lived (for example, to store it in a struct on the +A `Bound<'py, T>` is restricted to the Python lifetime `'py`. To make the object longer lived (for example, to store it in a struct on the Rust side), use `Py`. `Py` needs a `Python<'_>` token to allow access: ```rust @@ -269,7 +270,7 @@ Python::attach(move |py| { As detailed above, runtime borrow checking is currently enabled by default. But a class can opt of out it by declaring itself `frozen`. It can still use interior mutability via standard Rust types like `RefCell` or `Mutex`, but it is not bound to the implementation provided by PyO3 and can choose the most appropriate strategy on field-by-field basis. -Classes which are `frozen` and also `Sync`, e.g. they do use `Mutex` but not `RefCell`, can be accessed without needing the Python GIL via the `Bound::get` and `Py::get` methods: +Classes which are `frozen` and also `Sync`, e.g. they do use `Mutex` but not `RefCell`, can be accessed without needing a `Python` token via the `Bound::get` and `Py::get` methods: ```rust use std::sync::atomic::{AtomicUsize, Ordering}; @@ -566,7 +567,7 @@ The above would make the `num` field available for reading and writing as a `sel Properties can be readonly or writeonly by using just `#[pyo3(get)]` or `#[pyo3(set)]` respectively. To use these annotations, your field type must implement some conversion traits: -- For `get` the field type must implement both `IntoPy` and `Clone`. +- For `get` the field type `T` must implement either `&T: IntoPyObject` or `T: IntoPyObject + Clone`. - For `set` the field type must implement `FromPyObject`. For example, implementations of those traits are provided for the `Cell` type, if the inner type also implements the trait. This means you can use `#[pyo3(get, set)]` on fields wrapped in a `Cell`. @@ -685,7 +686,7 @@ impl MyClass { ``` Calls to these methods are protected by the GIL, so both `&self` and `&mut self` can be used. -The return type must be `PyResult` or `T` for some `T` that implements `IntoPy`; +The return type must be `PyResult` or `T` for some `T` that implements `IntoPyObject`; the latter is allowed if the method cannot raise Python exceptions. A `Python` parameter can be specified as part of method signature, in this case the `py` argument @@ -737,7 +738,7 @@ Declares a class method callable from Python. This may be the type object of a derived class. * The first parameter implicitly has type `&Bound<'_, PyType>`. * For details on `parameter-list`, see the documentation of `Method arguments` section. -* The return type must be `PyResult` or `T` for some `T` that implements `IntoPy`. +* The return type must be `PyResult` or `T` for some `T` that implements `IntoPyObject`. ### Constructors which accept a class argument @@ -765,7 +766,7 @@ impl BaseClass { To create a static method for a custom class, the method needs to be annotated with the `#[staticmethod]` attribute. The return type must be `T` or `PyResult` for some `T` that implements -`IntoPy`. +`IntoPyObject`. ```rust # use pyo3::prelude::*; @@ -827,7 +828,7 @@ impl MyClass { ## Classes as function arguments -Free functions defined using `#[pyfunction]` interact with classes through the same mechanisms as the self parameters of instance methods, i.e. they can take GIL-bound references, GIL-bound reference wrappers or GIL-independent references: +Free functions defined using `#[pyfunction]` interact with classes through the same mechanisms as the self parameters of instance methods, i.e. they can take Python-bound references, Python-bound reference wrappers or Python-independent references: ```rust,no_run # #![allow(dead_code)] @@ -1043,7 +1044,7 @@ Note that `text_signature` on `#[new]` is not compatible with compilation in 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. +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 Python lifetime `'py` should usually be derived from a `py: Python<'py>` token passed as an argument instead of the receiver. Specifically, signatures like diff --git a/guide/src/class/numeric.md b/guide/src/class/numeric.md index 2928a3b6260..c326e93b23e 100644 --- a/guide/src/class/numeric.md +++ b/guide/src/class/numeric.md @@ -326,9 +326,9 @@ impl Number { } #[pymodule] -fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> { - m.add_class::()?; - Ok(()) +mod my_module { + #[pymodule_export] + use super::Number; } # const SCRIPT: &'static std::ffi::CStr = pyo3::ffi::c_str!(r#" # def hash_djb2(s: str): @@ -408,12 +408,12 @@ unsigned long PyLong_AsUnsignedLongMask(PyObject *obj) We can call this function from Rust by using [`pyo3::ffi::PyLong_AsUnsignedLongMask`]. This is an *unsafe* function, which means we have to use an unsafe block to call it and take responsibility for upholding the contracts of this function. Let's review those contracts: -- The GIL must be held. If it's not, calling this function causes a data race. +- We must be attached to the interpreter. If we're not, calling this function causes a data race. - The pointer must be valid, i.e. it must be properly aligned and point to a valid Python object. Let's create that helper function. The signature has to be `fn(&Bound<'_, PyAny>) -> PyResult`. -- `&Bound<'_, PyAny>` represents a checked borrowed reference, so the pointer derived from it is valid (and not null). -- Whenever we have borrowed references to Python objects in scope, it is guaranteed that the GIL is held. This reference is also where we can get a [`Python`] token to use in our call to [`PyErr::take`]. +- `&Bound<'_, PyAny>` represents a checked bound reference, so the pointer derived from it is valid (and not null). +- Whenever we have bound references to Python objects in scope, it is guaranteed that we're attached to the interpreter. This reference is also where we can get a [`Python`] token to use in our call to [`PyErr::take`]. ```rust,no_run # #![allow(dead_code)] diff --git a/guide/src/class/object.md b/guide/src/class/object.md index 1c47c44a6e2..a4829cb754e 100644 --- a/guide/src/class/object.md +++ b/guide/src/class/object.md @@ -4,6 +4,7 @@ Recall the `Number` class from the previous chapter: ```rust,no_run # #![allow(dead_code)] +# fn main() {} use pyo3::prelude::*; #[pyclass] @@ -18,9 +19,9 @@ impl Number { } #[pymodule] -fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> { - m.add_class::()?; - Ok(()) +mod my_module { + #[pymodule_export] + use super::Number; } ``` @@ -331,6 +332,7 @@ impl Number { ### Final code ```rust,no_run +# fn main() {} use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; @@ -380,9 +382,9 @@ impl Number { } #[pymodule] -fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> { - m.add_class::()?; - Ok(()) +mod my_module { + #[pymodule_export] + use super::Number; } ``` diff --git a/guide/src/class/protocols.md b/guide/src/class/protocols.md index d0e07919862..e4ed8b6dae5 100644 --- a/guide/src/class/protocols.md +++ b/guide/src/class/protocols.md @@ -457,7 +457,7 @@ impl ClassWithGCSupport { ``` 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__`, +Most importantly, safe access to the interpreter is prohibited inside implementations of `__traverse__`, i.e. `Python::attach` 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). diff --git a/guide/src/conversions/tables.md b/guide/src/conversions/tables.md index cec255178ed..7fc09657aff 100644 --- a/guide/src/conversions/tables.md +++ b/guide/src/conversions/tables.md @@ -67,7 +67,7 @@ Using Rust library types as function arguments will incur a conversion cost comp However, once that conversion cost has been paid, the Rust standard library types offer a number of benefits: - You can write functionality in native-speed Rust code (free of Python's runtime costs). - You get better interoperability with the rest of the Rust ecosystem. -- You can use `Python::detach` to release the Python GIL and let other Python threads make progress while your Rust code is executing. +- You can use `Python::detach` to detach from the interpreter and let other Python threads make progress while your Rust code is executing. - You also benefit from stricter type checking. For example you can specify `Vec`, which will only accept a Python `list` containing integers. The Python-native equivalent, `&PyList`, would accept a Python `list` containing Python objects of any type. For most PyO3 usage the conversion cost is worth paying to get these benefits. As always, if you're not sure it's worth it in your case, benchmark it! diff --git a/guide/src/ecosystem/logging.md b/guide/src/ecosystem/logging.md index ba164e78e79..15ef2463d87 100644 --- a/guide/src/ecosystem/logging.md +++ b/guide/src/ecosystem/logging.md @@ -19,23 +19,23 @@ Use [`pyo3_log::init`][init] to install the logger in its default configuration. It's also possible to tweak its configuration (mostly to tune its performance). ```rust,no_run -use log::info; -use pyo3::prelude::*; - -#[pyfunction] -fn log_something() { - // This will use the logger installed in `my_module` to send the `info` - // message to the Python logging facilities. - info!("Something!"); -} - -#[pymodule] -fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> { - // A good place to install the Rust -> Python logger. - pyo3_log::init(); - - m.add_function(wrap_pyfunction!(log_something, m)?)?; - Ok(()) +#[pyo3::pymodule] +mod my_module { + use log::info; + use pyo3::prelude::*; + + #[pyfunction] + fn log_something() { + // This will use the logger installed in `my_module` to send the `info` + // message to the Python logging facilities. + info!("Something!"); + } + + #[pymodule_init] + fn init(m: &Bound<'_, PyModule>) -> PyResult<()> { + // A good place to install the Rust -> Python logger. + pyo3_log::init(); + } } ``` diff --git a/guide/src/exception.md b/guide/src/exception.md index 01552f1bf62..74b3dae4e3a 100644 --- a/guide/src/exception.md +++ b/guide/src/exception.md @@ -41,17 +41,18 @@ When using PyO3 to create an extension module, you can add the new exception to the module like this, so that it is importable from Python: ```rust,no_run +# fn main() {} use pyo3::prelude::*; use pyo3::exceptions::PyException; 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::())?; +mod mymodule { + #[pymodule_export] + use super::CustomError; - Ok(()) + // ... other elements added to module ... } ``` diff --git a/guide/src/features.md b/guide/src/features.md index 13c2d2b8d8f..5999c3f0247 100644 --- a/guide/src/features.md +++ b/guide/src/features.md @@ -65,12 +65,6 @@ Also, this feature adds the `pyo3::inspect` module, as well as `IntoPy::type_out This is a first step towards adding first-class support for generating type annotations automatically in PyO3, however work is needed to finish this off. All feedback and offers of help welcome on [issue #2454](https://github.com/PyO3/pyo3/issues/2454). -### `gil-refs` - -This feature is a backwards-compatibility feature to allow continued use of the "GIL Refs" APIs deprecated in PyO3 0.21. These APIs have performance drawbacks and soundness edge cases which the newer `Bound` smart pointer and accompanying APIs resolve. - -This feature and the APIs it enables is expected to be removed in a future PyO3 version. - ### `py-clone` This feature was introduced to ease migration. It was found that delayed reference counts cannot be made sound and hence `Clon`ing an instance of `Py` must panic without the GIL being held. To avoid migrations introducing new panics without warning, the `Clone` implementation itself is now gated behind this feature. diff --git a/guide/src/free-threading.md b/guide/src/free-threading.md index 3af858029f3..0cf062bea0c 100644 --- a/guide/src/free-threading.md +++ b/guide/src/free-threading.md @@ -119,16 +119,22 @@ free-threaded build. ## 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. +The free-threaded interpreter does not have a GIL. Many existing extensions +providing mutable data structures relied on the GIL provided locking for +operations on Python objects, to make interior mutability thread-safe. +Historically PyO3s API was designed around the same strong assumptions, but is +transitioning towards more general APIs applicable for both builds. + +Working with PyO3 under the free-threaded interpreter requires some additional +care and mental overhead compared with a GIL-enabled interpreter. Most notable +it is still neccessary to be attached (via [`Python::attach`]) to the Python +interpreter to perform any operation on Python objects. PyO3 models this the +same way as in GIL-enabled builds using the `Python` token, but unlike in +GIL-enabled builds it does not provide exclusive access anymore. Additionally it +is also still neccessary to detach (via [`Python::detach`]) from the interpreter +for possibly long running oprations that don't interact with the interpreter, +even though other Python threads are still able to run. Both operations are +explained in more details below. ### Many symbols exposed by PyO3 have `GIL` in the name @@ -136,8 +142,7 @@ 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 [`Python::attach`] and [`GILOnceCell`] is -historical. +in functions and types like [`GILOnceCell`] is historical. 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 diff --git a/guide/src/function.md b/guide/src/function.md index ca533f80cc9..7f7d79adf78 100644 --- a/guide/src/function.md +++ b/guide/src/function.md @@ -1,20 +1,18 @@ # Python functions -The `#[pyfunction]` attribute is used to define a Python function from a Rust function. Once defined, the function needs to be added to a [module](./module.md) using the `wrap_pyfunction!` macro. +The `#[pyfunction]` attribute is used to define a Python function from a Rust function. Once defined, the function needs to be added to a [module](./module.md). The following example defines a function called `double` in a Python module called `my_extension`: ```rust,no_run -use pyo3::prelude::*; - -#[pyfunction] -fn double(x: usize) -> usize { - x * 2 -} +#[pyo3::pymodule] +mod my_extension { + use pyo3::prelude::*; -#[pymodule] -fn my_extension(m: &Bound<'_, PyModule>) -> PyResult<()> { - m.add_function(wrap_pyfunction!(double, m)?) + #[pyfunction] + fn double(x: usize) -> usize { + x * 2 + } } ``` @@ -46,17 +44,16 @@ The `#[pyo3]` attribute can be used to modify properties of the generated Python `module_with_functions` as the Python function `no_args`: ```rust - use pyo3::prelude::*; - - #[pyfunction] - #[pyo3(name = "no_args")] - fn no_args_py() -> usize { - 42 - } + # use pyo3::prelude::*; + #[pyo3::pymodule] + mod module_with_functions { + use pyo3::prelude::*; - #[pymodule] - fn module_with_functions(m: &Bound<'_, PyModule>) -> PyResult<()> { - m.add_function(wrap_pyfunction!(no_args_py, m)?) + #[pyfunction] + #[pyo3(name = "no_args")] + fn no_args_py() -> usize { + 42 + } } # Python::attach(|py| { @@ -81,20 +78,18 @@ The `#[pyo3]` attribute can be used to modify properties of the generated Python The following example creates a function `pyfunction_with_module` which returns the containing module's name (i.e. `module_with_fn`): ```rust,no_run - use pyo3::prelude::*; - use pyo3::types::PyString; + #[pyo3::pymodule] + mod module_with_fn { + use pyo3::prelude::*; + use pyo3::types::PyString; - #[pyfunction] - #[pyo3(pass_module)] - fn pyfunction_with_module<'py>( - module: &Bound<'py, PyModule>, - ) -> PyResult> { - module.name() - } - - #[pymodule] - fn module_with_fn(m: &Bound<'_, PyModule>) -> PyResult<()> { - m.add_function(wrap_pyfunction!(pyfunction_with_module, m)?) + #[pyfunction] + #[pyo3(pass_module)] + fn pyfunction_with_module<'py>( + module: &Bound<'py, PyModule>, + ) -> PyResult> { + module.name() + } } ``` - `#[pyo3(warn(message = "...", category = ...))]` diff --git a/guide/src/function/signature.md b/guide/src/function/signature.md index 873ff0f594d..86fa1570672 100644 --- a/guide/src/function/signature.md +++ b/guide/src/function/signature.md @@ -11,18 +11,16 @@ This section of the guide goes into detail about use of the `#[pyo3(signature = For example, below is a function that accepts arbitrary keyword arguments (`**kwargs` in Python syntax) and returns the number that was passed: ```rust,no_run -use pyo3::prelude::*; -use pyo3::types::PyDict; - -#[pyfunction] -#[pyo3(signature = (**kwds))] -fn num_kwds(kwds: Option<&Bound<'_, PyDict>>) -> usize { - kwds.map_or(0, |dict| dict.len()) -} - -#[pymodule] -fn module_with_functions(m: &Bound<'_, PyModule>) -> PyResult<()> { - m.add_function(wrap_pyfunction!(num_kwds, m)?) +#[pyo3::pymodule] +mod module_with_functions { + use pyo3::prelude::*; + use pyo3::types::PyDict; + + #[pyfunction] + #[pyo3(signature = (**kwds))] + fn num_kwds(kwds: Option<&Bound<'_, PyDict>>) -> usize { + kwds.map_or(0, |dict| dict.len()) + } } ``` diff --git a/guide/src/getting-started.md b/guide/src/getting-started.md index dfa55d8cb8d..a75b7a75b6e 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.63. +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.74. If you can run `rustc --version` and the version is new enough you're good to go! @@ -146,20 +146,18 @@ classifiers = [ After this you can setup Rust code to be available in Python as below; for example, you can place this code in `src/lib.rs`: ```rust,no_run -use pyo3::prelude::*; - -/// Formats the sum of two numbers as string. -#[pyfunction] -fn sum_as_string(a: usize, b: usize) -> PyResult { - Ok((a + b).to_string()) -} - /// A Python module implemented in Rust. The name of this function must match /// the `lib.name` setting in the `Cargo.toml`, else Python will not be able to /// import the module. -#[pymodule] -fn pyo3_example(m: &Bound<'_, PyModule>) -> PyResult<()> { - m.add_function(wrap_pyfunction!(sum_as_string, m)?) +#[pyo3::pymodule] +mod pyo3_example { + use pyo3::prelude::*; + + /// Formats the sum of two numbers as string. + #[pyfunction] + fn sum_as_string(a: usize, b: usize) -> PyResult { + Ok((a + b).to_string()) + } } ``` diff --git a/guide/src/parallelism.md b/guide/src/parallelism.md index c9c01d4f3b8..a937b49764f 100644 --- a/guide/src/parallelism.md +++ b/guide/src/parallelism.md @@ -134,7 +134,7 @@ that deadlocks with the GIL in these cases. 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: +of booleans based on a predicate using a `rayon` parallel iterator: ```rust,no_run use pyo3::prelude::*; @@ -160,15 +160,15 @@ let allowed_ids: Vec = Python::attach(|outer_py| { 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 +It's important to note that there is an `outer_py` Python token as well as +an `inner_py` token. Sharing Python tokens between threads is not allowed +and threads must individually attach to the interpreter to access data wrapped by a Python object. It's also important to see that this example uses [`Python::detach`] to wrap the code that spawns OS threads via `rayon`. If this example didn't use -`detach`, 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 +`detach`, 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 `detach` allows the GIL to be released in the thread collecting the results from the worker threads. You should always call `detach` in situations that spawn worker threads, but especially so in diff --git a/guide/src/performance.md b/guide/src/performance.md index 0021a6c2033..1981eedabbc 100644 --- a/guide/src/performance.md +++ b/guide/src/performance.md @@ -53,9 +53,9 @@ fn frobnicate<'py>(value: &Bound<'py, PyAny>) -> PyResult> { } ``` -## Access to Bound implies access to GIL token +## Access to Bound implies access to Python token -Calling `Python::attach` is effectively a no-op when the GIL is already held, but checking that this is the case still has a cost. If an existing GIL token can not be accessed, for example when implementing a pre-existing trait, but a GIL-bound reference is available, this cost can be avoided by exploiting that access to GIL-bound reference gives zero-cost access to a GIL token via `Bound::py`. +Calling `Python::attach` is effectively a no-op when we're already attached to the interpreter, but checking that this is the case still has a cost. If an existing Python token can not be accessed, for example when implementing a pre-existing trait, but a Python-bound reference is available, this cost can be avoided by exploiting that access to Python-bound reference gives zero-cost access to a Python token via `Bound::py`. For example, instead of writing @@ -112,11 +112,11 @@ Rust tuples may make use of [`vectorcall`] where as `Bound<'_, PyTuple>` and `Py ## 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. +PyO3 uses global mutable state to keep track of deferred reference count updates implied by `impl Drop for Py` being called without being attached to the interpreter. The necessary synchronization to obtain and apply these reference count updates when PyO3-based code next attaches to the interpreter 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 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 being attached to the interpreter. If `pyo3_leak_on_drop_without_reference_pool` is additionally enabled, objects dropped without being attached to Python 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::attach` without making sure to re-acquire the GIL beforehand. For example, the following code +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::attach` without making sure to re-attach beforehand. For example, the following code ```rust,ignore # use pyo3::prelude::*; diff --git a/guide/src/python-from-rust/calling-existing-code.md b/guide/src/python-from-rust/calling-existing-code.md index 5c1b2701e01..90e8a3ee717 100644 --- a/guide/src/python-from-rust/calling-existing-code.md +++ b/guide/src/python-from-rust/calling-existing-code.md @@ -159,15 +159,14 @@ As an example, the below adds the module `foo` to the embedded interpreter: use pyo3::prelude::*; use pyo3::ffi::c_str; -#[pyfunction] -fn add_one(x: i64) -> i64 { - x + 1 -} - #[pymodule] -fn foo(foo_module: &Bound<'_, PyModule>) -> PyResult<()> { - foo_module.add_function(wrap_pyfunction!(add_one, foo_module)?)?; - Ok(()) +mod foo { + use pyo3::prelude::*; + + #[pyfunction] + fn add_one(x: i64) -> i64 { + x + 1 + } } fn main() -> PyResult<()> { diff --git a/guide/src/trait-bounds.md b/guide/src/trait-bounds.md index 94f01caa03e..0f14d8e3783 100644 --- a/guide/src/trait-bounds.md +++ b/guide/src/trait-bounds.md @@ -118,6 +118,7 @@ Let's add the PyO3 annotations and add a constructor: ```rust,no_run # #![allow(dead_code)] +# fn main() {} # pub trait Model { # fn set_variables(&mut self, inputs: &Vec); # fn compute(&mut self); @@ -130,12 +131,6 @@ struct UserModel { model: Py, } -#[pymodule] -fn trait_exposure(m: &Bound<'_, PyModule>) -> PyResult<()> { - m.add_class::()?; - Ok(()) -} - #[pymethods] impl UserModel { #[new] @@ -143,6 +138,12 @@ impl UserModel { UserModel { model } } } + +#[pymodule] +mod trait_exposure { + #[pymodule_export] + use super::UserModel; +} ``` Now we add the PyO3 annotations to the trait implementation: @@ -458,6 +459,7 @@ It is also required to make the struct public. ```rust,no_run # #![allow(dead_code)] +# fn main() {} use pyo3::prelude::*; use pyo3::types::PyList; @@ -482,13 +484,6 @@ pub struct UserModel { model: Py, } -#[pymodule] -fn trait_exposure(m: &Bound<'_, PyModule>) -> PyResult<()> { - m.add_class::()?; - m.add_function(wrap_pyfunction!(solve_wrapper, m)?)?; - Ok(()) -} - #[pymethods] impl UserModel { #[new] @@ -511,6 +506,12 @@ impl UserModel { } } +#[pymodule] +mod trait_exposure { + #[pymodule_export] + use super::{UserModel, solve_wrapper}; +} + impl Model for UserModel { fn set_variables(&mut self, var: &Vec) { println!("Rust calling Python to set the variables"); diff --git a/guide/src/types.md b/guide/src/types.md index 404779f2266..0b8e1fbcf87 100644 --- a/guide/src/types.md +++ b/guide/src/types.md @@ -30,7 +30,7 @@ Because `Py` is not bound to [the `'py` lifetime](./python-from-rust.md#the-p 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 + - Other functionality, such as [`Drop`][Drop], needs to check at runtime for attachment to the Python interpreter, 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 better for function arguments. From b481b82f0ee121f8ef0a53741369d5e880a7a39a Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sat, 12 Jul 2025 21:58:32 +0100 Subject: [PATCH 739/936] ci: allow slightly bigger drop in project coverage (#5236) --- codecov.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/codecov.yml b/codecov.yml index d30f0ff47dc..356facdc3ff 100644 --- a/codecov.yml +++ b/codecov.yml @@ -5,8 +5,12 @@ coverage: project: default: target: auto - # Allow a tiny drop of overall project coverage in PR to reduce spurious failures. - threshold: 0.25% + # Allow a tiny drop of overall project coverage in PR due to + # not all configurations being tested in PR runs. + # + # (Note that patch coverage will still be required to be at least + # the project coverage.) + threshold: 1% ignore: - tests/ From 1040c5a6ec4a5799a79e09dad5b4e99bae1dbdc7 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Mon, 14 Jul 2025 16:25:42 +0100 Subject: [PATCH 740/936] refactor: break `gil.rs` into parts (#5232) * refactor: break `gil.rs` into parts * fix PyPy build * Update src/internal/state.rs Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> * remove more "GIL" language from `state.rs` * Apply suggestions from code review Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> * update more comments * fixup PyPy case, comments in `interpreter_lifecycle.rs` --------- Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> --- src/gil.rs | 675 ----------------------------------- src/impl_.rs | 1 - src/impl_/not_send.rs | 7 - src/impl_/pymethods.rs | 4 +- src/impl_/trampoline.rs | 16 +- src/instance.rs | 6 +- src/internal.rs | 1 + src/internal/state.rs | 540 ++++++++++++++++++++++++++++ src/interpreter_lifecycle.rs | 141 ++++++++ src/lib.rs | 8 +- src/marker.rs | 24 +- src/sync.rs | 18 +- tests/ui/not_send.stderr | 13 +- tests/ui/not_send2.stderr | 13 +- 14 files changed, 736 insertions(+), 731 deletions(-) delete mode 100644 src/gil.rs delete mode 100644 src/impl_/not_send.rs create mode 100644 src/internal/state.rs create mode 100644 src/interpreter_lifecycle.rs diff --git a/src/gil.rs b/src/gil.rs deleted file mode 100644 index ade83e6c412..00000000000 --- a/src/gil.rs +++ /dev/null @@ -1,675 +0,0 @@ -//! Interaction with Python's global interpreter lock - -#[cfg(pyo3_disable_reference_pool)] -use crate::impl_::panic::PanicTrap; -use crate::{ffi, Python}; -use std::cell::Cell; -#[cfg(not(pyo3_disable_reference_pool))] -use std::sync::OnceLock; -use std::{mem, ptr::NonNull, sync}; - -static START: sync::Once = sync::Once::new(); - -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 - /// they are dropped. - /// - /// As a result, if this thread has the GIL, GIL_COUNT is greater than zero. - /// - /// 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) }; -} - -const GIL_LOCKED_DURING_TRAVERSE: isize = -1; - -/// Checks whether the GIL is acquired. -/// -/// Note: This uses pyo3's internal count rather than PyGILState_Check for two reasons: -/// 1) for performance -/// 2) PyGILState_Check always returns 1 if the sub-interpreter APIs have ever been called, -/// which could lead to incorrect conclusions that the GIL is held. -#[inline(always)] -fn gil_is_acquired() -> bool { - GIL_COUNT.try_with(|c| c.get() > 0).unwrap_or(false) -} - -/// Prepares the use of Python in a free-threaded context. -/// -/// If the Python interpreter is not already initialized, this function will initialize it with -/// signal handling disabled (Python will not raise the `KeyboardInterrupt` exception). Python -/// signal handling depends on the notion of a 'main thread', which must be the thread that -/// initializes the Python interpreter. -/// -/// If the Python interpreter is already initialized, this function has no effect. -/// -/// This function is unavailable under PyPy because PyPy cannot be embedded in Rust (or any other -/// software). Support for this is tracked on the -/// [PyPy issue tracker](https://github.com/pypy/pypy/issues/3836). -/// -/// # Examples -/// ```rust -/// use pyo3::prelude::*; -/// -/// # fn main() -> PyResult<()> { -/// pyo3::prepare_freethreaded_python(); -/// Python::attach(|py| py.run(pyo3::ffi::c_str!("print('Hello World')"), None, None)) -/// # } -/// ``` -#[cfg(not(any(PyPy, GraalPy)))] -pub fn prepare_freethreaded_python() { - // Protect against race conditions when Python is not yet initialized and multiple threads - // concurrently call 'prepare_freethreaded_python()'. Note that we do not protect against - // concurrent initialization of the Python runtime by other users of the Python C API. - START.call_once_force(|_| unsafe { - // Use call_once_force because if initialization panics, it's okay to try again. - if ffi::Py_IsInitialized() == 0 { - ffi::Py_InitializeEx(0); - - // Release the GIL. - ffi::PyEval_SaveThread(); - } - }); -} - -/// Executes the provided closure with an embedded Python interpreter. -/// -/// This function initializes the Python interpreter, executes the provided closure, and then -/// finalizes the Python interpreter. -/// -/// After execution all Python resources are cleaned up, and no further Python APIs can be called. -/// Because many Python modules implemented in C do not support multiple Python interpreters in a -/// single process, it is not safe to call this function more than once. (Many such modules will not -/// initialize correctly on the second run.) -/// -/// # Panics -/// - If the Python interpreter is already initialized before calling this function. -/// -/// # Safety -/// - This function should only ever be called once per process (usually as part of the `main` -/// function). It is also not thread-safe. -/// - No Python APIs can be used after this function has finished executing. -/// - The return value of the closure must not contain any Python value, _including_ `PyResult`. -/// -/// # Examples -/// -/// ```rust -/// unsafe { -/// pyo3::with_embedded_python_interpreter(|py| { -/// 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); -/// } -/// }); -/// } -/// ``` -#[cfg(not(any(PyPy, GraalPy)))] -pub unsafe fn with_embedded_python_interpreter(f: F) -> R -where - F: for<'p> FnOnce(Python<'p>) -> R, -{ - assert_eq!( - unsafe { ffi::Py_IsInitialized() }, - 0, - "called `with_embedded_python_interpreter` but a Python interpreter is already running." - ); - - unsafe { ffi::Py_InitializeEx(0) }; - - let result = { - 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. - py.import("threading").unwrap(); - - // Execute the closure. - f(py) - }; - - // Finalize the Python interpreter. - unsafe { ffi::Py_Finalize() }; - - result -} - -/// RAII type that represents the Global Interpreter Lock acquisition. -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 }, -} - -impl GILGuard { - /// PyO3 internal API for acquiring the GIL. The public API is Python::attach. - /// - /// 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() { - // SAFETY: We just checked that the GIL is already acquired. - return unsafe { Self::assume() }; - } - - // Maybe auto-initialize the GIL: - // - If auto-initialize feature set and supported, try to initialize the interpreter. - // - If the auto-initialize feature is set but unsupported, emit hard errors only when the - // extension-module feature is not activated - extension modules don't care about - // auto-initialize so this avoids breaking existing builds. - // - Otherwise, just check the GIL is initialized. - #[cfg(all(feature = "auto-initialize", not(any(PyPy, GraalPy))))] - { - prepare_freethreaded_python(); - } - #[cfg(not(all(feature = "auto-initialize", not(any(PyPy, GraalPy)))))] - { - // This is a "hack" to make running `cargo test` for PyO3 convenient (i.e. no need - // to specify `--features auto-initialize` manually. Tests within the crate itself - // all depend on the auto-initialize feature for conciseness but Cargo does not - // provide a mechanism to specify required features for tests. - #[cfg(not(any(PyPy, GraalPy)))] - if option_env!("CARGO_PRIMARY_PACKAGE").is_some() { - prepare_freethreaded_python(); - } - - START.call_once_force(|_| unsafe { - // Use call_once_force because if there is a panic because the interpreter is - // not initialized, it's fine for the user to initialize the interpreter and - // retry. - assert_ne!( - ffi::Py_IsInitialized(), - 0, - "The Python interpreter is not initialized and the `auto-initialize` \ - feature is not enabled.\n\n\ - Consider calling `pyo3::prepare_freethreaded_python()` before attempting \ - to use Python APIs." - ); - }); - } - - // SAFETY: We have ensured the Python interpreter is initialized. - unsafe { Self::acquire_unchecked() } - } - - /// Acquires the `GILGuard` without performing any state checking. - /// - /// 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) unsafe fn acquire_unchecked() -> Self { - if gil_is_acquired() { - return unsafe { Self::assume() }; - } - - let gstate = unsafe { ffi::PyGILState_Ensure() }; // acquire GIL - increment_gil_count(); - - #[cfg(not(pyo3_disable_reference_pool))] - if let Some(pool) = POOL.get() { - pool.update_counts(unsafe { Python::assume_gil_acquired() }); - } - GILGuard::Ensured { gstate } - } - - /// Acquires the `GILGuard` while assuming that the GIL is already held. - pub(crate) unsafe fn assume() -> Self { - increment_gil_count(); - let guard = GILGuard::Assumed; - #[cfg(not(pyo3_disable_reference_pool))] - if let Some(pool) = POOL.get() { - pool.update_counts(guard.python()); - } - guard - } - - /// Gets the Python token associated with this [`GILGuard`]. - #[inline] - pub fn python(&self) -> Python<'_> { - unsafe { Python::assume_gil_acquired() } - } -} - -/// The Drop implementation for `GILGuard` will release the GIL. -impl Drop for GILGuard { - fn drop(&mut self) { - match self { - GILGuard::Assumed => {} - GILGuard::Ensured { gstate } => unsafe { - // Drop the objects in the pool before attempting to release the thread state - ffi::PyGILState_Release(*gstate); - }, - } - decrement_gil_count(); - } -} - -// Vector of PyObject -type PyObjVec = Vec>; - -#[cfg(not(pyo3_disable_reference_pool))] -/// Thread-safe storage for objects which were dec_ref while the GIL was not held. -struct ReferencePool { - pending_decrefs: sync::Mutex, -} - -#[cfg(not(pyo3_disable_reference_pool))] -impl ReferencePool { - const fn new() -> Self { - Self { - pending_decrefs: sync::Mutex::new(Vec::new()), - } - } - - fn register_decref(&self, obj: NonNull) { - self.pending_decrefs.lock().unwrap().push(obj); - } - - fn update_counts(&self, _py: Python<'_>) { - let mut pending_decrefs = self.pending_decrefs.lock().unwrap(); - if pending_decrefs.is_empty() { - return; - } - - let decrefs = mem::take(&mut *pending_decrefs); - drop(pending_decrefs); - - for ptr in decrefs { - unsafe { ffi::Py_DECREF(ptr.as_ptr()) }; - } - } -} - -#[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: OnceLock = OnceLock::new(); - -#[cfg(not(pyo3_disable_reference_pool))] -fn get_pool() -> &'static ReferencePool { - POOL.get_or_init(ReferencePool::new) -} - -/// A guard which can be used to temporarily release the GIL and restore on `Drop`. -pub(crate) struct SuspendGIL { - count: isize, - tstate: *mut ffi::PyThreadState, -} - -impl SuspendGIL { - pub(crate) unsafe fn new() -> Self { - let count = GIL_COUNT.with(|c| c.replace(0)); - let tstate = unsafe { ffi::PyEval_SaveThread() }; - - Self { count, tstate } - } -} - -impl Drop for SuspendGIL { - fn drop(&mut self) { - GIL_COUNT.with(|c| c.set(self.count)); - unsafe { - 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))] - if let Some(pool) = POOL.get() { - pool.update_counts(Python::assume_gil_acquired()); - } - } - } -} - -/// Used to lock safe access to the GIL -pub(crate) struct LockGIL { - count: isize, -} - -impl LockGIL { - /// Lock access to the GIL while an implementation of `__traverse__` is running - pub fn during_traverse() -> Self { - Self::new(GIL_LOCKED_DURING_TRAVERSE) - } - - fn new(reason: isize) -> Self { - let count = GIL_COUNT.with(|c| c.replace(reason)); - - Self { count } - } - - #[cold] - fn bail(current: isize) { - match current { - GIL_LOCKED_DURING_TRAVERSE => panic!( - "Access to the GIL is prohibited while a __traverse__ implmentation is running." - ), - _ => panic!("Access to the GIL is currently prohibited."), - } - } -} - -impl Drop for LockGIL { - fn drop(&mut self) { - GIL_COUNT.with(|c| c.set(self.count)); - } -} - -/// Increments the reference count of a Python object if the GIL is held. If -/// the GIL is not held, this function will panic. -/// -/// # Safety -/// The object must be an owned Python reference. -#[cfg(feature = "py-clone")] -#[track_caller] -pub unsafe fn register_incref(obj: NonNull) { - if gil_is_acquired() { - unsafe { ffi::Py_INCREF(obj.as_ptr()) } - } else { - panic!("Cannot clone pointer into Python heap without the GIL being held."); - } -} - -/// Registers a Python object pointer inside the release pool, to have its reference count decreased -/// the next time the GIL is acquired in pyo3. -/// -/// If the GIL is held, the reference count will be decreased immediately instead of being queued -/// for later. -/// -/// # Safety -/// The object must be an owned Python reference. -#[track_caller] -pub unsafe fn register_decref(obj: NonNull) { - if gil_is_acquired() { - unsafe { ffi::Py_DECREF(obj.as_ptr()) } - } else { - #[cfg(not(pyo3_disable_reference_pool))] - get_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."); - } - } -} - -/// Increments pyo3's internal GIL count - to be called whenever GILPool or GILGuard is created. -#[inline(always)] -fn increment_gil_count() { - // Ignores the error in case this function called from `atexit`. - let _ = GIL_COUNT.try_with(|c| { - let current = c.get(); - if current < 0 { - LockGIL::bail(current); - } - c.set(current + 1); - }); -} - -/// Decrements pyo3's internal GIL count - to be called whenever GILPool or GILGuard is dropped. -#[inline(always)] -fn decrement_gil_count() { - // Ignores the error in case this function called from `atexit`. - let _ = GIL_COUNT.try_with(|c| { - let current = c.get(); - debug_assert!( - current > 0, - "Negative GIL count detected. Please report this error to the PyO3 repo as a bug." - ); - c.set(current - 1); - }); -} - -#[cfg(test)] -mod tests { - use super::GIL_COUNT; - #[cfg(not(pyo3_disable_reference_pool))] - use super::{get_pool, gil_is_acquired}; - use crate::{ffi, PyObject, Python}; - use crate::{gil::GILGuard, types::any::PyAnyMethods}; - use std::ptr::NonNull; - - fn get_object(py: Python<'_>) -> PyObject { - py.eval(ffi::c_str!("object()"), None, None) - .unwrap() - .unbind() - } - - #[cfg(not(pyo3_disable_reference_pool))] - fn pool_dec_refs_does_not_contain(obj: &PyObject) -> bool { - !get_pool() - .pending_decrefs - .lock() - .unwrap() - .contains(&unsafe { NonNull::new_unchecked(obj.as_ptr()) }) - } - - // 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 { - get_pool() - .pending_decrefs - .lock() - .unwrap() - .contains(&unsafe { NonNull::new_unchecked(obj.as_ptr()) }) - } - - #[test] - fn test_pyobject_drop_with_gil_decreases_refcnt() { - Python::attach(|py| { - let obj = get_object(py); - - // Create a reference to drop with the GIL. - let reference = obj.clone_ref(py); - - assert_eq!(obj.get_refcnt(py), 2); - #[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); - #[cfg(not(any(pyo3_disable_reference_pool)))] - assert!(pool_dec_refs_does_not_contain(&obj)); - }); - } - - #[test] - #[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::attach(|py| { - let obj = get_object(py); - // Create a reference to drop without the GIL. - let reference = obj.clone_ref(py); - - assert_eq!(obj.get_refcnt(py), 2); - assert!(pool_dec_refs_does_not_contain(&obj)); - - // Drop reference in a separate thread which doesn't have the GIL. - std::thread::spawn(move || drop(reference)).join().unwrap(); - - // 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::attach(|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)); - }); - } - - #[test] - #[allow(deprecated)] - fn test_gil_counts() { - // Check `attach` and GILGuard both increase counts correctly - let get_gil_count = || GIL_COUNT.with(|c| c.get()); - - assert_eq!(get_gil_count(), 0); - Python::attach(|_| { - assert_eq!(get_gil_count(), 1); - - let pool = unsafe { GILGuard::assume() }; - assert_eq!(get_gil_count(), 2); - - let pool2 = unsafe { GILGuard::assume() }; - assert_eq!(get_gil_count(), 3); - - drop(pool); - assert_eq!(get_gil_count(), 2); - - Python::attach(|_| { - // nested `attach` updates gil count - assert_eq!(get_gil_count(), 3); - }); - assert_eq!(get_gil_count(), 2); - - drop(pool2); - assert_eq!(get_gil_count(), 1); - }); - assert_eq!(get_gil_count(), 0); - } - - #[test] - fn test_detach() { - assert!(!gil_is_acquired()); - - Python::attach(|py| { - assert!(gil_is_acquired()); - - py.detach(move || { - assert!(!gil_is_acquired()); - - Python::attach(|_| assert!(gil_is_acquired())); - - assert!(!gil_is_acquired()); - }); - - assert!(gil_is_acquired()); - }); - - assert!(!gil_is_acquired()); - } - - #[cfg(feature = "py-clone")] - #[test] - #[should_panic] - fn test_detach_updates_refcounts() { - Python::attach(|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 which should panic - py.detach(|| obj.clone()); - }); - } - - #[test] - fn dropping_gil_does_not_invalidate_references() { - // Acquiring GIL for the second time should be safe - see #864 - Python::attach(|py| { - let obj = Python::attach(|_| 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); - }) - } - - #[cfg(feature = "py-clone")] - #[test] - fn test_clone_with_gil() { - Python::attach(|py| { - let obj = get_object(py); - let count = obj.get_refcnt(py); - - // Cloning with the GIL should increase reference count immediately - #[allow(clippy::redundant_clone)] - let c = obj.clone(); - assert_eq!(count + 1, c.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. - - use crate::ffi; - use crate::gil::GILGuard; - - Python::attach(|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. - let pool = unsafe { GILGuard::assume() }; - - // Rebuild obj so that it can be dropped - unsafe { - PyObject::from_owned_ptr( - pool.python(), - ffi::PyCapsule_GetPointer(capsule, std::ptr::null()) as _, - ) - }; - } - - let ptr = obj.into_ptr(); - - let capsule = - unsafe { ffi::PyCapsule_New(ptr as _, std::ptr::null(), Some(capsule_drop)) }; - - get_pool().register_decref(NonNull::new(capsule).unwrap()); - - // Updating the counts will call decref on the capsule, which calls capsule_drop - get_pool().update_counts(py); - }) - } - - #[test] - #[cfg(not(pyo3_disable_reference_pool))] - fn test_gil_guard_update_counts() { - use crate::gil::GILGuard; - - Python::attach(|py| { - let obj = get_object(py); - - // For GILGuard::acquire - - get_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)); - - // For GILGuard::assume - - get_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/src/impl_.rs b/src/impl_.rs index de82c650f43..8026312ed5c 100644 --- a/src/impl_.rs +++ b/src/impl_.rs @@ -17,7 +17,6 @@ pub mod freelist; pub mod frompyobject; #[cfg(feature = "experimental-inspect")] pub mod introspection; -pub(crate) mod not_send; pub mod panic; pub mod pycell; pub mod pyclass; diff --git a/src/impl_/not_send.rs b/src/impl_/not_send.rs deleted file mode 100644 index 3304d79a1ba..00000000000 --- a/src/impl_/not_send.rs +++ /dev/null @@ -1,7 +0,0 @@ -use std::marker::PhantomData; - -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>>); diff --git a/src/impl_/pymethods.rs b/src/impl_/pymethods.rs index fd4f6bbb3e0..dd3a4f89ba1 100644 --- a/src/impl_/pymethods.rs +++ b/src/impl_/pymethods.rs @@ -1,9 +1,9 @@ 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::internal::get_slot::{get_slot, TP_BASE, TP_CLEAR, TP_TRAVERSE}; +use crate::internal::state::ForbidAttaching; use crate::pycell::impl_::PyClassBorrowChecker as _; use crate::pycell::{PyBorrowError, PyBorrowMutError}; use crate::pyclass::boolean_struct::False; @@ -293,7 +293,7 @@ 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(); + let lock = ForbidAttaching::during_traverse(); let super_retval = unsafe { call_super_traverse(slf, visit, arg, current_traverse) }; if super_retval != 0 { diff --git a/src/impl_/trampoline.rs b/src/impl_/trampoline.rs index 01f8f5e8b0e..6bcf71c6ca7 100644 --- a/src/impl_/trampoline.rs +++ b/src/impl_/trampoline.rs @@ -9,7 +9,7 @@ use std::{ panic::{self, UnwindSafe}, }; -use crate::gil::GILGuard; +use crate::internal::state::AttachGuard; use crate::{ ffi, ffi_ptr_ext::FfiPtrExt, impl_::callback::PyCallbackOutput, impl_::panic::PanicTrap, impl_::pymethods::IPowModulo, panic::PanicException, types::PyModule, Py, PyResult, Python, @@ -168,12 +168,12 @@ trampoline!( ) -> *mut ffi::PyObject; ); -/// Implementation of trampoline functions, which sets up a GILPool and calls F. +/// Implementation of trampoline functions, which sets up an AttachGuard and calls F. /// /// 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. +/// The thread must already be attached to the interpreter when this is called. #[inline] pub(crate) unsafe fn trampoline(body: F) -> R where @@ -182,8 +182,8 @@ where { let trap = PanicTrap::new("uncaught panic at ffi boundary"); - // SAFETY: This function requires the GIL to already be held. - let guard = unsafe { GILGuard::assume() }; + // SAFETY: This function requires the thread to already be attached. + let guard = unsafe { AttachGuard::assume() }; let py = guard.python(); let out = panic_result_into_callback_output( py, @@ -222,7 +222,7 @@ where /// # Safety /// /// - ctx must be either a valid ffi::PyObject or NULL -/// - The GIL must already be held when this is called. +/// - The thread must be attached to the interpreter when this is called. #[inline] unsafe fn trampoline_unraisable(body: F, ctx: *mut ffi::PyObject) where @@ -230,8 +230,8 @@ where { let trap = PanicTrap::new("uncaught panic at ffi boundary"); - // SAFETY: The GIL is already held. - let guard = unsafe { GILGuard::assume() }; + // SAFETY: Thread is known to be attached. + let guard = unsafe { AttachGuard::assume() }; let py = guard.python(); if let Err(py_err) = panic::catch_unwind(move || body(py)) diff --git a/src/instance.rs b/src/instance.rs index bc9c949516f..5114d888d43 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -11,7 +11,7 @@ use crate::{ ffi, DowncastError, FromPyObject, PyAny, PyClass, PyClassInitializer, PyRef, PyRefMut, PyTypeInfo, Python, }; -use crate::{gil, PyTypeCheck}; +use crate::{internal::state, PyTypeCheck}; use std::marker::PhantomData; use std::mem::ManuallyDrop; use std::ops::Deref; @@ -1747,7 +1747,7 @@ impl Clone for Py { #[track_caller] fn clone(&self) -> Self { unsafe { - gil::register_incref(self.0); + state::register_incref(self.0); } Self(self.0, PhantomData) } @@ -1765,7 +1765,7 @@ impl Drop for Py { #[track_caller] fn drop(&mut self) { unsafe { - gil::register_decref(self.0); + state::register_decref(self.0); } } } diff --git a/src/internal.rs b/src/internal.rs index d42cc2b87fc..7299f90ed03 100644 --- a/src/internal.rs +++ b/src/internal.rs @@ -1,3 +1,4 @@ //! Holding place for code which is not intended to be reachable from outside of PyO3. pub(crate) mod get_slot; +pub(crate) mod state; diff --git a/src/internal/state.rs b/src/internal/state.rs new file mode 100644 index 00000000000..912434cb7c9 --- /dev/null +++ b/src/internal/state.rs @@ -0,0 +1,540 @@ +//! Interaction with attachment of the current thread to the Python interpreter. + +#[cfg(pyo3_disable_reference_pool)] +use crate::impl_::panic::PanicTrap; +use crate::{ffi, Python}; + +use std::cell::Cell; +#[cfg(not(pyo3_disable_reference_pool))] +use std::sync::OnceLock; +use std::{mem, ptr::NonNull, sync}; + +std::thread_local! { + /// This is an internal counter in pyo3 monitoring whether this thread is attached to the interpreter. + /// + /// It will be incremented whenever an AttachGuard is created, and decremented whenever + /// they are dropped. + /// + /// As a result, if this thread is attached to the interpreter, ATTACH_COUNT is greater than zero. + /// + /// Additionally, we sometimes need to prevent safe access to the Python interpreter, + /// e.g. when implementing `__traverse__`, which is represented by a negative value. + static ATTACH_COUNT: Cell = const { Cell::new(0) }; +} + +const ATTACH_FORBIDDEN_DURING_TRAVERSE: isize = -1; + +/// Checks whether the thread is attached to the Python interpreter. +/// +/// Note: This uses pyo3's internal count rather than PyGILState_Check for two reasons: +/// 1) for performance +/// 2) PyGILState_Check always returns 1 if the sub-interpreter APIs have ever been called, +/// which could lead to incorrect conclusions that the thread is attached. +#[inline(always)] +fn thread_is_attached() -> bool { + ATTACH_COUNT.try_with(|c| c.get() > 0).unwrap_or(false) +} + +/// RAII type that represents thread attachment to the interpreter. +pub(crate) enum AttachGuard { + /// Indicates the thread was already attached when this AttachGuard was acquired. + Assumed, + /// Indicates that we attached when this AttachGuard was acquired + Ensured { gstate: ffi::PyGILState_STATE }, +} + +impl AttachGuard { + /// PyO3 internal API for attaching to the Python interpreter. The public API is Python::attach. + /// + /// If the thread was already attached via PyO3, this returns + /// `AttachGuard::Assumed`. Otherwise, the thread will attach now and + /// `AttachGuard::Ensured` will be returned. + pub(crate) fn acquire() -> Self { + if thread_is_attached() { + // SAFETY: We just checked that the thread is already attached. + return unsafe { Self::assume() }; + } + + crate::interpreter_lifecycle::ensure_initialized(); + + // SAFETY: We have ensured the Python interpreter is initialized. + unsafe { Self::acquire_unchecked() } + } + + /// Acquires the `AttachGuard` without performing any state checking. + /// + /// This can be called in "unsafe" contexts where the normal interpreter state + /// checking performed by `AttachGuard::acquire` may fail. This includes calling + /// as part of multi-phase interpreter initialization. + /// + /// # Safety + /// + /// The caller must ensure that the Python interpreter is sufficiently initialized + /// for a thread to be able to attach to it. + pub(crate) unsafe fn acquire_unchecked() -> Self { + if thread_is_attached() { + return unsafe { Self::assume() }; + } + + // SAFETY: interpreter is sufficiently initialized to attach a thread. + let gstate = unsafe { ffi::PyGILState_Ensure() }; + increment_attach_count(); + + #[cfg(not(pyo3_disable_reference_pool))] + if let Some(pool) = POOL.get() { + pool.update_counts(unsafe { Python::assume_gil_acquired() }); + } + AttachGuard::Ensured { gstate } + } + + /// Acquires the `AttachGuard` while assuming that the thread is already attached + /// to the interpreter. + pub(crate) unsafe fn assume() -> Self { + increment_attach_count(); + let guard = AttachGuard::Assumed; + #[cfg(not(pyo3_disable_reference_pool))] + if let Some(pool) = POOL.get() { + pool.update_counts(guard.python()); + } + guard + } + + /// Gets the Python token associated with this [`AttachGuard`]. + #[inline] + pub fn python(&self) -> Python<'_> { + unsafe { Python::assume_gil_acquired() } + } +} + +/// The Drop implementation for `AttachGuard` will decrement the attach count (and potentially detach). +impl Drop for AttachGuard { + fn drop(&mut self) { + match self { + AttachGuard::Assumed => {} + AttachGuard::Ensured { gstate } => unsafe { + // Drop the objects in the pool before attempting to release the thread state + ffi::PyGILState_Release(*gstate); + }, + } + decrement_attach_count(); + } +} + +// Vector of PyObject +type PyObjVec = Vec>; + +#[cfg(not(pyo3_disable_reference_pool))] +/// Thread-safe storage for objects which were dec_ref while not attached. +struct ReferencePool { + pending_decrefs: sync::Mutex, +} + +#[cfg(not(pyo3_disable_reference_pool))] +impl ReferencePool { + const fn new() -> Self { + Self { + pending_decrefs: sync::Mutex::new(Vec::new()), + } + } + + fn register_decref(&self, obj: NonNull) { + self.pending_decrefs.lock().unwrap().push(obj); + } + + fn update_counts(&self, _py: Python<'_>) { + let mut pending_decrefs = self.pending_decrefs.lock().unwrap(); + if pending_decrefs.is_empty() { + return; + } + + let decrefs = mem::take(&mut *pending_decrefs); + drop(pending_decrefs); + + for ptr in decrefs { + unsafe { ffi::Py_DECREF(ptr.as_ptr()) }; + } + } +} + +#[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: OnceLock = OnceLock::new(); + +#[cfg(not(pyo3_disable_reference_pool))] +fn get_pool() -> &'static ReferencePool { + POOL.get_or_init(ReferencePool::new) +} + +/// A guard which can be used to temporarily detach from the interpreter and restore on `Drop`. +pub(crate) struct SuspendAttach { + count: isize, + tstate: *mut ffi::PyThreadState, +} + +impl SuspendAttach { + pub(crate) unsafe fn new() -> Self { + let count = ATTACH_COUNT.with(|c| c.replace(0)); + let tstate = unsafe { ffi::PyEval_SaveThread() }; + + Self { count, tstate } + } +} + +impl Drop for SuspendAttach { + fn drop(&mut self) { + ATTACH_COUNT.with(|c| c.set(self.count)); + unsafe { + ffi::PyEval_RestoreThread(self.tstate); + + // Update counts of `Py` that were dropped while not attached. + #[cfg(not(pyo3_disable_reference_pool))] + if let Some(pool) = POOL.get() { + pool.update_counts(Python::assume_gil_acquired()); + } + } + } +} + +/// Used to lock safe access to the interpreter +pub(crate) struct ForbidAttaching { + count: isize, +} + +impl ForbidAttaching { + /// Lock access to the interpreter while an implementation of `__traverse__` is running + pub fn during_traverse() -> Self { + Self::new(ATTACH_FORBIDDEN_DURING_TRAVERSE) + } + + fn new(reason: isize) -> Self { + let count = ATTACH_COUNT.with(|c| c.replace(reason)); + + Self { count } + } + + #[cold] + fn bail(current: isize) { + match current { + ATTACH_FORBIDDEN_DURING_TRAVERSE => panic!( + "Attaching a thread to the interpreter is prohibited while a __traverse__ implementation is running." + ), + _ => panic!("Attaching a thread to the interpreter is currently prohibited."), + } + } +} + +impl Drop for ForbidAttaching { + fn drop(&mut self) { + ATTACH_COUNT.with(|c| c.set(self.count)); + } +} + +/// Increments the reference count of a Python object if the thread is attached. If +/// the thread is not attached, this function will panic. +/// +/// # Safety +/// The object must be an owned Python reference. +#[cfg(feature = "py-clone")] +#[track_caller] +pub unsafe fn register_incref(obj: NonNull) { + if thread_is_attached() { + unsafe { ffi::Py_INCREF(obj.as_ptr()) } + } else { + panic!("Cannot clone pointer into Python heap without the thread being attached."); + } +} + +/// Registers a Python object pointer inside the release pool, to have its reference count decreased +/// the next time the thread is attached in pyo3. +/// +/// If the thread is attached, the reference count will be decreased immediately instead of being queued +/// for later. +/// +/// # Safety +/// The object must be an owned Python reference. +#[track_caller] +pub unsafe fn register_decref(obj: NonNull) { + if thread_is_attached() { + unsafe { ffi::Py_DECREF(obj.as_ptr()) } + } else { + #[cfg(not(pyo3_disable_reference_pool))] + get_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 thread being attached."); + } + } +} + +/// Increments pyo3's internal attach count - to be called whenever an AttachGuard is created. +#[inline(always)] +fn increment_attach_count() { + // Ignores the error in case this function called from `atexit`. + let _ = ATTACH_COUNT.try_with(|c| { + let current = c.get(); + if current < 0 { + ForbidAttaching::bail(current); + } + c.set(current + 1); + }); +} + +/// Decrements pyo3's internal attach count - to be called whenever AttachGuard is dropped. +#[inline(always)] +fn decrement_attach_count() { + // Ignores the error in case this function called from `atexit`. + let _ = ATTACH_COUNT.try_with(|c| { + let current = c.get(); + debug_assert!( + current > 0, + "Negative attach count detected. Please report this error to the PyO3 repo as a bug." + ); + c.set(current - 1); + }); +} + +#[cfg(test)] +mod tests { + use super::*; + + use crate::{ffi, types::PyAnyMethods, PyObject, Python}; + use std::ptr::NonNull; + + fn get_object(py: Python<'_>) -> PyObject { + py.eval(ffi::c_str!("object()"), None, None) + .unwrap() + .unbind() + } + + #[cfg(not(pyo3_disable_reference_pool))] + fn pool_dec_refs_does_not_contain(obj: &PyObject) -> bool { + !get_pool() + .pending_decrefs + .lock() + .unwrap() + .contains(&unsafe { NonNull::new_unchecked(obj.as_ptr()) }) + } + + // With free-threading, 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 { + get_pool() + .pending_decrefs + .lock() + .unwrap() + .contains(&unsafe { NonNull::new_unchecked(obj.as_ptr()) }) + } + + #[test] + fn test_pyobject_drop_attached_decreases_refcnt() { + Python::attach(|py| { + let obj = get_object(py); + + // Create a reference to drop while attached. + let reference = obj.clone_ref(py); + + assert_eq!(obj.get_refcnt(py), 2); + #[cfg(not(pyo3_disable_reference_pool))] + assert!(pool_dec_refs_does_not_contain(&obj)); + + // While attached, reference count will be decreased immediately. + drop(reference); + + assert_eq!(obj.get_refcnt(py), 1); + #[cfg(not(any(pyo3_disable_reference_pool)))] + assert!(pool_dec_refs_does_not_contain(&obj)); + }); + } + + #[test] + #[cfg(all(not(pyo3_disable_reference_pool), not(target_arch = "wasm32")))] // We are building wasm Python with pthreads disabled + fn test_pyobject_drop_detached_doesnt_decrease_refcnt() { + let obj = Python::attach(|py| { + let obj = get_object(py); + // Create a reference to drop while detached. + let reference = obj.clone_ref(py); + + assert_eq!(obj.get_refcnt(py), 2); + assert!(pool_dec_refs_does_not_contain(&obj)); + + // Drop reference in a separate (detached) thread. + std::thread::spawn(move || drop(reference)).join().unwrap(); + + // The reference count should not have changed, 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 + }); + + // On next attach, the reference is released + #[allow(unused)] + Python::attach(|py| { + // With free-threading, 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)); + }); + } + + #[test] + #[allow(deprecated)] + fn test_attach_counts() { + // Check `attach` and AttachGuard both increase counts correctly + let get_attach_count = || ATTACH_COUNT.with(|c| c.get()); + + assert_eq!(get_attach_count(), 0); + Python::attach(|_| { + assert_eq!(get_attach_count(), 1); + + let pool = unsafe { AttachGuard::assume() }; + assert_eq!(get_attach_count(), 2); + + let pool2 = unsafe { AttachGuard::assume() }; + assert_eq!(get_attach_count(), 3); + + drop(pool); + assert_eq!(get_attach_count(), 2); + + Python::attach(|_| { + // nested `attach` updates attach count + assert_eq!(get_attach_count(), 3); + }); + assert_eq!(get_attach_count(), 2); + + drop(pool2); + assert_eq!(get_attach_count(), 1); + }); + assert_eq!(get_attach_count(), 0); + } + + #[test] + fn test_detach() { + assert!(!thread_is_attached()); + + Python::attach(|py| { + assert!(thread_is_attached()); + + py.detach(move || { + assert!(!thread_is_attached()); + + Python::attach(|_| assert!(thread_is_attached())); + + assert!(!thread_is_attached()); + }); + + assert!(thread_is_attached()); + }); + + assert!(!thread_is_attached()); + } + + #[cfg(feature = "py-clone")] + #[test] + #[should_panic] + fn test_detach_updates_refcounts() { + Python::attach(|py| { + // Make a simple object with 1 reference + let obj = get_object(py); + assert!(obj.get_refcnt(py) == 1); + // Cloning the object when detached should panic + py.detach(|| obj.clone()); + }); + } + + #[test] + fn recursive_attach_ok() { + Python::attach(|py| { + let obj = Python::attach(|_| py.eval(ffi::c_str!("object()"), None, None).unwrap()); + assert_eq!(obj.get_refcnt(), 1); + }) + } + + #[cfg(feature = "py-clone")] + #[test] + fn test_clone_attached() { + Python::attach(|py| { + let obj = get_object(py); + let count = obj.get_refcnt(py); + + // Cloning when attached should increase reference count immediately + #[allow(clippy::redundant_clone)] + let c = obj.clone(); + assert_eq!(count + 1, c.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. + + use crate::ffi; + + Python::attach(|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. + let pool = unsafe { AttachGuard::assume() }; + + // Rebuild obj so that it can be dropped + unsafe { + PyObject::from_owned_ptr( + pool.python(), + ffi::PyCapsule_GetPointer(capsule, std::ptr::null()) as _, + ) + }; + } + + let ptr = obj.into_ptr(); + + let capsule = + unsafe { ffi::PyCapsule_New(ptr as _, std::ptr::null(), Some(capsule_drop)) }; + + get_pool().register_decref(NonNull::new(capsule).unwrap()); + + // Updating the counts will call decref on the capsule, which calls capsule_drop + get_pool().update_counts(py); + }) + } + + #[test] + #[cfg(not(pyo3_disable_reference_pool))] + fn test_attach_guard_update_counts() { + Python::attach(|py| { + let obj = get_object(py); + + // For AttachGuard::acquire + + get_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 = AttachGuard::acquire(); + assert!(pool_dec_refs_does_not_contain(&obj)); + + // For AttachGuard::assume + + get_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 { AttachGuard::assume() }; + assert!(pool_dec_refs_does_not_contain(&obj)); + }) + } +} diff --git a/src/interpreter_lifecycle.rs b/src/interpreter_lifecycle.rs new file mode 100644 index 00000000000..cb3b30d83ad --- /dev/null +++ b/src/interpreter_lifecycle.rs @@ -0,0 +1,141 @@ +#[cfg(not(any(PyPy, GraalPy)))] +use crate::{ffi, internal::state::AttachGuard, Python}; + +static START: std::sync::Once = std::sync::Once::new(); + +/// Prepares the use of Python in a free-threaded context. +/// +/// If the Python interpreter is not already initialized, this function will initialize it with +/// signal handling disabled (Python will not raise the `KeyboardInterrupt` exception). Python +/// signal handling depends on the notion of a 'main thread', which must be the thread that +/// initializes the Python interpreter. +/// +/// If the Python interpreter is already initialized, this function has no effect. +/// +/// This function is unavailable under PyPy because PyPy cannot be embedded in Rust (or any other +/// software). Support for this is tracked on the +/// [PyPy issue tracker](https://github.com/pypy/pypy/issues/3836). +/// +/// # Examples +/// ```rust +/// use pyo3::prelude::*; +/// +/// # fn main() -> PyResult<()> { +/// pyo3::prepare_freethreaded_python(); +/// Python::attach(|py| py.run(pyo3::ffi::c_str!("print('Hello World')"), None, None)) +/// # } +/// ``` +#[cfg(not(any(PyPy, GraalPy)))] +pub fn prepare_freethreaded_python() { + // Protect against race conditions when Python is not yet initialized and multiple threads + // concurrently call 'prepare_freethreaded_python()'. Note that we do not protect against + // concurrent initialization of the Python runtime by other users of the Python C API. + START.call_once_force(|_| unsafe { + // Use call_once_force because if initialization panics, it's okay to try again. + if ffi::Py_IsInitialized() == 0 { + ffi::Py_InitializeEx(0); + + // Release the GIL. + ffi::PyEval_SaveThread(); + } + }); +} + +/// Executes the provided closure with an embedded Python interpreter. +/// +/// This function initializes the Python interpreter, executes the provided closure, and then +/// finalizes the Python interpreter. +/// +/// After execution all Python resources are cleaned up, and no further Python APIs can be called. +/// Because many Python modules implemented in C do not support multiple Python interpreters in a +/// single process, it is not safe to call this function more than once. (Many such modules will not +/// initialize correctly on the second run.) +/// +/// # Panics +/// - If the Python interpreter is already initialized before calling this function. +/// +/// # Safety +/// - This function should only ever be called once per process (usually as part of the `main` +/// function). It is also not thread-safe. +/// - No Python APIs can be used after this function has finished executing. +/// - The return value of the closure must not contain any Python value, _including_ `PyResult`. +/// +/// # Examples +/// +/// ```rust +/// unsafe { +/// pyo3::with_embedded_python_interpreter(|py| { +/// 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); +/// } +/// }); +/// } +/// ``` +#[cfg(not(any(PyPy, GraalPy)))] +pub unsafe fn with_embedded_python_interpreter(f: F) -> R +where + F: for<'p> FnOnce(Python<'p>) -> R, +{ + assert_eq!( + unsafe { ffi::Py_IsInitialized() }, + 0, + "called `with_embedded_python_interpreter` but a Python interpreter is already running." + ); + + unsafe { ffi::Py_InitializeEx(0) }; + + let result = { + let guard = unsafe { AttachGuard::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("threading").unwrap(); + + // Execute the closure. + f(py) + }; + + // Finalize the Python interpreter. + unsafe { ffi::Py_Finalize() }; + + result +} + +pub(crate) fn ensure_initialized() { + // Maybe auto-initialize the interpreter: + // - If auto-initialize feature set and supported, try to initialize the interpreter. + // - If the auto-initialize feature is set but unsupported, emit hard errors only when the + // extension-module feature is not activated - extension modules don't care about + // auto-initialize so this avoids breaking existing builds. + // - Otherwise, just check the interpreter is initialized. + #[cfg(all(feature = "auto-initialize", not(any(PyPy, GraalPy))))] + { + prepare_freethreaded_python(); + } + #[cfg(not(all(feature = "auto-initialize", not(any(PyPy, GraalPy)))))] + { + // This is a "hack" to make running `cargo test` for PyO3 convenient (i.e. no need + // to specify `--features auto-initialize` manually. Tests within the crate itself + // all depend on the auto-initialize feature for conciseness but Cargo does not + // provide a mechanism to specify required features for tests. + #[cfg(not(any(PyPy, GraalPy)))] + if option_env!("CARGO_PRIMARY_PACKAGE").is_some() { + prepare_freethreaded_python(); + } + + START.call_once_force(|_| unsafe { + // Use call_once_force because if there is a panic because the interpreter is + // not initialized, it's fine for the user to initialize the interpreter and + // retry. + assert_ne!( + crate::ffi::Py_IsInitialized(), + 0, + "The Python interpreter is not initialized and the `auto-initialize` \ + feature is not enabled.\n\n\ + Consider calling `pyo3::prepare_freethreaded_python()` before attempting \ + to use Python APIs." + ); + }); + } +} diff --git a/src/lib.rs b/src/lib.rs index 370eee4fa7e..b25141f9669 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -343,9 +343,11 @@ pub use crate::class::*; pub use crate::conversion::{FromPyObject, IntoPyObject, IntoPyObjectExt}; 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, BoundObject, Py, PyObject}; +#[cfg(not(any(PyPy, GraalPy)))] +pub use crate::interpreter_lifecycle::{ + prepare_freethreaded_python, with_embedded_python_interpreter, +}; pub use crate::marker::Python; pub use crate::pycell::{PyRef, PyRefMut}; pub use crate::pyclass::PyClass; @@ -421,10 +423,10 @@ pub mod coroutine; mod err; pub mod exceptions; pub mod ffi; -mod gil; #[doc(hidden)] pub mod impl_; mod instance; +mod interpreter_lifecycle; pub mod marker; pub mod marshal; #[macro_use] diff --git a/src/marker.rs b/src/marker.rs index acfcd22b7de..4e26a4246fb 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -118,8 +118,7 @@ //! [`Py`]: crate::Py use crate::conversion::IntoPyObject; use crate::err::{self, PyResult}; -use crate::gil::{GILGuard, SuspendGIL}; -use crate::impl_::not_send::NotSend; +use crate::internal::state::{AttachGuard, SuspendAttach}; use crate::types::any::PyAnyMethods; use crate::types::{ PyAny, PyCode, PyCodeMethods, PyDict, PyEllipsis, PyModule, PyNone, PyNotImplemented, PyString, @@ -351,7 +350,11 @@ pub use nightly::Ungil; /// The [`Python<'py>`] type can be used to create references to variables owned by the Python /// interpreter, using functions such as [`Python::eval`] and [`PyModule::import`]. #[derive(Copy, Clone)] -pub struct Python<'py>(PhantomData<(&'py GILGuard, NotSend)>); +pub struct Python<'py>(PhantomData<&'py AttachGuard>, PhantomData); + +/// A marker type that makes the type !Send. +/// Workaround for lack of !Send on stable (). +struct NotSend(PhantomData<*mut Python<'static>>); impl Python<'_> { /// See [Python::attach] @@ -412,9 +415,7 @@ impl Python<'_> { where F: for<'py> FnOnce(Python<'py>) -> R, { - let guard = GILGuard::acquire(); - - // SAFETY: Either the GIL was already acquired or we just created a new `GILGuard`. + let guard = AttachGuard::acquire(); f(guard.python()) } @@ -447,7 +448,7 @@ impl Python<'_> { where F: for<'py> FnOnce(Python<'py>) -> R, { - let guard = unsafe { GILGuard::acquire_unchecked() }; + let guard = unsafe { AttachGuard::acquire_unchecked() }; f(guard.python()) } @@ -535,7 +536,7 @@ impl<'py> Python<'py> { // so that the GIL will be reacquired even if `f` panics. // The `Send` bound on the closure prevents the user from // transferring the `Python` token into the closure. - let _guard = unsafe { SuspendGIL::new() }; + let _guard = unsafe { SuspendAttach::new() }; f() } @@ -770,7 +771,7 @@ impl<'unbound> Python<'unbound> { /// [nomicon]: https://doc.rust-lang.org/nomicon/unbounded-lifetimes.html #[inline] pub unsafe fn assume_gil_acquired() -> Python<'unbound> { - Python(PhantomData) + Python(PhantomData, PhantomData) } } @@ -979,4 +980,9 @@ cls.func() runner.reproducer(py).unwrap(); }); } + + #[test] + fn python_is_zst() { + assert_eq!(std::mem::size_of::>(), 0); + } } diff --git a/src/sync.rs b/src/sync.rs index 2492fef7a15..6363ece75bd 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -5,7 +5,7 @@ //! //! [PEP 703]: https://peps.python.org/pep-703/ use crate::{ - gil::SuspendGIL, + internal::state::SuspendAttach, sealed::Sealed, types::{any::PyAnyMethods, PyAny, PyString}, Bound, Py, PyResult, PyTypeCheck, Python, @@ -611,7 +611,7 @@ impl OnceExt for parking_lot::Once { return; } - let ts_guard = unsafe { SuspendGIL::new() }; + let ts_guard = unsafe { SuspendAttach::new() }; self.call_once(move || { drop(ts_guard); @@ -628,7 +628,7 @@ impl OnceExt for parking_lot::Once { return; } - let ts_guard = unsafe { SuspendGIL::new() }; + let ts_guard = unsafe { SuspendAttach::new() }; self.call_once_force(move |state| { drop(ts_guard); @@ -672,7 +672,7 @@ impl MutexExt for std::sync::Mutex { // 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 ts_guard = unsafe { SuspendAttach::new() }; let res = self.lock(); drop(ts_guard); res @@ -691,7 +691,7 @@ impl MutexExt for lock_api::Mutex { return guard; } - let ts_guard = unsafe { SuspendGIL::new() }; + let ts_guard = unsafe { SuspendAttach::new() }; let res = self.lock(); drop(ts_guard); res @@ -713,7 +713,7 @@ where return guard; } - let ts_guard = unsafe { SuspendGIL::new() }; + let ts_guard = unsafe { SuspendAttach::new() }; let res = self.lock_arc(); drop(ts_guard); res @@ -728,7 +728,7 @@ where // 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 ts_guard = unsafe { SuspendAttach::new() }; once.call_once(move || { drop(ts_guard); @@ -744,7 +744,7 @@ where // 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 ts_guard = unsafe { SuspendAttach::new() }; once.call_once_force(move |state| { drop(ts_guard); @@ -764,7 +764,7 @@ where // 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 ts_guard = unsafe { SuspendAttach::new() }; // this trait is guarded by a rustc version config // so clippy's MSRV check is wrong diff --git a/tests/ui/not_send.stderr b/tests/ui/not_send.stderr index 7987c34c25a..ca8db2806e7 100644 --- a/tests/ui/not_send.stderr +++ b/tests/ui/not_send.stderr @@ -12,13 +12,12 @@ note: required because it appears within the type `PhantomData<*mut pyo3::Python | | pub struct PhantomData; | ^^^^^^^^^^^ -note: required because it appears within the type `impl_::not_send::NotSend` - --> src/impl_/not_send.rs +note: required because it appears within the type `pyo3::marker::NotSend` + --> src/marker.rs | - | pub(crate) struct NotSend(PhantomData<*mut Python<'static>>); - | ^^^^^^^ - = note: required because it appears within the type `(&pyo3::gil::GILGuard, impl_::not_send::NotSend)` -note: required because it appears within the type `PhantomData<(&pyo3::gil::GILGuard, impl_::not_send::NotSend)>` + | struct NotSend(PhantomData<*mut Python<'static>>); + | ^^^^^^^ +note: required because it appears within the type `PhantomData` --> $RUST/core/src/marker.rs | | pub struct PhantomData; @@ -26,7 +25,7 @@ note: required because it appears within the type `PhantomData<(&pyo3::gil::GILG note: required because it appears within the type `pyo3::Python<'_>` --> src/marker.rs | - | pub struct Python<'py>(PhantomData<(&'py GILGuard, NotSend)>); + | pub struct Python<'py>(PhantomData<&'py AttachGuard>, PhantomData); | ^^^^^^ = note: required for `&pyo3::Python<'_>` to implement `Send` note: required because it's used within this closure diff --git a/tests/ui/not_send2.stderr b/tests/ui/not_send2.stderr index f01bb9b5208..802081534b2 100644 --- a/tests/ui/not_send2.stderr +++ b/tests/ui/not_send2.stderr @@ -15,13 +15,12 @@ note: required because it appears within the type `PhantomData<*mut pyo3::Python | | pub struct PhantomData; | ^^^^^^^^^^^ -note: required because it appears within the type `impl_::not_send::NotSend` - --> src/impl_/not_send.rs +note: required because it appears within the type `pyo3::marker::NotSend` + --> src/marker.rs | - | pub(crate) struct NotSend(PhantomData<*mut Python<'static>>); - | ^^^^^^^ - = note: required because it appears within the type `(&pyo3::gil::GILGuard, impl_::not_send::NotSend)` -note: required because it appears within the type `PhantomData<(&pyo3::gil::GILGuard, impl_::not_send::NotSend)>` + | struct NotSend(PhantomData<*mut Python<'static>>); + | ^^^^^^^ +note: required because it appears within the type `PhantomData` --> $RUST/core/src/marker.rs | | pub struct PhantomData; @@ -29,7 +28,7 @@ note: required because it appears within the type `PhantomData<(&pyo3::gil::GILG note: required because it appears within the type `pyo3::Python<'_>` --> src/marker.rs | - | pub struct Python<'py>(PhantomData<(&'py GILGuard, NotSend)>); + | pub struct Python<'py>(PhantomData<&'py AttachGuard>, PhantomData); | ^^^^^^ note: required because it appears within the type `pyo3::Bound<'_, PyString>` --> src/instance.rs From 06453f80c615f92714264b37d8edfbdb6623d0b8 Mon Sep 17 00:00:00 2001 From: Jesse Rusak Date: Fri, 18 Jul 2025 05:36:22 -0400 Subject: [PATCH 741/936] Avoid using unwind handling in WASM (#5239) * Avoid using unwind handling in WASM * Add changelog entry --- newsfragments/5239.fixed.md | 1 + pyo3-ffi/src/pystate.rs | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) create mode 100644 newsfragments/5239.fixed.md diff --git a/newsfragments/5239.fixed.md b/newsfragments/5239.fixed.md new file mode 100644 index 00000000000..5361be01b9f --- /dev/null +++ b/newsfragments/5239.fixed.md @@ -0,0 +1 @@ +WASM targets no longer require exception handling support for Python < 3.14. diff --git a/pyo3-ffi/src/pystate.rs b/pyo3-ffi/src/pystate.rs index 2a41f937257..c9dc2b5ceda 100644 --- a/pyo3-ffi/src/pystate.rs +++ b/pyo3-ffi/src/pystate.rs @@ -81,10 +81,10 @@ pub enum PyGILState_STATE { PyGILState_UNLOCKED, } -#[cfg(not(Py_3_14))] +#[cfg(not(any(Py_3_14, target_arch = "wasm32")))] struct HangThread; -#[cfg(not(Py_3_14))] +#[cfg(not(any(Py_3_14, target_arch = "wasm32")))] impl Drop for HangThread { fn drop(&mut self) { loop { @@ -102,20 +102,20 @@ impl Drop for HangThread { // 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(not(Py_3_14))] + #[cfg(not(any(Py_3_14, target_arch = "wasm32")))] extern "C-unwind" { #[cfg_attr(PyPy, link_name = "PyPyGILState_Ensure")] pub fn PyGILState_Ensure() -> super::PyGILState_STATE; } - #[cfg(Py_3_14)] + #[cfg(any(Py_3_14, target_arch = "wasm32"))] extern "C" { #[cfg_attr(PyPy, link_name = "PyPyGILState_Ensure")] pub fn PyGILState_Ensure() -> super::PyGILState_STATE; } } -#[cfg(not(Py_3_14))] +#[cfg(not(any(Py_3_14, target_arch = "wasm32")))] 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 @@ -141,7 +141,7 @@ pub unsafe extern "C" fn PyGILState_Ensure() -> PyGILState_STATE { ret } -#[cfg(Py_3_14)] +#[cfg(any(Py_3_14, target_arch = "wasm32"))] pub use self::raw::PyGILState_Ensure; extern "C" { From 3ba8602d5c0cff3f26653377326cbba5e86ebfad Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 18 Jul 2025 12:02:21 +0100 Subject: [PATCH 742/936] ci: fix deploy to netlify (#5243) * fix deploy to netlify * fixup --- .github/workflows/netlify-build.yml | 27 +++++++++++++++++++-------- .github/workflows/netlify-deploy.yml | 12 +++++++++++- 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/.github/workflows/netlify-build.yml b/.github/workflows/netlify-build.yml index 4858a6ff428..5576456ba7f 100644 --- a/.github/workflows/netlify-build.yml +++ b/.github/workflows/netlify-build.yml @@ -24,11 +24,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - python-version: '3.12' - - uses: actions/setup-node@v4 - with: - node-version: '20.x' - registry-url: 'https://registry.npmjs.org/' + python-version: "3.13" - uses: dtolnay/rust-toolchain@nightly @@ -50,11 +46,16 @@ jobs: args: --version lycheeVersion: nightly + - name: Get current PyO3 version + run: | + PYO3_VERSION=$(cargo search pyo3 --limit 1 | head -1 | tr -s ' ' | cut -d ' ' -f 3 | tr -d '"') + echo "PYO3_VERSION=${PYO3_VERSION}" >> $GITHUB_ENV + - name: Prepare tag id: prepare_tag run: | TAG_NAME="${GITHUB_REF##*/}" - echo "::set-output name=tag_name::${TAG_NAME}" + echo "tag_name=${TAG_NAME}" >> $GITHUB_OUTPUT # Start with the current guides. - name: Get the guide @@ -64,8 +65,7 @@ jobs: # Set redirects. - name: Set redirects - run: | - source .netlify/redirect.sh + run: sh .netlify/redirect.sh # This builds the book and docs in netlify_build/main - name: Build the guide and public docs @@ -87,3 +87,14 @@ jobs: with: name: site path: ./netlify_build + + # Upload PR number for use in deploy workflow, will be empty if not a PR + - name: Prepare PR number for upload + id: pr + run: echo "pr_number=$(echo ${{ github.event.pull_request.number }})" >> .pr-number + + - name: Upload PR number + uses: actions/upload-artifact@v4 + with: + name: pr-number + path: .pr-number diff --git a/.github/workflows/netlify-deploy.yml b/.github/workflows/netlify-deploy.yml index d9ff526c3d8..d3fb11479fb 100644 --- a/.github/workflows/netlify-deploy.yml +++ b/.github/workflows/netlify-deploy.yml @@ -28,6 +28,16 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} run-id: ${{ github.event.workflow_run.id }} + - name: Download PR Number + uses: actions/download-artifact@v4 + with: + name: pr_number + + - name: Get PR Number + id: pr_number + run: | + echo "pr_number=$(cat .pr-number)" >> $GITHUB_OUTPUT + - name: Install netlify-cli run: | npm install -g netlify-cli @@ -38,4 +48,4 @@ jobs: DEBUG=* netlify deploy \ --site ${{ secrets.NETLIFY_SITE_ID }} \ --auth ${{ secrets.NETLIFY_TOKEN }} \ - ${{ github.event.workflow_run.head_branch == 'main' && '--prod' || '' }} + ${{ steps.pr_number.outputs.pr_number == '' && '--prod' || '' }} From 87c90a0da6185fb3c995f118e14c521b8bc3b641 Mon Sep 17 00:00:00 2001 From: Thomas Tanon Date: Fri, 18 Jul 2025 13:22:47 +0200 Subject: [PATCH 743/936] Introspection: properly tag modules as incomplete (#5207) --- newsfragments/5207.added.md | 1 + pyo3-introspection/src/introspection.rs | 15 ++++++++++++- pyo3-introspection/src/model.rs | 1 + pyo3-introspection/src/stubs.rs | 27 +++++++++++++++++++++++- pyo3-macros-backend/src/introspection.rs | 5 +++++ pyo3-macros-backend/src/module.rs | 3 ++- pytests/stubs/__init__.pyi | 3 +++ 7 files changed, 52 insertions(+), 3 deletions(-) create mode 100644 newsfragments/5207.added.md diff --git a/newsfragments/5207.added.md b/newsfragments/5207.added.md new file mode 100644 index 00000000000..23691f14bb5 --- /dev/null +++ b/newsfragments/5207.added.md @@ -0,0 +1 @@ +Type stubs: tag modules created using `#[pymodule]` or `#[pymodule_init]` functions as incomplete \ No newline at end of file diff --git a/pyo3-introspection/src/introspection.rs b/pyo3-introspection/src/introspection.rs index 0d77532ab6f..7c41a5f39d8 100644 --- a/pyo3-introspection/src/introspection.rs +++ b/pyo3-introspection/src/introspection.rs @@ -45,11 +45,19 @@ fn parse_chunks(chunks: &[Chunk], main_module_name: &str) -> Result { name, members, consts, + incomplete, id: _, } = chunk { if name == main_module_name { - return convert_module(name, members, consts, &chunks_by_id, &chunks_by_parent); + return convert_module( + name, + members, + consts, + *incomplete, + &chunks_by_id, + &chunks_by_parent, + ); } } } @@ -60,6 +68,7 @@ fn convert_module( name: &str, members: &[String], consts: &[ConstChunk], + incomplete: bool, chunks_by_id: &HashMap<&str, &Chunk>, chunks_by_parent: &HashMap<&str, Vec<&Chunk>>, ) -> Result { @@ -84,6 +93,7 @@ fn convert_module( value: c.value.clone(), }) .collect(), + incomplete, }) } @@ -102,12 +112,14 @@ fn convert_members( name, members, consts, + incomplete, id: _, } => { modules.push(convert_module( name, members, consts, + *incomplete, chunks_by_id, chunks_by_parent, )?); @@ -375,6 +387,7 @@ enum Chunk { name: String, members: Vec, consts: Vec, + incomplete: bool, }, Class { id: String, diff --git a/pyo3-introspection/src/model.rs b/pyo3-introspection/src/model.rs index 0ca7cf50fc1..d3f35ced85d 100644 --- a/pyo3-introspection/src/model.rs +++ b/pyo3-introspection/src/model.rs @@ -5,6 +5,7 @@ pub struct Module { pub classes: Vec, pub functions: Vec, pub consts: Vec, + pub incomplete: bool, } #[derive(Debug, Eq, PartialEq, Clone, Hash)] diff --git a/pyo3-introspection/src/stubs.rs b/pyo3-introspection/src/stubs.rs index 5f419aa0b19..d3d63119ce5 100644 --- a/pyo3-introspection/src/stubs.rs +++ b/pyo3-introspection/src/stubs.rs @@ -1,4 +1,4 @@ -use crate::model::{Argument, Class, Const, Function, Module, VariableLengthArgument}; +use crate::model::{Argument, Arguments, Class, Const, Function, Module, VariableLengthArgument}; use std::collections::{BTreeSet, HashMap}; use std::path::{Path, PathBuf}; @@ -43,6 +43,31 @@ fn module_stubs(module: &Module) -> String { for function in &module.functions { elements.push(function_stubs(function, &mut modules_to_import)); } + + // We generate a __getattr__ method to tag incomplete stubs + // See https://typing.python.org/en/latest/guides/writing_stubs.html#incomplete-stubs + if module.incomplete && !module.functions.iter().any(|f| f.name == "__getattr__") { + elements.push(function_stubs( + &Function { + name: "__getattr__".into(), + decorators: Vec::new(), + arguments: Arguments { + positional_only_arguments: Vec::new(), + arguments: vec![Argument { + name: "name".to_string(), + default_value: None, + annotation: Some("str".into()), + }], + vararg: None, + keyword_only_arguments: Vec::new(), + kwarg: None, + }, + returns: Some("_typeshed.Incomplete".into()), + }, + &mut modules_to_import, + )); + } + let mut final_elements = Vec::new(); for module_to_import in &modules_to_import { final_elements.push(format!("import {module_to_import}")); diff --git a/pyo3-macros-backend/src/introspection.rs b/pyo3-macros-backend/src/introspection.rs index c0e9ab2ddd3..119ac73805d 100644 --- a/pyo3-macros-backend/src/introspection.rs +++ b/pyo3-macros-backend/src/introspection.rs @@ -25,6 +25,7 @@ use syn::{Attribute, Ident, ReturnType, Type, TypePath}; static GLOBAL_COUNTER_FOR_UNIQUE_NAMES: AtomicUsize = AtomicUsize::new(0); +#[allow(clippy::too_many_arguments)] pub fn module_introspection_code<'a>( pyo3_crate_path: &PyO3CratePath, name: &str, @@ -33,6 +34,7 @@ pub fn module_introspection_code<'a>( consts: impl IntoIterator, consts_values: impl IntoIterator, consts_cfg_attrs: impl IntoIterator>, + incomplete: bool, ) -> TokenStream { IntrospectionNode::Map( [ @@ -74,6 +76,7 @@ pub fn module_introspection_code<'a>( .collect(), ), ), + ("incomplete", IntrospectionNode::Bool(incomplete)), ] .into(), ) @@ -309,6 +312,7 @@ fn argument_introspection_data<'a>( enum IntrospectionNode<'a> { String(Cow<'a, str>), + Bool(bool), IntrospectionId(Option>), InputType { rust_type: Type, nullable: bool }, OutputType { rust_type: Type }, @@ -342,6 +346,7 @@ impl IntrospectionNode<'_> { Self::String(string) => { content.push_str_to_escape(&string); } + Self::Bool(value) => content.push_str(if value { "true" } else { "false" }), Self::IntrospectionId(ident) => { content.push_str("\""); content.push_tokens(if let Some(ident) = ident { diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index dd22778465b..0adf753a2e8 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -354,6 +354,7 @@ pub fn pymodule_module_impl( &module_consts, &module_consts_values, &module_consts_cfg_attrs, + pymodule_init.is_some(), ); #[cfg(not(feature = "experimental-inspect"))] let introspection = quote! {}; @@ -438,7 +439,7 @@ pub fn pymodule_function_impl( #[cfg(feature = "experimental-inspect")] let introspection = - module_introspection_code(pyo3_path, &name.to_string(), &[], &[], &[], &[], &[]); + module_introspection_code(pyo3_path, &name.to_string(), &[], &[], &[], &[], &[], true); #[cfg(not(feature = "experimental-inspect"))] let introspection = quote! {}; #[cfg(feature = "experimental-inspect")] diff --git a/pytests/stubs/__init__.pyi b/pytests/stubs/__init__.pyi index e69de29bb2d..b88c3a5f3c3 100644 --- a/pytests/stubs/__init__.pyi +++ b/pytests/stubs/__init__.pyi @@ -0,0 +1,3 @@ +import _typeshed + +def __getattr__(name: str) -> _typeshed.Incomplete: ... From c653f6104a585f6ed11da460ac78c5a1e19ba596 Mon Sep 17 00:00:00 2001 From: Thomas Tanon Date: Fri, 18 Jul 2025 21:59:54 +0200 Subject: [PATCH 744/936] Introspection: allows to override annotations in signature (#5241) * Introspection: allows to override annotations in signature Allow type annotations on vargs and kwargs Example: #[pyfunction(signature = (a: "int", *_args: "str", _b: "int" = None, **_kwargs: "bool") -> "int")] * Update pyo3-macros-backend/src/introspection.rs Co-authored-by: David Hewitt * Update pytests/src/pyfunctions.rs Co-authored-by: David Hewitt * Code review feedbacks * Fixes test-introspection --------- Co-authored-by: David Hewitt --- newsfragments/5241.added.md | 1 + pyo3-introspection/src/introspection.rs | 1 + pyo3-introspection/src/model.rs | 2 + pyo3-introspection/src/stubs.rs | 26 +++- pyo3-macros-backend/src/introspection.rs | 84 +++++----- pyo3-macros-backend/src/method.rs | 16 ++ pyo3-macros-backend/src/pyclass.rs | 4 + .../src/pyfunction/signature.rs | 145 +++++++++++++++--- pytests/src/pyfunctions.rs | 12 +- pytests/stubs/pyfunctions.pyi | 3 + tests/test_compile_error.rs | 8 +- tests/ui/invalid_annotation.rs | 9 ++ tests/ui/invalid_annotation.stderr | 5 + tests/ui/invalid_annotation_return.rs | 9 ++ tests/ui/invalid_annotation_return.stderr | 5 + 15 files changed, 269 insertions(+), 61 deletions(-) create mode 100644 newsfragments/5241.added.md create mode 100644 tests/ui/invalid_annotation.rs create mode 100644 tests/ui/invalid_annotation.stderr create mode 100644 tests/ui/invalid_annotation_return.rs create mode 100644 tests/ui/invalid_annotation_return.stderr diff --git a/newsfragments/5241.added.md b/newsfragments/5241.added.md new file mode 100644 index 00000000000..ed70f89c963 --- /dev/null +++ b/newsfragments/5241.added.md @@ -0,0 +1 @@ +Introspection: Allow to set annotations in PyO3 signature attribute \ No newline at end of file diff --git a/pyo3-introspection/src/introspection.rs b/pyo3-introspection/src/introspection.rs index 7c41a5f39d8..0c4a7389aae 100644 --- a/pyo3-introspection/src/introspection.rs +++ b/pyo3-introspection/src/introspection.rs @@ -220,6 +220,7 @@ fn convert_argument(arg: &ChunkArgument) -> Argument { fn convert_variable_length_argument(arg: &ChunkArgument) -> VariableLengthArgument { VariableLengthArgument { name: arg.name.clone(), + annotation: arg.annotation.clone(), } } diff --git a/pyo3-introspection/src/model.rs b/pyo3-introspection/src/model.rs index d3f35ced85d..cfe0f329561 100644 --- a/pyo3-introspection/src/model.rs +++ b/pyo3-introspection/src/model.rs @@ -57,4 +57,6 @@ pub struct Argument { #[derive(Debug, Eq, PartialEq, Clone, Hash)] pub struct VariableLengthArgument { pub name: String, + /// Type annotation as a Python expression + pub annotation: Option, } diff --git a/pyo3-introspection/src/stubs.rs b/pyo3-introspection/src/stubs.rs index d3d63119ce5..8f815e9b852 100644 --- a/pyo3-introspection/src/stubs.rs +++ b/pyo3-introspection/src/stubs.rs @@ -123,7 +123,10 @@ fn function_stubs(function: &Function, modules_to_import: &mut BTreeSet) parameters.push(argument_stub(argument, modules_to_import)); } if let Some(argument) = &function.arguments.vararg { - parameters.push(format!("*{}", variable_length_argument_stub(argument))); + parameters.push(format!( + "*{}", + variable_length_argument_stub(argument, modules_to_import) + )); } else if !function.arguments.keyword_only_arguments.is_empty() { parameters.push("*".into()); } @@ -131,7 +134,10 @@ fn function_stubs(function: &Function, modules_to_import: &mut BTreeSet) parameters.push(argument_stub(argument, modules_to_import)); } if let Some(argument) = &function.arguments.kwarg { - parameters.push(format!("**{}", variable_length_argument_stub(argument))); + parameters.push(format!( + "**{}", + variable_length_argument_stub(argument, modules_to_import) + )); } let mut buffer = String::new(); for decorator in &function.decorators { @@ -175,8 +181,16 @@ fn argument_stub(argument: &Argument, modules_to_import: &mut BTreeSet) output } -fn variable_length_argument_stub(argument: &VariableLengthArgument) -> String { - argument.name.clone() +fn variable_length_argument_stub( + argument: &VariableLengthArgument, + modules_to_import: &mut BTreeSet, +) -> String { + let mut output = argument.name.clone(); + if let Some(annotation) = &argument.annotation { + output.push_str(": "); + output.push_str(annotation_stub(annotation, modules_to_import)); + } + output } fn annotation_stub<'a>(annotation: &'a str, modules_to_import: &mut BTreeSet) -> &'a str { @@ -210,6 +224,7 @@ mod tests { }], vararg: Some(VariableLengthArgument { name: "varargs".into(), + annotation: None, }), keyword_only_arguments: vec![Argument { name: "karg".into(), @@ -218,12 +233,13 @@ mod tests { }], kwarg: Some(VariableLengthArgument { name: "kwarg".into(), + annotation: Some("str".into()), }), }, returns: Some("list[str]".into()), }; assert_eq!( - "def func(posonly, /, arg, *varargs, karg: str, **kwarg) -> list[str]: ...", + "def func(posonly, /, arg, *varargs, karg: str, **kwarg: str) -> list[str]: ...", function_stubs(&function, &mut BTreeSet::new()) ) } diff --git a/pyo3-macros-backend/src/introspection.rs b/pyo3-macros-backend/src/introspection.rs index 119ac73805d..aa9fae83259 100644 --- a/pyo3-macros-backend/src/introspection.rs +++ b/pyo3-macros-backend/src/introspection.rs @@ -122,21 +122,29 @@ pub fn function_introspection_code( ), ( "returns", - match returns { - ReturnType::Default => IntrospectionNode::String("None".into()), - ReturnType::Type(_, ty) => match *ty { - Type::Tuple(t) if t.elems.is_empty() => { - // () is converted to None in return types - IntrospectionNode::String("None".into()) - } - mut ty => { - if let Some(class_type) = parent { - replace_self(&mut ty, class_type); + if let Some((_, returns)) = signature + .attribute + .as_ref() + .and_then(|attribute| attribute.value.returns.as_ref()) + { + IntrospectionNode::String(returns.to_python().into()) + } else { + match returns { + ReturnType::Default => IntrospectionNode::String("None".into()), + ReturnType::Type(_, ty) => match *ty { + Type::Tuple(t) if t.elems.is_empty() => { + // () is converted to None in return types + IntrospectionNode::String("None".into()) } - ty = ty.elide_lifetimes(); - IntrospectionNode::OutputType { rust_type: ty } - } - }, + mut ty => { + if let Some(class_type) = parent { + replace_self(&mut ty, class_type); + } + ty = ty.elide_lifetimes(); + IntrospectionNode::OutputType { rust_type: ty } + } + }, + } }, ), ]); @@ -181,12 +189,11 @@ fn arguments_introspection_data<'a>( first_argument: Option<&'a str>, class_type: Option<&Type>, ) -> IntrospectionNode<'a> { - let mut argument_desc = signature.arguments.iter().filter_map(|arg| { - if let FnArg::Regular(arg) = arg { - Some(arg) - } else { - None - } + let mut argument_desc = signature.arguments.iter().filter(|arg| { + matches!( + arg, + FnArg::Regular(_) | FnArg::VarArgs(_) | FnArg::KwArgs(_) + ) }); let mut posonlyargs = Vec::new(); @@ -207,7 +214,7 @@ fn arguments_introspection_data<'a>( .iter() .enumerate() { - let arg_desc = if let Some(arg_desc) = argument_desc.next() { + let arg_desc = if let Some(FnArg::Regular(arg_desc)) = argument_desc.next() { arg_desc } else { panic!("Less arguments than in python signature"); @@ -221,28 +228,32 @@ fn arguments_introspection_data<'a>( } if let Some(param) = &signature.python_signature.varargs { - vararg = Some(IntrospectionNode::Map( - [("name", IntrospectionNode::String(param.into()))].into(), - )); + let Some(FnArg::VarArgs(arg_desc)) = argument_desc.next() else { + panic!("Fewer arguments than in python signature"); + }; + let mut params = HashMap::from([("name", IntrospectionNode::String(param.into()))]); + if let Some(annotation) = &arg_desc.annotation { + params.insert("annotation", IntrospectionNode::String(annotation.into())); + } + vararg = Some(IntrospectionNode::Map(params)); } for (param, _) in &signature.python_signature.keyword_only_parameters { - let arg_desc = if let Some(arg_desc) = argument_desc.next() { - arg_desc - } else { + let Some(FnArg::Regular(arg_desc)) = argument_desc.next() else { panic!("Less arguments than in python signature"); }; kwonlyargs.push(argument_introspection_data(param, arg_desc, class_type)); } if let Some(param) = &signature.python_signature.kwargs { - kwarg = Some(IntrospectionNode::Map( - [ - ("name", IntrospectionNode::String(param.into())), - ("kind", IntrospectionNode::String("VAR_KEYWORD".into())), - ] - .into(), - )); + let Some(FnArg::KwArgs(arg_desc)) = argument_desc.next() else { + panic!("Less arguments than in python signature"); + }; + let mut params = HashMap::from([("name", IntrospectionNode::String(param.into()))]); + if let Some(annotation) = &arg_desc.annotation { + params.insert("annotation", IntrospectionNode::String(annotation.into())); + } + kwarg = Some(IntrospectionNode::Map(params)); } let mut map = HashMap::new(); @@ -276,7 +287,10 @@ fn argument_introspection_data<'a>( IntrospectionNode::String(desc.default_value().into()), ); } - if desc.from_py_with.is_none() { + + if let Some(annotation) = &desc.annotation { + params.insert("annotation", IntrospectionNode::String(annotation.into())); + } else if desc.from_py_with.is_none() { // If from_py_with is set we don't know anything on the input type if let Some(ty) = desc.option_wrapped_type { // Special case to properly generate a `T | None` annotation diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index df316c611ca..10aae4f4810 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -26,6 +26,8 @@ pub struct RegularArg<'a> { pub from_py_with: Option, pub default_value: Option, pub option_wrapped_type: Option<&'a syn::Type>, + #[cfg(feature = "experimental-inspect")] + pub annotation: Option, } impl RegularArg<'_> { @@ -55,6 +57,8 @@ impl RegularArg<'_> { pub struct VarargsArg<'a> { pub name: Cow<'a, syn::Ident>, pub ty: &'a syn::Type, + #[cfg(feature = "experimental-inspect")] + pub annotation: Option, } /// Pythons **kwarg argument @@ -62,6 +66,8 @@ pub struct VarargsArg<'a> { pub struct KwargsArg<'a> { pub name: Cow<'a, syn::Ident>, pub ty: &'a syn::Type, + #[cfg(feature = "experimental-inspect")] + pub annotation: Option, } #[derive(Clone, Debug)] @@ -121,12 +127,16 @@ impl<'a> FnArg<'a> { name, ty, option_wrapped_type: None, + #[cfg(feature = "experimental-inspect")] + annotation, .. }) = self { *self = Self::VarArgs(VarargsArg { name: name.clone(), ty, + #[cfg(feature = "experimental-inspect")] + annotation: annotation.clone(), }); Ok(self) } else { @@ -139,12 +149,16 @@ impl<'a> FnArg<'a> { name, ty, option_wrapped_type: Some(..), + #[cfg(feature = "experimental-inspect")] + annotation, .. }) = self { *self = Self::KwArgs(KwargsArg { name: name.clone(), ty, + #[cfg(feature = "experimental-inspect")] + annotation: annotation.clone(), }); Ok(self) } else { @@ -196,6 +210,8 @@ impl<'a> FnArg<'a> { from_py_with, default_value: None, option_wrapped_type: utils::option_type_argument(&cap.ty), + #[cfg(feature = "experimental-inspect")] + annotation: None, })) } } diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 4c402430597..5a5d8cab8a5 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -1646,6 +1646,8 @@ fn complex_enum_struct_variant_new<'a>( from_py_with: None, default_value: None, option_wrapped_type: None, + #[cfg(feature = "experimental-inspect")] + annotation: None, })); } args @@ -1703,6 +1705,8 @@ fn complex_enum_tuple_variant_new<'a>( from_py_with: None, default_value: None, option_wrapped_type: None, + #[cfg(feature = "experimental-inspect")] + annotation: None, })); } args diff --git a/pyo3-macros-backend/src/pyfunction/signature.rs b/pyo3-macros-backend/src/pyfunction/signature.rs index fac1541bdf1..bdf8c4a7cb6 100644 --- a/pyo3-macros-backend/src/pyfunction/signature.rs +++ b/pyo3-macros-backend/src/pyfunction/signature.rs @@ -1,3 +1,7 @@ +use crate::{ + attributes::{kw, KeywordAttribute}, + method::{FnArg, RegularArg}, +}; use proc_macro2::{Span, TokenStream}; use quote::ToTokens; use syn::{ @@ -8,38 +12,46 @@ use syn::{ Token, }; -use crate::{ - attributes::{kw, KeywordAttribute}, - method::{FnArg, RegularArg}, -}; - #[derive(Clone)] pub struct Signature { paren_token: syn::token::Paren, pub items: Punctuated, + pub returns: Option<(Token![->], PyTypeAnnotation)>, } impl Parse for Signature { fn parse(input: ParseStream<'_>) -> syn::Result { let content; let paren_token = syn::parenthesized!(content in input); - let items = content.parse_terminated(SignatureItem::parse, Token![,])?; - - Ok(Signature { paren_token, items }) + let returns = if input.peek(Token![->]) { + Some((input.parse()?, input.parse()?)) + } else { + None + }; + Ok(Signature { + paren_token, + items, + returns, + }) } } impl ToTokens for Signature { fn to_tokens(&self, tokens: &mut TokenStream) { self.paren_token - .surround(tokens, |tokens| self.items.to_tokens(tokens)) + .surround(tokens, |tokens| self.items.to_tokens(tokens)); + if let Some((arrow, returns)) = &self.returns { + arrow.to_tokens(tokens); + returns.to_tokens(tokens); + } } } #[derive(Clone, Debug, PartialEq, Eq)] pub struct SignatureItemArgument { pub ident: syn::Ident, + pub colon_and_annotation: Option<(Token![:], PyTypeAnnotation)>, pub eq_and_default: Option<(Token![=], syn::Expr)>, } @@ -57,12 +69,14 @@ pub struct SignatureItemVarargsSep { pub struct SignatureItemVarargs { pub sep: SignatureItemVarargsSep, pub ident: syn::Ident, + pub colon_and_annotation: Option<(Token![:], PyTypeAnnotation)>, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct SignatureItemKwargs { pub asterisks: (Token![*], Token![*]), pub ident: syn::Ident, + pub colon_and_annotation: Option<(Token![:], PyTypeAnnotation)>, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -88,6 +102,11 @@ impl Parse for SignatureItem { Ok(SignatureItem::Varargs(SignatureItemVarargs { sep, ident: input.parse()?, + colon_and_annotation: if input.peek(Token![:]) { + Some((input.parse()?, input.parse()?)) + } else { + None + }, })) } } @@ -115,6 +134,11 @@ impl Parse for SignatureItemArgument { fn parse(input: ParseStream<'_>) -> syn::Result { Ok(Self { ident: input.parse()?, + colon_and_annotation: if input.peek(Token![:]) { + Some((input.parse()?, input.parse()?)) + } else { + None + }, eq_and_default: if input.peek(Token![=]) { Some((input.parse()?, input.parse()?)) } else { @@ -127,6 +151,10 @@ impl Parse for SignatureItemArgument { impl ToTokens for SignatureItemArgument { fn to_tokens(&self, tokens: &mut TokenStream) { self.ident.to_tokens(tokens); + if let Some((colon, annotation)) = &self.colon_and_annotation { + colon.to_tokens(tokens); + annotation.to_tokens(tokens); + } if let Some((eq, default)) = &self.eq_and_default { eq.to_tokens(tokens); default.to_tokens(tokens); @@ -153,6 +181,11 @@ impl Parse for SignatureItemVarargs { Ok(Self { sep: input.parse()?, ident: input.parse()?, + colon_and_annotation: if input.peek(Token![:]) { + Some((input.parse()?, input.parse()?)) + } else { + None + }, }) } } @@ -169,6 +202,11 @@ impl Parse for SignatureItemKwargs { Ok(Self { asterisks: (input.parse()?, input.parse()?), ident: input.parse()?, + colon_and_annotation: if input.peek(Token![:]) { + Some((input.parse()?, input.parse()?)) + } else { + None + }, }) } } @@ -195,6 +233,27 @@ impl ToTokens for SignatureItemPosargsSep { } } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PyTypeAnnotation(syn::LitStr); + +impl Parse for PyTypeAnnotation { + fn parse(input: ParseStream<'_>) -> syn::Result { + Ok(Self(input.parse()?)) + } +} + +impl ToTokens for PyTypeAnnotation { + fn to_tokens(&self, tokens: &mut TokenStream) { + self.0.to_tokens(tokens); + } +} + +impl PyTypeAnnotation { + pub fn to_python(&self) -> String { + self.0.value() + } +} + pub type SignatureAttribute = KeywordAttribute; pub type ConstructorAttribute = KeywordAttribute; @@ -363,7 +422,7 @@ impl<'a> FunctionSignature<'a> { let mut next_non_py_argument_checked = |name: &syn::Ident| { for fn_arg in args_iter.by_ref() { match fn_arg { - crate::method::FnArg::Py(..) => { + FnArg::Py(..) => { // If the user incorrectly tried to include py: Python in the // signature, give a useful error as a hint. ensure_spanned!( @@ -373,7 +432,7 @@ impl<'a> FunctionSignature<'a> { // Otherwise try next argument. continue; } - crate::method::FnArg::CancelHandle(..) => { + FnArg::CancelHandle(..) => { // If the user incorrectly tried to include cancel: CoroutineCancel in the // signature, give a useful error as a hint. ensure_spanned!( @@ -401,6 +460,13 @@ impl<'a> FunctionSignature<'a> { ) }; + if let Some(returns) = &attribute.value.returns { + ensure_spanned!( + cfg!(feature = "experimental-inspect"), + returns.1.span() => "Return type annotation in the signature is only supported with the `experimental-inspect` feature" + ); + } + for item in &attribute.value.items { match item { SignatureItem::Argument(arg) => { @@ -411,15 +477,24 @@ impl<'a> FunctionSignature<'a> { arg.eq_and_default.is_none(), arg.span(), )?; - if let Some((_, default)) = &arg.eq_and_default { - 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 \ + let FnArg::Regular(fn_arg) = fn_arg 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." - ); + ); + }; + if let Some((_, default)) = &arg.eq_and_default { + fn_arg.default_value = Some(default.clone()); + } + if let Some((_, annotation)) = &arg.colon_and_annotation { + ensure_spanned!( + cfg!(feature = "experimental-inspect"), + annotation.span() => "Type annotations in the signature is only supported with the `experimental-inspect` feature" + ); + #[cfg(feature = "experimental-inspect")] + { + fn_arg.annotation = Some(annotation.to_python()); } } } @@ -430,11 +505,45 @@ impl<'a> FunctionSignature<'a> { let fn_arg = next_non_py_argument_checked(&varargs.ident)?; fn_arg.to_varargs_mut()?; parse_state.add_varargs(&mut python_signature, varargs)?; + if let Some((_, annotation)) = &varargs.colon_and_annotation { + ensure_spanned!( + cfg!(feature = "experimental-inspect"), + annotation.span() => "Type annotations in the signature is only supported with the `experimental-inspect` feature" + ); + #[cfg(feature = "experimental-inspect")] + { + let FnArg::VarArgs(fn_arg) = fn_arg 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." + ); + }; + fn_arg.annotation = Some(annotation.to_python()); + } + } } SignatureItem::Kwargs(kwargs) => { let fn_arg = next_non_py_argument_checked(&kwargs.ident)?; fn_arg.to_kwargs_mut()?; parse_state.add_kwargs(&mut python_signature, kwargs)?; + if let Some((_, annotation)) = &kwargs.colon_and_annotation { + ensure_spanned!( + cfg!(feature = "experimental-inspect"), + annotation.span() => "Type annotations in the signature is only supported with the `experimental-inspect` feature" + ); + #[cfg(feature = "experimental-inspect")] + { + let FnArg::KwArgs(fn_arg) = fn_arg 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." + ); + }; + fn_arg.annotation = Some(annotation.to_python()); + } + } } SignatureItem::PosargsSep(sep) => { parse_state.finish_pos_only_args(&mut python_signature, sep.span())? diff --git a/pytests/src/pyfunctions.rs b/pytests/src/pyfunctions.rs index 19e30712909..8d3da83cee3 100644 --- a/pytests/src/pyfunctions.rs +++ b/pytests/src/pyfunctions.rs @@ -77,11 +77,21 @@ fn with_typed_args(a: bool, b: u64, c: f64, d: &str) -> (bool, u64, f64, &str) { (a, b, c, d) } +#[pyfunction(signature = (a: "int", *_args: "str", _b: "int | None" = None, **_kwargs: "bool") -> "int")] +fn with_custom_type_annotations<'py>( + a: Any<'py>, + _args: Tuple<'py>, + _b: Option>, + _kwargs: Option>, +) -> Any<'py> { + a +} + #[pymodule] pub mod pyfunctions { #[pymodule_export] use super::{ args_kwargs, none, positional_only, simple, simple_args, simple_args_kwargs, simple_kwargs, - with_typed_args, + with_custom_type_annotations, with_typed_args, }; } diff --git a/pytests/stubs/pyfunctions.pyi b/pytests/stubs/pyfunctions.pyi index 8a9bbe4f501..251ff160409 100644 --- a/pytests/stubs/pyfunctions.pyi +++ b/pytests/stubs/pyfunctions.pyi @@ -19,6 +19,9 @@ def simple_args_kwargs( def simple_kwargs( a: typing.Any, b: typing.Any | None = None, c: typing.Any | None = None, **kwargs ) -> typing.Any: ... +def with_custom_type_annotations( + a: int, *_args: str, _b: int | None = None, **_kwargs: bool +) -> int: ... def with_typed_args( a: bool = False, b: int = 0, c: float = 0.0, d: str = "" ) -> typing.Any: ... diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index 82a128f3bf5..b7ed01a510b 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -28,6 +28,10 @@ fn test_compile_errors() { t.compile_fail("tests/ui/reject_generics.rs"); t.compile_fail("tests/ui/invalid_closure.rs"); t.compile_fail("tests/ui/pyclass_send.rs"); + #[cfg(not(feature = "experimental-inspect"))] + t.compile_fail("tests/ui/invalid_annotation.rs"); + #[cfg(not(feature = "experimental-inspect"))] + t.compile_fail("tests/ui/invalid_annotation_return.rs"); t.compile_fail("tests/ui/invalid_argument_attributes.rs"); t.compile_fail("tests/ui/invalid_intopy_derive.rs"); #[cfg(not(windows))] @@ -35,13 +39,13 @@ fn test_compile_errors() { 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"); - #[cfg(not(any(feature = "uuid")))] + #[cfg(not(feature = "uuid"))] 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(any(feature = "experimental-async")))] + #[cfg(not(feature = "experimental-async"))] t.compile_fail("tests/ui/invalid_async.rs"); t.compile_fail("tests/ui/invalid_intern_arg.rs"); t.compile_fail("tests/ui/invalid_frozen_pyclass_borrow.rs"); diff --git a/tests/ui/invalid_annotation.rs b/tests/ui/invalid_annotation.rs new file mode 100644 index 00000000000..d8b803550ca --- /dev/null +++ b/tests/ui/invalid_annotation.rs @@ -0,0 +1,9 @@ +use pyo3::prelude::*; + +#[pyfunction] +#[pyo3(signature = (a: "int"))] +fn check(a: usize) -> usize { + a +} + +fn main() {} diff --git a/tests/ui/invalid_annotation.stderr b/tests/ui/invalid_annotation.stderr new file mode 100644 index 00000000000..ff2981b4818 --- /dev/null +++ b/tests/ui/invalid_annotation.stderr @@ -0,0 +1,5 @@ +error: Type annotations in the signature is only supported with the `experimental-inspect` feature + --> tests/ui/invalid_annotation.rs:4:24 + | +4 | #[pyo3(signature = (a: "int"))] + | ^^^^^ diff --git a/tests/ui/invalid_annotation_return.rs b/tests/ui/invalid_annotation_return.rs new file mode 100644 index 00000000000..5cb4298ec8b --- /dev/null +++ b/tests/ui/invalid_annotation_return.rs @@ -0,0 +1,9 @@ +use pyo3::prelude::*; + +#[pyfunction] +#[pyo3(signature = (a) -> "int")] +fn check(a: usize) -> usize { + a +} + +fn main() {} diff --git a/tests/ui/invalid_annotation_return.stderr b/tests/ui/invalid_annotation_return.stderr new file mode 100644 index 00000000000..2a70079f82d --- /dev/null +++ b/tests/ui/invalid_annotation_return.stderr @@ -0,0 +1,5 @@ +error: Return type annotation in the signature is only supported with the `experimental-inspect` feature + --> tests/ui/invalid_annotation_return.rs:4:27 + | +4 | #[pyo3(signature = (a) -> "int")] + | ^^^^^ From ecf59e0b7f0ba447004ffa7541bf7e79febb2c20 Mon Sep 17 00:00:00 2001 From: anilbey Date: Sat, 19 Jul 2025 17:07:56 +0200 Subject: [PATCH 745/936] use lifetime elision in VisitMut (#5246) --- pyo3-macros-backend/src/introspection.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyo3-macros-backend/src/introspection.rs b/pyo3-macros-backend/src/introspection.rs index aa9fae83259..a6faf3a05a2 100644 --- a/pyo3-macros-backend/src/introspection.rs +++ b/pyo3-macros-backend/src/introspection.rs @@ -516,7 +516,7 @@ fn replace_self(ty: &mut Type, self_target: &Type) { self_target: &'a Type, } - impl<'a> VisitMut for SelfReplacementVisitor<'a> { + impl VisitMut for SelfReplacementVisitor<'_> { fn visit_type_mut(&mut self, ty: &mut Type) { if let syn::Type::Path(type_path) = ty { if type_path.qself.is_none() From 7836f150a0d0ba7d24264783ca6379dc1c61b8f4 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sun, 20 Jul 2025 21:05:36 +0100 Subject: [PATCH 746/936] add release notes for 0.25.1 (#5248) Co-authored-by: Alex Gaynor --- CHANGELOG.md | 25 ++++++++++++++++++- Cargo.toml | 6 ++--- 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/5121.added.md | 1 - newsfragments/5121.changed.md | 2 -- newsfragments/5145.packaging.md | 1 - newsfragments/5150.added.md | 1 - newsfragments/5156.fixed.md | 1 - newsfragments/5161.fixed.md | 1 - newsfragments/5174.added.md | 1 - newsfragments/5180.fixed.md | 1 - pyo3-build-config/Cargo.toml | 2 +- pyo3-ffi/Cargo.toml | 4 +-- pyo3-ffi/README.md | 4 +-- pyo3-introspection/Cargo.toml | 2 +- pyo3-macros-backend/Cargo.toml | 6 ++--- pyo3-macros/Cargo.toml | 4 +-- pyproject.toml | 2 +- tests/ui/reject_generics.stderr | 4 +-- 24 files changed, 48 insertions(+), 34 deletions(-) delete mode 100644 newsfragments/5121.added.md delete mode 100644 newsfragments/5121.changed.md delete mode 100644 newsfragments/5145.packaging.md delete mode 100644 newsfragments/5150.added.md delete mode 100644 newsfragments/5156.fixed.md delete mode 100644 newsfragments/5161.fixed.md delete mode 100644 newsfragments/5174.added.md delete mode 100644 newsfragments/5180.fixed.md diff --git a/CHANGELOG.md b/CHANGELOG.md index b13c92caef9..9d7bc1ac680 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,28 @@ To see unreleased changes, please see the [CHANGELOG on the main branch guide](h +## [0.25.1] - 2025-06-12 +### Packaging + +- Add support for Windows on ARM64. [#5145](https://github.com/PyO3/pyo3/pull/5145) +- Add `chrono-local` feature for optional conversions for chrono's `Local` timezone & `DateTime` instances. [#5174](https://github.com/PyO3/pyo3/pull/5174) + +### Added + +- Add FFI definition `PyBytes_AS_STRING`. [#5121](https://github.com/PyO3/pyo3/pull/5121) +- Add support for module associated consts introspection. [#5150](https://github.com/PyO3/pyo3/pull/5150) + +### Changed + +- Enable "vectorcall" FFI definitions on GraalPy. [#5121](https://github.com/PyO3/pyo3/pull/5121) +- Use `Py_Is` function on GraalPy [#5121](https://github.com/PyO3/pyo3/pull/5121) + +### Fixed + +- Report a better compile error for `async` declarations when not using `experimental-async` feature. [#5156](https://github.com/PyO3/pyo3/pull/5156) +- Fix implementation of `FromPyObject` for `uuid::Uuid` on big-endian architectures. [#5161](https://github.com/PyO3/pyo3/pull/5161) +- Fix segmentation faults on 32-bit x86 with Python 3.14. [#5180](https://github.com/PyO3/pyo3/pull/5180) + ## [0.25.0] - 2025-05-14 ### Packaging @@ -2198,7 +2220,8 @@ Yanked - Initial release -[Unreleased]: https://github.com/pyo3/pyo3/compare/v0.25.0...HEAD +[Unreleased]: https://github.com/pyo3/pyo3/compare/v0.25.1...HEAD +[0.25.0]: https://github.com/pyo3/pyo3/compare/v0.25.0...v0.25.1 [0.25.0]: https://github.com/pyo3/pyo3/compare/v0.24.2...v0.25.0 [0.24.2]: https://github.com/pyo3/pyo3/compare/v0.24.1...v0.24.2 [0.24.1]: https://github.com/pyo3/pyo3/compare/v0.24.0...v0.24.1 diff --git a/Cargo.toml b/Cargo.toml index 9f253b27ded..663165c59b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,10 +29,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.25.0" } +pyo3-ffi = { path = "pyo3-ffi", version = "=0.25.1" } # support crates for macros feature -pyo3-macros = { path = "pyo3-macros", version = "=0.25.0", optional = true } +pyo3-macros = { path = "pyo3-macros", version = "=0.25.1", optional = true } indoc = { version = "2.0.1", optional = true } unindent = { version = "0.2.1", optional = true } @@ -83,7 +83,7 @@ uuid = { version = "1.10.0", features = ["v4"] } parking_lot = { version = "0.12.3", features = ["arc_lock"] } [build-dependencies] -pyo3-build-config = { path = "pyo3-build-config", version = "=0.25.0", features = ["resolve-config"] } +pyo3-build-config = { path = "pyo3-build-config", version = "=0.25.1", features = ["resolve-config"] } [features] default = ["macros"] diff --git a/README.md b/README.md index 318ddadebb5..ae9259e9618 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ name = "string_sum" crate-type = ["cdylib"] [dependencies] -pyo3 = { version = "0.25.0", features = ["extension-module"] } +pyo3 = { version = "0.25.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.25.0" +version = "0.25.1" features = ["auto-initialize"] ``` diff --git a/examples/decorator/.template/pre-script.rhai b/examples/decorator/.template/pre-script.rhai index 80ebe3ce006..110a6e82130 100644 --- a/examples/decorator/.template/pre-script.rhai +++ b/examples/decorator/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.25.0"); +variable::set("PYO3_VERSION", "0.25.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 80ebe3ce006..110a6e82130 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.25.0"); +variable::set("PYO3_VERSION", "0.25.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 2a21f6b2e53..d21dae34ad8 100644 --- a/examples/plugin/.template/pre-script.rhai +++ b/examples/plugin/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.25.0"); +variable::set("PYO3_VERSION", "0.25.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 92eeabb3ea8..b27db6f2700 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.25.0"); +variable::set("PYO3_VERSION", "0.25.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 80ebe3ce006..110a6e82130 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.25.0"); +variable::set("PYO3_VERSION", "0.25.1"); file::rename(".template/Cargo.toml", "Cargo.toml"); file::rename(".template/pyproject.toml", "pyproject.toml"); file::delete(".template"); diff --git a/newsfragments/5121.added.md b/newsfragments/5121.added.md deleted file mode 100644 index fd79e5a8ea7..00000000000 --- a/newsfragments/5121.added.md +++ /dev/null @@ -1 +0,0 @@ -Add `PyBytes_AS_STRING` diff --git a/newsfragments/5121.changed.md b/newsfragments/5121.changed.md deleted file mode 100644 index 49b317cb96b..00000000000 --- a/newsfragments/5121.changed.md +++ /dev/null @@ -1,2 +0,0 @@ -Enable vectorcall methods on GraalPy -Call Py_Is function on GraalPy diff --git a/newsfragments/5145.packaging.md b/newsfragments/5145.packaging.md deleted file mode 100644 index a11116acc28..00000000000 --- a/newsfragments/5145.packaging.md +++ /dev/null @@ -1 +0,0 @@ -add support for Windows on ARM64. \ No newline at end of file diff --git a/newsfragments/5150.added.md b/newsfragments/5150.added.md deleted file mode 100644 index dec859ce549..00000000000 --- a/newsfragments/5150.added.md +++ /dev/null @@ -1 +0,0 @@ -- Add support for module associated consts introspection. \ No newline at end of file diff --git a/newsfragments/5156.fixed.md b/newsfragments/5156.fixed.md deleted file mode 100644 index 2c6049ef855..00000000000 --- a/newsfragments/5156.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Catching `async` declaration when not using `experimental-async` \ No newline at end of file diff --git a/newsfragments/5161.fixed.md b/newsfragments/5161.fixed.md deleted file mode 100644 index 1a40c79bbe8..00000000000 --- a/newsfragments/5161.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fixed FromPyObject impl for `uuid::Uuid` on big-endian architectures diff --git a/newsfragments/5174.added.md b/newsfragments/5174.added.md deleted file mode 100644 index 09b7dec02e7..00000000000 --- a/newsfragments/5174.added.md +++ /dev/null @@ -1 +0,0 @@ -Add conversions for chrono's `Local` timezone & `DateTime` instances. diff --git a/newsfragments/5180.fixed.md b/newsfragments/5180.fixed.md deleted file mode 100644 index 9be577d65ab..00000000000 --- a/newsfragments/5180.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fixed segmentation faults on 32-bit ix86 with Python 3.14 diff --git a/pyo3-build-config/Cargo.toml b/pyo3-build-config/Cargo.toml index 3786b4e6f7c..8315dae6747 100644 --- a/pyo3-build-config/Cargo.toml +++ b/pyo3-build-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-build-config" -version = "0.25.0" +version = "0.25.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 91c1517786a..3ce95cacf3d 100644 --- a/pyo3-ffi/Cargo.toml +++ b/pyo3-ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-ffi" -version = "0.25.0" +version = "0.25.1" description = "Python-API bindings for the PyO3 ecosystem" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -44,7 +44,7 @@ generate-import-lib = ["pyo3-build-config/python3-dll-a"] paste = "1" [build-dependencies] -pyo3-build-config = { path = "../pyo3-build-config", version = "=0.25.0", features = ["resolve-config"] } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.25.1", features = ["resolve-config"] } [lints] workspace = true diff --git a/pyo3-ffi/README.md b/pyo3-ffi/README.md index 09be0a06f66..dc5e12827af 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.25.0" +version = "0.25.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.25.0" +pyo3_build_config = "0.25.1" ``` If you need to use conditional compilation based on Python version or how diff --git a/pyo3-introspection/Cargo.toml b/pyo3-introspection/Cargo.toml index 55004ac497b..1012d4b2307 100644 --- a/pyo3-introspection/Cargo.toml +++ b/pyo3-introspection/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-introspection" -version = "0.25.0" +version = "0.25.1" description = "Introspect dynamic libraries built with PyO3 to get metadata about the exported Python types" authors = ["PyO3 Project and Contributors "] homepage = "https://github.com/pyo3/pyo3" diff --git a/pyo3-macros-backend/Cargo.toml b/pyo3-macros-backend/Cargo.toml index 506a2b8eb31..a85caac82f7 100644 --- a/pyo3-macros-backend/Cargo.toml +++ b/pyo3-macros-backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-macros-backend" -version = "0.25.0" +version = "0.25.1" description = "Code generation for PyO3 package" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -17,7 +17,7 @@ rust-version.workspace = true [dependencies] heck = "0.5" proc-macro2 = { version = "1.0.60", default-features = false } -pyo3-build-config = { path = "../pyo3-build-config", version = "=0.25.0", features = ["resolve-config"] } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.25.1", features = ["resolve-config"] } quote = { version = "1", default-features = false } [dependencies.syn] @@ -27,7 +27,7 @@ default-features = false features = ["derive", "parsing", "printing", "clone-impls", "full", "extra-traits", "visit-mut"] [build-dependencies] -pyo3-build-config = { path = "../pyo3-build-config", version = "=0.25.0" } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.25.1" } [lints] workspace = true diff --git a/pyo3-macros/Cargo.toml b/pyo3-macros/Cargo.toml index a38dac4b3a1..0e1d6d725c3 100644 --- a/pyo3-macros/Cargo.toml +++ b/pyo3-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-macros" -version = "0.25.0" +version = "0.25.1" description = "Proc macros for PyO3 package" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -23,7 +23,7 @@ experimental-inspect = ["pyo3-macros-backend/experimental-inspect"] 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.25.0" } +pyo3-macros-backend = { path = "../pyo3-macros-backend", version = "=0.25.1" } [lints] workspace = true diff --git a/pyproject.toml b/pyproject.toml index adfe3a27348..48316a920a5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ [tool.towncrier] filename = "CHANGELOG.md" -version = "0.25.0" +version = "0.25.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 9b16f1d68e7..02db8433838 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.25.0/class.html#no-generic-parameters +error: #[pyclass] cannot have generic parameters. For an explanation, see https://pyo3.rs/v0.25.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.25.0/class.html#no-lifetime-parameters +error: #[pyclass] cannot have lifetime parameters. For an explanation, see https://pyo3.rs/v0.25.1/class.html#no-lifetime-parameters --> tests/ui/reject_generics.rs:9:27 | 9 | struct ClassWithLifetimes<'a> { From 5e20e319bb176abacc96ac7d4771d33e78c5cb73 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers <7943856+bschoenmaeckers@users.noreply.github.com> Date: Thu, 24 Jul 2025 08:58:16 +0200 Subject: [PATCH 747/936] Impl `MutexExt` for parking_lot's/lock_api `ReentrantMutex` (#5258) * Impl `MutexExt` for parking_lot's/lock_api `ReentrantMutex` * Add tests * update features.md --- guide/src/features.md | 6 ++-- newsfragments/5258.added.md | 1 + src/sealed.rs | 6 +++- src/sync.rs | 59 +++++++++++++++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 newsfragments/5258.added.md diff --git a/guide/src/features.md b/guide/src/features.md index 5999c3f0247..2c8f14180a0 100644 --- a/guide/src/features.md +++ b/guide/src/features.md @@ -117,7 +117,7 @@ Adds a dependency on [anyhow](https://docs.rs/anyhow). Enables a conversion from ### `arc_lock` -Enables Pyo3's `MutexExt` trait for all Mutexes that extend on [`lock_api::Mutex`](https://docs.rs/lock_api/latest/lock_api/struct.Mutex.html) and are wrapped in an [`Arc`](https://doc.rust-lang.org/std/sync/struct.Arc.html) type. Like [`Arc`](https://docs.rs/parking_lot/latest/parking_lot/type.Mutex.html#method.lock_arc) +Enables Pyo3's `MutexExt` trait for all Mutexes that extend on [`lock_api::Mutex`](https://docs.rs/lock_api/latest/lock_api/struct.Mutex.html) or [`parking_lot::ReentrantMutex`](https://docs.rs/lock_api/latest/lock_api/struct.ReentrantMutex.html) and are wrapped in an [`Arc`](https://doc.rust-lang.org/std/sync/struct.Arc.html) type. Like [`Arc`](https://docs.rs/parking_lot/latest/parking_lot/type.Mutex.html#method.lock_arc) ### `bigdecimal` Adds a dependency on [bigdecimal](https://docs.rs/bigdecimal) and enables conversions into its [`BigDecimal`](https://docs.rs/bigdecimal/latest/bigdecimal/struct.BigDecimal.html) type. @@ -172,7 +172,7 @@ Adds a dependency on [jiff@0.2](https://docs.rs/jiff/0.2) and requires MSRV 1.70 ### `lock_api` -Adds a dependency on [lock_api](https://docs.rs/lock_api) and enables Pyo3's `MutexExt` trait for all mutexes that extend on [`lock_api::Mutex`](https://docs.rs/lock_api/latest/lock_api/struct.Mutex.html) (like `parking_lot` or `spin`). +Adds a dependency on [lock_api](https://docs.rs/lock_api) and enables Pyo3's `MutexExt` trait for all mutexes that extend on [`lock_api::Mutex`](https://docs.rs/lock_api/latest/lock_api/struct.Mutex.html) and [`parking_lot::ReentrantMutex`](https://docs.rs/lock_api/latest/lock_api/struct.ReentrantMutex.html) (like `parking_lot` or `spin`). ### `num-bigint` @@ -194,7 +194,7 @@ Adds a dependency on [ordered-float](https://docs.rs/ordered-float) and enables ### `parking-lot` -Adds a dependency on [parking_lot](https://docs.rs/parking_lot) and enables Pyo3's `OnceExt` & `MutexExt` traits for [`parking_lot::Once`](https://docs.rs/parking_lot/latest/parking_lot/struct.Once.html) and [`parking_lot::Mutex`](https://docs.rs/parking_lot/latest/parking_lot/type.Mutex.html) types. +Adds a dependency on [parking_lot](https://docs.rs/parking_lot) and enables Pyo3's `OnceExt` & `MutexExt` traits for [`parking_lot::Once`](https://docs.rs/parking_lot/latest/parking_lot/struct.Once.html) [`parking_lot::Mutex`](https://docs.rs/parking_lot/latest/parking_lot/type.Mutex.html) and [`parking_lot::ReentrantMutex`](https://docs.rs/parking_lot/latest/parking_lot/type.ReentrantMutex.html) types. ### `rust_decimal` diff --git a/newsfragments/5258.added.md b/newsfragments/5258.added.md new file mode 100644 index 00000000000..773a74ef134 --- /dev/null +++ b/newsfragments/5258.added.md @@ -0,0 +1 @@ +Impl `MutexExt` for parking_lot's/lock_api `ReentrantMutex` diff --git a/src/sealed.rs b/src/sealed.rs index 29ffa951f4e..66b80840097 100644 --- a/src/sealed.rs +++ b/src/sealed.rs @@ -62,4 +62,8 @@ impl Sealed for lock_api::Mutex {} #[cfg(feature = "parking_lot")] impl Sealed for parking_lot::Once {} #[cfg(feature = "arc_lock")] -impl Sealed for std::sync::Arc> {} +impl Sealed for std::sync::Arc> {} +#[cfg(feature = "lock_api")] +impl Sealed for lock_api::ReentrantMutex {} +#[cfg(feature = "arc_lock")] +impl Sealed for std::sync::Arc> {} diff --git a/src/sync.rs b/src/sync.rs index 6363ece75bd..bf70659d485 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -720,6 +720,52 @@ where } } +#[cfg(feature = "lock_api")] +impl MutexExt for lock_api::ReentrantMutex +where + R: lock_api::RawMutex, + G: lock_api::GetThreadId, +{ + type LockResult<'a> + = lock_api::ReentrantMutexGuard<'a, R, G, T> + where + Self: 'a; + + fn lock_py_attached(&self, _py: Python<'_>) -> lock_api::ReentrantMutexGuard<'_, R, G, T> { + if let Some(guard) = self.try_lock() { + return guard; + } + + let ts_guard = unsafe { SuspendAttach::new() }; + let res = self.lock(); + drop(ts_guard); + res + } +} + +#[cfg(feature = "arc_lock")] +impl MutexExt for std::sync::Arc> +where + R: lock_api::RawMutex, + G: lock_api::GetThreadId, +{ + type LockResult<'a> + = lock_api::ArcReentrantMutexGuard + where + Self: 'a; + + fn lock_py_attached(&self, _py: Python<'_>) -> lock_api::ArcReentrantMutexGuard { + if let Some(guard) = self.try_lock_arc() { + return guard; + } + + let ts_guard = unsafe { SuspendAttach::new() }; + let res = self.lock_arc(); + drop(ts_guard); + res + } +} + #[cold] fn init_once_py_attached(once: &Once, _py: Python<'_>, f: F) where @@ -1178,12 +1224,25 @@ mod tests { parking_lot::Mutex::new(Py::new(py, BoolWrapper(AtomicBool::new(false))).unwrap()) }); + test_mutex!(parking_lot::ReentrantMutexGuard<'_, _>, |py| { + parking_lot::ReentrantMutex::new( + Py::new(py, BoolWrapper(AtomicBool::new(false))).unwrap(), + ) + }); + #[cfg(feature = "arc_lock")] test_mutex!(parking_lot::ArcMutexGuard<_, _>, |py| { let mutex = parking_lot::Mutex::new(Py::new(py, BoolWrapper(AtomicBool::new(false))).unwrap()); std::sync::Arc::new(mutex) }); + + #[cfg(feature = "arc_lock")] + test_mutex!(parking_lot::ArcReentrantMutexGuard<_, _, _>, |py| { + let mutex = + parking_lot::ReentrantMutex::new(Py::new(py, BoolWrapper(AtomicBool::new(false))).unwrap()); + std::sync::Arc::new(mutex) + }); } #[cfg(not(target_arch = "wasm32"))] // We are building wasm Python with pthreads disabled From aa0fa4c4afb37f17032acf36ede97adfbc532dbd Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Thu, 24 Jul 2025 16:06:49 +0200 Subject: [PATCH 748/936] Towards soundness of PyByteArray::to_vec (#4742) * Towards soundness of PyByteArrayMethods::to_vec In free-threaded Python, to_vec needs to make sure to run inside a critical section so that no other Python thread is mutating the bytearray causing UB. See also https://github.com/PyO3/pyo3/issues/4736 * Add changelog entry * Reword changelog entry Co-authored-by: David Hewitt * Add test * Skip test failing for unsound CPython 3.13 * Improve docs * Add safety comment to unsafe invocation * Limit test by time AND number of rounds * Test: fix MSRV compatibility & simplify scenario * Fix example code * Further reduce required test duration * Skip threaded test on wasm * Rewrite cfg attribute for llvm-cov to not turn into llvm-cough --------- Co-authored-by: David Hewitt --- newsfragments/4742.fixed.md | 1 + src/types/bytearray.rs | 153 ++++++++++++++++++++++++++++++++++-- 2 files changed, 146 insertions(+), 8 deletions(-) create mode 100644 newsfragments/4742.fixed.md diff --git a/newsfragments/4742.fixed.md b/newsfragments/4742.fixed.md new file mode 100644 index 00000000000..7bfe38b0c38 --- /dev/null +++ b/newsfragments/4742.fixed.md @@ -0,0 +1 @@ +Use critical section in `PyByteArray::to_vec` on freethreaded build to replicate GIL-enabled "soundness". diff --git a/src/types/bytearray.rs b/src/types/bytearray.rs index 5aa7acceb86..da3eb3b2b20 100644 --- a/src/types/bytearray.rs +++ b/src/types/bytearray.rs @@ -2,6 +2,7 @@ use crate::err::{PyErr, PyResult}; use crate::ffi_ptr_ext::FfiPtrExt; use crate::instance::{Borrowed, Bound}; use crate::py_result_ext::PyResultExt; +use crate::sync::with_critical_section; use crate::types::any::PyAnyMethods; use crate::{ffi, PyAny, Python}; use std::slice; @@ -125,28 +126,29 @@ pub trait PyByteArrayMethods<'py>: crate::sealed::Sealed { /// /// As a result, this slice should only be used for short-lived operations without executing any /// Python code, such as copying into a Vec. + /// For free-threaded Python support see also [`with_critical_section`]. /// /// # Examples /// /// ```rust /// use pyo3::prelude::*; /// use pyo3::exceptions::PyRuntimeError; + /// use pyo3::sync::with_critical_section; /// 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 + /// let section = with_critical_section(bytes, || { + /// // SAFETY: We promise to not let the interpreter regain control over the bytearray /// // 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) - /// }; + /// slice.get(6..11) + /// .map(Vec::from) + /// .ok_or_else(|| PyRuntimeError::new_err("input is not long enough")) + /// })?; /// /// // Now we can do things with `section` and call PyO3 APIs again. /// // ... @@ -188,6 +190,9 @@ pub trait PyByteArrayMethods<'py>: crate::sealed::Sealed { /// # #[allow(dead_code)] /// #[pyfunction] /// fn bug(py: Python<'_>, bytes: &Bound<'_, PyByteArray>) { + /// // No critical section is being used. + /// // This means that for no-gil Python another thread could be modifying the + /// // bytearray concurrently and thus invalidate `slice` any time. /// let slice = unsafe { bytes.as_bytes() }; /// /// // This explicitly yields control back to the Python interpreter... @@ -267,7 +272,14 @@ impl<'py> PyByteArrayMethods<'py> for Bound<'py, PyByteArray> { } fn to_vec(&self) -> Vec { - unsafe { self.as_bytes() }.to_vec() + with_critical_section(self, || { + // SAFETY: + // * `self` is a `Bound` object, which guarantees that the Python GIL is held. + // * For no-gil Python, a critical section is used in lieu of the GIL. + // * We don't interact with the interpreter + // * We don't mutate the underlying slice + unsafe { self.as_bytes() }.to_vec() + }) } fn resize(&self, len: usize) -> PyResult<()> { @@ -444,4 +456,129 @@ mod tests { .is_instance_of::(py)); }) } + + // * wasm has no threading support + // * CPython 3.13t is unsound => test fails + #[cfg(all( + not(target_family = "wasm"), + any(Py_3_14, not(all(Py_3_13, Py_GIL_DISABLED))) + ))] + #[test] + fn test_data_integrity_in_critical_section() { + use crate::instance::Py; + use crate::sync::{with_critical_section, MutexExt}; + + use std::sync::atomic::{AtomicBool, Ordering}; + use std::sync::Mutex; + use std::thread; + use std::thread::ScopedJoinHandle; + use std::time::Duration; + + const SIZE: usize = 1_000_000; + const DATA_VALUE: u8 = 42; + + fn make_byte_array(py: Python<'_>, size: usize, value: u8) -> Bound<'_, PyByteArray> { + PyByteArray::new_with(py, size, |b| { + b.fill(value); + Ok(()) + }) + .unwrap() + } + + let data: Mutex> = Mutex::new(Python::attach(|py| { + make_byte_array(py, SIZE, DATA_VALUE).unbind() + })); + + fn get_data<'py>( + data: &Mutex>, + py: Python<'py>, + ) -> Bound<'py, PyByteArray> { + data.lock_py_attached(py).unwrap().bind(py).clone() + } + + fn set_data(data: &Mutex>, new: Bound<'_, PyByteArray>) { + let py = new.py(); + *data.lock_py_attached(py).unwrap() = new.unbind() + } + + let running = AtomicBool::new(true); + let extending = AtomicBool::new(false); + + // continuously extends and resets the bytearray in data + let worker1 = || { + let mut rounds = 0; + while running.load(Ordering::SeqCst) && rounds < 50 { + Python::attach(|py| { + let byte_array = get_data(&data, py); + extending.store(true, Ordering::SeqCst); + byte_array + .call_method("extend", (&byte_array,), None) + .unwrap(); + extending.store(false, Ordering::SeqCst); + set_data(&data, make_byte_array(py, SIZE, DATA_VALUE)); + rounds += 1; + }); + } + }; + + // continuously checks the integrity of bytearray in data + let worker2 = || { + while running.load(Ordering::SeqCst) { + if !extending.load(Ordering::SeqCst) { + // wait until we have a chance to read inconsistent state + continue; + } + Python::attach(|py| { + let read = get_data(&data, py); + if read.len() == SIZE { + // extend is still not done => wait even more + return; + } + with_critical_section(&read, || { + // SAFETY: we are in a critical section + // This is the whole point of the test: make sure that a + // critical section is sufficient to ensure that the data + // read is consistent. + unsafe { + let bytes = read.as_bytes(); + assert!(bytes.iter().rev().take(50).all(|v| *v == DATA_VALUE + && bytes.iter().take(50).all(|v| *v == DATA_VALUE))); + } + }); + }); + } + }; + + thread::scope(|s| { + let mut handle1 = Some(s.spawn(worker1)); + let mut handle2 = Some(s.spawn(worker2)); + let mut handles = [&mut handle1, &mut handle2]; + + let t0 = std::time::Instant::now(); + while t0.elapsed() < Duration::from_secs(1) { + for handle in &mut handles { + if handle + .as_ref() + .map(ScopedJoinHandle::is_finished) + .unwrap_or(false) + { + let res = handle.take().unwrap().join(); + if res.is_err() { + running.store(false, Ordering::SeqCst); + } + res.unwrap(); + } + } + if handles.iter().any(|handle| handle.is_none()) { + break; + } + } + running.store(false, Ordering::SeqCst); + for handle in &mut handles { + if let Some(handle) = handle.take() { + handle.join().unwrap() + } + } + }); + } } From c0a2e1d76b1f9d89a06c1a158313f434d0b04a91 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers <7943856+bschoenmaeckers@users.noreply.github.com> Date: Fri, 25 Jul 2025 13:25:48 +0200 Subject: [PATCH 749/936] Add `bytes` conversions (#5252) * initial implementation * apply review comments * update docs * update changelog * update guide * Address comments * update tests --------- Co-authored-by: Kyle Kosic --- Cargo.toml | 2 + guide/src/features.md | 3 + newsfragments/5252.added.md | 1 + src/conversions/bytes.rs | 129 ++++++++++++++++++++++++++++++++++++ src/conversions/mod.rs | 1 + 5 files changed, 136 insertions(+) create mode 100644 newsfragments/5252.added.md create mode 100644 src/conversions/bytes.rs diff --git a/Cargo.toml b/Cargo.toml index 663165c59b6..3758235defa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ inventory = { version = "0.3.5", optional = true } # crate integrations that can be added using the eponymous features anyhow = { version = "1.0.1", optional = true } bigdecimal = { version = "0.4.7", optional = true } +bytes = { version = "1.10", optional = true } chrono = { version = "0.4.25", default-features = false, optional = true } chrono-tz = { version = ">= 0.10, < 0.11", default-features = false, optional = true } either = { version = "1.9", optional = true } @@ -146,6 +147,7 @@ full = [ "anyhow", "arc_lock", "bigdecimal", + "bytes", "chrono", "chrono-local", "chrono-tz", diff --git a/guide/src/features.md b/guide/src/features.md index 2c8f14180a0..35338eb856c 100644 --- a/guide/src/features.md +++ b/guide/src/features.md @@ -122,6 +122,9 @@ Enables Pyo3's `MutexExt` trait for all Mutexes that extend on [`lock_api::Mutex ### `bigdecimal` Adds a dependency on [bigdecimal](https://docs.rs/bigdecimal) and enables conversions into its [`BigDecimal`](https://docs.rs/bigdecimal/latest/bigdecimal/struct.BigDecimal.html) type. +### `bytes` +Adds a dependency on [bytes](https://docs.rs/bytes/latest/bytes) and enables conversions into its [`Bytes`](https://docs.rs/bytes/latest/bytes/struct.Bytes.html) type. + ### `chrono` Adds a dependency on [chrono](https://docs.rs/chrono). Enables a conversion from [chrono](https://docs.rs/chrono)'s types to python: diff --git a/newsfragments/5252.added.md b/newsfragments/5252.added.md new file mode 100644 index 00000000000..3d9362e6c05 --- /dev/null +++ b/newsfragments/5252.added.md @@ -0,0 +1 @@ +Add bytes to/from python conversions. diff --git a/src/conversions/bytes.rs b/src/conversions/bytes.rs new file mode 100644 index 00000000000..673f9d6b2ba --- /dev/null +++ b/src/conversions/bytes.rs @@ -0,0 +1,129 @@ +#![cfg(feature = "bytes")] + +//! Conversions to and from [bytes](https://docs.rs/bytes/latest/bytes/)'s [`Bytes`]. +//! +//! This is useful for efficiently converting Python's `bytes` types efficiently. +//! While `bytes` will be directly borrowed, converting from `bytearray` will result in a copy. +//! +//! When converting `Bytes` back into Python, this will do a copy, just like `&[u8]` and `Vec`. +//! +//! # When to use `Bytes` +//! +//! Unless you specifically need [`Bytes`] for ref-counted ownership and sharing, +//! you may find that using `&[u8]`, `Vec`, [`Bound`], or [`PyBackedBytes`] +//! is simpler for most use cases. +//! +//! # Setup +//! +//! To use this feature, add in your **`Cargo.toml`**: +//! +//! ```toml +//! [dependencies] +//! bytes = "1.10" +#![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"), "\", features = [\"bytes\"] }")] +//! +//! Note that you must use compatible versions of bytes and PyO3. +//! +//! # Example +//! +//! Rust code to create functions which return `Bytes` or take `Bytes` as arguments: +//! +//! ```rust,no_run +//! use pyo3::prelude::*; +//! +//! #[pyfunction] +//! fn get_message_bytes() -> Bytes { +//! Bytes::from_static(b"Hello Python!") +//! } +//! +//! #[pyfunction] +//! fn num_bytes(bytes: Bytes) -> usize { +//! bytes.len() +//! } +//! +//! #[pymodule] +//! fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> { +//! m.add_function(wrap_pyfunction!(get_message_bytes, m)?)?; +//! m.add_function(wrap_pyfunction!(num_bytes, m)?)?; +//! Ok(()) +//! } +//! ``` +//! +//! Python code that calls these functions: +//! +//! ```python +//! from my_module import get_message_bytes, num_bytes +//! +//! message = get_message_bytes() +//! assert message == b"Hello Python!" +//! +//! size = num_bytes(message) +//! assert size == 13 +//! ``` +use bytes::Bytes; + +use crate::conversion::IntoPyObject; +use crate::instance::Bound; +use crate::pybacked::PyBackedBytes; +use crate::types::any::PyAnyMethods; +use crate::types::PyBytes; +use crate::{FromPyObject, PyAny, PyErr, PyResult, Python}; + +impl FromPyObject<'_> for Bytes { + fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { + Ok(Bytes::from_owner(ob.extract::()?)) + } +} + +impl<'py> IntoPyObject<'py> for Bytes { + type Target = PyBytes; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + Ok(PyBytes::new(py, &self)) + } +} + +impl<'py> IntoPyObject<'py> for &Bytes { + type Target = PyBytes; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + Ok(PyBytes::new(py, self)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::{PyByteArray, PyByteArrayMethods, PyBytes}; + use crate::Python; + + #[test] + fn test_bytes() { + Python::attach(|py| { + let py_bytes = PyBytes::new(py, b"foobar"); + let bytes: Bytes = py_bytes.extract().unwrap(); + assert_eq!(&*bytes, b"foobar"); + + let bytes = Bytes::from_static(b"foobar").into_pyobject(py).unwrap(); + assert!(bytes.is_instance_of::()); + }); + } + + #[test] + fn test_bytearray() { + Python::attach(|py| { + let py_bytearray = PyByteArray::new(py, b"foobar"); + let bytes: Bytes = py_bytearray.extract().unwrap(); + assert_eq!(&*bytes, b"foobar"); + + // Editing the bytearray should not change extracted Bytes + unsafe { py_bytearray.as_bytes_mut()[0] = b'x' }; + assert_eq!(&bytes, "foobar"); + assert_eq!(&py_bytearray.extract::>().unwrap(), b"xoobar"); + }); + } +} diff --git a/src/conversions/mod.rs b/src/conversions/mod.rs index c623ca7b379..05e2ea1d1d0 100644 --- a/src/conversions/mod.rs +++ b/src/conversions/mod.rs @@ -2,6 +2,7 @@ pub mod anyhow; pub mod bigdecimal; +pub mod bytes; pub mod chrono; pub mod chrono_tz; pub mod either; From 62bf7d84c2f289ed436d8fd5b5031d89d81a7563 Mon Sep 17 00:00:00 2001 From: Thomas Tanon Date: Fri, 25 Jul 2025 18:36:59 +0200 Subject: [PATCH 750/936] Stub generation: better imports (#5251) Look for valid identifiers in the type string instead of doing a dumb split --- newsfragments/5251.fixed.md | 1 + pyo3-introspection/Cargo.toml | 1 + pyo3-introspection/src/stubs.rs | 91 +++++++++++++++++++++++++++++++-- 3 files changed, 90 insertions(+), 3 deletions(-) create mode 100644 newsfragments/5251.fixed.md diff --git a/newsfragments/5251.fixed.md b/newsfragments/5251.fixed.md new file mode 100644 index 00000000000..ba58d25440d --- /dev/null +++ b/newsfragments/5251.fixed.md @@ -0,0 +1 @@ +Introspection: better automated imports generation \ No newline at end of file diff --git a/pyo3-introspection/Cargo.toml b/pyo3-introspection/Cargo.toml index 1012d4b2307..ccdc080c48e 100644 --- a/pyo3-introspection/Cargo.toml +++ b/pyo3-introspection/Cargo.toml @@ -13,6 +13,7 @@ anyhow = "1" goblin = "0.9.0" serde = { version = "1", features = ["derive"] } serde_json = "1" +unicode-ident = "1" [dev-dependencies] tempfile = "3.12.0" diff --git a/pyo3-introspection/src/stubs.rs b/pyo3-introspection/src/stubs.rs index 8f815e9b852..e8d9eb70c6b 100644 --- a/pyo3-introspection/src/stubs.rs +++ b/pyo3-introspection/src/stubs.rs @@ -1,6 +1,7 @@ use crate::model::{Argument, Arguments, Class, Const, Function, Module, VariableLengthArgument}; use std::collections::{BTreeSet, HashMap}; use std::path::{Path, PathBuf}; +use unicode_ident::{is_xid_continue, is_xid_start}; /// Generates the [type stubs](https://typing.readthedocs.io/en/latest/source/stubs.html) of a given module. /// It returns a map between the file name and the file content. @@ -194,18 +195,102 @@ fn variable_length_argument_stub( } fn annotation_stub<'a>(annotation: &'a str, modules_to_import: &mut BTreeSet) -> &'a str { - if let Some((module, _)) = annotation.rsplit_once('.') { - // TODO: this is very naive - modules_to_import.insert(module.into()); + // We iterate on the annotation string + // If it starts with a Python path like foo.bar, we add the module name (here foo) to the import list + // and we skip after it + let mut i = 0; + while i < annotation.len() { + if let Some(path) = path_prefix(&annotation[i..]) { + // We found a path! + i += path.len(); + if let Some((module, _)) = path.rsplit_once('.') { + modules_to_import.insert(module.into()); + } + } + i += 1; } annotation } +// If the input starts with a path like foo.bar, returns it +fn path_prefix(input: &str) -> Option<&str> { + let mut length = identifier_prefix(input)?.len(); + loop { + // We try to add another identifier to the path + let Some(remaining) = input[length..].strip_prefix('.') else { + break; + }; + let Some(id) = identifier_prefix(remaining) else { + break; + }; + length += id.len() + 1; + } + Some(&input[..length]) +} + +// If the input starts with an identifier like foo, returns it +fn identifier_prefix(input: &str) -> Option<&str> { + // We get the first char and validate it + let mut iter = input.chars(); + let first_char = iter.next()?; + if first_char != '_' && !is_xid_start(first_char) { + return None; + } + let mut length = first_char.len_utf8(); + // We add extra chars as much as we can + for c in iter { + if is_xid_continue(c) { + length += c.len_utf8(); + } else { + break; + } + } + Some(&input[0..length]) +} + #[cfg(test)] mod tests { use super::*; use crate::model::Arguments; + #[test] + fn annotation_stub_proper_imports() { + let mut modules_to_import = BTreeSet::new(); + + // Basic int + annotation_stub("int", &mut modules_to_import); + assert!(modules_to_import.is_empty()); + + // Simple path + annotation_stub("collections.abc.Iterable", &mut modules_to_import); + assert!(modules_to_import.contains("collections.abc")); + + // With underscore + annotation_stub("_foo._bar_baz", &mut modules_to_import); + assert!(modules_to_import.contains("_foo")); + + // Basic generic + annotation_stub("typing.List[int]", &mut modules_to_import); + assert!(modules_to_import.contains("typing")); + + // Complex generic + annotation_stub("typing.List[foo.Bar[int]]", &mut modules_to_import); + assert!(modules_to_import.contains("foo")); + + // Callable + annotation_stub( + "typing.Callable[[int, baz.Bar], bar.Baz[bool]]", + &mut modules_to_import, + ); + assert!(modules_to_import.contains("bar")); + assert!(modules_to_import.contains("baz")); + + // Union + annotation_stub("a.B | b.C", &mut modules_to_import); + assert!(modules_to_import.contains("a")); + assert!(modules_to_import.contains("b")); + } + #[test] fn function_stubs_with_variable_length() { let function = Function { From 0fb103f75c644f6adafe62e046b634b8adfb4bec Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sat, 26 Jul 2025 09:31:40 +0100 Subject: [PATCH 751/936] ci: move netlify site build to nox (#5262) * ci: move netlify site build to nox * fixup * fixup `--preview` build * fix pr number upload * add branch preview * try fix metadata upload --- .github/workflows/netlify-build.yml | 41 +++----- .github/workflows/netlify-deploy.yml | 43 ++++++-- noxfile.py | 148 ++++++++++++++++++++++++++- 3 files changed, 194 insertions(+), 38 deletions(-) diff --git a/.github/workflows/netlify-build.yml b/.github/workflows/netlify-build.yml index 5576456ba7f..bc603172d77 100644 --- a/.github/workflows/netlify-build.yml +++ b/.github/workflows/netlify-build.yml @@ -57,29 +57,11 @@ jobs: TAG_NAME="${GITHUB_REF##*/}" echo "tag_name=${TAG_NAME}" >> $GITHUB_OUTPUT - # Start with the current guides. - - name: Get the guide + # Build the site + - name: Prepare the versioned guide entries run: | - wget -qc https://github.com/PyO3/pyo3/archive/gh-pages.tar.gz -O - | tar -xz - mv pyo3-gh-pages netlify_build - - # Set redirects. - - name: Set redirects - run: sh .netlify/redirect.sh - - # This builds the book and docs in netlify_build/main - - name: Build the guide and public docs - run: | - python -m pip install --upgrade pip && pip install nox towncrier - towncrier build --yes --version Unreleased --date TBC - nox -s check-guide - mv target/guide/ netlify_build/main/ - nox -s docs - mv target/doc netlify_build/main/doc/ - echo "" > netlify_build/main/doc/index.html - nox -s docs -- nightly internal - mkdir -p netlify_build/internal - mv target/doc netlify_build/internal/ + python -m pip install --upgrade pip && pip install nox towncrier requests + nox -s build-netlify-site -- ${{ github.ref == 'refs/heads/main' && '' || '--preview' }} # Upload the built site as an artifact for deploy workflow to consume - name: Upload Build Artifact @@ -88,13 +70,16 @@ jobs: name: site path: ./netlify_build - # Upload PR number for use in deploy workflow, will be empty if not a PR - - name: Prepare PR number for upload + # Upload metadata for use in deploy workflow, will be empty if not a PR + - name: Prepare metadata for upload id: pr - run: echo "pr_number=$(echo ${{ github.event.pull_request.number }})" >> .pr-number + run: | + mkdir -p metadata + echo "pr_number=$(echo ${{ github.event.pull_request.number }})" >> metadata/pr_number + echo "pr_head_sha=$(echo ${{ github.event.pull_request.head.sha }})" >> metadata/pr_head_sha - - name: Upload PR number + - name: Upload PR metadata uses: actions/upload-artifact@v4 with: - name: pr-number - path: .pr-number + name: metadata + path: metadata diff --git a/.github/workflows/netlify-deploy.yml b/.github/workflows/netlify-deploy.yml index d3fb11479fb..47a45a3b47c 100644 --- a/.github/workflows/netlify-deploy.yml +++ b/.github/workflows/netlify-deploy.yml @@ -28,15 +28,17 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} run-id: ${{ github.event.workflow_run.id }} - - name: Download PR Number + - name: Download PR Metadata uses: actions/download-artifact@v4 with: - name: pr_number + name: metadata + path: .metadata - - name: Get PR Number - id: pr_number + - name: Get PR metadata + id: pr_metadata run: | - echo "pr_number=$(cat .pr-number)" >> $GITHUB_OUTPUT + echo "pr_number=$(cat .metadata/pr_number)" >> $GITHUB_OUTPUT + echo "pr_head_sha=$(cat .metadata/pr_head_sha)" >> $GITHUB_OUTPUT - name: Install netlify-cli run: | @@ -48,4 +50,33 @@ jobs: DEBUG=* netlify deploy \ --site ${{ secrets.NETLIFY_SITE_ID }} \ --auth ${{ secrets.NETLIFY_TOKEN }} \ - ${{ steps.pr_number.outputs.pr_number == '' && '--prod' || '' }} + ${{ steps.pr_metadata.outputs.pr_number == '' && '--prod' || '' }} + --json | tee deploy_output.json + + # credit: https://www.raulmelo.me/en/blog/deploying-netlify-github-actions-guide + - name: Generate URL Preview + id: url_preview + if: ${{ steps.pr_metadata.outputs.pr_head_sha != '' }} + run: | + NETLIFY_PREVIEW_URL=$(jq -r '.deploy_url' deploy_output.json) + echo "NETLIFY_PREVIEW_URL=$NETLIFY_PREVIEW_URL" >> "$GITHUB_OUTPUT" + + - name: Post Netlify Preview Status to PR + if: ${{ steps.pr_metadata.outputs.pr_head_sha != '' }} + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const prNumber = Number('${{ steps.pr_metadata.outputs.pr_number }}'); + const previewUrl = '${{ steps.url_preview.outputs.NETLIFY_PREVIEW_URL }}'; + const commitSha = '${{ steps.pr_metadata.outputs.pr_head_sha }}'; + + await github.rest.repos.createCommitStatus({ + owner: context.repo.owner, + repo: context.repo.repo, + sha: commitSha, + state: 'success', + target_url: previewUrl, + description: 'Netlify Preview', + context: 'netlify/preview' + }); diff --git a/noxfile.py b/noxfile.py index 77d2b2476ae..5c365892d29 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1,3 +1,4 @@ +import io import json import os import re @@ -5,6 +6,7 @@ import subprocess import sys import sysconfig +import tarfile import tempfile from contextlib import contextmanager from functools import lru_cache @@ -31,6 +33,11 @@ except ImportError: toml = None +try: + import requests +except ImportError: + requests = None + nox.options.sessions = ["test", "clippy", "rustfmt", "ruff", "docs"] PYO3_DIR = Path(__file__).parent @@ -433,20 +440,23 @@ def test_emscripten(session: nox.Session): @nox.session(venv_backend="none") -def docs(session: nox.Session) -> None: +def docs(session: nox.Session, nightly: bool = False, internal: bool = False) -> None: rustdoc_flags = ["-Dwarnings"] toolchain_flags = [] cargo_flags = [] + nightly = nightly or ("nightly" in session.posargs) + internal = internal or ("internal" in session.posargs) + if "open" in session.posargs: cargo_flags.append("--open") - if "nightly" in session.posargs: + if nightly: rustdoc_flags.append("--cfg docsrs") toolchain_flags.append("+nightly") cargo_flags.extend(["-Z", "unstable-options", "-Z", "rustdoc-scrape-examples"]) - if "nightly" in session.posargs and "internal" in session.posargs: + if internal: rustdoc_flags.append("--Z unstable-options") rustdoc_flags.append("--document-hidden-items") rustdoc_flags.extend(("--html-after-content", ".netlify/internal_banner.html")) @@ -476,13 +486,143 @@ def docs(session: nox.Session) -> None: @nox.session(name="build-guide", venv_backend="none") def build_guide(session: nox.Session): shutil.rmtree(PYO3_GUIDE_TARGET, ignore_errors=True) - _run(session, "mdbook", "build", "-d", PYO3_GUIDE_TARGET, "guide", *session.posargs) + _run( + session, + "mdbook", + "build", + "-d", + str(PYO3_GUIDE_TARGET), + "guide", + *session.posargs, + external=True, + ) for license in ("LICENSE-APACHE", "LICENSE-MIT"): target_file = PYO3_GUIDE_TARGET / license target_file.unlink(missing_ok=True) shutil.copy(PYO3_DIR / license, target_file) +@nox.session(name="build-netlify-site") +def build_netlify_site(session: nox.Session): + # Remove netlify_build directory if it exists + netlify_build = Path("netlify_build") + if netlify_build.exists(): + shutil.rmtree(netlify_build) + + url = "https://github.com/PyO3/pyo3/archive/gh-pages.tar.gz" + response = requests.get(url, stream=True) + response.raise_for_status() + with tarfile.open(fileobj=io.BytesIO(response.content), mode="r:gz") as tar: + tar.extractall() + shutil.move("pyo3-gh-pages", "netlify_build") + + preview = "--preview" in session.posargs + if preview: + session.posargs.remove("--preview") + + _build_netlify_redirects(preview) + + session.install("towncrier") + # Save a copy of the changelog to restore later + changelog = (PYO3_DIR / "CHANGELOG.md").read_text() + + # Build the changelog + session.run( + "towncrier", "build", "--keep", "--version", "Unreleased", "--date", "TBC" + ) + + # Build the guide + build_guide(session) + PYO3_GUIDE_TARGET.rename("netlify_build/main") + + # Restore the original changelog + (PYO3_DIR / "CHANGELOG.md").write_text(changelog) + session.run("git", "restore", "--staged", "CHANGELOG.md", external=True) + + # Build the main branch docs + docs(session) + PYO3_DOCS_TARGET.rename("netlify_build/main/doc") + + Path("netlify_build/main/doc/index.html").write_text( + "" + ) + + # Build the internal docs + docs(session, nightly=True, internal=True) + PYO3_DOCS_TARGET.rename("netlify_build/internal") + + +def _build_netlify_redirects(preview: bool) -> None: + current_version = os.environ.get("PYO3_VERSION") + + with ( + open("netlify_build/_redirects", "w") as redirects_file, + open("netlify_build/_headers", "w") as headers_file, + ): + for d in glob("netlify_build/v*"): + version = d.removeprefix("netlify_build/v") + full_directory = d + "/" + redirects_file.write( + f"/v{version}/doc/* https://docs.rs/pyo3/{version}/:splat\n" + ) + if version != current_version: + # for old versions, mark the files in the latest version as the canonical URL + for file in glob(f"{d}/**", recursive=True): + file_path = file.removeprefix(full_directory) + # remove index.html and/or .html suffix to match the page URL on the + # final netlfiy site + url_path = file_path + if file_path == "index.html": + url_path = "" + + url_path = url_path.removesuffix(".html") + + # if the file exists in the latest version, add a canonical + # URL as a header + for url in ( + f"/v{version}/{url_path}", + *( + (f"/v{version}/{file_path}",) + if file_path != url_path + else () + ), + ): + headers_file.write(url + "\n") + if os.path.exists( + f"netlify_build/v{current_version}/{file_path}" + ): + headers_file.write( + f' Link: ; rel="canonical"\n' + ) + else: + # this file doesn't exist in the latest guide, don't + # index it + headers_file.write(" X-Robots-Tag: noindex\n") + + # Add latest redirect + if current_version is not None: + redirects_file.write(f"/latest/* /v{current_version}/:splat 302\n") + + # some backwards compatbiility redirects + redirects_file.write( + """\ +/latest/building_and_distribution/* /latest/building-and-distribution/:splat 302 +/latest/building_and_distribution/multiple_python_versions/* /latest/building-and-distribution/multiple-python-versions:splat 302 +/latest/function/error_handling/* /latest/function/error-handling/:splat 302 +/latest/getting_started/* /latest/getting-started/:splat 302 +/latest/python_from_rust/* /latest/python-from-rust/:splat 302 +/latest/python_typing_hints/* /latest/python-typing-hints/:splat 302 +/latest/trait_bounds/* /latest/trait-bounds/:splat 302 +""" + ) + + # Add landing page redirect + if preview: + redirects_file.write("/ /main/ 302\n") + else: + redirects_file.write(f"/ /v{current_version}/ 302\n") + + @nox.session(name="check-guide", venv_backend="none") def check_guide(session: nox.Session): # reuse other sessions, but with default args From 71d202d5cb93b2ae411ab1fe19e48ef1399a4b66 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sat, 26 Jul 2025 12:21:20 +0100 Subject: [PATCH 752/936] fixup netlify deploy via nox (#5264) --- .github/workflows/netlify-deploy.yml | 2 ++ noxfile.py | 9 ++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/netlify-deploy.yml b/.github/workflows/netlify-deploy.yml index 47a45a3b47c..237a45f3237 100644 --- a/.github/workflows/netlify-deploy.yml +++ b/.github/workflows/netlify-deploy.yml @@ -33,6 +33,8 @@ jobs: with: name: metadata path: .metadata + github-token: ${{ secrets.GITHUB_TOKEN }} + run-id: ${{ github.event.workflow_run.id }} - name: Get PR metadata id: pr_metadata diff --git a/noxfile.py b/noxfile.py index 5c365892d29..e324fa5bfe6 100644 --- a/noxfile.py +++ b/noxfile.py @@ -8,7 +8,7 @@ import sysconfig import tarfile import tempfile -from contextlib import contextmanager +from contextlib import ExitStack, contextmanager from functools import lru_cache from glob import glob from pathlib import Path @@ -555,10 +555,9 @@ def build_netlify_site(session: nox.Session): def _build_netlify_redirects(preview: bool) -> None: current_version = os.environ.get("PYO3_VERSION") - with ( - open("netlify_build/_redirects", "w") as redirects_file, - open("netlify_build/_headers", "w") as headers_file, - ): + with ExitStack() as stack: + redirects_file = stack.enter_context(open("netlify_build/_redirects", "w")) + headers_file = stack.enter_context(open("netlify_build/_headers", "w")) for d in glob("netlify_build/v*"): version = d.removeprefix("netlify_build/v") full_directory = d + "/" From 6150f822d0f562d87a62ccd0caebbe7d0fd9eb71 Mon Sep 17 00:00:00 2001 From: Jake Marchewitz <7871951+jjmarchewitz@users.noreply.github.com> Date: Sat, 26 Jul 2025 13:32:42 -0400 Subject: [PATCH 753/936] Report multiple errors from proc-macros (#5159) * working example * working on test cases * tests * rm ErrorCombiner * update everywhere I think needs it * revert test * rm comment * rm test changes * clippy * pymethod test * refactoring and progress? * fixed signature test * add more tests * uncomment test cases * newsfragment * clippy * updated errors * allow unused * more testing --------- Co-authored-by: David Hewitt --- .gitignore | 1 + newsfragments/5159.fixed.md | 1 + pyo3-macros-backend/src/attributes.rs | 30 +--- pyo3-macros-backend/src/combine_errors.rs | 36 ++++ pyo3-macros-backend/src/lib.rs | 1 + pyo3-macros-backend/src/module.rs | 39 +++-- pyo3-macros-backend/src/pyclass.rs | 42 ++--- pyo3-macros-backend/src/pyfunction.rs | 3 +- pyo3-macros-backend/src/pyimpl.rs | 130 ++++++++------- tests/test_pyfunction.rs | 1 + tests/ui/invalid_async.stderr | 6 + tests/ui/invalid_pyfunction_signatures.rs | 14 +- tests/ui/invalid_pyfunction_signatures.stderr | 14 +- tests/ui/invalid_pymethods.rs | 52 ++---- tests/ui/invalid_pymethods.stderr | 154 ++++++++++-------- tests/ui/invalid_pymodule_args.rs | 10 +- tests/ui/invalid_pymodule_args.stderr | 24 +++ 17 files changed, 317 insertions(+), 241 deletions(-) create mode 100644 newsfragments/5159.fixed.md create mode 100644 pyo3-macros-backend/src/combine_errors.rs diff --git a/.gitignore b/.gitignore index 3ad8328cd15..da64603db5f 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,4 @@ lcov.info coverage.json netlify_build/ .nox/ +.vscode/ diff --git a/newsfragments/5159.fixed.md b/newsfragments/5159.fixed.md new file mode 100644 index 00000000000..089de2c17d6 --- /dev/null +++ b/newsfragments/5159.fixed.md @@ -0,0 +1 @@ +Changed macro implementations to show multiple errors to users instead of only the first one. \ No newline at end of file diff --git a/pyo3-macros-backend/src/attributes.rs b/pyo3-macros-backend/src/attributes.rs index 711653fe430..af81e7aea69 100644 --- a/pyo3-macros-backend/src/attributes.rs +++ b/pyo3-macros-backend/src/attributes.rs @@ -10,6 +10,8 @@ use syn::{ Attribute, Expr, ExprPath, Ident, Index, LitBool, LitStr, Member, Path, Result, Token, }; +use crate::combine_errors::CombineErrors; + pub mod kw { syn::custom_keyword!(annotation); syn::custom_keyword!(attribute); @@ -395,41 +397,23 @@ pub fn take_attributes( pub fn take_pyo3_options(attrs: &mut Vec) -> Result> { let mut out = Vec::new(); - 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); + out.extend(options.into_iter().map(|a| Ok(a))); Ok(true) } else { Ok(false) } } Err(err) => { - all_errors.combine(err); + out.push(Err(err)); Ok(true) } })?; - 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); - } - } + let out: Vec = out.into_iter().try_combine_syn_errors()?; - pub fn ensure_empty(self) -> Result<()> { - if let Some(error) = self.0 { - Err(error) - } else { - Ok(()) - } - } + Ok(out) } diff --git a/pyo3-macros-backend/src/combine_errors.rs b/pyo3-macros-backend/src/combine_errors.rs new file mode 100644 index 00000000000..235831ac486 --- /dev/null +++ b/pyo3-macros-backend/src/combine_errors.rs @@ -0,0 +1,36 @@ +pub(crate) trait CombineErrors: Iterator { + type Ok; + fn try_combine_syn_errors(self) -> syn::Result>; +} + +impl CombineErrors for I +where + I: Iterator>, +{ + type Ok = T; + + fn try_combine_syn_errors(self) -> syn::Result> { + let mut oks: Vec = Vec::new(); + let mut errors: Vec = Vec::new(); + + for res in self { + match res { + Ok(val) => oks.push(val), + Err(e) => errors.push(e), + } + } + + let mut err_iter = errors.into_iter(); + let mut err = match err_iter.next() { + // There are no errors + None => return Ok(oks), + Some(e) => e, + }; + + for e in err_iter { + err.combine(e); + } + + Err(err) + } +} diff --git a/pyo3-macros-backend/src/lib.rs b/pyo3-macros-backend/src/lib.rs index 8919830dda9..f9e3cb865db 100644 --- a/pyo3-macros-backend/src/lib.rs +++ b/pyo3-macros-backend/src/lib.rs @@ -9,6 +9,7 @@ mod utils; mod attributes; +mod combine_errors; mod derive_attributes; mod frompyobject; mod intopyobject; diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index 0adf753a2e8..e53c35c465b 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -8,6 +8,7 @@ use crate::{ self, kw, take_attributes, take_pyo3_options, CrateAttribute, GILUsedAttribute, ModuleAttribute, NameAttribute, SubmoduleAttribute, }, + combine_errors::CombineErrors, get_doc, pyclass::PyClassPyO3Option, pyfunction::{impl_wrap_pyfunction, PyFunctionOptions}, @@ -67,20 +68,25 @@ impl PyModuleOptions { } }; } - 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)" - ), - PyModulePyO3Option::GILUsed(gil_used) => { - set_option!(gil_used) + attrs + .into_iter() + .map(|attr| { + 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)" + ), + PyModulePyO3Option::GILUsed(gil_used) => { + set_option!(gil_used) + } } - } - } + + Ok(()) + }) + .try_combine_syn_errors()?; Ok(()) } } @@ -154,7 +160,7 @@ pub fn pymodule_module_impl( let mut module_consts_values = Vec::new(); let mut module_consts_cfg_attrs = Vec::new(); - for item in &mut *items { + let _: Vec<()> = (*items).iter_mut().map(|item|{ match item { Item::Use(item_use) => { let is_pymodule_export = @@ -293,7 +299,7 @@ pub fn pymodule_module_impl( } Item::Const(item) => { if !find_and_remove_attribute(&mut item.attrs, "pymodule_export") { - continue; + return Ok(()); } module_consts.push(item.ident.clone()); module_consts_values.push(expr_to_python(&item.expr)); @@ -343,7 +349,8 @@ pub fn pymodule_module_impl( } _ => (), } - } + Ok(()) + }).try_combine_syn_errors()?; #[cfg(feature = "experimental-inspect")] let introspection = module_introspection_code( diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 5a5d8cab8a5..98c78e3cf91 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -10,10 +10,10 @@ 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, ErrorCombiner, ExtendsAttribute, - FreelistAttribute, ModuleAttribute, NameAttribute, NameLitStr, RenameAllAttribute, - StrFormatterAttribute, + self, kw, take_pyo3_options, CrateAttribute, ExtendsAttribute, FreelistAttribute, + ModuleAttribute, NameAttribute, NameLitStr, RenameAllAttribute, StrFormatterAttribute, }; +use crate::combine_errors::CombineErrors; #[cfg(feature = "experimental-inspect")] use crate::introspection::{class_introspection_code, introspection_id_const}; use crate::konst::{ConstAttributes, ConstSpec}; @@ -269,48 +269,42 @@ 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() - .filter_map( + .map( |field| match FieldPyO3Options::take_pyo3_options(&mut field.attrs) { - Ok(options) => Some((&*field, options)), - Err(e) => { - all_errors.combine(e); - None - } + Ok(options) => Ok((&*field, options)), + Err(e) => Err(e), }, ) .collect::>(), syn::Fields::Unnamed(fields) => fields .unnamed .iter_mut() - .filter_map( + .map( |field| match FieldPyO3Options::take_pyo3_options(&mut field.attrs) { - Ok(options) => Some((&*field, options)), - Err(e) => { - all_errors.combine(e); - None - } + Ok(options) => Ok((&*field, options)), + Err(e) => Err(e), }, ) .collect::>(), syn::Fields::Unit => { + let mut results = Vec::new(); + if let Some(attr) = args.options.set_all { - return Err(syn::Error::new_spanned(attr, UNIT_SET)); + results.push(Err(syn::Error::new_spanned(attr, UNIT_SET))); }; if let Some(attr) = args.options.get_all { - return Err(syn::Error::new_spanned(attr, UNIT_GET)); + results.push(Err(syn::Error::new_spanned(attr, UNIT_GET))); }; - // No fields for unit struct - Vec::new() - } - }; - all_errors.ensure_empty()?; + results + } + } + .into_iter() + .try_combine_syn_errors()?; if let Some(attr) = args.options.get_all { for (_, FieldPyO3Options { get, .. }) in &mut field_options { diff --git a/pyo3-macros-backend/src/pyfunction.rs b/pyo3-macros-backend/src/pyfunction.rs index 46f68f6ce28..5731cb8f510 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -1,4 +1,5 @@ use crate::attributes::KeywordAttribute; +use crate::combine_errors::CombineErrors; #[cfg(feature = "experimental-inspect")] use crate::introspection::{function_introspection_code, introspection_id_const}; use crate::utils::{Ctx, LitCStr}; @@ -372,7 +373,7 @@ pub fn impl_wrap_pyfunction( 0 }) .map(FnArg::parse) - .collect::>>()?; + .try_combine_syn_errors()?; let signature = if let Some(signature) = signature { FunctionSignature::from_arguments_and_attribute(arguments, signature)? diff --git a/pyo3-macros-backend/src/pyimpl.rs b/pyo3-macros-backend/src/pyimpl.rs index 575cd079ef4..6c111774ced 100644 --- a/pyo3-macros-backend/src/pyimpl.rs +++ b/pyo3-macros-backend/src/pyimpl.rs @@ -1,5 +1,6 @@ use std::collections::HashSet; +use crate::combine_errors::CombineErrors; #[cfg(feature = "experimental-inspect")] use crate::introspection::function_introspection_code; #[cfg(feature = "experimental-inspect")] @@ -122,73 +123,78 @@ pub fn impl_methods( let mut implemented_proto_fragments = HashSet::new(); - 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()); - - check_pyfunction(&ctx.pyo3_path, meth)?; - let method = PyMethod::parse(&mut meth.sig, &mut meth.attrs, fun_options)?; - #[cfg(feature = "experimental-inspect")] - extra_fragments.push(method_introspection_code(&method.spec, ty, ctx)); - match pymethod::gen_py_method(ty, method, &meth.attrs, ctx)? { - GeneratedPyMethod::Method(MethodAndMethodDef { - associated_method, - method_def, - }) => { - let attrs = get_cfg_attributes(&meth.attrs); - associated_methods.push(quote!(#(#attrs)* #associated_method)); - methods.push(quote!(#(#attrs)* #method_def)); - } - GeneratedPyMethod::SlotTraitImpl(method_name, token_stream) => { - implemented_proto_fragments.insert(method_name); - let attrs = get_cfg_attributes(&meth.attrs); - extra_fragments.push(quote!(#(#attrs)* #token_stream)); - } - GeneratedPyMethod::Proto(MethodAndSlotDef { - associated_method, - slot_def, - }) => { - let attrs = get_cfg_attributes(&meth.attrs); - proto_impls.push(quote!(#(#attrs)* #slot_def)); - associated_methods.push(quote!(#(#attrs)* #associated_method)); + let _: Vec<()> = impls + .iter_mut() + .map(|iimpl| { + 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()); + + check_pyfunction(&ctx.pyo3_path, meth)?; + let method = PyMethod::parse(&mut meth.sig, &mut meth.attrs, fun_options)?; + #[cfg(feature = "experimental-inspect")] + extra_fragments.push(method_introspection_code(&method.spec, ty, ctx)); + match pymethod::gen_py_method(ty, method, &meth.attrs, ctx)? { + GeneratedPyMethod::Method(MethodAndMethodDef { + associated_method, + method_def, + }) => { + let attrs = get_cfg_attributes(&meth.attrs); + associated_methods.push(quote!(#(#attrs)* #associated_method)); + methods.push(quote!(#(#attrs)* #method_def)); + } + GeneratedPyMethod::SlotTraitImpl(method_name, token_stream) => { + implemented_proto_fragments.insert(method_name); + let attrs = get_cfg_attributes(&meth.attrs); + extra_fragments.push(quote!(#(#attrs)* #token_stream)); + } + GeneratedPyMethod::Proto(MethodAndSlotDef { + associated_method, + slot_def, + }) => { + let attrs = get_cfg_attributes(&meth.attrs); + proto_impls.push(quote!(#(#attrs)* #slot_def)); + associated_methods.push(quote!(#(#attrs)* #associated_method)); + } } } - } - syn::ImplItem::Const(konst) => { - let ctx = &Ctx::new(&options.krate, None); - let attributes = ConstAttributes::from_attrs(&mut konst.attrs)?; - if attributes.is_class_attr { - let spec = ConstSpec { - rust_ident: konst.ident.clone(), - attributes, - }; - let attrs = get_cfg_attributes(&konst.attrs); - let MethodAndMethodDef { - associated_method, - method_def, - } = gen_py_const(ty, &spec, ctx); - methods.push(quote!(#(#attrs)* #method_def)); - associated_methods.push(quote!(#(#attrs)* #associated_method)); - if is_proto_method(&spec.python_name().to_string()) { - // If this is a known protocol method e.g. __contains__, then allow this - // symbol even though it's not an uppercase constant. - konst - .attrs - .push(syn::parse_quote!(#[allow(non_upper_case_globals)])); + syn::ImplItem::Const(konst) => { + let ctx = &Ctx::new(&options.krate, None); + let attributes = ConstAttributes::from_attrs(&mut konst.attrs)?; + if attributes.is_class_attr { + let spec = ConstSpec { + rust_ident: konst.ident.clone(), + attributes, + }; + let attrs = get_cfg_attributes(&konst.attrs); + let MethodAndMethodDef { + associated_method, + method_def, + } = gen_py_const(ty, &spec, ctx); + methods.push(quote!(#(#attrs)* #method_def)); + associated_methods.push(quote!(#(#attrs)* #associated_method)); + if is_proto_method(&spec.python_name().to_string()) { + // If this is a known protocol method e.g. __contains__, then allow this + // symbol even though it's not an uppercase constant. + konst + .attrs + .push(syn::parse_quote!(#[allow(non_upper_case_globals)])); + } } } + syn::ImplItem::Macro(m) => bail_spanned!( + m.span() => + "macros cannot be used as items in `#[pymethods]` impl blocks\n\ + = note: this was previously accepted and ignored" + ), + _ => {} } - syn::ImplItem::Macro(m) => bail_spanned!( - m.span() => - "macros cannot be used as items in `#[pymethods]` impl blocks\n\ - = note: this was previously accepted and ignored" - ), - _ => {} - } - } + Ok(()) + }) + .try_combine_syn_errors()?; + let ctx = &Ctx::new(&options.krate, None); add_shared_proto_slots(ty, &mut proto_impls, implemented_proto_fragments, ctx); diff --git a/tests/test_pyfunction.rs b/tests/test_pyfunction.rs index 39790e34652..8411017e574 100644 --- a/tests/test_pyfunction.rs +++ b/tests/test_pyfunction.rs @@ -591,6 +591,7 @@ fn test_return_value_borrows_from_arguments() { #[test] fn test_some_wrap_arguments() { // https://github.com/PyO3/pyo3/issues/3460 + #[allow(unused)] const NONE: Option = None; #[pyfunction(signature = (a = 1, b = Some(2), c = None, d = NONE))] fn some_wrap_arguments( diff --git a/tests/ui/invalid_async.stderr b/tests/ui/invalid_async.stderr index 5573cdf388a..0bf9f53f8c3 100644 --- a/tests/ui/invalid_async.stderr +++ b/tests/ui/invalid_async.stderr @@ -9,3 +9,9 @@ error: async functions are only supported with the `experimental-async` feature | 13 | async fn __anext__(mut _pyself: PyRefMut<'_, Self>) -> PyResult { | ^^^^^ + +error: async functions are only supported with the `experimental-async` feature + --> tests/ui/invalid_async.rs:17:5 + | +17 | async fn foo(&self) {} + | ^^^^^ diff --git a/tests/ui/invalid_pyfunction_signatures.rs b/tests/ui/invalid_pyfunction_signatures.rs index 86033aa12ea..d6799885cbb 100644 --- a/tests/ui/invalid_pyfunction_signatures.rs +++ b/tests/ui/invalid_pyfunction_signatures.rs @@ -55,9 +55,17 @@ struct MyClass; #[pymethods] impl MyClass { - #[args(x)] - #[pyo3(signature = (x))] - fn method_with_both_args_and_signature(&self, x: i32) { + #[pyo3(signature = (**kwargs, *, *args, x))] + fn multiple_errors_same_order(kwargs: Option<&PyDict>, args: &PyTuple, x: i32) { + let _ = kwargs; + let _ = args; + let _ = x; + } + + #[pyo3(signature = (**kwargs, *, *args, x))] + fn multiple_errors_different_order(args: &PyTuple, x: i32, kwargs: Option<&PyDict>) { + let _ = kwargs; + let _ = args; let _ = x; } } diff --git a/tests/ui/invalid_pyfunction_signatures.stderr b/tests/ui/invalid_pyfunction_signatures.stderr index 60498ebd931..c7ddb11dd37 100644 --- a/tests/ui/invalid_pyfunction_signatures.stderr +++ b/tests/ui/invalid_pyfunction_signatures.stderr @@ -52,8 +52,14 @@ error: arguments of type `Python` must not be part of the signature 48 | #[pyfunction(signature = (py))] | ^^ -error: cannot find attribute `args` in this scope - --> tests/ui/invalid_pyfunction_signatures.rs:58:7 +error: expected argument from function definition `args` but got argument `kwargs` + --> tests/ui/invalid_pyfunction_signatures.rs:58:27 | -58 | #[args(x)] - | ^^^^ +58 | #[pyo3(signature = (**kwargs, *, *args, x))] + | ^^^^^^ + +error: expected argument from function definition `x` but got argument `kwargs` + --> tests/ui/invalid_pyfunction_signatures.rs:65:27 + | +65 | #[pyo3(signature = (**kwargs, *, *args, x))] + | ^^^^^^ diff --git a/tests/ui/invalid_pymethods.rs b/tests/ui/invalid_pymethods.rs index 75ad12c26d2..372490ca580 100644 --- a/tests/ui/invalid_pymethods.rs +++ b/tests/ui/invalid_pymethods.rs @@ -7,33 +7,18 @@ struct MyClass {} impl MyClass { #[classattr] fn class_attr_with_args(_foo: i32) {} -} -#[pymethods] -impl MyClass { #[classattr(foobar)] const CLASS_ATTR_WITH_ATTRIBUTE_ARG: i32 = 3; -} -#[pymethods] -impl MyClass { fn staticmethod_without_attribute() {} -} -#[pymethods] -impl MyClass { #[staticmethod] fn staticmethod_with_receiver(&self) {} -} -#[pymethods] -impl MyClass { #[classmethod] fn classmethod_with_receiver(&self) {} -} -#[pymethods] -impl MyClass { #[classmethod] fn classmethod_missing_argument() -> Self { Self {} @@ -66,37 +51,22 @@ impl MyClass { impl MyClass { #[pyo3(name = "__call__", text_signature = "()")] fn text_signature_on_call() {} -} -#[pymethods] -impl MyClass { #[getter(x)] #[pyo3(text_signature = "()")] fn text_signature_on_getter(&self) {} -} -#[pymethods] -impl MyClass { #[setter(x)] #[pyo3(text_signature = "()")] fn text_signature_on_setter(&self) {} -} -#[pymethods] -impl MyClass { #[classattr] #[pyo3(text_signature = "()")] fn text_signature_on_classattr() {} -} -#[pymethods] -impl MyClass { #[pyo3(text_signature = 1)] fn invalid_text_signature() {} -} -#[pymethods] -impl MyClass { #[pyo3(text_signature = "()")] #[pyo3(text_signature = None)] fn duplicate_text_signature() {} @@ -107,17 +77,11 @@ impl MyClass { #[getter(x)] #[pyo3(signature = ())] fn signature_on_getter(&self) {} -} -#[pymethods] -impl MyClass { #[setter(x)] #[pyo3(signature = ())] fn signature_on_setter(&self) {} -} -#[pymethods] -impl MyClass { #[classattr] #[pyo3(signature = ())] fn signature_on_classattr() {} @@ -172,10 +136,7 @@ impl MyClass { #[pymethods] impl MyClass { fn impl_trait_method_first_arg(_impl_trait: impl AsRef) {} -} -#[pymethods] -impl MyClass { fn impl_trait_method_second_arg(&self, _impl_trait: impl AsRef) {} } @@ -199,4 +160,17 @@ impl MyClass { macro_invocation!(); } +#[pymethods] +impl MyClass { + #[staticmethod] + #[classmethod] + fn multiple_errors_static_and_class_method() {} + + #[staticmethod] + fn multiple_errors_staticmethod_with_receiver(&self) {} + + #[classmethod] + fn multiple_errors_classmethod_with_receiver(&self) {} +} + fn main() {} diff --git a/tests/ui/invalid_pymethods.stderr b/tests/ui/invalid_pymethods.stderr index 2b6e195ab7d..34cd22992e5 100644 --- a/tests/ui/invalid_pymethods.stderr +++ b/tests/ui/invalid_pymethods.stderr @@ -5,184 +5,202 @@ error: #[classattr] can only have one argument (of type pyo3::Python) | ^^^ error: `#[classattr]` does not take any arguments - --> tests/ui/invalid_pymethods.rs:14:5 + --> tests/ui/invalid_pymethods.rs:11:5 | -14 | #[classattr(foobar)] +11 | #[classattr(foobar)] | ^ error: static method needs #[staticmethod] attribute - --> tests/ui/invalid_pymethods.rs:20:5 + --> tests/ui/invalid_pymethods.rs:14:5 | -20 | fn staticmethod_without_attribute() {} +14 | fn staticmethod_without_attribute() {} | ^^ error: unexpected receiver - --> tests/ui/invalid_pymethods.rs:26:35 + --> tests/ui/invalid_pymethods.rs:17:35 | -26 | fn staticmethod_with_receiver(&self) {} +17 | fn staticmethod_with_receiver(&self) {} | ^ error: Expected `&Bound` or `Py` as the first argument to `#[classmethod]` - --> tests/ui/invalid_pymethods.rs:32:33 + --> tests/ui/invalid_pymethods.rs:20:33 | -32 | fn classmethod_with_receiver(&self) {} +20 | fn classmethod_with_receiver(&self) {} | ^^^^^^^ error: Expected `&Bound` or `Py` as the first argument to `#[classmethod]` - --> tests/ui/invalid_pymethods.rs:38:36 + --> tests/ui/invalid_pymethods.rs:23:36 | -38 | fn classmethod_missing_argument() -> Self { +23 | fn classmethod_missing_argument() -> Self { | ^^ error: expected receiver for `#[getter]` - --> tests/ui/invalid_pymethods.rs:56:5 + --> tests/ui/invalid_pymethods.rs:41:5 | -56 | fn getter_without_receiver() {} +41 | fn getter_without_receiver() {} | ^^ error: expected receiver for `#[setter]` - --> tests/ui/invalid_pymethods.rs:62:5 + --> tests/ui/invalid_pymethods.rs:47:5 | -62 | fn setter_without_receiver() {} +47 | fn setter_without_receiver() {} | ^^ error: static method needs #[staticmethod] attribute - --> tests/ui/invalid_pymethods.rs:68:5 + --> tests/ui/invalid_pymethods.rs:53:5 | -68 | fn text_signature_on_call() {} +53 | fn text_signature_on_call() {} | ^^ error: `text_signature` not allowed with `getter` - --> tests/ui/invalid_pymethods.rs:74:12 + --> tests/ui/invalid_pymethods.rs:56:12 | -74 | #[pyo3(text_signature = "()")] +56 | #[pyo3(text_signature = "()")] | ^^^^^^^^^^^^^^ error: `text_signature` not allowed with `setter` - --> tests/ui/invalid_pymethods.rs:81:12 + --> tests/ui/invalid_pymethods.rs:60:12 | -81 | #[pyo3(text_signature = "()")] +60 | #[pyo3(text_signature = "()")] | ^^^^^^^^^^^^^^ error: `text_signature` not allowed with `classattr` - --> tests/ui/invalid_pymethods.rs:88:12 + --> tests/ui/invalid_pymethods.rs:64:12 | -88 | #[pyo3(text_signature = "()")] +64 | #[pyo3(text_signature = "()")] | ^^^^^^^^^^^^^^ error: expected a string literal or `None` - --> tests/ui/invalid_pymethods.rs:94:30 + --> tests/ui/invalid_pymethods.rs:67:30 | -94 | #[pyo3(text_signature = 1)] +67 | #[pyo3(text_signature = 1)] | ^ error: `text_signature` may only be specified once - --> tests/ui/invalid_pymethods.rs:101:12 - | -101 | #[pyo3(text_signature = None)] - | ^^^^^^^^^^^^^^ + --> tests/ui/invalid_pymethods.rs:71:12 + | +71 | #[pyo3(text_signature = None)] + | ^^^^^^^^^^^^^^ error: `signature` not allowed with `getter` - --> tests/ui/invalid_pymethods.rs:108:12 - | -108 | #[pyo3(signature = ())] - | ^^^^^^^^^ + --> tests/ui/invalid_pymethods.rs:78:12 + | +78 | #[pyo3(signature = ())] + | ^^^^^^^^^ error: `signature` not allowed with `setter` - --> tests/ui/invalid_pymethods.rs:115:12 - | -115 | #[pyo3(signature = ())] - | ^^^^^^^^^ + --> tests/ui/invalid_pymethods.rs:82:12 + | +82 | #[pyo3(signature = ())] + | ^^^^^^^^^ error: `signature` not allowed with `classattr` - --> tests/ui/invalid_pymethods.rs:122:12 - | -122 | #[pyo3(signature = ())] - | ^^^^^^^^^ + --> tests/ui/invalid_pymethods.rs:86:12 + | +86 | #[pyo3(signature = ())] + | ^^^^^^^^^ error: `#[new]` may not be combined with `#[classmethod]` `#[staticmethod]`, `#[classattr]`, `#[getter]`, and `#[setter]` - --> tests/ui/invalid_pymethods.rs:128:7 - | -128 | #[new] - | ^^^ + --> tests/ui/invalid_pymethods.rs:92:7 + | +92 | #[new] + | ^^^ error: `#[new]` does not take any arguments = help: did you mean `#[new] #[pyo3(signature = ())]`? - --> tests/ui/invalid_pymethods.rs:139:7 + --> tests/ui/invalid_pymethods.rs:103:7 | -139 | #[new(signature = ())] +103 | #[new(signature = ())] | ^^^ error: `#[new]` does not take any arguments = note: this was previously accepted and ignored - --> tests/ui/invalid_pymethods.rs:145:11 + --> tests/ui/invalid_pymethods.rs:109:11 | -145 | #[new = ()] // in this form there's no suggestion to move arguments to `#[pyo3()]` attribute +109 | #[new = ()] // in this form there's no suggestion to move arguments to `#[pyo3()]` attribute | ^ error: `#[classmethod]` does not take any arguments = help: did you mean `#[classmethod] #[pyo3(signature = ())]`? - --> tests/ui/invalid_pymethods.rs:151:7 + --> tests/ui/invalid_pymethods.rs:115:7 | -151 | #[classmethod(signature = ())] +115 | #[classmethod(signature = ())] | ^^^^^^^^^^^ error: `#[staticmethod]` does not take any arguments = help: did you mean `#[staticmethod] #[pyo3(signature = ())]`? - --> tests/ui/invalid_pymethods.rs:157:7 + --> tests/ui/invalid_pymethods.rs:121:7 | -157 | #[staticmethod(signature = ())] +121 | #[staticmethod(signature = ())] | ^^^^^^^^^^^^ error: `#[classattr]` does not take any arguments = help: did you mean `#[classattr] #[pyo3(signature = ())]`? - --> tests/ui/invalid_pymethods.rs:163:7 + --> tests/ui/invalid_pymethods.rs:127:7 | -163 | #[classattr(signature = ())] +127 | #[classattr(signature = ())] | ^^^^^^^^^ error: Python functions cannot have generic type parameters - --> tests/ui/invalid_pymethods.rs:169:23 + --> tests/ui/invalid_pymethods.rs:133:23 | -169 | fn generic_method(_value: T) {} +133 | fn generic_method(_value: T) {} | ^ error: Python functions cannot have `impl Trait` arguments - --> tests/ui/invalid_pymethods.rs:174:49 + --> tests/ui/invalid_pymethods.rs:138:49 | -174 | fn impl_trait_method_first_arg(_impl_trait: impl AsRef) {} +138 | fn impl_trait_method_first_arg(_impl_trait: impl AsRef) {} | ^^^^ error: Python functions cannot have `impl Trait` arguments - --> tests/ui/invalid_pymethods.rs:179:57 + --> tests/ui/invalid_pymethods.rs:140:57 | -179 | fn impl_trait_method_second_arg(&self, _impl_trait: impl AsRef) {} +140 | 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:184:12 + --> tests/ui/invalid_pymethods.rs:145:12 | -184 | #[pyo3(pass_module)] +145 | #[pyo3(pass_module)] | ^^^^^^^^^^^ error: Python objects are shared, so 'self' cannot be moved out of the Python interpreter. Try `&self`, `&mut self, `slf: PyRef<'_, Self>` or `slf: PyRefMut<'_, Self>`. - --> tests/ui/invalid_pymethods.rs:190:29 + --> tests/ui/invalid_pymethods.rs:151:29 | -190 | fn method_self_by_value(self) {} +151 | fn method_self_by_value(self) {} | ^^^^ error: macros cannot be used as items in `#[pymethods]` impl blocks = note: this was previously accepted and ignored - --> tests/ui/invalid_pymethods.rs:199:5 + --> tests/ui/invalid_pymethods.rs:160:5 | -199 | macro_invocation!(); +160 | macro_invocation!(); | ^^^^^^^^^^^^^^^^ +error: `#[staticmethod]` may not be combined with `#[classmethod]` + --> tests/ui/invalid_pymethods.rs:165:7 + | +165 | #[staticmethod] + | ^^^^^^^^^^^^ + +error: unexpected receiver + --> tests/ui/invalid_pymethods.rs:170:51 + | +170 | fn multiple_errors_staticmethod_with_receiver(&self) {} + | ^ + +error: Expected `&Bound` or `Py` as the first argument to `#[classmethod]` + --> tests/ui/invalid_pymethods.rs:173:49 + | +173 | fn multiple_errors_classmethod_with_receiver(&self) {} + | ^^^^^^^ + error[E0277]: the trait bound `NotATypeObject: From>` is not satisfied - --> tests/ui/invalid_pymethods.rs:48:45 + --> tests/ui/invalid_pymethods.rs:33:45 | -48 | fn classmethod_wrong_first_argument(_t: NotATypeObject) -> Self { +33 | fn classmethod_wrong_first_argument(_t: NotATypeObject) -> Self { | ^^^^^^^^^^^^^^ the trait `From>` is not implemented for `NotATypeObject` | = note: required for `BoundRef<'_, '_, PyType>` to implement `Into` diff --git a/tests/ui/invalid_pymodule_args.rs b/tests/ui/invalid_pymodule_args.rs index 37e53960fd3..4a6e65de150 100644 --- a/tests/ui/invalid_pymodule_args.rs +++ b/tests/ui/invalid_pymodule_args.rs @@ -5,4 +5,12 @@ fn module(m: &Bound<'_, PyModule>) -> PyResult<()> { Ok(()) } -fn main(){} +#[pyo3::pymodule(gil_used = false, gil_used = true, name = "foo", name = "bar")] +fn module_fn_multiple_errors(m: &Bound<'_, PyModule>) -> PyResult<()> { + Ok(()) +} + +#[pyo3::pymodule(gil_used = false, gil_used = true, name = "foo", name = "bar")] +mod pyo3_module_multiple_errors {} + +fn main() {} diff --git a/tests/ui/invalid_pymodule_args.stderr b/tests/ui/invalid_pymodule_args.stderr index 23a5109b4cb..f741d83a82d 100644 --- a/tests/ui/invalid_pymodule_args.stderr +++ b/tests/ui/invalid_pymodule_args.stderr @@ -3,3 +3,27 @@ error: expected one of: `name`, `crate`, `module`, `submodule`, `gil_used` | 3 | #[pymodule(some_arg)] | ^^^^^^^^ + +error: `gil_used` may only be specified once + --> tests/ui/invalid_pymodule_args.rs:8:36 + | +8 | #[pyo3::pymodule(gil_used = false, gil_used = true, name = "foo", name = "bar")] + | ^^^^^^^^ + +error: `name` may only be specified once + --> tests/ui/invalid_pymodule_args.rs:8:67 + | +8 | #[pyo3::pymodule(gil_used = false, gil_used = true, name = "foo", name = "bar")] + | ^^^^ + +error: `gil_used` may only be specified once + --> tests/ui/invalid_pymodule_args.rs:13:36 + | +13 | #[pyo3::pymodule(gil_used = false, gil_used = true, name = "foo", name = "bar")] + | ^^^^^^^^ + +error: `name` may only be specified once + --> tests/ui/invalid_pymodule_args.rs:13:67 + | +13 | #[pyo3::pymodule(gil_used = false, gil_used = true, name = "foo", name = "bar")] + | ^^^^ From da6dc9b2b6813bdfc80d3962e3be6f6a35d450d9 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sat, 26 Jul 2025 18:44:52 +0100 Subject: [PATCH 754/936] more fixes to netlify deploy (#5266) --- .github/workflows/netlify-build.yml | 4 ++-- .github/workflows/netlify-deploy.yml | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/netlify-build.yml b/.github/workflows/netlify-build.yml index bc603172d77..11f4e00fb11 100644 --- a/.github/workflows/netlify-build.yml +++ b/.github/workflows/netlify-build.yml @@ -75,8 +75,8 @@ jobs: id: pr run: | mkdir -p metadata - echo "pr_number=$(echo ${{ github.event.pull_request.number }})" >> metadata/pr_number - echo "pr_head_sha=$(echo ${{ github.event.pull_request.head.sha }})" >> metadata/pr_head_sha + echo "${{ github.event.pull_request.number }}" >> metadata/pr_number + echo "${{ github.event.pull_request.head.sha }}" >> metadata/pr_head_sha - name: Upload PR metadata uses: actions/upload-artifact@v4 diff --git a/.github/workflows/netlify-deploy.yml b/.github/workflows/netlify-deploy.yml index 237a45f3237..fb2a2b724fe 100644 --- a/.github/workflows/netlify-deploy.yml +++ b/.github/workflows/netlify-deploy.yml @@ -36,6 +36,9 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} run-id: ${{ github.event.workflow_run.id }} + - name: Debug head branch + run: echo ${{ github.event.workflow_run.head_branch }} + - name: Get PR metadata id: pr_metadata run: | @@ -52,7 +55,7 @@ jobs: DEBUG=* netlify deploy \ --site ${{ secrets.NETLIFY_SITE_ID }} \ --auth ${{ secrets.NETLIFY_TOKEN }} \ - ${{ steps.pr_metadata.outputs.pr_number == '' && '--prod' || '' }} + ${{ steps.pr_metadata.outputs.pr_number == '' && '--prod' || '' }} \ --json | tee deploy_output.json # credit: https://www.raulmelo.me/en/blog/deploying-netlify-github-actions-guide From de0bbeea7d0f383b3617b8567435a542523d8c4b Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sat, 26 Jul 2025 22:09:14 +0100 Subject: [PATCH 755/936] docs: improve note on `PyTuple::new` (#5267) --- src/types/tuple.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/types/tuple.rs b/src/types/tuple.rs index 6b7deadb6c9..21667159f91 100644 --- a/src/types/tuple.rs +++ b/src/types/tuple.rs @@ -63,9 +63,12 @@ pyobject_native_type_core!(PyTuple, pyobject_native_static_type_object!(ffi::PyT impl PyTuple { /// 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 - /// iterable that doesn't implement [`ExactSizeIterator`], create a Rust tuple with the given - /// elements and convert it at once using `into_py`. + /// If you want to create a [`PyTuple`] with elements of different or unknown types, create a Rust + /// tuple with the given elements and convert it at once using [`into_pyobject()`][crate::IntoPyObject]. + /// (`IntoPyObject` is implemented for tuples of up to 12 elements.) + /// + /// To create a [`PyTuple`] from an iterable that doesn't implement [`ExactSizeIterator`], + /// collect the elements into a `Vec` first. /// /// # Examples /// @@ -78,6 +81,10 @@ impl PyTuple { /// let elements: Vec = vec![0, 1, 2, 3, 4, 5]; /// let tuple = PyTuple::new(py, elements)?; /// assert_eq!(format!("{:?}", tuple), "(0, 1, 2, 3, 4, 5)"); + /// + /// // alternative using `into_pyobject()` + /// let tuple = (0, "hello", true).into_pyobject(py)?; + /// assert_eq!(format!("{:?}", tuple), "(0, 'hello', True)"); /// # Ok(()) /// }) /// # } From 51ab0a1b4ba266394541400e7ffdb0a563e61c4b Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Mon, 28 Jul 2025 10:19:17 +0100 Subject: [PATCH 756/936] docs: fix redirect on prod site (#5274) --- .github/workflows/netlify-build.yml | 2 +- .github/workflows/netlify-deploy.yml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/netlify-build.yml b/.github/workflows/netlify-build.yml index 11f4e00fb11..cb24c5fda3e 100644 --- a/.github/workflows/netlify-build.yml +++ b/.github/workflows/netlify-build.yml @@ -61,7 +61,7 @@ jobs: - name: Prepare the versioned guide entries run: | python -m pip install --upgrade pip && pip install nox towncrier requests - nox -s build-netlify-site -- ${{ github.ref == 'refs/heads/main' && '' || '--preview' }} + nox -s build-netlify-site -- ${{ (github.ref != 'refs/heads/main' && '--preview') || '' }} # Upload the built site as an artifact for deploy workflow to consume - name: Upload Build Artifact diff --git a/.github/workflows/netlify-deploy.yml b/.github/workflows/netlify-deploy.yml index fb2a2b724fe..84ac507abcb 100644 --- a/.github/workflows/netlify-deploy.yml +++ b/.github/workflows/netlify-deploy.yml @@ -36,8 +36,8 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} run-id: ${{ github.event.workflow_run.id }} - - name: Debug head branch - run: echo ${{ github.event.workflow_run.head_branch }} + - name: Debug workflow run + run: echo ${{ toJSON(github.event.workflow_run) }} - name: Get PR metadata id: pr_metadata @@ -82,6 +82,6 @@ jobs: sha: commitSha, state: 'success', target_url: previewUrl, - description: 'Netlify Preview', - context: 'netlify/preview' + description: 'click to view Netlify preview deploy', + context: 'netlify-deploy / preview' }); From 22756697b5a78117125d9abefc3403072b68eb8b Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Mon, 28 Jul 2025 13:43:04 +0100 Subject: [PATCH 757/936] ci: make deploy prod detection simpler & more robust (#5275) * ci: remove debug print from netlify deploy * make deploy prod detection simpler & more robust --- .github/workflows/netlify-build.yml | 14 -------------- .github/workflows/netlify-deploy.yml | 18 ++++-------------- 2 files changed, 4 insertions(+), 28 deletions(-) diff --git a/.github/workflows/netlify-build.yml b/.github/workflows/netlify-build.yml index cb24c5fda3e..fa815fc10e0 100644 --- a/.github/workflows/netlify-build.yml +++ b/.github/workflows/netlify-build.yml @@ -69,17 +69,3 @@ jobs: with: name: site path: ./netlify_build - - # Upload metadata for use in deploy workflow, will be empty if not a PR - - name: Prepare metadata for upload - id: pr - run: | - mkdir -p metadata - echo "${{ github.event.pull_request.number }}" >> metadata/pr_number - echo "${{ github.event.pull_request.head.sha }}" >> metadata/pr_head_sha - - - name: Upload PR metadata - uses: actions/upload-artifact@v4 - with: - name: metadata - path: metadata diff --git a/.github/workflows/netlify-deploy.yml b/.github/workflows/netlify-deploy.yml index 84ac507abcb..e01188eb8b2 100644 --- a/.github/workflows/netlify-deploy.yml +++ b/.github/workflows/netlify-deploy.yml @@ -36,15 +36,6 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} run-id: ${{ github.event.workflow_run.id }} - - name: Debug workflow run - run: echo ${{ toJSON(github.event.workflow_run) }} - - - name: Get PR metadata - id: pr_metadata - run: | - echo "pr_number=$(cat .metadata/pr_number)" >> $GITHUB_OUTPUT - echo "pr_head_sha=$(cat .metadata/pr_head_sha)" >> $GITHUB_OUTPUT - - name: Install netlify-cli run: | npm install -g netlify-cli @@ -55,26 +46,25 @@ jobs: DEBUG=* netlify deploy \ --site ${{ secrets.NETLIFY_SITE_ID }} \ --auth ${{ secrets.NETLIFY_TOKEN }} \ - ${{ steps.pr_metadata.outputs.pr_number == '' && '--prod' || '' }} \ + ${{ ((github.event.workflow_run.head_repository.full_name == 'PyO3/pyo3') && (github.event.workflow_run.head_branch == 'main') && '--prod') || '' }} \ --json | tee deploy_output.json # credit: https://www.raulmelo.me/en/blog/deploying-netlify-github-actions-guide - name: Generate URL Preview id: url_preview - if: ${{ steps.pr_metadata.outputs.pr_head_sha != '' }} + if: ${{ github.event.workflow_run.event == 'pull_request' }} run: | NETLIFY_PREVIEW_URL=$(jq -r '.deploy_url' deploy_output.json) echo "NETLIFY_PREVIEW_URL=$NETLIFY_PREVIEW_URL" >> "$GITHUB_OUTPUT" - name: Post Netlify Preview Status to PR - if: ${{ steps.pr_metadata.outputs.pr_head_sha != '' }} + if: ${{ github.event.workflow_run.event == 'pull_request' }} uses: actions/github-script@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - const prNumber = Number('${{ steps.pr_metadata.outputs.pr_number }}'); const previewUrl = '${{ steps.url_preview.outputs.NETLIFY_PREVIEW_URL }}'; - const commitSha = '${{ steps.pr_metadata.outputs.pr_head_sha }}'; + const commitSha = '${{ github.event.workflow_run.head_sha }}'; await github.rest.repos.createCommitStatus({ owner: context.repo.owner, From 076617135b34e14775510ab87ec74621b3d30213 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Mon, 28 Jul 2025 17:06:20 +0100 Subject: [PATCH 758/936] ci: don't need metadata for netlify deploy (#5276) --- .github/workflows/netlify-deploy.yml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.github/workflows/netlify-deploy.yml b/.github/workflows/netlify-deploy.yml index e01188eb8b2..27141d16f90 100644 --- a/.github/workflows/netlify-deploy.yml +++ b/.github/workflows/netlify-deploy.yml @@ -28,14 +28,6 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} run-id: ${{ github.event.workflow_run.id }} - - name: Download PR Metadata - uses: actions/download-artifact@v4 - with: - name: metadata - path: .metadata - github-token: ${{ secrets.GITHUB_TOKEN }} - run-id: ${{ github.event.workflow_run.id }} - - name: Install netlify-cli run: | npm install -g netlify-cli From 70e417bd7c9c547092d5f2494399decdfa45d01a Mon Sep 17 00:00:00 2001 From: Thomas Tanon Date: Tue, 29 Jul 2025 10:46:58 +0200 Subject: [PATCH 759/936] Introspection: Adds a guide page on stubs (#5255) * Introspection: Adds a guide page on stubs * Talk about type stubs in signature.md * Add links between type-stub.md and python-typing-hints.md * Fixes CI --- guide/src/SUMMARY.md | 1 + guide/src/function/signature.md | 31 +++++++++++++ guide/src/python-typing-hints.md | 2 +- guide/src/type-stub.md | 79 ++++++++++++++++++++++++++++++++ newsfragments/5255.added.md | 1 + 5 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 guide/src/type-stub.md create mode 100644 newsfragments/5255.added.md diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md index cf987b72625..2f863231984 100644 --- a/guide/src/SUMMARY.md +++ b/guide/src/SUMMARY.md @@ -30,6 +30,7 @@ - [Debugging](debugging.md) - [Features reference](features.md) - [Performance](performance.md) +- [Type stub generation and introspection](type-stub.md) - [Advanced topics](advanced.md) - [Building and distribution](building-and-distribution.md) - [Supporting multiple Python versions](building-and-distribution/multiple-python-versions.md) diff --git a/guide/src/function/signature.md b/guide/src/function/signature.md index 86fa1570672..ced323a5c9a 100644 --- a/guide/src/function/signature.md +++ b/guide/src/function/signature.md @@ -248,3 +248,34 @@ True Docstring: This function adds two unsigned 64-bit integers. Type: builtin_function_or_method ``` + +### Type annotations in the signature + +When the `experimental-inspect` Cargo feature is enabled, the `signature` attribute can also contain type hints: +```rust +use pyo3::prelude::*; + +#[pymodule] +pub mod example { + use pyo3::prelude::*; + + #[pyfunction] + #[pyo3(signature = (arg: "list[int]") -> "list[int]")] + fn list_of_int_identity(arg: Bound<'_, PyAny>) -> Bound<'_, PyAny> { + arg + } +} +``` + +It enables the [work-in-progress capacity of PyO3 to autogenerate type stubs](../type-stub.md) to generate a file with the correct type hints: +```python +def list_of_int_identity(arg: list[int]) -> list[int]: ... +``` +instead of the generic: +```python +import typing + +def list_of_int_identity(arg: typing.Any) -> typing.Any: ... +``` + +Note that currently type annotations must be written as Rust strings. diff --git a/guide/src/python-typing-hints.md b/guide/src/python-typing-hints.md index eb39bb35fb0..76ad0b6aa21 100644 --- a/guide/src/python-typing-hints.md +++ b/guide/src/python-typing-hints.md @@ -4,7 +4,7 @@ PyO3 provides an easy to use interface to code native Python libraries in Rust. Currently the best solution for the problem is to manually maintain `*.pyi` files and ship them along with the package. -There is a sketch of a roadmap towards completing [the `experimental-inspect` feature](./features.md#experimental-inspect) which may eventually lead to automatic type annotations generated by PyO3. This needs more testing and implementation, please see [issue #2454](https://github.com/PyO3/pyo3/issues/2454). +PyO3 is working on automated their generation. See the [type stub generation](type-stub.md) documentation for a description of the current state of automated generation. ## Introduction to `pyi` files diff --git a/guide/src/type-stub.md b/guide/src/type-stub.md new file mode 100644 index 00000000000..913791fb286 --- /dev/null +++ b/guide/src/type-stub.md @@ -0,0 +1,79 @@ +# Type stub generation (`*.pyi` files) and introspection + +*This feature is still in active development. See [the related issue](https://github.com/PyO3/pyo3/issues/5137).* + +*For documentation on type stubs and how to use them with stable PyO3, refer to [this page](python-typing-hints.md)* + +PyO3 has a work in progress support to generate [type stub files](https://typing.python.org/en/latest/spec/distributing.html#stub-files). + +It works using: +1. PyO3 macros (`#[pyclass]`) that generate constant JSON strings that are then included in the built binaries by rustc if the `experimental-inspect` feature is enabled. +2. The `pyo3-introspection` crate that can parse the generated binaries, extract the JSON strings and build stub files from it. +3. [Not done yet] Build tools like `maturin` exposing `pyo3-introspection` features in their CLI API. + +For example, the following Rust code +```rust +#[pymodule] +pub mod example { + use pyo3::prelude::*; + + #[pymodule_export] + pub const CONSTANT: &str = "FOO"; + + #[pyclass(eq)] + #[derive(Eq)] + struct Class { + value: usize + } + + #[pymethods] + impl Class { + #[new] + fn new(value: usize) -> Self { + Self { value } + } + + #[getter] + fn value(&self) -> usize { + self.value + } + } + + #[pyfunction] + #[pyo3(signature = (arg: "list[int]") -> "list[int]")] + fn list_of_int_identity(arg: Bound<'_, PyAny>) -> Bound<'_, PyAny> { + arg + } +} +``` +will generate the following stub file: +```python +import typing + +CONSTANT: typing.Final = "FOO" + +class Class: + def __init__(self, value: int) -> None: ... + + @property + def value(self) -> int: ... + + def __eq__(self, other: Class) -> bool: ... + def __ne__(self, other: Class) -> bool: ... + +def list_of_int_identity(arg: list[int]) -> list[int]: ... +``` + +The only piece of added syntax is that the `#[pyo3(signature = ...)]` attribute +can now contain type annotations like `#[pyo3(signature = (arg: "list[int]") -> "list[int]")]` +(note the `""` around type annotations). +This is useful when PyO3 is not able to derive proper type annotations by itself. + +## Constraints and limitations + +- The `experimental-inspect` feature is required to generate the introspection fragments. +- Lots of features are not implemented yet. See [the related issue](https://github.com/PyO3/pyo3/issues/5137) for a list of them. +- Introspection only works with Python modules declared with an inline Rust module. Modules declared using a function are not supported. +- `FromPyObject::INPUT_TYPE` and `IntoPyObject::OUTPUT_TYPE` must be implemented for PyO3 to get the proper input/output type annotations to use. +- Because `FromPyObject::INPUT_TYPE` and `IntoPyObject::OUTPUT_TYPE` are `const` it is not possible to build yet smart generic annotations for containers like `concat!("list[", T::OUTPUT_TYPE, "]")`. See [this tracking issue](https://github.com/rust-lang/rust/issues/76560). +- PyO3 is not able to introspect the content of `#[pymodule]` and `#[pymodule_init]` functions. If they are present, the module is tagged as incomplete using a fake `def __getattr__(name: str) -> Incomplete: ...` function [following best practices](https://typing.python.org/en/latest/guides/writing_stubs.html#incomplete-stubs). diff --git a/newsfragments/5255.added.md b/newsfragments/5255.added.md new file mode 100644 index 00000000000..d4dec2f63cf --- /dev/null +++ b/newsfragments/5255.added.md @@ -0,0 +1 @@ +Guide page on type stubs and introspection \ No newline at end of file From ee92789a790ea05bea0791dc86d23eef658cafe4 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Thu, 31 Jul 2025 22:28:44 +0200 Subject: [PATCH 760/936] ci: unpin Python version (#5280) --- .github/workflows/build.yml | 3 --- .github/workflows/ci.yml | 4 +--- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4b0ec314fd6..e99126fa6c5 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -110,9 +110,6 @@ jobs: - '.github/workflows/build.yml' - name: Run pyo3-ffi-check - # Python 3.13.4 has an issue on Windows where the headers are configured to always be free-threaded - # https://discuss.python.org/t/heads-up-3-13-5-release-coming-soon/94535 - continue-on-error: ${{ inputs.python-version == '3.13' && (inputs.os == 'windows-latest' || inputs.os == 'windows-11-arm') }} # 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 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a7a245350dd..b625f17573d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -159,9 +159,7 @@ jobs: components: clippy,rust-src - uses: actions/setup-python@v5 with: - # FIXME: 3.13.4 has an issue on windows so pinned to 3.13.3 - # once 3.13.5 is out, unpin the patch version - python-version: "3.13.3" + python-version: "3.13" architecture: ${{ matrix.platform.python-architecture }} - uses: Swatinem/rust-cache@v2 with: From 98fa8250c298d9fbdba896f26a9b429f94061d47 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 1 Aug 2025 10:04:42 +0100 Subject: [PATCH 761/936] ci: don't run `clippy` on `main`, stop caching (#5268) --- .github/workflows/ci.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b625f17573d..cf57ebd3751 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -84,6 +84,8 @@ jobs: CARGO_BUILD_TARGET: x86_64-unknown-linux-gnu clippy: + # Don't run clippy on `main` because it's already run in the merge queue. + if: github.ref != 'refs/heads/main' needs: [fmt] runs-on: ${{ matrix.platform.os }} strategy: @@ -161,9 +163,6 @@ jobs: with: python-version: "3.13" architecture: ${{ matrix.platform.python-architecture }} - - uses: Swatinem/rust-cache@v2 - with: - save-if: ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'CI-save-pr-cache') }} # windows on arm image contains x86-64 libclang - name: Install LLVM and Clang if: matrix.platform.os == 'windows-11-arm' From 16d039b43d4cdd4ac98fd2d4fc3928edb803c28d Mon Sep 17 00:00:00 2001 From: Olga Pustovalova <162949+olp-cs@users.noreply.github.com> Date: Fri, 1 Aug 2025 13:33:28 +0300 Subject: [PATCH 762/936] rename `pyo3::prepare_freethreaded_python` to `Python::initialize` (#5247) * Rename `pyo3::prepare_freethreaded_python` to `pyo3::initialize_python` * Rename `pyo3::prepare_freethreaded_python` to `pyo3::initialize_python` * Add newsfragment * Rename to `Python::initialize`; update migration docs * Fix Clippy errors for PyPy 3.9, 3.10, 3.11 when running `nox -s clippy-all` * Update after reviews * call local function instead --------- Co-authored-by: David Hewitt --- examples/plugin/src/main.rs | 2 +- guide/src/building-and-distribution.md | 2 +- guide/src/features.md | 4 +-- guide/src/migration.md | 4 ++- newsfragments/5247.changed.md | 1 + src/conversions/chrono.rs | 2 +- src/conversions/chrono_tz.rs | 2 +- src/conversions/either.rs | 2 +- src/conversions/jiff.rs | 2 +- src/conversions/time.rs | 2 +- src/interpreter_lifecycle.rs | 42 +++++++++----------------- src/lib.rs | 1 + src/macros.rs | 2 +- src/marker.rs | 33 ++++++++++++++++++-- 14 files changed, 59 insertions(+), 42 deletions(-) create mode 100644 newsfragments/5247.changed.md diff --git a/examples/plugin/src/main.rs b/examples/plugin/src/main.rs index beb04305f67..677d9e37b53 100644 --- a/examples/plugin/src/main.rs +++ b/examples/plugin/src/main.rs @@ -7,7 +7,7 @@ fn main() -> Result<(), Box> { //"export" our API module to the python runtime pyo3::append_to_inittab!(pylib_module); //spawn runtime - pyo3::prepare_freethreaded_python(); + Python::initialize(); //import path for python let path = Path::new("./python_plugin/"); //do useful work diff --git a/guide/src/building-and-distribution.md b/guide/src/building-and-distribution.md index 45a82b22cb7..c22fe21894e 100644 --- a/guide/src/building-and-distribution.md +++ b/guide/src/building-and-distribution.md @@ -278,7 +278,7 @@ If you encounter these or other complications when linking the interpreter stati ### Import your module when embedding the Python interpreter -When you run your Rust binary with an embedded interpreter, any `#[pymodule]` created modules won't be accessible to import unless added to a table called `PyImport_Inittab` before the embedded interpreter is initialized. This will cause Python statements in your embedded interpreter such as `import your_new_module` to fail. You can call the macro [`append_to_inittab`]({{#PYO3_DOCS_URL}}/pyo3/macro.append_to_inittab.html) with your module before initializing the Python interpreter to add the module function into that table. (The Python interpreter will be initialized by calling `prepare_freethreaded_python`, `with_embedded_python_interpreter`, or `Python::attach` with the [`auto-initialize`](features.md#auto-initialize) feature enabled.) +When you run your Rust binary with an embedded interpreter, any `#[pymodule]` created modules won't be accessible to import unless added to a table called `PyImport_Inittab` before the embedded interpreter is initialized. This will cause Python statements in your embedded interpreter such as `import your_new_module` to fail. You can call the macro [`append_to_inittab`]({{#PYO3_DOCS_URL}}/pyo3/macro.append_to_inittab.html) with your module before initializing the Python interpreter to add the module function into that table. (The Python interpreter will be initialized by calling `Python::initialize`, `with_embedded_python_interpreter`, or `Python::attach` with the [`auto-initialize`](features.md#auto-initialize) feature enabled.) ## Cross Compiling diff --git a/guide/src/features.md b/guide/src/features.md index 35338eb856c..f9706b586b5 100644 --- a/guide/src/features.md +++ b/guide/src/features.md @@ -45,9 +45,9 @@ section for further detail. ### `auto-initialize` -This feature changes [`Python::attach`]({{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.attach) to automatically initialize a Python interpreter (by calling [`prepare_freethreaded_python`]({{#PYO3_DOCS_URL}}/pyo3/fn.prepare_freethreaded_python.html)) if needed. +This feature changes [`Python::attach`]({{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.attach) to automatically initialize a Python interpreter (by calling [`Python::initialize`]({{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.initialize)) if needed. -If you do not enable this feature, you should call `pyo3::prepare_freethreaded_python()` before attempting to call any other Python APIs. +If you do not enable this feature, you should call `Python::initialize()` before attempting to call any other Python APIs. ## Advanced Features diff --git a/guide/src/migration.md b/guide/src/migration.md index 6d7ba316185..a642718c113 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -4,13 +4,15 @@ 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.25.* to 0.26 -### Rename of `Python::with_gil` and `Python::allow_threads` +### Rename of `Python::with_gil`, `Python::allow_threads`, and `pyo3::prepare_freethreaded_python`
Click to expand The names for these APIs were created when the global interpreter lock (GIL) was mandatory. With the introduction of free-threading in Python 3.13 this is no longer the case, and the naming does not has no universal meaning anymore. For this reason we chose to rename these to more modern terminology introduced in free-threading: + - `Python::with_gil` is now called `Python::attach`, it attaches a Python thread-state to the current thread. In GIL enabled builds there can only be 1 thread attached to the interpreter, in free-threading there can be more. - `Python::allow_threads` is now called `Python::detach`, it detaches a previously attached thread-state. +- `pyo3::prepare_freethreaded_python` is now called `Python::initialize`.
## from 0.24.* to 0.25 diff --git a/newsfragments/5247.changed.md b/newsfragments/5247.changed.md new file mode 100644 index 00000000000..0ab8eae0662 --- /dev/null +++ b/newsfragments/5247.changed.md @@ -0,0 +1 @@ +rename `pyo3::prepare_freethreaded_python` to `Python::initialize` \ No newline at end of file diff --git a/src/conversions/chrono.rs b/src/conversions/chrono.rs index 7036d2098bf..2d0f96d6b13 100644 --- a/src/conversions/chrono.rs +++ b/src/conversions/chrono.rs @@ -23,7 +23,7 @@ //! use pyo3::{Python, PyResult, IntoPyObject, types::PyAnyMethods}; //! //! fn main() -> PyResult<()> { -//! pyo3::prepare_freethreaded_python(); +//! Python::initialize(); //! Python::attach(|py| { //! // Build some chrono values //! let chrono_datetime = Utc.with_ymd_and_hms(2022, 1, 1, 12, 0, 0).unwrap(); diff --git a/src/conversions/chrono_tz.rs b/src/conversions/chrono_tz.rs index e619c95c6fa..ff2b6b74d54 100644 --- a/src/conversions/chrono_tz.rs +++ b/src/conversions/chrono_tz.rs @@ -24,7 +24,7 @@ //! use pyo3::{Python, PyResult, IntoPyObject, types::PyAnyMethods}; //! //! fn main() -> PyResult<()> { -//! pyo3::prepare_freethreaded_python(); +//! Python::initialize(); //! Python::attach(|py| { //! // Convert to Python //! let py_tzinfo = Tz::Europe__Paris.into_pyobject(py)?; diff --git a/src/conversions/either.rs b/src/conversions/either.rs index 6db97dc30a3..d073f3e2706 100644 --- a/src/conversions/either.rs +++ b/src/conversions/either.rs @@ -29,7 +29,7 @@ //! use pyo3::{Python, PyResult, IntoPyObject, types::PyAnyMethods}; //! //! fn main() -> PyResult<()> { -//! pyo3::prepare_freethreaded_python(); +//! Python::initialize(); //! Python::attach(|py| { //! // Create a string and an int in Python. //! let py_str = "crab".into_pyobject(py)?; diff --git a/src/conversions/jiff.rs b/src/conversions/jiff.rs index 2976139bcd8..bd46294cacb 100644 --- a/src/conversions/jiff.rs +++ b/src/conversions/jiff.rs @@ -28,7 +28,7 @@ //! # fn main() -> () {} //! # #[cfg(not(windows))] //! fn main() -> PyResult<()> { -//! pyo3::prepare_freethreaded_python(); +//! Python::initialize(); //! Python::attach(|py| { //! // Build some jiff values //! let jiff_zoned = Zoned::now(); diff --git a/src/conversions/time.rs b/src/conversions/time.rs index 95a19259211..448a199a19b 100644 --- a/src/conversions/time.rs +++ b/src/conversions/time.rs @@ -21,7 +21,7 @@ //! use pyo3::{Python, PyResult, IntoPyObject, types::PyAnyMethods}; //! //! fn main() -> PyResult<()> { -//! pyo3::prepare_freethreaded_python(); +//! Python::initialize(); //! Python::attach(|py| { //! // Create a fixed date and time (2022-01-01 12:00:00 UTC) //! let date = Date::from_calendar_date(2022, Month::January, 1).unwrap(); diff --git a/src/interpreter_lifecycle.rs b/src/interpreter_lifecycle.rs index cb3b30d83ad..d672f887e02 100644 --- a/src/interpreter_lifecycle.rs +++ b/src/interpreter_lifecycle.rs @@ -3,32 +3,10 @@ use crate::{ffi, internal::state::AttachGuard, Python}; static START: std::sync::Once = std::sync::Once::new(); -/// Prepares the use of Python in a free-threaded context. -/// -/// If the Python interpreter is not already initialized, this function will initialize it with -/// signal handling disabled (Python will not raise the `KeyboardInterrupt` exception). Python -/// signal handling depends on the notion of a 'main thread', which must be the thread that -/// initializes the Python interpreter. -/// -/// If the Python interpreter is already initialized, this function has no effect. -/// -/// This function is unavailable under PyPy because PyPy cannot be embedded in Rust (or any other -/// software). Support for this is tracked on the -/// [PyPy issue tracker](https://github.com/pypy/pypy/issues/3836). -/// -/// # Examples -/// ```rust -/// use pyo3::prelude::*; -/// -/// # fn main() -> PyResult<()> { -/// pyo3::prepare_freethreaded_python(); -/// Python::attach(|py| py.run(pyo3::ffi::c_str!("print('Hello World')"), None, None)) -/// # } -/// ``` #[cfg(not(any(PyPy, GraalPy)))] -pub fn prepare_freethreaded_python() { +pub(crate) fn initialize() { // Protect against race conditions when Python is not yet initialized and multiple threads - // concurrently call 'prepare_freethreaded_python()'. Note that we do not protect against + // concurrently call 'initialize()'. Note that we do not protect against // concurrent initialization of the Python runtime by other users of the Python C API. START.call_once_force(|_| unsafe { // Use call_once_force because if initialization panics, it's okay to try again. @@ -41,6 +19,14 @@ pub fn prepare_freethreaded_python() { }); } +/// See [Python::initialize] +#[cfg(not(any(PyPy, GraalPy)))] +#[inline] +#[deprecated(note = "use `Python::initialize` instead", since = "0.26.0")] +pub fn prepare_freethreaded_python() { + initialize(); +} + /// Executes the provided closure with an embedded Python interpreter. /// /// This function initializes the Python interpreter, executes the provided closure, and then @@ -111,17 +97,17 @@ pub(crate) fn ensure_initialized() { // - Otherwise, just check the interpreter is initialized. #[cfg(all(feature = "auto-initialize", not(any(PyPy, GraalPy))))] { - prepare_freethreaded_python(); + initialize(); } #[cfg(not(all(feature = "auto-initialize", not(any(PyPy, GraalPy)))))] { // This is a "hack" to make running `cargo test` for PyO3 convenient (i.e. no need - // to specify `--features auto-initialize` manually. Tests within the crate itself + // to specify `--features auto-initialize` manually). Tests within the crate itself // all depend on the auto-initialize feature for conciseness but Cargo does not // provide a mechanism to specify required features for tests. #[cfg(not(any(PyPy, GraalPy)))] if option_env!("CARGO_PRIMARY_PACKAGE").is_some() { - prepare_freethreaded_python(); + initialize(); } START.call_once_force(|_| unsafe { @@ -133,7 +119,7 @@ pub(crate) fn ensure_initialized() { 0, "The Python interpreter is not initialized and the `auto-initialize` \ feature is not enabled.\n\n\ - Consider calling `pyo3::prepare_freethreaded_python()` before attempting \ + Consider calling `Python::initialize()` before attempting \ to use Python APIs." ); }); diff --git a/src/lib.rs b/src/lib.rs index b25141f9669..89eecbde800 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -345,6 +345,7 @@ pub use crate::conversion::{FromPyObject, IntoPyObject, IntoPyObjectExt}; pub use crate::err::{DowncastError, DowncastIntoError, PyErr, PyErrArguments, PyResult, ToPyErr}; pub use crate::instance::{Borrowed, Bound, BoundObject, Py, PyObject}; #[cfg(not(any(PyPy, GraalPy)))] +#[allow(deprecated)] pub use crate::interpreter_lifecycle::{ prepare_freethreaded_python, with_embedded_python_interpreter, }; diff --git a/src/macros.rs b/src/macros.rs index 7727a461d2a..e5e45097fd8 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -169,7 +169,7 @@ macro_rules! wrap_pymodule { /// Add the module to the initialization table in order to make embedded Python code to use it. /// Module name is the argument. /// -/// Use it before [`prepare_freethreaded_python`](crate::prepare_freethreaded_python) and +/// Use it before [`Python::initialize`](crate::marker::Python::initialize) and /// leave feature `auto-initialize` off #[cfg(not(any(PyPy, GraalPy)))] #[macro_export] diff --git a/src/marker.rs b/src/marker.rs index 4e26a4246fb..f68d6161419 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -379,9 +379,9 @@ impl Python<'_> { /// initialized, this function will initialize it. See #[cfg_attr( not(any(PyPy, GraalPy)), - doc = "[`prepare_freethreaded_python`](crate::prepare_freethreaded_python)" + doc = "[`Python::initialize`](crate::marker::Python::initialize)" )] - #[cfg_attr(PyPy, doc = "`prepare_freethreaded_python`")] + #[cfg_attr(PyPy, doc = "`Python::initialize")] /// for details. /// /// If the current thread does not yet have a Python "thread state" associated with it, @@ -419,6 +419,33 @@ impl Python<'_> { f(guard.python()) } + /// Prepares the use of Python. + /// + /// If the Python interpreter is not already initialized, this function will initialize it with + /// signal handling disabled (Python will not raise the `KeyboardInterrupt` exception). Python + /// signal handling depends on the notion of a 'main thread', which must be the thread that + /// initializes the Python interpreter. + /// + /// If the Python interpreter is already initialized, this function has no effect. + /// + /// This function is unavailable under PyPy because PyPy cannot be embedded in Rust (or any other + /// software). Support for this is tracked on the + /// [PyPy issue tracker](https://github.com/pypy/pypy/issues/3836). + /// + /// # Examples + /// ```rust + /// use pyo3::prelude::*; + /// + /// # fn main() -> PyResult<()> { + /// Python::initialize(); + /// Python::attach(|py| py.run(pyo3::ffi::c_str!("print('Hello World')"), None, None)) + /// # } + /// ``` + #[cfg(not(any(PyPy, GraalPy)))] + pub fn initialize() { + crate::interpreter_lifecycle::initialize(); + } + /// Like [`Python::attach`] except Python interpreter state checking is skipped. /// /// Normally when the GIL is acquired, we check that the Python interpreter is an @@ -889,7 +916,7 @@ mod tests { // 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(); + Python::initialize(); let state = unsafe { crate::ffi::PyGILState_Check() }; assert_eq!(state, GIL_NOT_HELD); From 5c6f556df10f33ceeea6c276141807f5e16b1384 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers <7943856+bschoenmaeckers@users.noreply.github.com> Date: Fri, 1 Aug 2025 14:01:17 +0200 Subject: [PATCH 763/936] Refactor jiff tests to fix #5278 (#5279) * Refactor jiff tests to fix #5278 * Update src/conversions/jiff.rs --------- Co-authored-by: David Hewitt --- src/conversions/jiff.rs | 197 +++++++++++++++++++++------------------- 1 file changed, 104 insertions(+), 93 deletions(-) diff --git a/src/conversions/jiff.rs b/src/conversions/jiff.rs index bd46294cacb..1ba3894183a 100644 --- a/src/conversions/jiff.rs +++ b/src/conversions/jiff.rs @@ -939,21 +939,37 @@ mod tests { use std::ffi::CString; // This is to skip the test if we are creating an invalid date, like February 31. - fn try_date(year: i32, month: u32, day: u32) -> PyResult { - Ok(Date::new( - year.try_into()?, - month.try_into()?, - day.try_into()?, - )?) + #[track_caller] + fn try_date(year: i16, month: i8, day: i8) -> Result { + let location = std::panic::Location::caller(); + Date::new(year, month, day) + .map_err(|err| TestCaseError::reject(format!("{location}: {err:?}"))) } - fn try_time(hour: u32, min: u32, sec: u32, micro: u32) -> PyResult
+### `PyMemoryError` now maps to `io::ErrorKind::OutOfMemory` when converted to `io::Error` +
+Click to expand + +Previously, converting a `PyMemoryError` into a Rust `io::Error` would result in an error with kind `Other`. Now, it produces an error with kind `OutOfMemory`. +Similarly, converting an `io::Error` with kind `OutOfMemory` back into a Python error would previously yield a generic `PyOSError`. Now, it yields a `PyMemoryError`. + +This change makes error conversions more precise and matches the semantics of out-of-memory errors between Python and Rust. +
+ ## from 0.24.* to 0.25 ### `AsPyPointer` removal
diff --git a/newsfragments/5256.added.md b/newsfragments/5256.added.md new file mode 100644 index 00000000000..189701405d1 --- /dev/null +++ b/newsfragments/5256.added.md @@ -0,0 +1 @@ +Allow converting `PyMemoryError` into/from `io::ErrorKind::OutOfMemory` diff --git a/src/err/impls.rs b/src/err/impls.rs index 9d93ef33647..dac01e32da5 100644 --- a/src/err/impls.rs +++ b/src/err/impls.rs @@ -26,6 +26,8 @@ impl From for io::Error { io::ErrorKind::WouldBlock } else if err.is_instance_of::(py) { io::ErrorKind::TimedOut + } else if err.is_instance_of::(py) { + io::ErrorKind::OutOfMemory } else { #[cfg(io_error_more)] if err.is_instance_of::(py) { @@ -63,6 +65,7 @@ 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), + io::ErrorKind::OutOfMemory => exceptions::PyMemoryError::new_err(err), #[cfg(io_error_more)] io::ErrorKind::IsADirectory => exceptions::PyIsADirectoryError::new_err(err), #[cfg(io_error_more)] From 7559c00071588ed2c1dd91ae2c2f343d79f5da2b Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Wed, 6 Aug 2025 22:52:04 +0200 Subject: [PATCH 772/936] introduce `PyClassGuard(Mut)` as future replacement for `PyRef(Mut)` (#5233) * introduce `PyClassGuard(Mut)` as future replacement for `PyRef(Mut)` * use `PyClassGuard(Mut)` in the macros * add `NonNull` macro helpers * add `IntoPyObject` impls * impl `FromPyObjectBound` instead of `PyFunctionArgument` * add newsfragment * add Ungil, Send, Sync impls * fix leaked borrow in `PyClassGuard::into_super` Co-authored-by: David Hewitt --------- Co-authored-by: David Hewitt --- newsfragments/5233.added.md | 1 + pyo3-macros-backend/src/method.rs | 2 +- pyo3-macros-backend/src/pyclass.rs | 35 +- pyo3-macros-backend/src/pymethod.rs | 13 +- src/conversion.rs | 4 + src/impl_/extract_argument.rs | 63 +- src/impl_/pyclass.rs | 19 +- src/impl_/pymethods.rs | 31 +- src/instance.rs | 37 + src/lib.rs | 2 +- src/pyclass.rs | 2 + src/pyclass/guard.rs | 834 ++++++++++++++++++ tests/ui/invalid_cancel_handle.stderr | 48 +- tests/ui/invalid_frozen_pyclass_borrow.stderr | 12 +- tests/ui/invalid_pymethod_enum.stderr | 24 +- tests/ui/invalid_pymethods.stderr | 2 +- 16 files changed, 1016 insertions(+), 113 deletions(-) create mode 100644 newsfragments/5233.added.md create mode 100644 src/pyclass/guard.rs diff --git a/newsfragments/5233.added.md b/newsfragments/5233.added.md new file mode 100644 index 00000000000..38ac4051890 --- /dev/null +++ b/newsfragments/5233.added.md @@ -0,0 +1 @@ +added new `PyClassGuard(Mut)` pyclass holders. In the future they will replace `PyRef(Mut)` \ No newline at end of file diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index 816a9016ae2..ed28aff1a1c 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -1126,7 +1126,7 @@ fn parse_method_attributes(attrs: &mut Vec) -> Result` or `slf: PyClassGuardMut<'_, Self>`."; fn ensure_signatures_on_valid_method( fn_type: &FnType, diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 98c78e3cf91..9ee1e0dc5f5 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -1227,10 +1227,9 @@ 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<#pyo3_path::PyObject> { + fn #field_name(slf: #pyo3_path::PyClassGuard<'_, Self>, py: #pyo3_path::Python<'_>) -> #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, .. } => #pyo3_path::impl_::pyclass::ConvertField::< @@ -1304,10 +1303,9 @@ 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<#pyo3_path::PyObject> { + fn #field_name(slf: #pyo3_path::PyClassGuard<'_, Self>, py: #pyo3_path::Python<'_>) -> #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), *) => #pyo3_path::impl_::pyclass::ConvertField::< @@ -1337,7 +1335,7 @@ fn impl_complex_enum_tuple_variant_len( let Ctx { pyo3_path, .. } = ctx; let mut len_method_impl: syn::ImplItemFn = parse_quote! { - fn __len__(slf: #pyo3_path::PyRef) -> #pyo3_path::PyResult { + fn __len__(slf: #pyo3_path::PyClassGuard<'_, Self>) -> #pyo3_path::PyResult { ::std::result::Result::Ok(#num_fields) } }; @@ -1360,14 +1358,13 @@ fn impl_complex_enum_tuple_variant_getitem( .map(|i| { let field_access = format_ident!("_{}", i); quote! { #i => - #pyo3_path::IntoPyObjectExt::into_py_any(#variant_cls::#field_access(slf)?, py) + #pyo3_path::IntoPyObjectExt::into_py_any(#variant_cls::#field_access(slf, py)?, 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(); + fn __getitem__(slf: #pyo3_path::PyClassGuard<'_, Self>, py: #pyo3_path::Python<'_>, idx: usize) -> #pyo3_path::PyResult< #pyo3_path::PyObject> { match idx { #( #match_arms, )* _ => ::std::result::Result::Err(#pyo3_path::exceptions::PyIndexError::new_err("tuple index out of range")), @@ -1738,7 +1735,9 @@ fn complex_enum_variant_field_getter<'a>( field_span: Span, ctx: &Ctx, ) -> Result { - let signature = crate::pyfunction::FunctionSignature::from_arguments(vec![]); + let mut arg = parse_quote!(py: Python<'_>); + let py = FnArg::parse(&mut arg)?; + let signature = crate::pyfunction::FunctionSignature::from_arguments(vec![py]); let self_type = crate::method::SelfType::TryFromBoundRef(field_span); @@ -2154,40 +2153,40 @@ impl<'a> PyClassImplsBuilder<'a> { }; if self.attr.options.frozen.is_some() { quote! { - impl<'a, 'py> #pyo3_path::impl_::extract_argument::PyFunctionArgument<'a, 'py, false> for &'a #cls + impl<'a, 'holder, 'py> #pyo3_path::impl_::extract_argument::PyFunctionArgument<'a, 'holder, 'py, false> for &'holder #cls { - type Holder = ::std::option::Option<#pyo3_path::PyRef<'py, #cls>>; + type Holder = ::std::option::Option<#pyo3_path::PyClassGuard<'a, #cls>>; #input_type #[inline] - fn extract(obj: &'a #pyo3_path::Bound<'py, #pyo3_path::PyAny>, holder: &'a mut Self::Holder) -> #pyo3_path::PyResult { + fn extract(obj: &'a #pyo3_path::Bound<'py, #pyo3_path::PyAny>, holder: &'holder mut Self::Holder) -> #pyo3_path::PyResult { #pyo3_path::impl_::extract_argument::extract_pyclass_ref(obj, holder) } } } } else { quote! { - impl<'a, 'py> #pyo3_path::impl_::extract_argument::PyFunctionArgument<'a, 'py, false> for &'a #cls + impl<'a, 'holder, 'py> #pyo3_path::impl_::extract_argument::PyFunctionArgument<'a, 'holder, 'py, false> for &'holder #cls { - type Holder = ::std::option::Option<#pyo3_path::PyRef<'py, #cls>>; + type Holder = ::std::option::Option<#pyo3_path::PyClassGuard<'a, #cls>>; #input_type #[inline] - fn extract(obj: &'a #pyo3_path::Bound<'py, #pyo3_path::PyAny>, holder: &'a mut Self::Holder) -> #pyo3_path::PyResult { + fn extract(obj: &'a #pyo3_path::Bound<'py, #pyo3_path::PyAny>, holder: &'holder mut Self::Holder) -> #pyo3_path::PyResult { #pyo3_path::impl_::extract_argument::extract_pyclass_ref(obj, holder) } } - impl<'a, 'py> #pyo3_path::impl_::extract_argument::PyFunctionArgument<'a, 'py, false> for &'a mut #cls + impl<'a, 'holder, 'py> #pyo3_path::impl_::extract_argument::PyFunctionArgument<'a, 'holder, 'py, false> for &'holder mut #cls { - type Holder = ::std::option::Option<#pyo3_path::PyRefMut<'py, #cls>>; + type Holder = ::std::option::Option<#pyo3_path::PyClassGuardMut<'a, #cls>>; #input_type #[inline] - fn extract(obj: &'a #pyo3_path::Bound<'py, #pyo3_path::PyAny>, holder: &'a mut Self::Holder) -> #pyo3_path::PyResult { + fn extract(obj: &'a #pyo3_path::Bound<'py, #pyo3_path::PyAny>, holder: &'holder mut Self::Holder) -> #pyo3_path::PyResult { #pyo3_path::impl_::extract_argument::extract_pyclass_ref_mut(obj, holder) } } diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index 70aa813b143..fe9e347dc09 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -14,7 +14,7 @@ use crate::{ use crate::{quotes, utils}; use proc_macro2::{Span, TokenStream}; use quote::{format_ident, quote, quote_spanned, ToTokens}; -use syn::{ext::IdentExt, spanned::Spanned, Result}; +use syn::{ext::IdentExt, spanned::Spanned, Ident, Result}; /// Generated code for a single pymethod item. pub struct MethodAndMethodDef { @@ -1147,6 +1147,7 @@ impl Ty { extract_error_mode, holders, arg, + format_ident!("ref_from_ptr"), quote! { #ident }, ctx ), @@ -1154,6 +1155,7 @@ impl Ty { extract_error_mode, holders, arg, + format_ident!("ref_from_ptr"), quote! { if #ident.is_null() { #pyo3_path::ffi::Py_None() @@ -1167,13 +1169,15 @@ impl Ty { extract_error_mode, holders, arg, - quote! { #ident.as_ptr() }, + format_ident!("ref_from_non_null"), + quote! { #ident }, ctx ), Ty::IPowModulo => extract_object( extract_error_mode, holders, arg, + format_ident!("ref_from_ptr"), quote! { #ident.as_ptr() }, ctx ), @@ -1203,6 +1207,7 @@ fn extract_object( extract_error_mode: ExtractErrorMode, holders: &mut Holders, arg: &FnArg<'_>, + ref_from_method: Ident, source_ptr: TokenStream, ctx: &Ctx, ) -> TokenStream { @@ -1220,7 +1225,7 @@ fn extract_object( quote! { #pyo3_path::impl_::extract_argument::from_py_with( - unsafe { #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(py, &#source_ptr).0 }, + unsafe { #pyo3_path::impl_::pymethods::BoundRef::#ref_from_method(py, &#source_ptr).0 }, #name, #extractor, ) @@ -1235,7 +1240,7 @@ fn extract_object( _, { #pyo3_path::impl_::pyclass::IsOption::<#ty>::VALUE } >( - unsafe { #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr(py, &#source_ptr).0 }, + unsafe { #pyo3_path::impl_::pymethods::BoundRef::#ref_from_method(py, &#source_ptr).0 }, &mut #holder, #name ) diff --git a/src/conversion.rs b/src/conversion.rs index e5cf5bfd59d..9595462098c 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -315,6 +315,8 @@ pub trait FromPyObject<'py>: Sized { } mod from_py_object_bound_sealed { + use crate::{pyclass::boolean_struct::False, PyClass, PyClassGuard, PyClassGuardMut}; + /// Private seal for the `FromPyObjectBound` trait. /// /// This prevents downstream types from implementing the trait before @@ -324,6 +326,8 @@ 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> {} + impl Sealed for PyClassGuard<'_, T> where T: PyClass {} + impl Sealed for PyClassGuardMut<'_, T> where T: PyClass {} impl Sealed for &'_ str {} impl Sealed for std::borrow::Cow<'_, str> {} impl Sealed for &'_ [u8] {} diff --git a/src/impl_/extract_argument.rs b/src/impl_/extract_argument.rs index 65848117d24..77c875c41c0 100644 --- a/src/impl_/extract_argument.rs +++ b/src/impl_/extract_argument.rs @@ -4,7 +4,8 @@ use crate::{ ffi, pyclass::boolean_struct::False, types::{any::PyAnyMethods, dict::PyDictMethods, tuple::PyTupleMethods, PyDict, PyTuple}, - Borrowed, Bound, PyAny, PyClass, PyErr, PyRef, PyRefMut, PyResult, PyTypeCheck, Python, + Borrowed, Bound, PyAny, PyClass, PyClassGuard, PyClassGuardMut, PyErr, PyResult, PyTypeCheck, + Python, }; /// Helper type used to keep implementation more concise. @@ -20,19 +21,19 @@ 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, const IS_OPTION: bool>: Sized + 'a { +pub trait PyFunctionArgument<'a, 'holder, 'py, const IS_OPTION: bool>: Sized { type Holder: FunctionArgumentHolder; /// Provides the type hint information for which Python types are allowed. #[cfg(feature = "experimental-inspect")] const INPUT_TYPE: &'static str; - fn extract(obj: &'a Bound<'py, PyAny>, holder: &'a mut Self::Holder) -> PyResult; + fn extract(obj: &'a Bound<'py, PyAny>, holder: &'holder mut Self::Holder) -> PyResult; } -impl<'a, 'py, T> PyFunctionArgument<'a, 'py, false> for T +impl<'a, 'holder, 'py, T> PyFunctionArgument<'a, 'holder, 'py, false> for T where - T: FromPyObjectBound<'a, 'py> + 'a, + T: FromPyObjectBound<'a, 'py>, { type Holder = (); @@ -40,12 +41,12 @@ where const INPUT_TYPE: &'static str = T::INPUT_TYPE; #[inline] - fn extract(obj: &'a Bound<'py, PyAny>, _: &'a mut ()) -> PyResult { + fn extract(obj: &'a Bound<'py, PyAny>, _: &'holder mut ()) -> PyResult { obj.extract() } } -impl<'a, 'py, T: 'py> PyFunctionArgument<'a, 'py, false> for &'a Bound<'py, T> +impl<'a, 'holder, 'py, T: 'py> PyFunctionArgument<'a, 'holder, 'py, false> for &'a Bound<'py, T> where T: PyTypeCheck, { @@ -55,14 +56,14 @@ where const INPUT_TYPE: &'static str = T::PYTHON_TYPE; #[inline] - fn extract(obj: &'a Bound<'py, PyAny>, _: &'a mut ()) -> PyResult { + fn extract(obj: &'a Bound<'py, PyAny>, _: &'holder mut ()) -> PyResult { obj.downcast().map_err(Into::into) } } -impl<'a, 'py, T> PyFunctionArgument<'a, 'py, true> for Option +impl<'a, 'holder, 'py, T> PyFunctionArgument<'a, 'holder, 'py, true> for Option where - T: PyFunctionArgument<'a, 'py, false>, // inner `Option`s will use `FromPyObject` + T: PyFunctionArgument<'a, 'holder, 'py, false>, // inner `Option`s will use `FromPyObject` { type Holder = T::Holder; @@ -70,7 +71,7 @@ where const INPUT_TYPE: &'static str = "typing.Any | None"; #[inline] - fn extract(obj: &'a Bound<'py, PyAny>, holder: &'a mut T::Holder) -> PyResult { + fn extract(obj: &'a Bound<'py, PyAny>, holder: &'holder mut T::Holder) -> PyResult { if obj.is_none() { Ok(None) } else { @@ -80,7 +81,7 @@ where } #[cfg(all(Py_LIMITED_API, not(Py_3_10)))] -impl<'a> PyFunctionArgument<'a, '_, false> for &'a str { +impl<'a, 'holder> PyFunctionArgument<'a, 'holder, '_, false> for &'holder str { type Holder = Option>; #[cfg(feature = "experimental-inspect")] @@ -89,7 +90,7 @@ impl<'a> PyFunctionArgument<'a, '_, false> for &'a str { #[inline] fn extract( obj: &'a Bound<'_, PyAny>, - holder: &'a mut Option>, + holder: &'holder mut Option>, ) -> PyResult { Ok(holder.insert(obj.extract()?)) } @@ -110,30 +111,32 @@ impl FunctionArgumentHolder for Option { } #[inline] -pub fn extract_pyclass_ref<'a, 'py: 'a, T: PyClass>( +pub fn extract_pyclass_ref<'a, 'holder, 'py, T: PyClass>( obj: &'a Bound<'py, PyAny>, - holder: &'a mut Option>, -) -> PyResult<&'a T> { - Ok(&*holder.insert(obj.extract()?)) + holder: &'holder mut Option>, +) -> PyResult<&'holder T> { + Ok(&*holder.insert(PyClassGuard::try_borrow(obj.downcast()?.as_unbound())?)) } #[inline] -pub fn extract_pyclass_ref_mut<'a, 'py: 'a, T: PyClass>( +pub fn extract_pyclass_ref_mut<'a, 'holder, 'py, T: PyClass>( obj: &'a Bound<'py, PyAny>, - holder: &'a mut Option>, -) -> PyResult<&'a mut T> { - Ok(&mut *holder.insert(obj.extract()?)) + holder: &'holder mut Option>, +) -> PyResult<&'holder mut T> { + Ok(&mut *holder.insert(PyClassGuardMut::try_borrow_mut( + obj.downcast()?.as_unbound(), + )?)) } /// The standard implementation of how PyO3 extracts a `#[pyfunction]` or `#[pymethod]` function argument. #[doc(hidden)] -pub fn extract_argument<'a, 'py, T, const IS_OPTION: bool>( +pub fn extract_argument<'a, 'holder, 'py, T, const IS_OPTION: bool>( obj: &'a Bound<'py, PyAny>, - holder: &'a mut T::Holder, + holder: &'holder mut T::Holder, arg_name: &str, ) -> PyResult where - T: PyFunctionArgument<'a, 'py, IS_OPTION>, + T: PyFunctionArgument<'a, 'holder, 'py, IS_OPTION>, { match PyFunctionArgument::extract(obj, holder) { Ok(value) => Ok(value), @@ -144,14 +147,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, const IS_OPTION: bool>( +pub fn extract_optional_argument<'a, 'holder, 'py, T, const IS_OPTION: bool>( obj: Option<&'a Bound<'py, PyAny>>, - holder: &'a mut T::Holder, + holder: &'holder mut T::Holder, arg_name: &str, default: fn() -> Option, ) -> PyResult> where - T: PyFunctionArgument<'a, 'py, IS_OPTION>, + T: PyFunctionArgument<'a, 'holder, 'py, IS_OPTION>, { match obj { Some(obj) => { @@ -168,14 +171,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, const IS_OPTION: bool>( +pub fn extract_argument_with_default<'a, 'holder, 'py, T, const IS_OPTION: bool>( obj: Option<&'a Bound<'py, PyAny>>, - holder: &'a mut T::Holder, + holder: &'holder mut T::Holder, arg_name: &str, default: fn() -> T, ) -> PyResult where - T: PyFunctionArgument<'a, 'py, IS_OPTION>, + T: PyFunctionArgument<'a, 'holder, 'py, IS_OPTION>, { match obj { Some(obj) => extract_argument(obj, holder, arg_name), diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index 3191b8b902b..bbde7a0f0fe 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -7,9 +7,10 @@ use crate::{ pyclass_init::PyObjectInit, pymethods::{PyGetterDef, PyMethodDefType}, }, + internal_tricks::ptr_from_ref, pycell::PyBorrowError, types::{any::PyAnyMethods, PyBool}, - Borrowed, BoundObject, IntoPyObject, IntoPyObjectExt, Py, PyAny, PyClass, PyErr, PyRef, + Borrowed, BoundObject, IntoPyObject, IntoPyObjectExt, Py, PyAny, PyClass, PyClassGuard, PyErr, PyResult, PyTypeInfo, Python, }; use std::{ @@ -923,7 +924,7 @@ macro_rules! generate_pyclass_richcompare_slot { } pub use generate_pyclass_richcompare_slot; -use super::{pycell::PyClassObject, pymethods::BoundRef}; +use super::pycell::PyClassObject; /// Implements a freelist. /// @@ -1368,15 +1369,11 @@ impl< /// 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> { - unsafe { - BoundRef::ref_from_ptr(py, obj) - .downcast_unchecked::() - .try_borrow() - } +unsafe fn ensure_no_mutable_alias<'a, ClassT: PyClass>( + _py: Python<'_>, + obj: &'a *mut ffi::PyObject, +) -> Result, PyBorrowError> { + unsafe { PyClassGuard::try_borrow(&*ptr_from_ref(obj).cast::>()) } } /// calculates the field pointer from an PyObject pointer diff --git a/src/impl_/pymethods.rs b/src/impl_/pymethods.rs index dd3a4f89ba1..1087517c4d5 100644 --- a/src/impl_/pymethods.rs +++ b/src/impl_/pymethods.rs @@ -10,15 +10,16 @@ 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, + ffi, Bound, DowncastError, Py, PyAny, PyClass, PyClassGuard, PyClassGuardMut, + 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; +use std::ptr::{null_mut, NonNull}; use super::trampoline; use crate::internal_tricks::{clear_eq, traverse_eq}; @@ -645,6 +646,10 @@ impl<'a, 'py> BoundRef<'a, 'py, PyAny> { unsafe { Bound::ref_from_ptr_or_opt(py, ptr).as_ref().map(BoundRef) } } + pub unsafe fn ref_from_non_null(py: Python<'py>, ptr: &'a NonNull) -> Self { + unsafe { Self(Bound::ref_from_non_null(py, ptr)) } + } + pub fn downcast(self) -> Result, DowncastError<'a, 'py>> { self.0.downcast::().map(BoundRef) } @@ -654,11 +659,27 @@ impl<'a, 'py> BoundRef<'a, 'py, PyAny> { } } +impl<'a, 'py, T: PyClass> TryFrom> for PyClassGuard<'a, T> { + type Error = PyBorrowError; + #[inline] + fn try_from(value: BoundRef<'a, 'py, T>) -> Result { + PyClassGuard::try_borrow(value.0.as_unbound()) + } +} + +impl<'a, 'py, T: PyClass> TryFrom> for PyClassGuardMut<'a, T> { + type Error = PyBorrowMutError; + #[inline] + fn try_from(value: BoundRef<'a, 'py, T>) -> Result { + PyClassGuardMut::try_borrow_mut(value.0.as_unbound()) + } +} + impl<'a, 'py, T: PyClass> TryFrom> for PyRef<'py, T> { type Error = PyBorrowError; #[inline] fn try_from(value: BoundRef<'a, 'py, T>) -> Result { - value.0.try_borrow() + PyRef::try_borrow(value.0) } } @@ -666,7 +687,7 @@ impl<'a, 'py, T: PyClass> TryFrom> for PyRe type Error = PyBorrowMutError; #[inline] fn try_from(value: BoundRef<'a, 'py, T>) -> Result { - value.0.try_borrow_mut() + PyRefMut::try_borrow(value.0) } } diff --git a/src/instance.rs b/src/instance.rs index 5114d888d43..510f21c3d3f 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -224,6 +224,23 @@ impl<'py> Bound<'py, PyAny> { ) -> &'a Option { unsafe { &*ptr_from_ref(ptr).cast::>>() } } + + /// This slightly strange method is used to obtain `&Bound` from a [`NonNull`] in macro + /// code where we need to constrain the lifetime `'a` safely. + /// + /// Note that `'py` is required to outlive `'a` implicitly by the nature of the fact that `&'a + /// Bound<'py>` means that `Bound<'py>` exists for at least the lifetime `'a`. + /// + /// # Safety + /// - `ptr` must be a valid pointer to a Python object for the lifetime `'a`. The `ptr` can be + /// either a borrowed reference or an owned reference, it does not matter, as this is just + /// `&Bound` there will never be any ownership transfer. + pub(crate) unsafe fn ref_from_non_null<'a>( + _py: Python<'py>, + ptr: &'a NonNull, + ) -> &'a Self { + unsafe { NonNull::from(ptr).cast().as_ref() } + } } impl<'py, T> Bound<'py, T> @@ -704,6 +721,17 @@ impl<'a, 'py, T> Borrowed<'a, 'py, T> { } } +impl<'a, T: PyClass> Borrowed<'a, '_, T> { + /// Get a view on the underlying `PyClass` contents. + #[inline] + pub(crate) fn get_class_object(self) -> &'a PyClassObject { + // Safety: Borrowed<'a, '_, T: PyClass> is known to contain an object + // which is laid out in memory as a PyClassObject and lives for at + // least 'a. + unsafe { &*self.as_ptr().cast::>() } + } +} + impl<'a, 'py> Borrowed<'a, 'py, PyAny> { /// Constructs a new `Borrowed<'a, 'py, PyAny>` from a pointer. Panics if `ptr` is null. /// @@ -771,6 +799,15 @@ impl<'a, 'py> Borrowed<'a, 'py, PyAny> { Self(unsafe { NonNull::new_unchecked(ptr) }, PhantomData, py) } + /// # Safety + /// This similar to `std::slice::from_raw_parts`, the lifetime `'a` is + /// completely defined by the caller and it is the caller's responsibility + /// to ensure that the reference this is derived from is valid for the + /// lifetime `'a`. + pub(crate) unsafe fn from_non_null(py: Python<'py>, ptr: NonNull) -> Self { + Self(ptr, PhantomData, py) + } + #[inline] pub(crate) fn downcast(self) -> Result, DowncastError<'a, 'py>> where diff --git a/src/lib.rs b/src/lib.rs index 89eecbde800..cda313c2c91 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -351,7 +351,7 @@ pub use crate::interpreter_lifecycle::{ }; pub use crate::marker::Python; pub use crate::pycell::{PyRef, PyRefMut}; -pub use crate::pyclass::PyClass; +pub use crate::pyclass::{PyClass, PyClassGuard, PyClassGuardMut}; pub use crate::pyclass_init::PyClassInitializer; pub use crate::type_object::{PyTypeCheck, PyTypeInfo}; pub use crate::types::PyAny; diff --git a/src/pyclass.rs b/src/pyclass.rs index 086bd2435c3..5d212a810f7 100644 --- a/src/pyclass.rs +++ b/src/pyclass.rs @@ -4,10 +4,12 @@ use std::{cmp::Ordering, os::raw::c_int}; mod create_type_object; mod gc; +mod guard; pub(crate) use self::create_type_object::{create_type_object, PyClassTypeObject}; pub use self::gc::{PyTraverseError, PyVisit}; +pub use self::guard::{PyClassGuard, PyClassGuardMut}; /// Types that can be used as Python classes. /// diff --git a/src/pyclass/guard.rs b/src/pyclass/guard.rs new file mode 100644 index 00000000000..a14e7f9b7b0 --- /dev/null +++ b/src/pyclass/guard.rs @@ -0,0 +1,834 @@ +use crate::conversion::FromPyObjectBound; +use crate::impl_::pycell::{PyClassObject, PyClassObjectLayout as _}; +use crate::pycell::PyBorrowMutError; +use crate::pycell::{impl_::PyClassBorrowChecker, PyBorrowError}; +use crate::pyclass::boolean_struct::False; +use crate::{ffi, Borrowed, IntoPyObject, Py, PyClass}; +use std::convert::Infallible; +use std::marker::PhantomData; +use std::ops::{Deref, DerefMut}; +use std::ptr::NonNull; + +/// A wrapper type for an immutably borrowed value from a `PyClass`. +/// +/// Rust has strict aliasing rules - you can either have any number of immutable +/// (shared) references or one mutable reference. Python's ownership model is +/// the complete opposite of that - any Python object can be referenced any +/// number of times, and mutation is allowed from any reference. +/// +/// 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 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. +/// - 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 [`PyClassGuard`] and +/// [`PyClassGuardMut`] defined in this module. This works similar to std's +/// [`RefCell`](std::cell::RefCell) type. Especially when building for +/// free-threaded Python it gets harder to track which thread borrows which +/// object at any time. This can lead to method calls failing with +/// [`PyBorrowError`]. In these cases consider using `frozen` classes together +/// with Rust interior mutability primitives like [`Mutex`](std::sync::Mutex) +/// instead of using [`PyClassGuardMut`] to get mutable access. +/// +/// # Examples +/// +/// You can use [`PyClassGuard`] as an alternative to a `&self` receiver when +/// - you need to access the pointer of the `PyClass`, or +/// - you want to get a super class. +/// ``` +/// # use pyo3::prelude::*; +/// # use pyo3::PyClassGuard; +/// #[pyclass(subclass)] +/// struct Parent { +/// basename: &'static str, +/// } +/// +/// #[pyclass(extends=Parent)] +/// struct Child { +/// name: &'static str, +/// } +/// +/// #[pymethods] +/// impl Child { +/// #[new] +/// fn new() -> (Self, Parent) { +/// (Child { name: "Caterpillar" }, Parent { basename: "Butterfly" }) +/// } +/// +/// fn format(slf: PyClassGuard<'_, Self>) -> String { +/// // We can get &Self::BaseType by as_super +/// let basename = slf.as_super().basename; +/// format!("{}(base: {})", slf.name, basename) +/// } +/// } +/// # Python::attach(|py| { +/// # let sub = Py::new(py, Child::new()).unwrap(); +/// # pyo3::py_run!(py, sub, "assert sub.format() == 'Caterpillar(base: Butterfly)', sub.format()"); +/// # }); +/// ``` +/// +/// See also [`PyClassGuardMut`] and the [guide] for more information. +/// +/// [Interior Mutability]: +/// https://doc.rust-lang.org/book/ch15-05-interior-mutability.html +/// "RefCell and the Interior Mutability Pattern - The Rust Programming +/// Language" +/// [guide]: https://pyo3.rs/latest/class.html#bound-and-interior-mutability +/// "Bound and interior mutability" +#[repr(transparent)] +pub struct PyClassGuard<'a, T: PyClass> { + ptr: NonNull, + marker: PhantomData<&'a Py>, +} + +impl<'a, T: PyClass> PyClassGuard<'a, T> { + pub(crate) fn try_borrow(obj: &'a Py) -> Result { + Self::try_from_class_object(obj.get_class_object()) + } + + fn try_from_class_object(obj: &'a PyClassObject) -> Result { + obj.ensure_threadsafe(); + obj.borrow_checker().try_borrow().map(|_| Self { + ptr: NonNull::from(obj).cast(), + marker: PhantomData, + }) + } + + pub(crate) fn as_class_object(&self) -> &'a PyClassObject { + // SAFETY: `ptr` by construction points to a `PyClassObject` and is + // valid for at least 'a + unsafe { self.ptr.cast().as_ref() } + } +} + +impl<'a, T, U> PyClassGuard<'a, T> +where + T: PyClass, + U: PyClass, +{ + /// Borrows a shared reference to `PyClassGuard`. + /// + /// With the help of this method, you can access attributes and call methods + /// on the superclass without consuming the `PyClassGuard`. This method + /// can also be chained to access the super-superclass (and so on). + /// + /// # Examples + /// ``` + /// # use pyo3::prelude::*; + /// # use pyo3::PyClassGuard; + /// #[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: PyClassGuard<'_, Self>) -> String { + /// format!("{} {}", slf.as_super().base_name_len(), slf.sub_name_len()) + /// } + /// } + /// # Python::attach(|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) -> &PyClassGuard<'a, U> { + // SAFETY: `PyClassGuard` and `PyClassGuard` have the same layout + unsafe { NonNull::from(self).cast().as_ref() } + } + + /// Gets a `PyClassGuard`. + /// + /// With the help of this method, you can get hold of instances of the + /// super-superclass when needed. + /// + /// # Examples + /// ``` + /// # use pyo3::prelude::*; + /// # use pyo3::PyClassGuard; + /// #[pyclass(subclass)] + /// struct Base1 { + /// name1: &'static str, + /// } + /// + /// #[pyclass(extends=Base1, subclass)] + /// struct Base2 { + /// name2: &'static str, + /// } + /// + /// #[pyclass(extends=Base2)] + /// struct Sub { + /// name3: &'static str, + /// } + /// + /// #[pymethods] + /// impl Sub { + /// #[new] + /// fn new() -> PyClassInitializer { + /// PyClassInitializer::from(Base1 { name1: "base1" }) + /// .add_subclass(Base2 { name2: "base2" }) + /// .add_subclass(Self { name3: "sub" }) + /// } + /// fn name(slf: PyClassGuard<'_, Self>) -> String { + /// let subname = slf.name3; + /// let super_ = slf.into_super(); + /// format!("{} {} {}", super_.as_super().name1, super_.name2, subname) + /// } + /// } + /// # Python::attach(|py| { + /// # let sub = Py::new(py, Sub::new()).unwrap(); + /// # pyo3::py_run!(py, sub, "assert sub.name() == 'base1 base2 sub'") + /// # }); + /// ``` + pub fn into_super(self) -> PyClassGuard<'a, U> { + let t_not_frozen = !::VALUE; + let u_frozen = ::VALUE; + if t_not_frozen && u_frozen { + // If `T` is a mutable subclass of a frozen `U` base, then it is possible that we need + // to release the borrow count now. (e.g. `U` may have a noop borrow checker so dropping + // the `PyRef` later would noop and leak the borrow we currently hold.) + // + // However it's nontrivial, if `U` is frozen but itself has a mutable base class `V`, + // then the borrow checker of both `T` and `U` is the shared borrow checker of `V`. + // + // But it's really hard to prove that in the type system, the soundest thing we can do + // is just add a borrow to `U` now and then release the borrow of `T`. + + self.as_super() + .as_class_object() + .borrow_checker() + .try_borrow() + .expect("this object is already borrowed"); + + self.as_class_object().borrow_checker().release_borrow() + }; + PyClassGuard { + ptr: std::mem::ManuallyDrop::new(self).ptr, + marker: PhantomData, + } + } +} + +impl Deref for PyClassGuard<'_, T> { + type Target = T; + + #[inline] + fn deref(&self) -> &T { + // SAFETY: `PyClassObject` constains a valid `T`, by construction no + // mutable alias is enforced + unsafe { &*self.as_class_object().get_ptr().cast_const() } + } +} + +impl<'a, 'py, T: PyClass> FromPyObjectBound<'a, 'py> for PyClassGuard<'a, T> { + fn from_py_object_bound(obj: Borrowed<'a, 'py, crate::PyAny>) -> crate::PyResult { + Self::try_from_class_object(obj.downcast()?.get_class_object()).map_err(Into::into) + } +} + +impl<'a, 'py, T: PyClass> IntoPyObject<'py> for PyClassGuard<'a, T> { + type Target = T; + type Output = Borrowed<'a, 'py, T>; + type Error = Infallible; + + #[inline] + fn into_pyobject(self, py: crate::Python<'py>) -> Result { + (&self).into_pyobject(py) + } +} + +impl<'a, 'py, T: PyClass> IntoPyObject<'py> for &PyClassGuard<'a, T> { + type Target = T; + type Output = Borrowed<'a, 'py, T>; + type Error = Infallible; + + #[inline] + fn into_pyobject(self, py: crate::Python<'py>) -> Result { + // SAFETY: `ptr` is guaranteed to be valid for 'a and points to an + // object of type T + unsafe { Ok(Borrowed::from_non_null(py, self.ptr).downcast_unchecked()) } + } +} + +impl Drop for PyClassGuard<'_, T> { + /// Releases the shared borrow + fn drop(&mut self) { + self.as_class_object().borrow_checker().release_borrow() + } +} + +// SAFETY: `PyClassGuard` only provides access to the inner `T` (and no other +// Python APIs) which does not require a Python thread state +#[cfg(feature = "nightly")] +unsafe impl crate::marker::Ungil for PyClassGuard<'_, T> {} +// SAFETY: we provide access to +// - `&T`, which requires `T: Sync` to be Send and `T: Sync` to be Sync +unsafe impl Send for PyClassGuard<'_, T> {} +unsafe impl Sync for PyClassGuard<'_, T> {} + +/// A wrapper type for a mutably borrowed value from a `PyClass` +/// +/// # When *not* to use [`PyClassGuardMut`] +/// +/// Usually you can use `&mut` references as method and function receivers and +/// arguments, and you won't need to use [`PyClassGuardMut`] directly: +/// +/// ```rust,no_run +/// use pyo3::prelude::*; +/// +/// #[pyclass] +/// struct Number { +/// inner: u32, +/// } +/// +/// #[pymethods] +/// impl Number { +/// fn increment(&mut self) { +/// self.inner += 1; +/// } +/// } +/// ``` +/// +/// The [`#[pymethods]`](crate::pymethods) proc macro will generate this wrapper +/// function (and more), using [`PyClassGuardMut`] under the hood: +/// +/// ```rust,no_run +/// # use pyo3::prelude::*; +/// # #[pyclass] +/// # struct Number { +/// # inner: u32, +/// # } +/// # +/// # #[pymethods] +/// # impl Number { +/// # fn increment(&mut self) { +/// # self.inner += 1; +/// # } +/// # } +/// # +/// // The function which is exported to Python looks roughly like the following +/// unsafe extern "C" fn __pymethod_increment__( +/// _slf: *mut ::pyo3::ffi::PyObject, +/// _args: *mut ::pyo3::ffi::PyObject, +/// ) -> *mut ::pyo3::ffi::PyObject { +/// unsafe fn inner<'py>( +/// py: ::pyo3::Python<'py>, +/// _slf: *mut ::pyo3::ffi::PyObject, +/// ) -> ::pyo3::PyResult<*mut ::pyo3::ffi::PyObject> { +/// let function = Number::increment; +/// # #[allow(clippy::let_unit_value)] +/// let mut holder_0 = ::pyo3::impl_::extract_argument::FunctionArgumentHolder::INIT; +/// let result = { +/// let ret = function(::pyo3::impl_::extract_argument::extract_pyclass_ref_mut::( +/// unsafe { ::pyo3::impl_::pymethods::BoundRef::ref_from_ptr(py, &_slf) }.0, +/// &mut holder_0, +/// )?); +/// { +/// let result = { +/// let obj = ret; +/// # #[allow(clippy::useless_conversion)] +/// ::pyo3::impl_::wrap::converter(&obj) +/// .wrap(obj) +/// .map_err(::core::convert::Into::<::pyo3::PyErr>::into) +/// }; +/// ::pyo3::impl_::wrap::converter(&result).map_into_ptr(py, result) +/// } +/// }; +/// result +/// } +/// +/// unsafe { +/// ::pyo3::impl_::trampoline::noargs( +/// _slf, +/// _args, +/// inner, +/// ) +/// } +/// } +/// ``` +/// +/// # When to use [`PyClassGuardMut`] +/// ## Using PyClasses from Rust +/// +/// However, we *do* need [`PyClassGuardMut`] if we want to call its methods +/// from Rust: +/// ```rust +/// # use pyo3::prelude::*; +/// # use pyo3::{PyClassGuard, PyClassGuardMut}; +/// # +/// # #[pyclass] +/// # struct Number { +/// # inner: u32, +/// # } +/// # +/// # #[pymethods] +/// # impl Number { +/// # fn increment(&mut self) { +/// # self.inner += 1; +/// # } +/// # } +/// # fn main() -> PyResult<()> { +/// Python::attach(|py| { +/// let n = Py::new(py, Number { inner: 0 })?; +/// +/// // We borrow the guard and then dereference +/// // it to get a mutable reference to Number +/// let mut guard: PyClassGuardMut<'_, Number> = n.extract(py)?; +/// let n_mutable: &mut Number = &mut *guard; +/// +/// n_mutable.increment(); +/// +/// // To avoid panics we must dispose of the +/// // `PyClassGuardMut` before borrowing again. +/// drop(guard); +/// +/// let n_immutable: &Number = &*n.extract::>(py)?; +/// assert_eq!(n_immutable.inner, 1); +/// +/// Ok(()) +/// }) +/// # } +/// ``` +/// ## Dealing with possibly overlapping mutable references +/// +/// It is also necessary to use [`PyClassGuardMut`] if you can receive mutable +/// arguments that may overlap. Suppose the following function that swaps the +/// values of two `Number`s: +/// ``` +/// # use pyo3::prelude::*; +/// # #[pyclass] +/// # pub struct Number { +/// # inner: u32, +/// # } +/// #[pyfunction] +/// fn swap_numbers(a: &mut Number, b: &mut Number) { +/// std::mem::swap(&mut a.inner, &mut b.inner); +/// } +/// # fn main() { +/// # Python::attach(|py| { +/// # let n = Py::new(py, Number{inner: 35}).unwrap(); +/// # let n2 = n.clone_ref(py); +/// # assert!(n.is(&n2)); +/// # let fun = pyo3::wrap_pyfunction!(swap_numbers, py).unwrap(); +/// # fun.call1((n, n2)).expect_err("Managed to create overlapping mutable references. Note: this is undefined behaviour."); +/// # }); +/// # } +/// ``` +/// When users pass in the same `Number` as both arguments, one of the mutable +/// borrows will fail and raise a `RuntimeError`: +/// ```text +/// >>> a = Number() +/// >>> swap_numbers(a, a) +/// Traceback (most recent call last): +/// File "", line 1, in +/// RuntimeError: Already borrowed +/// ``` +/// +/// It is better to write that function like this: +/// ```rust +/// # use pyo3::prelude::*; +/// # use pyo3::{PyClassGuard, PyClassGuardMut}; +/// # #[pyclass] +/// # pub struct Number { +/// # inner: u32, +/// # } +/// #[pyfunction] +/// fn swap_numbers(a: &Bound<'_, Number>, b: &Bound<'_, Number>) -> PyResult<()> { +/// // Check that the pointers are unequal +/// if !a.is(b) { +/// let mut a: PyClassGuardMut<'_, Number> = a.extract()?; +/// let mut b: PyClassGuardMut<'_, Number> = b.extract()?; +/// std::mem::swap(&mut a.inner, &mut b.inner); +/// } else { +/// // Do nothing - they are the same object, so don't need swapping. +/// } +/// Ok(()) +/// } +/// # fn main() { +/// # // With duplicate numbers +/// # Python::attach(|py| { +/// # let n = Py::new(py, Number{inner: 35}).unwrap(); +/// # let n2 = n.clone_ref(py); +/// # assert!(n.is(&n2)); +/// # let fun = pyo3::wrap_pyfunction!(swap_numbers, py).unwrap(); +/// # fun.call1((n, n2)).unwrap(); +/// # }); +/// # +/// # // With two different numbers +/// # Python::attach(|py| { +/// # let n = Py::new(py, Number{inner: 35}).unwrap(); +/// # let n2 = Py::new(py, Number{inner: 42}).unwrap(); +/// # assert!(!n.is(&n2)); +/// # let fun = pyo3::wrap_pyfunction!(swap_numbers, py).unwrap(); +/// # fun.call1((&n, &n2)).unwrap(); +/// # let n: u32 = n.extract::>(py).unwrap().inner; +/// # let n2: u32 = n2.extract::>(py).unwrap().inner; +/// # assert_eq!(n, 42); +/// # assert_eq!(n2, 35); +/// # }); +/// # } +/// ``` +/// See [`PyClassGuard`] and the [guide] for more information. +/// +/// [guide]: https://pyo3.rs/latest/class.html#bound-and-interior-mutability +/// "Bound and interior mutability" +#[repr(transparent)] +pub struct PyClassGuardMut<'a, T: PyClass> { + ptr: NonNull, + marker: PhantomData<&'a Py>, +} + +impl<'a, T: PyClass> PyClassGuardMut<'a, T> { + pub(crate) fn try_borrow_mut(obj: &'a Py) -> Result { + Self::try_from_class_object(obj.get_class_object()) + } + + fn try_from_class_object(obj: &'a PyClassObject) -> Result { + obj.ensure_threadsafe(); + obj.borrow_checker().try_borrow_mut().map(|_| Self { + ptr: NonNull::from(obj).cast(), + marker: PhantomData, + }) + } + + pub(crate) fn as_class_object(&self) -> &'a PyClassObject { + // SAFETY: `ptr` by construction points to a `PyClassObject` and is + // valid for at least 'a + unsafe { self.ptr.cast().as_ref() } + } +} + +impl<'a, T, U> PyClassGuardMut<'a, T> +where + T: PyClass, + U: PyClass, +{ + /// Borrows a mutable reference to `PyClassGuardMut`. + /// + /// With the help of this method, you can mutate attributes and call + /// mutating methods on the superclass without consuming the + /// `PyClassGuardMut`. This method can also be chained to access the + /// super-superclass (and so on). + /// + /// See [`PyClassGuard::as_super`] for more. + pub fn as_super(&mut self) -> &mut PyClassGuardMut<'a, U> { + // SAFETY: `PyClassGuardMut` and `PyClassGuardMut` have the same layout + unsafe { NonNull::from(self).cast().as_mut() } + } + + /// Gets a `PyClassGuardMut`. + /// + /// See [`PyClassGuard::into_super`] for more. + pub fn into_super(self) -> PyClassGuardMut<'a, U> { + // `PyClassGuardMut` is only available for non-frozen classes, so there + // is no possibility of leaking borrows like `PyClassGuard` + PyClassGuardMut { + ptr: std::mem::ManuallyDrop::new(self).ptr, + marker: PhantomData, + } + } +} + +impl> Deref for PyClassGuardMut<'_, T> { + type Target = T; + + #[inline] + fn deref(&self) -> &T { + // SAFETY: `PyClassObject` constains a valid `T`, by construction no + // alias is enforced + unsafe { &*self.as_class_object().get_ptr().cast_const() } + } +} +impl> DerefMut for PyClassGuardMut<'_, T> { + #[inline] + fn deref_mut(&mut self) -> &mut T { + // SAFETY: `PyClassObject` constains a valid `T`, by construction no + // alias is enforced + unsafe { &mut *self.as_class_object().get_ptr() } + } +} + +impl<'a, 'py, T: PyClass> FromPyObjectBound<'a, 'py> for PyClassGuardMut<'a, T> { + fn from_py_object_bound(obj: Borrowed<'a, 'py, crate::PyAny>) -> crate::PyResult { + Self::try_from_class_object(obj.downcast()?.get_class_object()).map_err(Into::into) + } +} + +impl<'a, 'py, T: PyClass> IntoPyObject<'py> for PyClassGuardMut<'a, T> { + type Target = T; + type Output = Borrowed<'a, 'py, T>; + type Error = Infallible; + + #[inline] + fn into_pyobject(self, py: crate::Python<'py>) -> Result { + (&self).into_pyobject(py) + } +} + +impl<'a, 'py, T: PyClass> IntoPyObject<'py> for &PyClassGuardMut<'a, T> { + type Target = T; + type Output = Borrowed<'a, 'py, T>; + type Error = Infallible; + + #[inline] + fn into_pyobject(self, py: crate::Python<'py>) -> Result { + // SAFETY: `ptr` is guaranteed to be valid for 'a and points to an + // object of type T + unsafe { Ok(Borrowed::from_non_null(py, self.ptr).downcast_unchecked()) } + } +} + +impl> Drop for PyClassGuardMut<'_, T> { + /// Releases the mutable borrow + fn drop(&mut self) { + self.as_class_object().borrow_checker().release_borrow_mut() + } +} + +// SAFETY: `PyClassGuardMut` only provides access to the inner `T` (and no other +// Python APIs) which does not require a Python thread state +#[cfg(feature = "nightly")] +unsafe impl> crate::marker::Ungil for PyClassGuardMut<'_, T> {} +// SAFETY: we provide access to +// - `&T`, which requires `T: Sync` to be Send and `T: Sync` to be Sync +// - `&mut T`, which requires `T: Send` to be Send and `T: Sync` to be Sync +unsafe impl + Send + Sync> Send for PyClassGuardMut<'_, T> {} +unsafe impl + Sync> Sync for PyClassGuardMut<'_, T> {} + +#[cfg(test)] +#[cfg(feature = "macros")] +mod tests { + use super::{PyClassGuard, PyClassGuardMut}; + use crate::{types::PyAnyMethods as _, IntoPyObject as _, Py, PyErr, Python}; + + #[test] + fn test_into_frozen_super_released_borrow() { + #[crate::pyclass] + #[pyo3(crate = "crate", subclass, frozen)] + struct BaseClass {} + + #[crate::pyclass] + #[pyo3(crate = "crate", extends=BaseClass, subclass)] + struct SubClass {} + + #[crate::pymethods] + #[pyo3(crate = "crate")] + impl SubClass { + #[new] + fn new(py: Python<'_>) -> Py { + let init = crate::PyClassInitializer::from(BaseClass {}).add_subclass(SubClass {}); + Py::new(py, init).expect("allocation error") + } + } + + Python::attach(|py| { + let obj = SubClass::new(py); + drop(PyClassGuard::try_borrow(&obj).unwrap().into_super()); + assert!(PyClassGuardMut::try_borrow_mut(&obj).is_ok()); + }) + } + + #[test] + fn test_into_frozen_super_mutable_base_holds_borrow() { + #[crate::pyclass] + #[pyo3(crate = "crate", subclass)] + struct BaseClass {} + + #[crate::pyclass] + #[pyo3(crate = "crate", extends=BaseClass, subclass, frozen)] + struct SubClass {} + + #[crate::pyclass] + #[pyo3(crate = "crate", extends=SubClass, subclass)] + struct SubSubClass {} + + #[crate::pymethods] + #[pyo3(crate = "crate")] + impl SubSubClass { + #[new] + fn new(py: Python<'_>) -> Py { + let init = crate::PyClassInitializer::from(BaseClass {}) + .add_subclass(SubClass {}) + .add_subclass(SubSubClass {}); + Py::new(py, init).expect("allocation error") + } + } + + Python::attach(|py| { + let obj = SubSubClass::new(py); + let _super_borrow = PyClassGuard::try_borrow(&obj).unwrap().into_super(); + // the whole object still has an immutable borrow, so we cannot + // borrow any part mutably (the borrowflag is shared) + assert!(PyClassGuardMut::try_borrow_mut(&obj).is_err()); + }) + } + + #[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 { + #[pyo3(get)] + val3: usize, + } + + #[crate::pymethods] + #[pyo3(crate = "crate")] + impl SubSubClass { + #[new] + fn new(py: Python<'_>) -> Py { + let init = crate::PyClassInitializer::from(BaseClass { val1: 10 }) + .add_subclass(SubClass { val2: 15 }) + .add_subclass(SubSubClass { val3: 20 }); + Py::new(py, init).expect("allocation error") + } + + fn get_values(self_: PyClassGuard<'_, 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_: PyClassGuardMut<'_, Self>) { + self_.as_super().as_super().val1 *= 2; + self_.as_super().val2 *= 2; + self_.val3 *= 2; + } + + fn __add__<'a>( + mut slf: PyClassGuardMut<'a, Self>, + other: PyClassGuard<'a, Self>, + ) -> PyClassGuardMut<'a, Self> { + slf.val3 += other.val3; + slf + } + + fn __rsub__<'a>( + slf: PyClassGuard<'a, Self>, + mut other: PyClassGuardMut<'a, Self>, + ) -> PyClassGuardMut<'a, Self> { + other.val3 -= slf.val3; + other + } + } + + #[test] + fn test_pyclassguard_into_pyobject() { + Python::attach(|py| { + let class = Py::new(py, BaseClass { val1: 42 })?; + let guard = PyClassGuard::try_borrow(&class).unwrap(); + let new_ref = (&guard).into_pyobject(py)?; + assert!(new_ref.is(&class)); + let new = guard.into_pyobject(py)?; + assert!(new.is(&class)); + Ok::<_, PyErr>(()) + }) + .unwrap(); + } + + #[test] + fn test_pyclassguardmut_into_pyobject() { + Python::attach(|py| { + let class = Py::new(py, BaseClass { val1: 42 })?; + let guard = PyClassGuardMut::try_borrow_mut(&class).unwrap(); + let new_ref = (&guard).into_pyobject(py)?; + assert!(new_ref.is(&class)); + let new = guard.into_pyobject(py)?; + assert!(new.is(&class)); + Ok::<_, PyErr>(()) + }) + .unwrap(); + } + #[test] + fn test_pyclassguard_as_super() { + Python::attach(|py| { + let obj = SubSubClass::new(py).into_bound(py); + let pyref = PyClassGuard::try_borrow(obj.as_unbound()).unwrap(); + assert_eq!(pyref.as_super().as_super().val1, 10); + assert_eq!(pyref.as_super().val2, 15); + assert_eq!(pyref.val3, 20); + assert_eq!(SubSubClass::get_values(pyref), (10, 15, 20)); + }); + } + + #[test] + fn test_pyclassguardmut_as_super() { + Python::attach(|py| { + let obj = SubSubClass::new(py).into_bound(py); + assert_eq!( + SubSubClass::get_values(PyClassGuard::try_borrow(obj.as_unbound()).unwrap()), + (10, 15, 20) + ); + { + let mut pyrefmut = PyClassGuardMut::try_borrow_mut(obj.as_unbound()).unwrap(); + assert_eq!(pyrefmut.as_super().as_super().val1, 10); + pyrefmut.as_super().as_super().val1 -= 5; + pyrefmut.as_super().val2 -= 5; + pyrefmut.val3 -= 5; + } + assert_eq!( + SubSubClass::get_values(PyClassGuard::try_borrow(obj.as_unbound()).unwrap()), + (5, 10, 15) + ); + SubSubClass::double_values(PyClassGuardMut::try_borrow_mut(obj.as_unbound()).unwrap()); + assert_eq!( + SubSubClass::get_values(PyClassGuard::try_borrow(obj.as_unbound()).unwrap()), + (10, 20, 30) + ); + }); + } + + #[test] + fn test_extract_guard() { + Python::attach(|py| { + let obj1 = SubSubClass::new(py); + let obj2 = SubSubClass::new(py); + crate::py_run!(py, obj1 obj2, "assert ((obj1 + obj2) - obj2).val3 == obj1.val3"); + }); + } + + #[test] + fn test_pyclassguards_in_python() { + Python::attach(|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)"); + }); + } +} diff --git a/tests/ui/invalid_cancel_handle.stderr b/tests/ui/invalid_cancel_handle.stderr index ca9274f1785..930a9e487c0 100644 --- a/tests/ui/invalid_cancel_handle.stderr +++ b/tests/ui/invalid_cancel_handle.stderr @@ -22,7 +22,7 @@ error: `from_py_with` and `cancel_handle` cannot be specified together 24 | #[pyo3(cancel_handle, from_py_with = cancel_handle)] _param: pyo3::coroutine::CancelHandle, | ^^^^^^^^^^^^^ -error[E0277]: the trait bound `CancelHandle: PyFunctionArgument<'_, '_, false>` 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) {} @@ -31,22 +31,22 @@ error[E0277]: the trait bound `CancelHandle: PyFunctionArgument<'_, '_, false>` = 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<'_, '_, false>` + = note: required for `CancelHandle` to implement `PyFunctionArgument<'_, '_, '_, false>` -error[E0277]: the trait bound `CancelHandle: PyFunctionArgument<'_, '_, false>` 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, 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>` + = help: the following other types implement trait `PyFunctionArgument<'a, 'holder, 'py, IS_OPTION>`: + `&'a pyo3::Bound<'py, T>` implements `PyFunctionArgument<'a, 'holder, 'py, false>` + `&'holder mut pyo3::coroutine::Coroutine` implements `PyFunctionArgument<'a, 'holder, 'py, false>` + `&'holder pyo3::coroutine::Coroutine` implements `PyFunctionArgument<'a, 'holder, 'py, false>` + `Option` implements `PyFunctionArgument<'a, 'holder, 'py, true>` = note: required for `CancelHandle` to implement `FromPyObject<'_>` = note: required for `CancelHandle` to implement `FromPyObjectBound<'_, '_>` - = note: required for `CancelHandle` to implement `PyFunctionArgument<'_, '_, false>` + = note: required for `CancelHandle` to implement `PyFunctionArgument<'_, '_, '_, false>` error[E0308]: mismatched types --> tests/ui/invalid_cancel_handle.rs:16:1 @@ -64,7 +64,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<'_, '_, false>` 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) {} @@ -73,35 +73,35 @@ error[E0277]: the trait bound `CancelHandle: PyFunctionArgument<'_, '_, false>` = 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<'_, '_, false>` + = 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, const IS_OPTION: bool>( + | pub fn extract_argument<'a, 'holder, 'py, T, const IS_OPTION: bool>( | ---------------- required by a bound in this function ... - | T: PyFunctionArgument<'a, 'py, IS_OPTION>, - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `extract_argument` + | T: PyFunctionArgument<'a, 'holder, 'py, IS_OPTION>, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `extract_argument` -error[E0277]: the trait bound `CancelHandle: PyFunctionArgument<'_, '_, false>` 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, 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>` + = help: the following other types implement trait `PyFunctionArgument<'a, 'holder, 'py, IS_OPTION>`: + `&'a pyo3::Bound<'py, T>` implements `PyFunctionArgument<'a, 'holder, 'py, false>` + `&'holder mut pyo3::coroutine::Coroutine` implements `PyFunctionArgument<'a, 'holder, 'py, false>` + `&'holder pyo3::coroutine::Coroutine` implements `PyFunctionArgument<'a, 'holder, 'py, false>` + `Option` implements `PyFunctionArgument<'a, 'holder, 'py, true>` = note: required for `CancelHandle` to implement `FromPyObject<'_>` = note: required for `CancelHandle` to implement `FromPyObjectBound<'_, '_>` - = note: required for `CancelHandle` to implement `PyFunctionArgument<'_, '_, false>` + = 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, const IS_OPTION: bool>( + | pub fn extract_argument<'a, 'holder, 'py, T, const IS_OPTION: bool>( | ---------------- required by a bound in this function ... - | T: PyFunctionArgument<'a, 'py, IS_OPTION>, - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `extract_argument` + | T: PyFunctionArgument<'a, 'holder, 'py, IS_OPTION>, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `extract_argument` diff --git a/tests/ui/invalid_frozen_pyclass_borrow.stderr b/tests/ui/invalid_frozen_pyclass_borrow.stderr index 9e86e8f3b3e..b7a06dc9c67 100644 --- a/tests/ui/invalid_frozen_pyclass_borrow.stderr +++ b/tests/ui/invalid_frozen_pyclass_borrow.stderr @@ -18,8 +18,8 @@ note: expected this to be `False` 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` + | pub fn extract_pyclass_ref_mut<'a, 'holder, 'py, T: PyClass>( + | ^^^^^^^^^^^^^^ required by this bound in `extract_pyclass_ref_mut` = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0271]: type mismatch resolving `::Frozen == False` @@ -33,11 +33,11 @@ note: expected this to be `False` | 3 | #[pyclass(frozen)] | ^^^^^^^^^^^^^^^^^^ -note: required by a bound in `PyRefMut` - --> src/pycell.rs +note: required by a bound in `PyClassGuardMut` + --> src/pyclass/guard.rs | - | pub struct PyRefMut<'p, T: PyClass> { - | ^^^^^^^^^^^^^^ required by this bound in `PyRefMut` + | pub struct PyClassGuardMut<'a, T: PyClass> { + | ^^^^^^^^^^^^^^ required by this bound in `PyClassGuardMut` = note: this error originates in the attribute macro `pymethods` which comes from the expansion of the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0271]: type mismatch resolving `::Frozen == False` diff --git a/tests/ui/invalid_pymethod_enum.stderr b/tests/ui/invalid_pymethod_enum.stderr index 1bc513abc6a..e93f8fe64ca 100644 --- a/tests/ui/invalid_pymethod_enum.stderr +++ b/tests/ui/invalid_pymethod_enum.stderr @@ -12,8 +12,8 @@ note: expected this to be `False` 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` + | pub fn extract_pyclass_ref_mut<'a, 'holder, 'py, T: PyClass>( + | ^^^^^^^^^^^^^^ required by this bound in `extract_pyclass_ref_mut` = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0271]: type mismatch resolving `::Frozen == False` @@ -27,11 +27,11 @@ note: expected this to be `False` | 3 | #[pyclass] | ^^^^^^^^^^ -note: required by a bound in `PyRefMut` - --> src/pycell.rs +note: required by a bound in `PyClassGuardMut` + --> src/pyclass/guard.rs | - | pub struct PyRefMut<'p, T: PyClass> { - | ^^^^^^^^^^^^^^ required by this bound in `PyRefMut` + | pub struct PyClassGuardMut<'a, T: PyClass> { + | ^^^^^^^^^^^^^^ required by this bound in `PyClassGuardMut` = note: this error originates in the attribute macro `pymethods` which comes from the expansion of the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0271]: type mismatch resolving `::Frozen == False` @@ -48,8 +48,8 @@ note: expected this to be `False` 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` + | pub fn extract_pyclass_ref_mut<'a, 'holder, 'py, T: PyClass>( + | ^^^^^^^^^^^^^^ required by this bound in `extract_pyclass_ref_mut` = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0271]: type mismatch resolving `::Frozen == False` @@ -63,9 +63,9 @@ note: expected this to be `False` | 19 | #[pyclass] | ^^^^^^^^^^ -note: required by a bound in `PyRefMut` - --> src/pycell.rs +note: required by a bound in `PyClassGuardMut` + --> src/pyclass/guard.rs | - | pub struct PyRefMut<'p, T: PyClass> { - | ^^^^^^^^^^^^^^ required by this bound in `PyRefMut` + | pub struct PyClassGuardMut<'a, T: PyClass> { + | ^^^^^^^^^^^^^^ required by this bound in `PyClassGuardMut` = note: this error originates in the attribute macro `pymethods` which comes from the expansion of the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/invalid_pymethods.stderr b/tests/ui/invalid_pymethods.stderr index 34cd22992e5..66acc374f63 100644 --- a/tests/ui/invalid_pymethods.stderr +++ b/tests/ui/invalid_pymethods.stderr @@ -166,7 +166,7 @@ error: `pass_module` cannot be used on Python methods | ^^^^^^^^^^^ error: Python objects are shared, so 'self' cannot be moved out of the Python interpreter. - Try `&self`, `&mut self, `slf: PyRef<'_, Self>` or `slf: PyRefMut<'_, Self>`. + Try `&self`, `&mut self, `slf: PyClassGuard<'_, Self>` or `slf: PyClassGuardMut<'_, Self>`. --> tests/ui/invalid_pymethods.rs:151:29 | 151 | fn method_self_by_value(self) {} From 1fb38617f0e9954a11a84950efefe5357766f905 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Wed, 6 Aug 2025 22:28:58 +0100 Subject: [PATCH 773/936] docs: fix link to pyo3 function options (#5295) --- pyo3-macros/src/lib.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pyo3-macros/src/lib.rs b/pyo3-macros/src/lib.rs index 5d16a28a97f..c2e42faf61b 100644 --- a/pyo3-macros/src/lib.rs +++ b/pyo3-macros/src/lib.rs @@ -85,12 +85,12 @@ pub fn pyclass(attr: TokenStream, input: TokenStream) -> TokenStream { /// | Annotation | Description | /// | :- | :- | /// | [`#[new]`][4] | Defines the class constructor, like Python's `__new__` method. | -/// | [`#[getter]`][5] and [`#[setter]`][5] | These define getters and setters, similar to Python's `@property` decorator. This is useful for getters/setters that require computation or side effects; if that is not the case consider using [`#[pyo3(get, set)]`][11] on the struct's field(s).| +/// | [`#[getter]`][5] and [`#[setter]`][5] | These define getters and setters, similar to Python's `@property` decorator. This is useful for getters/setters that require computation or side effects; if that is not the case consider using [`#[pyo3(get, set)]`][12] on the struct's field(s).| /// | [`#[staticmethod]`][6]| Defines the method as a staticmethod, like Python's `@staticmethod` decorator.| /// | [`#[classmethod]`][7] | Defines the method as a classmethod, like Python's `@classmethod` decorator.| /// | [`#[classattr]`][9] | Defines a class variable. | /// | [`#[args]`][10] | Deprecated way to define a method's default arguments and allows the function to receive `*args` and `**kwargs`. Use `#[pyo3(signature = (...))]` instead. | -/// | [`#[pyo3( | Any of the `#[pyo3]` options supported on [`macro@pyfunction`]. | +/// | [`#[pyo3( | Any of the `#[pyo3]` options supported on [`macro@pyfunction`]. | /// /// For more on creating class methods, /// see the [class section of the guide][1]. @@ -109,7 +109,8 @@ pub fn pyclass(attr: TokenStream, input: TokenStream) -> TokenStream { #[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")] +#[doc = concat!("[11]: https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/function.html#function-options")] +#[doc = concat!("[12]: 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") { From bfbcb6e4e5e1094e91c9c5c4ba90812a791f219c Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Thu, 7 Aug 2025 00:19:33 +0100 Subject: [PATCH 774/936] ci: fix clippy beta incompatible msrv warning (#5296) --- src/err/impls.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/err/impls.rs b/src/err/impls.rs index dac01e32da5..d763ba93d80 100644 --- a/src/err/impls.rs +++ b/src/err/impls.rs @@ -30,6 +30,7 @@ impl From for io::Error { io::ErrorKind::OutOfMemory } else { #[cfg(io_error_more)] + #[allow(clippy::incompatible_msrv)] // gated by `io_error_more` if err.is_instance_of::(py) { io::ErrorKind::IsADirectory } else if err.is_instance_of::(py) { From cb0a209e891e5ff19b2d12035ac57e51615533c4 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Thu, 7 Aug 2025 06:21:08 +0100 Subject: [PATCH 775/936] fix crash when dropping `PyBuffer` after interpreter finalized (#5242) * fix crash when dropping `PyBuffer` after interpreter finalized * newsfragment * fixup CI * fixup test cfgs * add more test coverage * fix cfg combinations --- newsfragments/5242.fixed.md | 1 + src/buffer.rs | 14 ++++- src/internal/state.rs | 32 ++++++++++++ src/marker.rs | 43 ++++++++++++++- tests/test_gc.rs | 52 +++++++++++++++++++ .../test_pybuffer_drop_without_interpreter.rs | 34 ++++++++++++ 6 files changed, 174 insertions(+), 2 deletions(-) create mode 100644 newsfragments/5242.fixed.md create mode 100644 tests/test_pybuffer_drop_without_interpreter.rs diff --git a/newsfragments/5242.fixed.md b/newsfragments/5242.fixed.md new file mode 100644 index 00000000000..04210360e0c --- /dev/null +++ b/newsfragments/5242.fixed.md @@ -0,0 +1 @@ +Fix segfault when dropping `PyBuffer` after the Python interpreter has been finalized. diff --git a/src/buffer.rs b/src/buffer.rs index 6bef44cfa21..fe824b69450 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -622,7 +622,19 @@ impl PyBuffer { impl Drop for PyBuffer { fn drop(&mut self) { - Python::attach(|_| unsafe { ffi::PyBuffer_Release(&mut *self.0) }); + fn inner(buf: &mut Pin>) { + if Python::try_attach(|_| unsafe { ffi::PyBuffer_Release(&mut **buf) }).is_none() + && crate::internal::state::is_in_gc_traversal() + { + eprintln!("Warning: PyBuffer dropped while in GC traversal, this is a bug and will leak memory."); + } + // If `try_attach` failed and `is_in_gc_traversal()` is false, then probably the interpreter has + // already finalized and we can just assume that the underlying memory has already been freed. + // + // So we don't handle that case here. + } + + inner(&mut self.0); } } diff --git a/src/internal/state.rs b/src/internal/state.rs index 912434cb7c9..8095071af88 100644 --- a/src/internal/state.rs +++ b/src/internal/state.rs @@ -61,6 +61,30 @@ impl AttachGuard { unsafe { Self::acquire_unchecked() } } + /// Variant of the above which will will return `None` if the interpreter cannot be attached to. + #[cfg(any(not(Py_LIMITED_API), Py_3_11, test))] // see Python::try_attach + pub(crate) fn try_acquire() -> Option { + match ATTACH_COUNT.try_with(|c| c.get()) { + Ok(i) if i > 0 => { + // SAFETY: We just checked that the thread is already attached. + return Some(unsafe { Self::assume() }); + } + // Cannot attach during GC traversal. + Ok(ATTACH_FORBIDDEN_DURING_TRAVERSE) => return None, + // other cases handled below + _ => {} + } + + // SAFETY: This API is always sound to call + if unsafe { ffi::Py_IsInitialized() } == 0 { + // If the interpreter is not initialized, we cannot attach. + return None; + } + + // SAFETY: We have ensured the Python interpreter is initialized. + Some(unsafe { Self::acquire_unchecked() }) + } + /// Acquires the `AttachGuard` without performing any state checking. /// /// This can be called in "unsafe" contexts where the normal interpreter state @@ -275,6 +299,14 @@ pub unsafe fn register_decref(obj: NonNull) { } } +/// Private helper function to check if we are currently in a GC traversal (as detected by PyO3). +#[cfg(any(not(Py_LIMITED_API), Py_3_11))] +pub(crate) fn is_in_gc_traversal() -> bool { + ATTACH_COUNT + .try_with(|c| c.get() == ATTACH_FORBIDDEN_DURING_TRAVERSE) + .unwrap_or(false) +} + /// Increments pyo3's internal attach count - to be called whenever an AttachGuard is created. #[inline(always)] fn increment_attach_count() { diff --git a/src/marker.rs b/src/marker.rs index f68d6161419..c36cd7111e8 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -419,6 +419,22 @@ impl Python<'_> { f(guard.python()) } + /// Variant of [`Python::attach`] which will do no work if the interpreter is in a + /// state where it cannot be attached to: + /// - in the middle of GC traversal + /// - not initialized + #[inline] + #[track_caller] + #[cfg(any(not(Py_LIMITED_API), Py_3_11, test))] // only used in buffer.rs for now, allow in test cfg for simplicity + // TODO: make this API public? + pub(crate) fn try_attach(f: F) -> Option + where + F: for<'py> FnOnce(Python<'py>) -> R, + { + let guard = AttachGuard::try_acquire()?; + Some(f(guard.python())) + } + /// Prepares the use of Python. /// /// If the Python interpreter is not already initialized, this function will initialize it with @@ -805,7 +821,10 @@ impl<'unbound> Python<'unbound> { #[cfg(test)] mod tests { use super::*; - use crate::types::{IntoPyDict, PyList}; + use crate::{ + internal::state::ForbidAttaching, + types::{IntoPyDict, PyList}, + }; #[test] fn test_eval() { @@ -1012,4 +1031,26 @@ cls.func() fn python_is_zst() { assert_eq!(std::mem::size_of::>(), 0); } + + #[test] + fn test_try_attach_fail_during_gc() { + Python::attach(|_| { + assert!(Python::try_attach(|_| {}).is_some()); + + let guard = ForbidAttaching::during_traverse(); + assert!(Python::try_attach(|_| {}).is_none()); + drop(guard); + + assert!(Python::try_attach(|_| {}).is_some()); + }) + } + + #[test] + fn test_try_attach_ok_when_detached() { + Python::attach(|py| { + py.detach(|| { + assert!(Python::try_attach(|_| {}).is_some()); + }); + }); + } } diff --git a/tests/test_gc.rs b/tests/test_gc.rs index e8a608ff00c..284b1eae863 100644 --- a/tests/test_gc.rs +++ b/tests/test_gc.rs @@ -739,3 +739,55 @@ extern "C" fn visit_error( ) -> std::os::raw::c_int { -1 } + +#[test] +#[cfg(any(not(Py_LIMITED_API), Py_3_11))] // buffer availability +fn test_drop_buffer_during_traversal_without_gil() { + use pyo3::buffer::PyBuffer; + use pyo3::types::PyBytes; + + // `PyBuffer` has a drop method which attempts to attach to the Python interpreter, + // if the thread is during traverse we leak it for safety. This should _never_ be happening + // so it's purely a user bug, but we leak to be safe. + + #[pyclass] + struct BufferDropDuringTraversal { + inner: Mutex)>>, + cycle: Option, + } + + #[pymethods] + impl BufferDropDuringTraversal { + #[allow(clippy::unnecessary_wraps)] + fn __traverse__(&self, _visit: PyVisit<'_>) -> Result<(), PyTraverseError> { + self.inner.lock().unwrap().take(); + Ok(()) + } + + fn __clear__(&mut self) { + self.cycle = None; + } + } + + let (guard, check) = drop_check(); + Python::attach(|py| { + let obj = Py::new( + py, + BufferDropDuringTraversal { + inner: Mutex::new(Some(( + guard, + PyBuffer::get(&PyBytes::new(py, b"test")).unwrap(), + ))), + cycle: None, + }, + ) + .unwrap(); + + obj.borrow_mut(py).cycle = Some(obj.clone_ref(py).into_any()); + + let ptr = obj.as_ptr(); + drop(obj); + + check.assert_drops_with_gc(ptr); + }); +} diff --git a/tests/test_pybuffer_drop_without_interpreter.rs b/tests/test_pybuffer_drop_without_interpreter.rs new file mode 100644 index 00000000000..bd1521ae573 --- /dev/null +++ b/tests/test_pybuffer_drop_without_interpreter.rs @@ -0,0 +1,34 @@ +#![cfg(any(not(Py_LIMITED_API), Py_3_11))] // buffer availability +#![cfg(not(any(PyPy, GraalPy)))] // cannot control interpreter lifecycle in PyPy or GraalPy + +//! Dropping `Py` after the interpreter has been finalized should be sound. +//! +//! See e.g. https://github.com/PyO3/pyo3/issues/4632 for an extension of this problem +//! where the interpreter was finalized before `PyBuffer` was dropped. +//! +//! This test runs in its own process to control the interpreter lifecycle. + +use pyo3::{buffer::PyBuffer, types::PyBytes}; + +#[test] +fn test_pybuffer_drop_without_interpreter() { + // SAFETY: this is knowingly unsafe as we're preserving the `Py` object + // after the Python interpreter has been finalized. + // + // However we should still be able to drop it without causing undefined behavior, + // so that process shutdown is sound. + let obj: PyBuffer = unsafe { + pyo3::with_embedded_python_interpreter(|py| { + PyBuffer::get(&PyBytes::new(py, b"abcdef")).unwrap() + }) + }; + + // there should be no interpreter outside of the `with_embedded_python_interpreter` block + assert_eq!(unsafe { pyo3_ffi::Py_IsInitialized() }, 0); + + // dropping object should be sound + drop(obj); + + // dropping object should not re-initialize the interpreter + assert_eq!(unsafe { pyo3_ffi::Py_IsInitialized() }, 0); +} From 9a0007d77a2caab8bc28b2d573344de6360a47b4 Mon Sep 17 00:00:00 2001 From: Thomas Tanon Date: Thu, 7 Aug 2025 10:22:15 +0200 Subject: [PATCH 776/936] Introspect class associated constants (#5272) * Introspect class associated constants Adds to the introspection data the concept of "attribute" that covers constants Merge module associated constants into it Attributes might also be handy to represent other mutable attributes in the future * Code review feedbacks * Sort modules members --- newsfragments/5272.added.md | 2 + pyo3-introspection/src/introspection.rs | 132 +++++++++++++---------- pyo3-introspection/src/model.rs | 10 +- pyo3-introspection/src/stubs.rs | 30 ++++-- pyo3-macros-backend/src/introspection.rs | 98 ++++++++++------- pyo3-macros-backend/src/module.rs | 33 ++++-- pyo3-macros-backend/src/pyimpl.rs | 13 ++- pytests/src/consts.rs | 11 ++ pytests/stubs/consts.pyi | 5 +- src/types/float.rs | 12 +++ 10 files changed, 232 insertions(+), 114 deletions(-) create mode 100644 newsfragments/5272.added.md diff --git a/newsfragments/5272.added.md b/newsfragments/5272.added.md new file mode 100644 index 00000000000..5b43bad7dc1 --- /dev/null +++ b/newsfragments/5272.added.md @@ -0,0 +1,2 @@ +Introspection: support of class associated constants +Introspection: consolidate constants representation into a more generic attribute concept \ No newline at end of file diff --git a/pyo3-introspection/src/introspection.rs b/pyo3-introspection/src/introspection.rs index 0c4a7389aae..eedbd625e5c 100644 --- a/pyo3-introspection/src/introspection.rs +++ b/pyo3-introspection/src/introspection.rs @@ -1,4 +1,6 @@ -use crate::model::{Argument, Arguments, Class, Const, Function, Module, VariableLengthArgument}; +use crate::model::{ + Argument, Arguments, Attribute, Class, Function, Module, VariableLengthArgument, +}; use anyhow::{bail, ensure, Context, Result}; use goblin::elf::Elf; use goblin::mach::load_command::CommandVariant; @@ -25,35 +27,33 @@ fn parse_chunks(chunks: &[Chunk], main_module_name: &str) -> Result { let mut chunks_by_id = HashMap::<&str, &Chunk>::new(); let mut chunks_by_parent = HashMap::<&str, Vec<&Chunk>>::new(); for chunk in chunks { - if let Some(id) = match chunk { - Chunk::Module { id, .. } => Some(id), - Chunk::Class { id, .. } => Some(id), - Chunk::Function { id, .. } => id.as_ref(), - } { + let (id, parent) = match chunk { + Chunk::Module { id, .. } | Chunk::Class { id, .. } => (Some(id.as_str()), None), + Chunk::Function { id, parent, .. } | Chunk::Attribute { id, parent, .. } => { + (id.as_deref(), parent.as_deref()) + } + }; + if let Some(id) = id { chunks_by_id.insert(id, chunk); } - if let Some(parent) = match chunk { - Chunk::Module { .. } | Chunk::Class { .. } => None, - Chunk::Function { parent, .. } => parent.as_ref(), - } { + if let Some(parent) = parent { chunks_by_parent.entry(parent).or_default().push(chunk); } } // We look for the root chunk for chunk in chunks { if let Chunk::Module { + id, name, members, - consts, incomplete, - id: _, } = chunk { if name == main_module_name { return convert_module( + id, name, members, - consts, *incomplete, &chunks_by_id, &chunks_by_parent, @@ -65,18 +65,18 @@ fn parse_chunks(chunks: &[Chunk], main_module_name: &str) -> Result { } fn convert_module( + id: &str, name: &str, members: &[String], - consts: &[ConstChunk], incomplete: bool, chunks_by_id: &HashMap<&str, &Chunk>, chunks_by_parent: &HashMap<&str, Vec<&Chunk>>, ) -> Result { - let (modules, classes, functions) = convert_members( - &members + let (modules, classes, functions, attributes) = convert_members( + members .iter() .filter_map(|id| chunks_by_id.get(id.as_str()).copied()) - .collect::>(), + .chain(chunks_by_parent.get(&id).into_iter().flatten().copied()), chunks_by_id, chunks_by_parent, )?; @@ -86,39 +86,35 @@ fn convert_module( modules, classes, functions, - consts: consts - .iter() - .map(|c| Const { - name: c.name.clone(), - value: c.value.clone(), - }) - .collect(), + attributes, incomplete, }) } +type Members = (Vec, Vec, Vec, Vec); + /// Convert a list of members of a module or a class -fn convert_members( - chunks: &[&Chunk], +fn convert_members<'a>( + chunks: impl IntoIterator, chunks_by_id: &HashMap<&str, &Chunk>, chunks_by_parent: &HashMap<&str, Vec<&Chunk>>, -) -> Result<(Vec, Vec, Vec)> { +) -> Result { let mut modules = Vec::new(); let mut classes = Vec::new(); let mut functions = Vec::new(); + let mut attributes = Vec::new(); for chunk in chunks { match chunk { Chunk::Module { name, + id, members, - consts, incomplete, - id: _, } => { modules.push(convert_module( + id, name, members, - consts, *incomplete, chunks_by_id, chunks_by_parent, @@ -135,9 +131,34 @@ fn convert_members( decorators, returns, } => functions.push(convert_function(name, arguments, decorators, returns)), + Chunk::Attribute { + name, + id: _, + parent: _, + value, + annotation, + } => attributes.push(convert_attribute(name, value, annotation)), } } - Ok((modules, classes, functions)) + // We sort elements to get a stable output + modules.sort_by(|l, r| l.name.cmp(&r.name)); + classes.sort_by(|l, r| l.name.cmp(&r.name)); + functions.sort_by(|l, r| match l.name.cmp(&r.name) { + Ordering::Equal => { + // We put the getter before the setter + if l.decorators.iter().any(|d| d == "property") { + Ordering::Less + } else if r.decorators.iter().any(|d| d == "property") { + Ordering::Greater + } else { + // We pick an ordering based on decorators + l.decorators.cmp(&r.decorators) + } + } + o => o, + }); + attributes.sort_by(|l, r| l.name.cmp(&r.name)); + Ok((modules, classes, functions, attributes)) } fn convert_class( @@ -146,11 +167,8 @@ fn convert_class( chunks_by_id: &HashMap<&str, &Chunk>, chunks_by_parent: &HashMap<&str, Vec<&Chunk>>, ) -> Result { - let (nested_modules, nested_classes, mut methods) = convert_members( - chunks_by_parent - .get(&id) - .map(Vec::as_slice) - .unwrap_or_default(), + let (nested_modules, nested_classes, methods, attributes) = convert_members( + chunks_by_parent.get(&id).into_iter().flatten().copied(), chunks_by_id, chunks_by_parent, )?; @@ -162,24 +180,10 @@ fn convert_class( nested_classes.is_empty(), "Nested classes are not supported yet" ); - // We sort methods to get a stable output - methods.sort_by(|l, r| match l.name.cmp(&r.name) { - Ordering::Equal => { - // We put the getter before the setter - if l.decorators.iter().any(|d| d == "property") { - Ordering::Less - } else if r.decorators.iter().any(|d| d == "property") { - Ordering::Greater - } else { - // We pick an ordering based on decorators - l.decorators.cmp(&r.decorators) - } - } - o => o, - }); Ok(Class { name: name.into(), methods, + attributes, }) } @@ -224,6 +228,14 @@ fn convert_variable_length_argument(arg: &ChunkArgument) -> VariableLengthArgume } } +fn convert_attribute(name: &str, value: &Option, annotation: &Option) -> Attribute { + Attribute { + name: name.into(), + value: value.clone(), + annotation: annotation.clone(), + } +} + fn find_introspection_chunks_in_binary_object(path: &Path) -> Result> { let library_content = fs::read(path).with_context(|| format!("Failed to read {}", path.display()))?; @@ -387,7 +399,6 @@ enum Chunk { id: String, name: String, members: Vec, - consts: Vec, incomplete: bool, }, Class { @@ -406,12 +417,17 @@ enum Chunk { #[serde(default)] returns: Option, }, -} - -#[derive(Deserialize)] -struct ConstChunk { - name: String, - value: String, + Attribute { + #[serde(default)] + id: Option, + #[serde(default)] + parent: Option, + name: String, + #[serde(default)] + value: Option, + #[serde(default)] + annotation: Option, + }, } #[derive(Deserialize)] diff --git a/pyo3-introspection/src/model.rs b/pyo3-introspection/src/model.rs index cfe0f329561..9f86bb7e303 100644 --- a/pyo3-introspection/src/model.rs +++ b/pyo3-introspection/src/model.rs @@ -4,7 +4,7 @@ pub struct Module { pub modules: Vec, pub classes: Vec, pub functions: Vec, - pub consts: Vec, + pub attributes: Vec, pub incomplete: bool, } @@ -12,6 +12,7 @@ pub struct Module { pub struct Class { pub name: String, pub methods: Vec, + pub attributes: Vec, } #[derive(Debug, Eq, PartialEq, Clone, Hash)] @@ -25,9 +26,12 @@ pub struct Function { } #[derive(Debug, Eq, PartialEq, Clone, Hash)] -pub struct Const { +pub struct Attribute { pub name: String, - pub value: String, + /// Value as a Python expression if easily expressible + pub value: Option, + /// Type annotation as a Python expression + pub annotation: Option, } #[derive(Debug, Eq, PartialEq, Clone, Hash)] diff --git a/pyo3-introspection/src/stubs.rs b/pyo3-introspection/src/stubs.rs index e8d9eb70c6b..baad91dd6e2 100644 --- a/pyo3-introspection/src/stubs.rs +++ b/pyo3-introspection/src/stubs.rs @@ -1,4 +1,6 @@ -use crate::model::{Argument, Arguments, Class, Const, Function, Module, VariableLengthArgument}; +use crate::model::{ + Argument, Arguments, Attribute, Class, Function, Module, VariableLengthArgument, +}; use std::collections::{BTreeSet, HashMap}; use std::path::{Path, PathBuf}; use unicode_ident::{is_xid_continue, is_xid_start}; @@ -35,8 +37,8 @@ fn add_module_stub_files( fn module_stubs(module: &Module) -> String { let mut modules_to_import = BTreeSet::new(); let mut elements = Vec::new(); - for konst in &module.consts { - elements.push(const_stubs(konst, &mut modules_to_import)); + for attribute in &module.attributes { + elements.push(attribute_stubs(attribute, &mut modules_to_import)); } for class in &module.classes { elements.push(class_stubs(class, &mut modules_to_import)); @@ -99,10 +101,15 @@ fn module_stubs(module: &Module) -> String { fn class_stubs(class: &Class, modules_to_import: &mut BTreeSet) -> String { let mut buffer = format!("class {}:", class.name); - if class.methods.is_empty() { + if class.methods.is_empty() && class.attributes.is_empty() { buffer.push_str(" ..."); return buffer; } + for attribute in &class.attributes { + // We do the indentation + buffer.push_str("\n "); + buffer.push_str(&attribute_stubs(attribute, modules_to_import).replace('\n', "\n ")); + } for method in &class.methods { // We do the indentation buffer.push_str("\n "); @@ -159,10 +166,17 @@ fn function_stubs(function: &Function, modules_to_import: &mut BTreeSet) buffer } -fn const_stubs(konst: &Const, modules_to_import: &mut BTreeSet) -> String { - modules_to_import.insert("typing".to_string()); - let Const { name, value } = konst; - format!("{name}: typing.Final = {value}") +fn attribute_stubs(attribute: &Attribute, modules_to_import: &mut BTreeSet) -> String { + let mut output = attribute.name.clone(); + if let Some(annotation) = &attribute.annotation { + output.push_str(": "); + output.push_str(annotation_stub(annotation, modules_to_import)); + } + if let Some(value) = &attribute.value { + output.push_str(" = "); + output.push_str(value); + } + output } fn argument_stub(argument: &Argument, modules_to_import: &mut BTreeSet) -> String { diff --git a/pyo3-macros-backend/src/introspection.rs b/pyo3-macros-backend/src/introspection.rs index a6faf3a05a2..4d7507129a6 100644 --- a/pyo3-macros-backend/src/introspection.rs +++ b/pyo3-macros-backend/src/introspection.rs @@ -19,7 +19,6 @@ use std::collections::HashMap; use std::hash::{Hash, Hasher}; use std::mem::take; use std::sync::atomic::{AtomicUsize, Ordering}; -use syn::ext::IdentExt; use syn::visit_mut::{visit_type_mut, VisitMut}; use syn::{Attribute, Ident, ReturnType, Type, TypePath}; @@ -31,9 +30,6 @@ pub fn module_introspection_code<'a>( name: &str, members: impl IntoIterator, members_cfg_attrs: impl IntoIterator>, - consts: impl IntoIterator, - consts_values: impl IntoIterator, - consts_cfg_attrs: impl IntoIterator>, incomplete: bool, ) -> TokenStream { IntrospectionNode::Map( @@ -59,23 +55,6 @@ pub fn module_introspection_code<'a>( .collect(), ), ), - ( - "consts", - IntrospectionNode::List( - consts - .into_iter() - .zip(consts_values) - .zip(consts_cfg_attrs) - .filter_map(|((ident, value), attributes)| { - if attributes.is_empty() { - Some(const_introspection_code(ident, value)) - } else { - None // TODO: properly interpret cfg attributes - } - }) - .collect(), - ), - ), ("incomplete", IntrospectionNode::Bool(incomplete)), ] .into(), @@ -141,7 +120,10 @@ pub fn function_introspection_code( replace_self(&mut ty, class_type); } ty = ty.elide_lifetimes(); - IntrospectionNode::OutputType { rust_type: ty } + IntrospectionNode::OutputType { + rust_type: ty, + is_final: false, + } } }, } @@ -170,18 +152,53 @@ pub fn function_introspection_code( IntrospectionNode::Map(desc).emit(pyo3_crate_path) } -fn const_introspection_code<'a>(ident: &'a Ident, value: &'a String) -> IntrospectionNode<'a> { - IntrospectionNode::Map( - [ - ("type", IntrospectionNode::String("const".into())), - ( - "name", - IntrospectionNode::String(ident.unraw().to_string().into()), - ), - ("value", IntrospectionNode::String(value.into())), - ] - .into(), - ) +pub fn attribute_introspection_code( + pyo3_crate_path: &PyO3CratePath, + parent: Option<&Type>, + name: String, + value: String, + mut rust_type: Type, + is_final: bool, +) -> TokenStream { + let mut desc = HashMap::from([ + ("type", IntrospectionNode::String("attribute".into())), + ("name", IntrospectionNode::String(name.into())), + ( + "parent", + IntrospectionNode::IntrospectionId(parent.map(Cow::Borrowed)), + ), + ]); + if value == "..." { + // We need to set a type, but not need to set the value to ..., all attributes have a value + if let Some(parent) = parent { + replace_self(&mut rust_type, parent); + } + rust_type = rust_type.elide_lifetimes(); + desc.insert( + "annotation", + IntrospectionNode::OutputType { + rust_type, + is_final, + }, + ); + } else { + desc.insert( + "annotation", + if is_final { + // Type checkers can infer the type from the value because it's typing.Literal[value] + // So, following stubs best practices, we only write typing.Final and not + // typing.Final[typing.literal[value]] + IntrospectionNode::String("typing.Final".into()) + } else { + IntrospectionNode::OutputType { + rust_type, + is_final, + } + }, + ); + desc.insert("value", IntrospectionNode::String(value.into())); + } + IntrospectionNode::Map(desc).emit(pyo3_crate_path) } fn arguments_introspection_data<'a>( @@ -329,7 +346,7 @@ enum IntrospectionNode<'a> { Bool(bool), IntrospectionId(Option>), InputType { rust_type: Type, nullable: bool }, - OutputType { rust_type: Type }, + OutputType { rust_type: Type, is_final: bool }, Map(HashMap<&'static str, IntrospectionNode<'a>>), List(Vec>), } @@ -381,9 +398,18 @@ impl IntrospectionNode<'_> { } content.push_str("\""); } - Self::OutputType { rust_type } => { + Self::OutputType { + rust_type, + is_final, + } => { content.push_str("\""); + if is_final { + content.push_str("typing.Final["); + } content.push_tokens(quote! { <#rust_type as #pyo3_crate_path::impl_::introspection::PyReturnType>::OUTPUT_TYPE }); + if is_final { + content.push_str("]"); + } content.push_str("\""); } Self::Map(map) => { diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index e53c35c465b..44363570e70 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -1,7 +1,10 @@ //! Code generation for the function that initializes a python module and adds classes and function. #[cfg(feature = "experimental-inspect")] -use crate::introspection::{introspection_id_const, module_introspection_code}; +use crate::introspection::{ + attribute_introspection_code, introspection_id_const, module_introspection_code, +}; +#[cfg(feature = "experimental-inspect")] use crate::utils::expr_to_python; use crate::{ attributes::{ @@ -124,6 +127,10 @@ pub fn pymodule_module_impl( let mut module_items = Vec::new(); let mut module_items_cfg_attrs = Vec::new(); + #[cfg(feature = "experimental-inspect")] + let mut introspection_chunks = Vec::new(); + #[cfg(not(feature = "experimental-inspect"))] + let introspection_chunks = Vec::::new(); fn extract_use_items( source: &syn::UseTree, @@ -157,7 +164,6 @@ pub fn pymodule_module_impl( let mut pymodule_init = None; let mut module_consts = Vec::new(); - let mut module_consts_values = Vec::new(); let mut module_consts_cfg_attrs = Vec::new(); let _: Vec<()> = (*items).iter_mut().map(|item|{ @@ -302,8 +308,23 @@ pub fn pymodule_module_impl( return Ok(()); } module_consts.push(item.ident.clone()); - module_consts_values.push(expr_to_python(&item.expr)); module_consts_cfg_attrs.push(get_cfg_attributes(&item.attrs)); + #[cfg(feature = "experimental-inspect")] + { + let cfg_attrs = get_cfg_attributes(&item.attrs); + let chunk = attribute_introspection_code( + pyo3_path, + None, + item.ident.unraw().to_string(), + expr_to_python(&item.expr), + (*item.ty).clone(), + true, + ); + introspection_chunks.push(quote! { + #(#cfg_attrs)* + #chunk + }); + } } Item::Static(item) => { ensure_spanned!( @@ -358,9 +379,6 @@ pub fn pymodule_module_impl( &name.to_string(), &module_items, &module_items_cfg_attrs, - &module_consts, - &module_consts_values, - &module_consts_cfg_attrs, pymodule_init.is_some(), ); #[cfg(not(feature = "experimental-inspect"))] @@ -399,6 +417,7 @@ pub fn pymodule_module_impl( #initialization #introspection #introspection_id + #(#introspection_chunks)* fn __pyo3_pymodule(module: &#pyo3_path::Bound<'_, #pyo3_path::types::PyModule>) -> #pyo3_path::PyResult<()> { use #pyo3_path::impl_::pymodule::PyAddToModule; @@ -446,7 +465,7 @@ pub fn pymodule_function_impl( #[cfg(feature = "experimental-inspect")] let introspection = - module_introspection_code(pyo3_path, &name.to_string(), &[], &[], &[], &[], &[], true); + module_introspection_code(pyo3_path, &name.unraw().to_string(), &[], &[], true); #[cfg(not(feature = "experimental-inspect"))] let introspection = quote! {}; #[cfg(feature = "experimental-inspect")] diff --git a/pyo3-macros-backend/src/pyimpl.rs b/pyo3-macros-backend/src/pyimpl.rs index 0259c59b8b8..388e5184bdf 100644 --- a/pyo3-macros-backend/src/pyimpl.rs +++ b/pyo3-macros-backend/src/pyimpl.rs @@ -2,9 +2,11 @@ use std::collections::HashSet; use crate::combine_errors::CombineErrors; #[cfg(feature = "experimental-inspect")] -use crate::introspection::function_introspection_code; +use crate::introspection::{attribute_introspection_code, function_introspection_code}; #[cfg(feature = "experimental-inspect")] use crate::method::{FnSpec, FnType}; +#[cfg(feature = "experimental-inspect")] +use crate::utils::expr_to_python; use crate::utils::{has_attribute, has_attribute_with_namespace, Ctx, PyO3CratePath}; use crate::{ attributes::{take_pyo3_options, CrateAttribute}, @@ -183,6 +185,15 @@ pub fn impl_methods( .attrs .push(syn::parse_quote!(#[allow(non_upper_case_globals)])); } + #[cfg(feature = "experimental-inspect")] + extra_fragments.push(attribute_introspection_code( + &ctx.pyo3_path, + Some(ty), + spec.python_name().to_string(), + expr_to_python(&konst.expr), + konst.ty.clone(), + true, + )); } } syn::ImplItem::Macro(m) => bail_spanned!( diff --git a/pytests/src/consts.rs b/pytests/src/consts.rs index a0bdf3a9d3e..eec09a71819 100644 --- a/pytests/src/consts.rs +++ b/pytests/src/consts.rs @@ -2,9 +2,20 @@ use pyo3::pymodule; #[pymodule] pub mod consts { + use pyo3::{pyclass, pymethods}; + #[pymodule_export] pub const PI: f64 = std::f64::consts::PI; // Exports PI constant as part of the module #[pymodule_export] pub const SIMPLE: &str = "SIMPLE"; + + #[pyclass] + struct ClassWithConst {} + + #[pymethods] + impl ClassWithConst { + #[classattr] + const INSTANCE: Self = ClassWithConst {}; + } } diff --git a/pytests/stubs/consts.pyi b/pytests/stubs/consts.pyi index 9cf89e44883..ddbd6e7a09a 100644 --- a/pytests/stubs/consts.pyi +++ b/pytests/stubs/consts.pyi @@ -1,4 +1,7 @@ import typing -PI: typing.Final = ... +PI: typing.Final[float] SIMPLE: typing.Final = "SIMPLE" + +class ClassWithConst: + INSTANCE: typing.Final[typing.Any] diff --git a/src/types/float.rs b/src/types/float.rs index 04b2dc158b2..920a99a09c5 100644 --- a/src/types/float.rs +++ b/src/types/float.rs @@ -74,6 +74,9 @@ impl<'py> IntoPyObject<'py> for f64 { type Output = Bound<'py, Self::Target>; type Error = Infallible; + #[cfg(feature = "experimental-inspect")] + const OUTPUT_TYPE: &'static str = "float"; + #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { Ok(PyFloat::new(py, self)) @@ -90,6 +93,9 @@ impl<'py> IntoPyObject<'py> for &f64 { type Output = Bound<'py, Self::Target>; type Error = Infallible; + #[cfg(feature = "experimental-inspect")] + const OUTPUT_TYPE: &'static str = f64::OUTPUT_TYPE; + #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { (*self).into_pyobject(py) @@ -139,6 +145,9 @@ impl<'py> IntoPyObject<'py> for f32 { type Output = Bound<'py, Self::Target>; type Error = Infallible; + #[cfg(feature = "experimental-inspect")] + const OUTPUT_TYPE: &'static str = "float"; + #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { Ok(PyFloat::new(py, self.into())) @@ -155,6 +164,9 @@ impl<'py> IntoPyObject<'py> for &f32 { type Output = Bound<'py, Self::Target>; type Error = Infallible; + #[cfg(feature = "experimental-inspect")] + const OUTPUT_TYPE: &'static str = f32::OUTPUT_TYPE; + #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { (*self).into_pyobject(py) From 4b5a8ffb3b481fdfa76a3bb173eec0f3ce84662f Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Fri, 8 Aug 2025 13:05:56 +0200 Subject: [PATCH 777/936] ci: updates for Rust 1.89 (#5302) * ci: updates for Rust 1.89 * enable doctests on wasm --------- Co-authored-by: David Hewitt --- guide/src/class.md | 12 ++++++------ guide/src/function/signature.md | 2 ++ noxfile.py | 1 + src/types/module.rs | 5 ++++- tests/ui/not_send.stderr | 4 ++-- tests/ui/not_send2.stderr | 4 ++-- 6 files changed, 17 insertions(+), 11 deletions(-) diff --git a/guide/src/class.md b/guide/src/class.md index 73c6af4dadc..59873a27ac7 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -1394,26 +1394,26 @@ impl pyo3::PyClass for MyClass { type Frozen = pyo3::pyclass::boolean_struct::False; } -impl<'a, 'py> pyo3::impl_::extract_argument::PyFunctionArgument<'a, 'py, false> for &'a MyClass +impl<'a, 'holder, 'py> pyo3::impl_::extract_argument::PyFunctionArgument<'a, 'holder, 'py, false> for &'holder MyClass { - type Holder = ::std::option::Option>; + type Holder = ::std::option::Option>; #[cfg(feature = "experimental-inspect")] const INPUT_TYPE: &'static str = "MyClass"; #[inline] - fn extract(obj: &'a pyo3::Bound<'py, PyAny>, holder: &'a mut Self::Holder) -> pyo3::PyResult { + fn extract(obj: &'a pyo3::Bound<'py, PyAny>, holder: &'holder mut Self::Holder) -> pyo3::PyResult { pyo3::impl_::extract_argument::extract_pyclass_ref(obj, holder) } } -impl<'a, 'py> pyo3::impl_::extract_argument::PyFunctionArgument<'a, 'py, false> for &'a mut MyClass +impl<'a, 'holder, 'py> pyo3::impl_::extract_argument::PyFunctionArgument<'a, 'holder, 'py, false> for &'holder mut MyClass { - type Holder = ::std::option::Option>; + type Holder = ::std::option::Option>; #[cfg(feature = "experimental-inspect")] const INPUT_TYPE: &'static str = "MyClass"; #[inline] - fn extract(obj: &'a pyo3::Bound<'py, PyAny>, holder: &'a mut Self::Holder) -> pyo3::PyResult { + fn extract(obj: &'a pyo3::Bound<'py, PyAny>, holder: &'holder mut Self::Holder) -> pyo3::PyResult { pyo3::impl_::extract_argument::extract_pyclass_ref_mut(obj, holder) } } diff --git a/guide/src/function/signature.md b/guide/src/function/signature.md index ced323a5c9a..913e9d4c3ec 100644 --- a/guide/src/function/signature.md +++ b/guide/src/function/signature.md @@ -253,6 +253,7 @@ Type: builtin_function_or_method When the `experimental-inspect` Cargo feature is enabled, the `signature` attribute can also contain type hints: ```rust +# #[cfg(feature = "experimental-inspect")] { use pyo3::prelude::*; #[pymodule] @@ -265,6 +266,7 @@ pub mod example { arg } } +# } ``` It enables the [work-in-progress capacity of PyO3 to autogenerate type stubs](../type-stub.md) to generate a file with the correct type hints: diff --git a/noxfile.py b/noxfile.py index e324fa5bfe6..e5e092fa2c1 100644 --- a/noxfile.py +++ b/noxfile.py @@ -428,6 +428,7 @@ def test_emscripten(session: nox.Session): "-C link-arg=-sALLOW_MEMORY_GROWTH=1", ] ) + session.env["RUSTDOCFLAGS"] = session.env["RUSTFLAGS"] session.env["CARGO_BUILD_TARGET"] = target session.env["PYO3_CROSS_LIB_DIR"] = pythonlibdir _run(session, "rustup", "target", "add", target, "--toolchain", "stable") diff --git a/src/types/module.rs b/src/types/module.rs index e5bd408a47d..2c7064d86d5 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -140,6 +140,8 @@ impl PyModule { /// use std::ffi::CString; /// /// # fn main() -> PyResult<()> { + /// # #[cfg(not(target_arch = "wasm32"))] // node fs doesn't see this file, maybe cwd wrong? + /// # { /// // This path is resolved by however the platform resolves paths, /// // which also makes this less portable. Consider using `include_str` /// // if you just want to bundle a script with your module. @@ -149,7 +151,8 @@ impl PyModule { /// PyModule::from_code(py, CString::new(code)?.as_c_str(), c_str!("example.py"), c_str!("example"))?; /// Ok(()) /// })?; - /// Ok(()) + /// # } + /// # Ok(()) /// # } /// ``` pub fn from_code<'py>( diff --git a/tests/ui/not_send.stderr b/tests/ui/not_send.stderr index ca8db2806e7..0cb4039b186 100644 --- a/tests/ui/not_send.stderr +++ b/tests/ui/not_send.stderr @@ -10,7 +10,7 @@ error[E0277]: `*mut pyo3::Python<'static>` cannot be shared between threads safe note: required because it appears within the type `PhantomData<*mut pyo3::Python<'static>>` --> $RUST/core/src/marker.rs | - | pub struct PhantomData; + | pub struct PhantomData; | ^^^^^^^^^^^ note: required because it appears within the type `pyo3::marker::NotSend` --> src/marker.rs @@ -20,7 +20,7 @@ note: required because it appears within the type `pyo3::marker::NotSend` note: required because it appears within the type `PhantomData` --> $RUST/core/src/marker.rs | - | pub struct PhantomData; + | pub struct PhantomData; | ^^^^^^^^^^^ note: required because it appears within the type `pyo3::Python<'_>` --> src/marker.rs diff --git a/tests/ui/not_send2.stderr b/tests/ui/not_send2.stderr index 802081534b2..3bc3c297527 100644 --- a/tests/ui/not_send2.stderr +++ b/tests/ui/not_send2.stderr @@ -13,7 +13,7 @@ error[E0277]: `*mut pyo3::Python<'static>` cannot be shared between threads safe note: required because it appears within the type `PhantomData<*mut pyo3::Python<'static>>` --> $RUST/core/src/marker.rs | - | pub struct PhantomData; + | pub struct PhantomData; | ^^^^^^^^^^^ note: required because it appears within the type `pyo3::marker::NotSend` --> src/marker.rs @@ -23,7 +23,7 @@ note: required because it appears within the type `pyo3::marker::NotSend` note: required because it appears within the type `PhantomData` --> $RUST/core/src/marker.rs | - | pub struct PhantomData; + | pub struct PhantomData; | ^^^^^^^^^^^ note: required because it appears within the type `pyo3::Python<'_>` --> src/marker.rs From ca8bb5949517ae85ec714926d50ee0c72a6d24ba Mon Sep 17 00:00:00 2001 From: Thomas Tanon Date: Fri, 8 Aug 2025 13:17:02 +0200 Subject: [PATCH 778/936] Publish pyo3-introspection (#5300) Might be useful for people starting to play with introspection --- newsfragments/5300.added.md | 1 + noxfile.py | 1 + 2 files changed, 2 insertions(+) create mode 100644 newsfragments/5300.added.md diff --git a/newsfragments/5300.added.md b/newsfragments/5300.added.md new file mode 100644 index 00000000000..ebcff684f17 --- /dev/null +++ b/newsfragments/5300.added.md @@ -0,0 +1 @@ +Publish `pyo3-introspection` to crates.io \ No newline at end of file diff --git a/noxfile.py b/noxfile.py index e5e092fa2c1..c84c4bcb4b8 100644 --- a/noxfile.py +++ b/noxfile.py @@ -317,6 +317,7 @@ def publish(session: nox.Session) -> None: _run_cargo_publish(session, package="pyo3-macros") _run_cargo_publish(session, package="pyo3-ffi") _run_cargo_publish(session, package="pyo3") + _run_cargo_publish(session, package="pyo3-introspection") @nox.session(venv_backend="none") From 93e308ebfc1cce3d498091f4b14d3f0dbcd79e15 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 8 Aug 2025 12:17:09 +0100 Subject: [PATCH 779/936] ci: fix more clippy beta warnings (#5299) --- pyo3-ffi/src/moduleobject.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyo3-ffi/src/moduleobject.rs b/pyo3-ffi/src/moduleobject.rs index 2417664a421..1eff7a39491 100644 --- a/pyo3-ffi/src/moduleobject.rs +++ b/pyo3-ffi/src/moduleobject.rs @@ -94,6 +94,7 @@ pub const Py_mod_gil: c_int = 4; // skipped private _Py_mod_LAST_SLOT #[cfg(Py_3_12)] +#[allow(clippy::zero_ptr)] // matches the way that the rest of these constants are defined pub const Py_MOD_MULTIPLE_INTERPRETERS_NOT_SUPPORTED: *mut c_void = 0 as *mut c_void; #[cfg(Py_3_12)] pub const Py_MOD_MULTIPLE_INTERPRETERS_SUPPORTED: *mut c_void = 1 as *mut c_void; @@ -101,6 +102,7 @@ pub const Py_MOD_MULTIPLE_INTERPRETERS_SUPPORTED: *mut c_void = 1 as *mut c_void pub const Py_MOD_PER_INTERPRETER_GIL_SUPPORTED: *mut c_void = 2 as *mut c_void; #[cfg(Py_3_13)] +#[allow(clippy::zero_ptr)] // matches the way that the rest of these constants are defined 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; From a52aee22202ce03b2279233ebfbdc23e8962f690 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 8 Aug 2025 15:56:49 +0100 Subject: [PATCH 780/936] docs: hash requires eq and frozen (#5305) --- guide/pyclass-parameters.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guide/pyclass-parameters.md b/guide/pyclass-parameters.md index 27b15118dd4..8fdb2122ff8 100644 --- a/guide/pyclass-parameters.md +++ b/guide/pyclass-parameters.md @@ -12,7 +12,7 @@ | `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. | | `generic` | Implements runtime parametrization for the class following [PEP 560](https://peps.python.org/pep-0560/). | | `get_all` | Generates getters for all fields of the pyclass. | -| `hash` | Implements `__hash__` using the `Hash` implementation of the underlying Rust datatype. | +| `hash` | Implements `__hash__` using the `Hash` implementation of the underlying Rust datatype. *Requires `eq` and `frozen`* | | `immutable_type` | Makes the type object immutable. Supported on 3.14+ with the `abi3` feature active, or 3.10+ otherwise. | | `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`. | From 7450e9870dec0bb86dbf4acf0c8e7e7717a7dd4c Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 8 Aug 2025 16:45:18 +0100 Subject: [PATCH 781/936] deprecate `GILProtected` (#5285) * deprecate `GILProtected` * newsfragment * Update guide/src/migration.md Co-authored-by: Bas Schoenmaeckers <7943856+bschoenmaeckers@users.noreply.github.com> * fixup warnings * fixup one more case * also deprecate in guide * skip doctest on free threaded --------- Co-authored-by: Bas Schoenmaeckers <7943856+bschoenmaeckers@users.noreply.github.com> --- guide/src/free-threading.md | 3 ++- guide/src/migration.md | 43 +++++++++++++++++++++++++++++++++++ newsfragments/5285.changed.md | 1 + src/sync.rs | 7 ++++++ 4 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 newsfragments/5285.changed.md diff --git a/guide/src/free-threading.md b/guide/src/free-threading.md index 0cf062bea0c..11e805c538d 100644 --- a/guide/src/free-threading.md +++ b/guide/src/free-threading.md @@ -337,7 +337,7 @@ Python::attach(|py| { ### `GILProtected` is not exposed -[`GILProtected`] is a PyO3 type that allows mutable access to static data by +[`GILProtected`] is a (deprecated) 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 @@ -348,6 +348,7 @@ be sufficient. Before: ```rust +# #![allow(deprecated)] # fn main() { # #[cfg(not(Py_GIL_DISABLED))] { # use pyo3::prelude::*; diff --git a/guide/src/migration.md b/guide/src/migration.md index 042ed5128fa..2085c58aca3 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -7,6 +7,7 @@ For a detailed list of all changes, see the [CHANGELOG](changelog.md). ### Rename of `Python::with_gil`, `Python::allow_threads`, and `pyo3::prepare_freethreaded_python`
Click to expand + The names for these APIs were created when the global interpreter lock (GIL) was mandatory. With the introduction of free-threading in Python 3.13 this is no longer the case, and the naming does not has no universal meaning anymore. For this reason we chose to rename these to more modern terminology introduced in free-threading: @@ -15,6 +16,48 @@ For this reason we chose to rename these to more modern terminology introduced i - `pyo3::prepare_freethreaded_python` is now called `Python::initialize`.
+### Deprecation of `GILProtected` +
+Click to expand + +As another cleanup related to concurrency primitives designed for a Python constrained by the GIL, the `GILProtected` type is now deprecated. Prefer to use concurrency primitives which are compatible with free-threaded Python, such as [`std::sync::Mutex`](https://doc.rust-lang.org/std/sync/struct.Mutex.html) (in combination with PyO3's [`MutexExt`]({{#PYO3_DOCS_URL}}/pyo3/sync/trait.MutexExt.html) trait). + +Before: + +```rust +# #![allow(deprecated)] +# use pyo3::prelude::*; +# fn main() { +# #[cfg(not(Py_GIL_DISABLED))] { +use pyo3::sync::GILProtected; +use std::cell::RefCell; +# Python::attach(|py| { +static NUMBERS: GILProtected>> = GILProtected::new(RefCell::new(Vec::new())); +Python::attach(|py| { + NUMBERS.get(py).borrow_mut().push(42); +}); +# }) +# } +# } +``` + +After: + +```rust +# use pyo3::prelude::*; +use pyo3::sync::MutexExt; +use std::sync::Mutex; +# fn main() { +# Python::attach(|py| { +static NUMBERS: Mutex> = Mutex::new(Vec::new()); +Python::attach(|py| { + NUMBERS.lock_py_attached(py).expect("no poisoning").push(42); +}); +# }) +# } +``` + + ### `PyMemoryError` now maps to `io::ErrorKind::OutOfMemory` when converted to `io::Error`
Click to expand diff --git a/newsfragments/5285.changed.md b/newsfragments/5285.changed.md new file mode 100644 index 00000000000..9d1523a72a7 --- /dev/null +++ b/newsfragments/5285.changed.md @@ -0,0 +1 @@ +Deprecate `GILProtected`. diff --git a/src/sync.rs b/src/sync.rs index bf70659d485..7e71ca6a533 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -33,6 +33,7 @@ use crate::PyVisit; /// Combining `GILProtected` with `RefCell` enables mutable access to static data: /// /// ``` +/// # #![allow(deprecated)] /// # use pyo3::prelude::*; /// use pyo3::sync::GILProtected; /// use std::cell::RefCell; @@ -43,11 +44,16 @@ use crate::PyVisit; /// NUMBERS.get(py).borrow_mut().push(42); /// }); /// ``` +#[deprecated( + since = "0.26.0", + note = "Prefer an interior mutability primitive compatible with free-threaded Python, such as `Mutex` in combination with the `MutexExt` trait" +)] #[cfg(not(Py_GIL_DISABLED))] pub struct GILProtected { value: T, } +#[allow(deprecated)] #[cfg(not(Py_GIL_DISABLED))] impl GILProtected { /// Place the given value under the protection of the GIL. @@ -66,6 +72,7 @@ impl GILProtected { } } +#[allow(deprecated)] #[cfg(not(Py_GIL_DISABLED))] unsafe impl Sync for GILProtected where T: Send {} From 3ecdb85cdbdf10dc694306c6d837462334c634ef Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Fri, 8 Aug 2025 09:55:41 -0600 Subject: [PATCH 782/936] Update ABI3_MAX_MINOR for Python 3.14 (#5297) * Update ABI3_MAX_MINOR for Python 3.14 * add test * fix format --------- Co-authored-by: David Hewitt --- noxfile.py | 26 ++++++++++++++++++++++++++ pyo3-build-config/src/impl_.rs | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index c84c4bcb4b8..f1ccdd06035 100644 --- a/noxfile.py +++ b/noxfile.py @@ -895,6 +895,32 @@ def test_version_limits(session: nox.Session): config_file.set("PyPy", "3.8") _run_cargo(session, "check", env=env, expect_error=True) + # attempt to build with latest version and check that abi3 version + # configured matches the feature + max_minor_version = max(int(v.split(".")[1]) for v in PY_VERSIONS if "t" not in v) + with tempfile.TemporaryFile() as stderr: + env = os.environ.copy() + env["PYO3_PRINT_CONFIG"] = "1" # get diagnostics from the build + env["PYO3_NO_PYTHON"] = "1" # isolate the build from local Python + _run_cargo( + session, + "check", + f"--features=pyo3/abi3-py3{max_minor_version}", + env=env, + stderr=stderr, + expect_error=True, + ) + stderr.seek(0) + stderr = stderr.read().decode() + # NB if this assertion fails with something like + # "An abi3-py3* feature must be specified when compiling without a Python + # interpreter." + # + # then `ABI3_MAX_MINOR` in `pyo3-build-config/src/impl_.rs` is probably outdated. + assert f"version=3.{max_minor_version}" in stderr, ( + f"Expected to see version=3.{max_minor_version}, got: \n\n{stderr}" + ) + @nox.session(name="check-feature-powerset", venv_backend="none") def check_feature_powerset(session: nox.Session): diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 14ab9f7f7b7..aae49026a41 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 = 13; +pub(crate) const ABI3_MAX_MINOR: u8 = 14; #[cfg(test)] thread_local! { From 26e9cc5902d42c5413a7cb2396bfd2424feefd34 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Fri, 8 Aug 2025 19:56:39 +0200 Subject: [PATCH 783/936] add `Bound::cast` and friends (#5289) --- examples/getitem/src/lib.rs | 2 +- examples/maturin-starter/src/lib.rs | 2 +- examples/setuptools-rust-starter/src/lib.rs | 2 +- guide/src/class.md | 4 +- guide/src/conversions/traits.md | 2 +- guide/src/function.md | 2 +- guide/src/performance.md | 8 +- .../python-from-rust/calling-existing-code.md | 4 +- guide/src/types.md | 26 +- newsfragments/5289.added.md | 1 + pyo3-benches/benches/bench_any.rs | 4 +- pyo3-benches/benches/bench_dict.rs | 2 +- pyo3-benches/benches/bench_extract.rs | 38 +-- pyo3-benches/benches/bench_frompyobject.rs | 12 +- pyo3-benches/benches/bench_list.rs | 2 +- pyo3-benches/benches/bench_tuple.rs | 2 +- pyo3-macros-backend/src/pyclass.rs | 8 +- pytests/src/lib.rs | 2 +- src/conversion.rs | 9 +- src/conversions/chrono.rs | 16 +- src/conversions/hashbrown.rs | 6 +- src/conversions/indexmap.rs | 2 +- src/conversions/jiff.rs | 14 +- src/conversions/num_bigint.rs | 22 +- src/conversions/num_complex.rs | 12 +- src/conversions/smallvec.rs | 4 +- src/conversions/std/array.rs | 12 +- src/conversions/std/map.rs | 4 +- src/conversions/std/num.rs | 16 +- src/conversions/std/osstr.rs | 9 +- src/conversions/std/set.rs | 8 +- src/conversions/std/slice.rs | 12 +- src/conversions/std/string.rs | 10 +- src/conversions/std/time.rs | 6 +- src/conversions/std/vec.rs | 4 +- src/conversions/time.rs | 18 +- src/err/err_state.rs | 10 +- src/err/mod.rs | 4 +- src/exceptions.rs | 5 +- src/impl_/coroutine.rs | 6 +- src/impl_/extract_argument.rs | 13 +- src/impl_/pyclass.rs | 3 +- src/impl_/pyclass_init.rs | 2 +- src/impl_/pymethods.rs | 5 +- src/instance.rs | 296 ++++++++++++++++-- src/marker.rs | 2 +- src/marshal.rs | 2 +- src/py_result_ext.rs | 14 +- src/pybacked.rs | 11 +- src/pycell.rs | 7 +- src/pyclass/guard.rs | 8 +- src/pyclass_init.rs | 3 +- src/sync.rs | 5 +- src/tests/common.rs | 2 +- src/type_object.rs | 3 +- src/types/any.rs | 51 ++- src/types/boolobject.rs | 6 +- src/types/bytearray.rs | 7 +- src/types/bytes.rs | 13 +- src/types/capsule.rs | 2 +- src/types/code.rs | 4 +- src/types/complex.rs | 14 +- src/types/datetime.rs | 70 ++--- src/types/dict.rs | 20 +- src/types/ellipsis.rs | 4 +- src/types/float.rs | 4 +- src/types/frozenset.rs | 6 +- src/types/function.rs | 10 +- src/types/genericalias.rs | 2 +- src/types/iterator.rs | 8 +- src/types/list.rs | 47 ++- src/types/mapping.rs | 28 +- src/types/mappingproxy.rs | 26 +- src/types/memoryview.rs | 2 +- src/types/mod.rs | 2 +- src/types/module.rs | 22 +- src/types/none.rs | 6 +- src/types/notimplemented.rs | 4 +- src/types/pysuper.rs | 2 +- src/types/range.rs | 2 +- src/types/sequence.rs | 82 +++-- src/types/set.rs | 9 +- src/types/slice.rs | 6 +- src/types/string.rs | 34 +- src/types/traceback.rs | 2 +- src/types/tuple.rs | 38 +-- src/types/typeobject.rs | 27 +- src/types/weakref/anyref.rs | 17 +- src/types/weakref/proxy.rs | 8 +- src/types/weakref/reference.rs | 7 +- tests/test_class_attributes.rs | 2 +- tests/test_class_new.rs | 10 +- tests/test_enum.rs | 4 +- tests/test_frompy_intopy_roundtrip.rs | 4 +- tests/test_gc.rs | 2 +- tests/test_intopyobject.rs | 8 +- tests/test_mapping.rs | 4 +- tests/test_proto_methods.rs | 6 +- tests/test_pyfunction.rs | 2 +- tests/test_sequence.rs | 4 +- tests/test_various.rs | 2 +- tests/ui/forbid_unsafe.rs | 2 +- 102 files changed, 768 insertions(+), 582 deletions(-) create mode 100644 newsfragments/5289.added.md diff --git a/examples/getitem/src/lib.rs b/examples/getitem/src/lib.rs index ba850a06b8d..5739b67eb31 100644 --- a/examples/getitem/src/lib.rs +++ b/examples/getitem/src/lib.rs @@ -25,7 +25,7 @@ impl ExampleContainer { fn __getitem__(&self, key: &Bound<'_, PyAny>) -> PyResult { if let Ok(position) = key.extract::() { return Ok(position); - } else if let Ok(slice) = key.downcast::() { + } else if let Ok(slice) = key.cast::() { // 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 isize).unwrap(); diff --git a/examples/maturin-starter/src/lib.rs b/examples/maturin-starter/src/lib.rs index 4c2a30d3a5d..5e0be857391 100644 --- a/examples/maturin-starter/src/lib.rs +++ b/examples/maturin-starter/src/lib.rs @@ -28,7 +28,7 @@ fn maturin_starter(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { // e.g. from maturin_starter.submodule import SubmoduleClass let sys = PyModule::import(py, "sys")?; - let sys_modules: Bound<'_, PyDict> = sys.getattr("modules")?.downcast_into()?; + let sys_modules: Bound<'_, PyDict> = sys.getattr("modules")?.cast_into()?; sys_modules.set_item("maturin_starter.submodule", m.getattr("submodule")?)?; Ok(()) diff --git a/examples/setuptools-rust-starter/src/lib.rs b/examples/setuptools-rust-starter/src/lib.rs index a26623bc044..e4ff036d850 100644 --- a/examples/setuptools-rust-starter/src/lib.rs +++ b/examples/setuptools-rust-starter/src/lib.rs @@ -28,7 +28,7 @@ fn _setuptools_rust_starter(py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult // e.g. from setuptools_rust_starter.submodule import SubmoduleClass let sys = PyModule::import(py, "sys")?; - let sys_modules: Bound<'_, PyDict> = sys.getattr("modules")?.downcast_into()?; + let sys_modules: Bound<'_, PyDict> = sys.getattr("modules")?.cast_into()?; sys_modules.set_item("setuptools_rust_starter.submodule", m.getattr("submodule")?)?; Ok(()) diff --git a/guide/src/class.md b/guide/src/class.md index 59873a27ac7..30a0cf68b0f 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -447,7 +447,7 @@ This is not supported when building for the Python limited API (aka the `abi3` f To convert between the Rust type and its native base class, you can take `slf` as a Python object. To access the Rust fields use `slf.borrow()` or -`slf.borrow_mut()`, and to access the base class use `slf.downcast::()`. +`slf.borrow_mut()`, and to access the base class use `slf.cast::()`. ```rust # #[cfg(not(Py_LIMITED_API))] { @@ -470,7 +470,7 @@ impl DictWithCounter { fn set(slf: &Bound<'_, Self>, key: String, value: Bound<'_, PyAny>) -> PyResult<()> { slf.borrow_mut().counter.entry(key.clone()).or_insert(0); - let dict = slf.downcast::()?; + let dict = slf.cast::()?; dict.set_item(key, value) } } diff --git a/guide/src/conversions/traits.md b/guide/src/conversions/traits.md index 6565026e89c..a7c8d03bd4b 100644 --- a/guide/src/conversions/traits.md +++ b/guide/src/conversions/traits.md @@ -397,7 +397,7 @@ enum RustyEnum<'py> { # assert_eq!( # b"text", # match rust_thing { -# RustyEnum::CatchAll(ref i) => i.downcast::()?.as_bytes(), +# RustyEnum::CatchAll(ref i) => i.cast::()?.as_bytes(), # other => unreachable!("Error extracting: {:?}", other), # } # ); diff --git a/guide/src/function.md b/guide/src/function.md index 8db7cc00831..1ecd436df63 100644 --- a/guide/src/function.md +++ b/guide/src/function.md @@ -134,7 +134,7 @@ The `#[pyo3]` attribute can be used to modify properties of the generated Python # let catch_warnings = warnings # .getattr("catch_warnings")? # .call((), Some(&kwargs))?; - # let list = catch_warnings.call_method0("__enter__")?.downcast_into()?; + # let list = catch_warnings.call_method0("__enter__")?.cast_into()?; # warnings.getattr("simplefilter")?.call1(("always",))?; // show all warnings # f(&list); # catch_warnings diff --git a/guide/src/performance.md b/guide/src/performance.md index 1981eedabbc..998d1f8b940 100644 --- a/guide/src/performance.md +++ b/guide/src/performance.md @@ -2,7 +2,7 @@ To achieve the best possible performance, it is useful to be aware of several tricks and sharp edges concerning PyO3's API. -## `extract` versus `downcast` +## `extract` versus `cast` Pythonic API implemented using PyO3 are often polymorphic, i.e. they will accept `&Bound<'_, PyAny>` and try to turn this into multiple more concrete types to which the requested operation is applied. This often leads to chains of calls to `extract`, e.g. @@ -31,7 +31,7 @@ fn frobnicate<'py>(value: &Bound<'py, PyAny>) -> PyResult> { } ``` -This suboptimal as the `FromPyObject` trait requires `extract` to have a `Result` return type. For native types like `PyList`, it faster to use `downcast` (which `extract` calls internally) when the error value is ignored. This avoids the costly conversion of a `PyDowncastError` to a `PyErr` required to fulfil the `FromPyObject` contract, i.e. +This suboptimal as the `FromPyObject` trait requires `extract` to have a `Result` return type. For native types like `PyList`, it faster to use `cast` (which `extract` calls internally) when the error value is ignored. This avoids the costly conversion of a `PyDowncastError` to a `PyErr` required to fulfil the `FromPyObject` contract, i.e. ```rust,no_run # #![allow(dead_code)] @@ -42,8 +42,8 @@ This suboptimal as the `FromPyObject` trait requires `extract` to have a `Res # #[pyfunction] fn frobnicate<'py>(value: &Bound<'py, PyAny>) -> PyResult> { - // Use `downcast` instead of `extract` as turning `PyDowncastError` into `PyErr` is quite costly. - if let Ok(list) = value.downcast::() { + // Use `cast` instead of `extract` as turning `PyDowncastError` into `PyErr` is quite costly. + if let Ok(list) = value.cast::() { frobnicate_list(list) } else if let Ok(vec) = value.extract::>>() { frobnicate_vec(vec) diff --git a/guide/src/python-from-rust/calling-existing-code.md b/guide/src/python-from-rust/calling-existing-code.md index 90e8a3ee717..b516b18663b 100644 --- a/guide/src/python-from-rust/calling-existing-code.md +++ b/guide/src/python-from-rust/calling-existing-code.md @@ -197,7 +197,7 @@ fn main() -> PyResult<()> { // Import and get sys.modules let sys = PyModule::import(py, "sys")?; - let py_modules: Bound<'_, PyDict> = sys.getattr("modules")?.downcast_into()?; + let py_modules: Bound<'_, PyDict> = sys.getattr("modules")?.cast_into()?; // Insert foo into sys.modules py_modules.set_item("foo", foo_module)?; @@ -302,7 +302,7 @@ fn main() -> PyResult<()> { let syspath = py .import("sys")? .getattr("path")? - .downcast_into::()?; + .cast_into::()?; syspath.insert(0, path)?; let app: Py = PyModule::from_code(py, py_app.as_c_str(), c_str!("app.py"), c_str!(""))? .getattr("run")? diff --git a/guide/src/types.md b/guide/src/types.md index 0b8e1fbcf87..891d639e02e 100644 --- a/guide/src/types.md +++ b/guide/src/types.md @@ -239,7 +239,7 @@ fn get_first_item<'py>(list: &Bound<'py, PyList>) -> PyResult> ### Casting between Python object types -To cast `Bound<'py, T>` smart pointers to some other type, use the [`.downcast()`][PyAnyMethods::downcast] family of functions. This converts `&Bound<'py, T>` to a different `&Bound<'py, U>`, without transferring ownership. There is also [`.downcast_into()`][PyAnyMethods::downcast_into] to convert `Bound<'py, T>` to `Bound<'py, U>` with transfer of ownership. These methods are available for all types `T` which implement the [`PyTypeCheck`] trait. +To cast `Bound<'py, T>` smart pointers to some other type, use the [`.cast()`][Bound::cast] family of functions. This converts `&Bound<'py, T>` to a different `&Bound<'py, U>`, without transferring ownership. There is also [`.cast_into()`][Bound::cast_into] to convert `Bound<'py, T>` to `Bound<'py, U>` with transfer of ownership. These methods are available for all types `T` which implement the [`PyTypeCheck`] trait. Casting to `Bound<'py, PyAny>` can be done with `.as_any()` or `.into_any()`. @@ -252,17 +252,17 @@ For example, the following snippet shows how to cast `Bound<'py, PyAny>` to `Bou // create a new Python `tuple`, and use `.into_any()` to erase the type 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()?; +// use `.cast()` to cast to `PyTuple` without transferring ownership +let _: &Bound<'py, PyTuple> = obj.cast()?; -// use `.downcast_into()` to cast to `PyTuple` with transfer of ownership -let _: Bound<'py, PyTuple> = obj.downcast_into()?; +// use `.cast_into()` to cast to `PyTuple` with transfer of ownership +let _: Bound<'py, PyTuple> = obj.cast_into()?; # Ok(()) # } # Python::attach(example).unwrap() ``` -Custom [`#[pyclass]`][pyclass] types implement [`PyTypeCheck`], so `.downcast()` also works for these types. The snippet below is the same as the snippet above casting instead to a custom type `MyClass`: +Custom [`#[pyclass]`][pyclass] types implement [`PyTypeCheck`], so `.cast()` also works for these types. The snippet below is the same as the snippet above casting instead to a custom type `MyClass`: ```rust use pyo3::prelude::*; @@ -274,11 +274,11 @@ struct MyClass {} // create a new Python `tuple`, and use `.into_any()` to erase the type let obj: Bound<'py, PyAny> = Bound::new(py, MyClass {})?.into_any(); -// use `.downcast()` to cast to `MyClass` without transferring ownership -let _: &Bound<'py, MyClass> = obj.downcast()?; +// use `.cast()` to cast to `MyClass` without transferring ownership +let _: &Bound<'py, MyClass> = obj.cast()?; -// use `.downcast_into()` to cast to `MyClass` with transfer of ownership -let _: Bound<'py, MyClass> = obj.downcast_into()?; +// use `.cast_into()` to cast to `MyClass` with transfer of ownership +let _: Bound<'py, MyClass> = obj.cast_into()?; # Ok(()) # } # Python::attach(example).unwrap() @@ -286,7 +286,7 @@ let _: Bound<'py, MyClass> = obj.downcast_into()?; ### Extracting Rust data from Python objects -To extract Rust data from Python objects, use [`.extract()`][PyAnyMethods::extract] instead of `.downcast()`. This method is available for all types which implement the [`FromPyObject`] trait. +To extract Rust data from Python objects, use [`.extract()`][PyAnyMethods::extract] instead of `.cast()`. This method is available for all types which implement the [`FromPyObject`] trait. For example, the following snippet extracts a Rust tuple of integers from a Python tuple: @@ -313,8 +313,8 @@ for more detail. [Py]: {{#PYO3_DOCS_URL}}/pyo3/struct.Py.html [PyAnyMethods::add]: {{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.add [PyAnyMethods::extract]: {{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.extract -[PyAnyMethods::downcast]: {{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.downcast -[PyAnyMethods::downcast_into]: {{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.downcast_into +[Bound::cast]: {{#PYO3_DOCS_URL}}/pyo3/struct.Bound.html#method.cast +[Bound::cast_into]: {{#PYO3_DOCS_URL}}/pyo3/struct.Bound.html#method.cast_into [`PyTypeCheck`]: {{#PYO3_DOCS_URL}}/pyo3/type_object/trait.PyTypeCheck.html [`PyAnyMethods`]: {{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html [`PyDictMethods`]: {{#PYO3_DOCS_URL}}/pyo3/types/trait.PyDictMethods.html diff --git a/newsfragments/5289.added.md b/newsfragments/5289.added.md new file mode 100644 index 00000000000..5cdee26e1c9 --- /dev/null +++ b/newsfragments/5289.added.md @@ -0,0 +1 @@ +added `Bound::cast` family of functions superseding the `PyAnyMethods::downcast` family \ No newline at end of file diff --git a/pyo3-benches/benches/bench_any.rs b/pyo3-benches/benches/bench_any.rs index 673f23e5fb7..827f4713990 100644 --- a/pyo3-benches/benches/bench_any.rs +++ b/pyo3-benches/benches/bench_any.rs @@ -52,9 +52,9 @@ fn find_object_type(obj: &Bound<'_, PyAny>) -> ObjectType { ObjectType::Str } else if obj.is_instance_of::() { ObjectType::Tuple - } else if obj.downcast::().is_ok() { + } else if obj.cast::().is_ok() { ObjectType::Sequence - } else if obj.downcast::().is_ok() { + } else if obj.cast::().is_ok() { ObjectType::Mapping } else { ObjectType::Unknown diff --git a/pyo3-benches/benches/bench_dict.rs b/pyo3-benches/benches/bench_dict.rs index 8867da1ee50..7959a60d7ca 100644 --- a/pyo3-benches/benches/bench_dict.rs +++ b/pyo3-benches/benches/bench_dict.rs @@ -96,7 +96,7 @@ fn mapping_from_dict(b: &mut Bencher<'_>) { .map(|i| (i, i * 2)) .into_py_dict(py) .unwrap(); - b.iter(|| black_box(dict).downcast::().unwrap()); + b.iter(|| black_box(dict).cast::().unwrap()); }); } diff --git a/pyo3-benches/benches/bench_extract.rs b/pyo3-benches/benches/bench_extract.rs index db1785a2331..1364185bcdd 100644 --- a/pyo3-benches/benches/bench_extract.rs +++ b/pyo3-benches/benches/bench_extract.rs @@ -27,22 +27,22 @@ fn extract_str_extract_fail(bench: &mut Bencher<'_>) { } #[cfg(Py_3_10)] -fn extract_str_downcast_success(bench: &mut Bencher<'_>) { +fn extract_str_cast_success(bench: &mut Bencher<'_>) { Python::attach(|py| { let s = PyString::new(py, "Hello, World!").into_any(); bench.iter(|| { - let py_str = black_box(&s).downcast::().unwrap(); + let py_str = black_box(&s).cast::().unwrap(); py_str.to_str().unwrap() }); }); } -fn extract_str_downcast_fail(bench: &mut Bencher<'_>) { +fn extract_str_cast_fail(bench: &mut Bencher<'_>) { Python::attach(|py| { let d = PyDict::new(py).into_any(); - bench.iter(|| match black_box(&d).downcast::() { + bench.iter(|| match black_box(&d).cast::() { Ok(v) => panic!("should err {}", v), Err(e) => e, }); @@ -68,22 +68,22 @@ fn extract_int_extract_fail(bench: &mut Bencher<'_>) { }); } -fn extract_int_downcast_success(bench: &mut Bencher<'_>) { +fn extract_int_cast_success(bench: &mut Bencher<'_>) { Python::attach(|py| { let int = 123i32.into_pyobject(py).unwrap(); bench.iter(|| { - let py_int = black_box(&int).downcast::().unwrap(); + let py_int = black_box(&int).cast::().unwrap(); py_int.extract::().unwrap() }); }); } -fn extract_int_downcast_fail(bench: &mut Bencher<'_>) { +fn extract_int_cast_fail(bench: &mut Bencher<'_>) { Python::attach(|py| { let d = PyDict::new(py).into_any(); - bench.iter(|| match black_box(&d).downcast::() { + bench.iter(|| match black_box(&d).cast::() { Ok(v) => panic!("should err {}", v), Err(e) => black_box(e), }); @@ -109,22 +109,22 @@ fn extract_float_extract_fail(bench: &mut Bencher<'_>) { }); } -fn extract_float_downcast_success(bench: &mut Bencher<'_>) { +fn extract_float_cast_success(bench: &mut Bencher<'_>) { Python::attach(|py| { let float = 23.42f64.into_pyobject(py).unwrap(); bench.iter(|| { - let py_float = black_box(&float).downcast::().unwrap(); + let py_float = black_box(&float).cast::().unwrap(); py_float.value() }); }); } -fn extract_float_downcast_fail(bench: &mut Bencher<'_>) { +fn extract_float_cast_fail(bench: &mut Bencher<'_>) { Python::attach(|py| { let d = PyDict::new(py).into_any(); - bench.iter(|| match black_box(&d).downcast::() { + bench.iter(|| match black_box(&d).cast::() { Ok(v) => panic!("should err {}", v), Err(e) => e, }); @@ -135,22 +135,22 @@ fn criterion_benchmark(c: &mut Criterion) { c.bench_function("extract_str_extract_success", extract_str_extract_success); c.bench_function("extract_str_extract_fail", extract_str_extract_fail); #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] - c.bench_function("extract_str_downcast_success", extract_str_downcast_success); - c.bench_function("extract_str_downcast_fail", extract_str_downcast_fail); + c.bench_function("extract_str_cast_success", extract_str_cast_success); + c.bench_function("extract_str_cast_fail", extract_str_cast_fail); c.bench_function("extract_int_extract_success", extract_int_extract_success); c.bench_function("extract_int_extract_fail", extract_int_extract_fail); - c.bench_function("extract_int_downcast_success", extract_int_downcast_success); - c.bench_function("extract_int_downcast_fail", extract_int_downcast_fail); + c.bench_function("extract_int_cast_success", extract_int_cast_success); + c.bench_function("extract_int_cast_fail", extract_int_cast_fail); c.bench_function( "extract_float_extract_success", extract_float_extract_success, ); c.bench_function("extract_float_extract_fail", extract_float_extract_fail); c.bench_function( - "extract_float_downcast_success", - extract_float_downcast_success, + "extract_float_cast_success", + extract_float_cast_success, ); - c.bench_function("extract_float_downcast_fail", extract_float_downcast_fail); + c.bench_function("extract_float_cast_fail", extract_float_cast_fail); } criterion_group!(benches, criterion_benchmark); diff --git a/pyo3-benches/benches/bench_frompyobject.rs b/pyo3-benches/benches/bench_frompyobject.rs index 51bc825ffe9..045678f33c4 100644 --- a/pyo3-benches/benches/bench_frompyobject.rs +++ b/pyo3-benches/benches/bench_frompyobject.rs @@ -23,11 +23,11 @@ fn enum_from_pyobject(b: &mut Bencher<'_>) { }) } -fn list_via_downcast(b: &mut Bencher<'_>) { +fn list_via_cast(b: &mut Bencher<'_>) { Python::attach(|py| { let any = PyList::empty(py).into_any(); - b.iter(|| black_box(&any).downcast::().unwrap()); + b.iter(|| black_box(&any).cast::().unwrap()); }) } @@ -39,11 +39,11 @@ fn list_via_extract(b: &mut Bencher<'_>) { }) } -fn not_a_list_via_downcast(b: &mut Bencher<'_>) { +fn not_a_list_via_cast(b: &mut Bencher<'_>) { Python::attach(|py| { let any = PyString::new(py, "foobar").into_any(); - b.iter(|| black_box(&any).downcast::().unwrap_err()); + b.iter(|| black_box(&any).cast::().unwrap_err()); }) } @@ -76,11 +76,11 @@ fn not_a_list_via_extract_enum(b: &mut Bencher<'_>) { fn criterion_benchmark(c: &mut Criterion) { c.bench_function("enum_from_pyobject", enum_from_pyobject); - c.bench_function("list_via_downcast", list_via_downcast); + c.bench_function("list_via_cast", list_via_cast); c.bench_function("list_via_extract", list_via_extract); - c.bench_function("not_a_list_via_downcast", not_a_list_via_downcast); + c.bench_function("not_a_list_via_cast", not_a_list_via_cast); c.bench_function("not_a_list_via_extract", not_a_list_via_extract); c.bench_function("not_a_list_via_extract_enum", not_a_list_via_extract_enum); } diff --git a/pyo3-benches/benches/bench_list.rs b/pyo3-benches/benches/bench_list.rs index 6f3b231d3e3..fe36b92ca23 100644 --- a/pyo3-benches/benches/bench_list.rs +++ b/pyo3-benches/benches/bench_list.rs @@ -85,7 +85,7 @@ fn sequence_from_list(b: &mut Bencher<'_>) { Python::attach(|py| { const LEN: usize = 50_000; let list = &PyList::new(py, 0..LEN).unwrap(); - b.iter(|| black_box(list).downcast::().unwrap()); + b.iter(|| black_box(list).cast::().unwrap()); }); } diff --git a/pyo3-benches/benches/bench_tuple.rs b/pyo3-benches/benches/bench_tuple.rs index d3e37a0e653..77c829945d6 100644 --- a/pyo3-benches/benches/bench_tuple.rs +++ b/pyo3-benches/benches/bench_tuple.rs @@ -95,7 +95,7 @@ fn sequence_from_tuple(b: &mut Bencher<'_>) { Python::attach(|py| { const LEN: usize = 50_000; let tuple = PyTuple::new(py, 0..LEN).unwrap().into_any(); - b.iter(|| black_box(&tuple).downcast::().unwrap()); + b.iter(|| black_box(&tuple).cast::().unwrap()); }); } diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 9ee1e0dc5f5..419af70c4d8 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -1056,7 +1056,7 @@ fn impl_complex_enum( 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())) } + unsafe { #pyo3_path::Bound::new(py, pyclass_init).map(|b| b.cast_into_unchecked()) } } } }); @@ -1915,7 +1915,7 @@ fn pyclass_richcmp_simple_enum( 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) { + if let ::std::result::Result::Ok(other) = other.cast::() { let other = &*other.borrow(); return match op { #arms @@ -1928,7 +1928,7 @@ fn pyclass_richcmp_simple_enum( 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__()) + other.cast::().map(|o| o.borrow().__pyo3__int__()) }) { return match op { #arms @@ -1979,7 +1979,7 @@ fn pyclass_richcmp( op: #pyo3_path::pyclass::CompareOp ) -> #pyo3_path::PyResult<#pyo3_path::PyObject> { let self_val = self; - if let ::std::result::Result::Ok(other) = #pyo3_path::types::PyAnyMethods::downcast::(other) { + if let ::std::result::Result::Ok(other) = other.cast::() { let other = &*other.borrow(); match op { #arms diff --git a/pytests/src/lib.rs b/pytests/src/lib.rs index e5f00701525..0480847094e 100644 --- a/pytests/src/lib.rs +++ b/pytests/src/lib.rs @@ -49,7 +49,7 @@ mod pyo3_pytests { // e.g. import pyo3_pytests.buf_and_str as bas let sys = PyModule::import(m.py(), "sys")?; - let sys_modules = sys.getattr("modules")?.downcast_into::()?; + let sys_modules = sys.getattr("modules")?.cast_into::()?; sys_modules.set_item("pyo3_pytests.awaitable", m.getattr("awaitable")?)?; sys_modules.set_item("pyo3_pytests.buf_and_str", m.getattr("buf_and_str")?)?; sys_modules.set_item("pyo3_pytests.comparisons", m.getattr("comparisons")?)?; diff --git a/src/conversion.rs b/src/conversion.rs index 9595462098c..dc851e9f8d2 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -3,7 +3,6 @@ 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::{Borrowed, Bound, BoundObject, Py, PyAny, PyClass, PyErr, PyRef, PyRefMut, Python}; use std::convert::Infallible; @@ -370,7 +369,7 @@ pub trait FromPyObjectBound<'a, 'py>: Sized + from_py_object_bound_sealed::Seale /// Extracts `Self` from the bound smart pointer `obj`. /// /// Users are advised against calling this method directly: instead, use this via - /// [`Bound<'_, PyAny>::extract`] or [`Py::extract`]. + /// [`Bound<'_, PyAny>::extract`](crate::types::any::PyAnyMethods::extract) or [`Py::extract`]. fn from_py_object_bound(ob: Borrowed<'a, 'py, PyAny>) -> PyResult; /// Extracts the type hint information for this type when it appears as an argument. @@ -409,7 +408,7 @@ where T: PyClass + Clone, { fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { - let bound = obj.downcast::()?; + let bound = obj.cast::()?; Ok(bound.try_borrow()?.clone()) } } @@ -419,7 +418,7 @@ where T: PyClass, { fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { - obj.downcast::()?.try_borrow().map_err(Into::into) + obj.cast::()?.try_borrow().map_err(Into::into) } } @@ -428,7 +427,7 @@ where T: PyClass, { fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { - obj.downcast::()?.try_borrow_mut().map_err(Into::into) + obj.cast::()?.try_borrow_mut().map_err(Into::into) } } diff --git a/src/conversions/chrono.rs b/src/conversions/chrono.rs index 2d0f96d6b13..634c1a1d212 100644 --- a/src/conversions/chrono.rs +++ b/src/conversions/chrono.rs @@ -110,7 +110,7 @@ impl<'py> IntoPyObject<'py> for &Duration { impl FromPyObject<'_> for Duration { fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { - let delta = ob.downcast::()?; + let delta = ob.cast::()?; // Python size are much lower than rust size so we do not need bound checks. // 0 <= microseconds < 1000000 // 0 <= seconds < 3600*24 @@ -164,7 +164,7 @@ impl<'py> IntoPyObject<'py> for &NaiveDate { impl FromPyObject<'_> for NaiveDate { fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { - let date = ob.downcast::()?; + let date = ob.cast::()?; py_date_to_naive_date(date) } } @@ -206,7 +206,7 @@ impl<'py> IntoPyObject<'py> for &NaiveTime { impl FromPyObject<'_> for NaiveTime { fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { - let time = ob.downcast::()?; + let time = ob.cast::()?; py_time_to_naive_time(time) } } @@ -249,7 +249,7 @@ impl<'py> IntoPyObject<'py> for &NaiveDateTime { impl FromPyObject<'_> for NaiveDateTime { fn extract_bound(dt: &Bound<'_, PyAny>) -> PyResult { - let dt = dt.downcast::()?; + let dt = dt.cast::()?; // If the user tries to convert a timezone aware datetime into a naive one, // we return a hard error. We could silently remove tzinfo, or assume local timezone @@ -287,7 +287,7 @@ where type Error = PyErr; fn into_pyobject(self, py: Python<'py>) -> Result { - let tz = self.timezone().into_bound_py_any(py)?.downcast_into()?; + let tz = self.timezone().into_bound_py_any(py)?.cast_into()?; let DateArgs { year, month, day } = (&self.naive_local().date()).into(); let TimeArgs { @@ -326,7 +326,7 @@ where impl FromPyObject<'py>> FromPyObject<'_> for DateTime { fn extract_bound(dt: &Bound<'_, PyAny>) -> PyResult> { - let dt = dt.downcast::()?; + let dt = dt.cast::()?; let tzinfo = dt.get_tzinfo(); let tz = if let Some(tzinfo) = tzinfo { @@ -388,7 +388,7 @@ impl FromPyObject<'_> for FixedOffset { /// Note that the conversion will result in precision lost in microseconds as chrono offset /// does not supports microseconds. fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { - let ob = ob.downcast::()?; + let ob = ob.cast::()?; // Passing Python's None to the `utcoffset` function will only // work for timezones defined as fixed offsets in Python. @@ -481,7 +481,7 @@ impl FromPyObject<'_> for Local { if ob.eq(local_tz)? { Ok(Local) } else { - let name = local_tz.getattr("key")?.downcast_into::()?; + let name = local_tz.getattr("key")?.cast_into::()?; Err(PyValueError::new_err(format!( "expected local timezone {}", name.to_cow()? diff --git a/src/conversions/hashbrown.rs b/src/conversions/hashbrown.rs index fe3cf1e00d3..26fcc5c627e 100644 --- a/src/conversions/hashbrown.rs +++ b/src/conversions/hashbrown.rs @@ -74,7 +74,7 @@ where S: hash::BuildHasher + Default, { fn extract_bound(ob: &Bound<'py, PyAny>) -> Result { - let dict = ob.downcast::()?; + let dict = ob.cast::()?; let mut ret = hashbrown::HashMap::with_capacity_and_hasher(dict.len(), S::default()); for (k, v) in dict { ret.insert(k.extract()?, v.extract()?); @@ -117,10 +117,10 @@ where S: hash::BuildHasher + Default, { fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { - match ob.downcast::() { + match ob.cast::() { Ok(set) => set.iter().map(|any| any.extract()).collect(), Err(err) => { - if let Ok(frozen_set) = ob.downcast::() { + if let Ok(frozen_set) = ob.cast::() { frozen_set.iter().map(|any| any.extract()).collect() } else { Err(PyErr::from(err)) diff --git a/src/conversions/indexmap.rs b/src/conversions/indexmap.rs index f9244b1b1d9..b3e8190d471 100644 --- a/src/conversions/indexmap.rs +++ b/src/conversions/indexmap.rs @@ -137,7 +137,7 @@ where S: hash::BuildHasher + Default, { fn extract_bound(ob: &Bound<'py, PyAny>) -> Result { - let dict = ob.downcast::()?; + let dict = ob.cast::()?; let mut ret = indexmap::IndexMap::with_capacity_and_hasher(dict.len(), S::default()); for (k, v) in dict { ret.insert(k.extract()?, v.extract()?); diff --git a/src/conversions/jiff.rs b/src/conversions/jiff.rs index 1ba3894183a..fd2503646c0 100644 --- a/src/conversions/jiff.rs +++ b/src/conversions/jiff.rs @@ -156,7 +156,7 @@ impl<'py> IntoPyObject<'py> for &Date { impl<'py> FromPyObject<'py> for Date { fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { - let date = ob.downcast::()?; + let date = ob.cast::()?; #[cfg(not(Py_LIMITED_API))] { @@ -208,7 +208,7 @@ impl<'py> IntoPyObject<'py> for &Time { impl<'py> FromPyObject<'py> for Time { fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { - let ob = ob.downcast::()?; + let ob = ob.cast::()?; pytime_to_time(ob) } @@ -236,7 +236,7 @@ impl<'py> IntoPyObject<'py> for &DateTime { impl<'py> FromPyObject<'py> for DateTime { fn extract_bound(dt: &Bound<'py, PyAny>) -> PyResult { - let dt = dt.downcast::()?; + let dt = dt.cast::()?; let has_tzinfo = dt.get_tzinfo().is_some(); if has_tzinfo { @@ -285,7 +285,7 @@ impl<'py> IntoPyObject<'py> for &Zoned { impl<'py> FromPyObject<'py> for Zoned { fn extract_bound(dt: &Bound<'py, PyAny>) -> PyResult { - let dt = dt.downcast::()?; + let dt = dt.cast::()?; let tz = dt .get_tzinfo() @@ -342,7 +342,7 @@ impl<'py> IntoPyObject<'py> for &TimeZone { impl<'py> FromPyObject<'py> for TimeZone { fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { - let ob = ob.downcast::()?; + let ob = ob.cast::()?; let attr = intern!(ob.py(), "key"); if ob.hasattr(attr)? { @@ -380,7 +380,7 @@ impl<'py> IntoPyObject<'py> for Offset { impl<'py> FromPyObject<'py> for Offset { fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { let py = ob.py(); - let ob = ob.downcast::()?; + let ob = ob.cast::()?; let py_timedelta = ob.call_method1(intern!(py, "utcoffset"), (PyNone::get(py),))?; if py_timedelta.is_none() { @@ -427,7 +427,7 @@ impl<'py> IntoPyObject<'py> for SignedDuration { impl<'py> FromPyObject<'py> for SignedDuration { fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { - let delta = ob.downcast::()?; + let delta = ob.cast::()?; #[cfg(not(Py_LIMITED_API))] let (seconds, microseconds) = { diff --git a/src/conversions/num_bigint.rs b/src/conversions/num_bigint.rs index 73c6ef44bee..f9b4f11fa69 100644 --- a/src/conversions/num_bigint.rs +++ b/src/conversions/num_bigint.rs @@ -50,11 +50,8 @@ #[cfg(Py_LIMITED_API)] use crate::types::{bytes::PyBytesMethods, PyBytes}; use crate::{ - conversion::IntoPyObject, - ffi, - instance::Bound, - types::{any::PyAnyMethods, PyInt}, - FromPyObject, Py, PyAny, PyErr, PyResult, Python, + conversion::IntoPyObject, ffi, instance::Bound, types::PyInt, FromPyObject, Py, PyAny, PyErr, + PyResult, Python, }; use num_bigint::{BigInt, BigUint}; @@ -95,13 +92,14 @@ macro_rules! bigint_conversion { $is_signed.into(), ) .assume_owned(py) - .downcast_into_unchecked()) + .cast_into_unchecked()) } } #[cfg(Py_LIMITED_API)] fn into_pyobject(self, py: Python<'py>) -> Result { use $crate::py_result_ext::PyResultExt; + use $crate::types::any::PyAnyMethods; let bytes = $to_bytes(&self); let bytes_obj = PyBytes::new(py, &bytes); let kwargs = if $is_signed { @@ -114,7 +112,7 @@ macro_rules! bigint_conversion { unsafe { py.get_type::() .call_method("from_bytes", (bytes_obj, "little"), kwargs.as_ref()) - .downcast_into_unchecked() + .cast_into_unchecked() } } } @@ -130,7 +128,7 @@ impl<'py> FromPyObject<'py> for BigInt { 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 = if let Ok(long) = ob.cast::() { long } else { num_owned = unsafe { Py::from_owned_ptr_or_err(py, ffi::PyNumber_Index(ob.as_ptr()))? }; @@ -178,7 +176,7 @@ impl<'py> FromPyObject<'py> for BigUint { 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 = if let Ok(long) = ob.cast::() { long } else { num_owned = unsafe { Py::from_owned_ptr_or_err(py, ffi::PyNumber_Index(ob.as_ptr()))? }; @@ -276,6 +274,7 @@ fn int_to_py_bytes<'py>( is_signed: bool, ) -> PyResult> { use crate::intern; + use crate::types::any::PyAnyMethods; let py = long.py(); let kwargs = if is_signed { let kwargs = crate::types::PyDict::new(py); @@ -289,7 +288,7 @@ fn int_to_py_bytes<'py>( (n_bytes, intern!(py, "little")), kwargs.as_ref(), )?; - Ok(bytes.downcast_into()?) + Ok(bytes.cast_into()?) } #[inline] @@ -308,6 +307,7 @@ fn int_n_bits(long: &Bound<'_, PyInt>) -> PyResult { #[cfg(Py_LIMITED_API)] { + use crate::types::any::PyAnyMethods; // slow path long.call_method0(crate::intern!(py, "bit_length")) .and_then(|any| any.extract()) @@ -318,7 +318,7 @@ fn int_n_bits(long: &Bound<'_, PyInt>) -> PyResult { mod tests { use super::*; use crate::tests::common::generate_unique_module_name; - use crate::types::{PyDict, PyModule}; + use crate::types::{PyAnyMethods as _, PyDict, PyModule}; use indoc::indoc; use pyo3_ffi::c_str; diff --git a/src/conversions/num_complex.rs b/src/conversions/num_complex.rs index 6df4db5353b..1ed689c81c5 100644 --- a/src/conversions/num_complex.rs +++ b/src/conversions/num_complex.rs @@ -94,10 +94,8 @@ //! assert result == [complex(1,-1), complex(-2,0)] //! ``` use crate::{ - ffi, - ffi_ptr_ext::FfiPtrExt, - types::{any::PyAnyMethods, PyComplex}, - Bound, FromPyObject, PyAny, PyErr, PyResult, Python, + ffi, ffi_ptr_ext::FfiPtrExt, types::PyComplex, Bound, FromPyObject, PyAny, PyErr, PyResult, + Python, }; use num_complex::Complex; use std::os::raw::c_double; @@ -111,7 +109,7 @@ impl PyComplex { unsafe { ffi::PyComplex_FromDoubles(complex.re.into(), complex.im.into()) .assume_owned(py) - .downcast_into_unchecked() + .cast_into_unchecked() } } } @@ -129,7 +127,7 @@ macro_rules! complex_conversion { Ok( ffi::PyComplex_FromDoubles(self.re as c_double, self.im as c_double) .assume_owned(py) - .downcast_into_unchecked(), + .cast_into_unchecked(), ) } } @@ -163,6 +161,7 @@ macro_rules! complex_conversion { #[cfg(any(Py_LIMITED_API, PyPy))] unsafe { + use $crate::types::any::PyAnyMethods; let complex; let obj = if obj.is_instance_of::() { obj @@ -198,6 +197,7 @@ complex_conversion!(f64); mod tests { use super::*; use crate::tests::common::generate_unique_module_name; + use crate::types::PyAnyMethods as _; use crate::types::{complex::PyComplexMethods, PyModule}; use crate::IntoPyObject; use pyo3_ffi::c_str; diff --git a/src/conversions/smallvec.rs b/src/conversions/smallvec.rs index 25d97fc1a22..e6e7262590f 100644 --- a/src/conversions/smallvec.rs +++ b/src/conversions/smallvec.rs @@ -97,7 +97,7 @@ where // to support this function and if not, we will only fail extraction safely. let seq = unsafe { if ffi::PySequence_Check(obj.as_ptr()) != 0 { - obj.downcast_unchecked::() + obj.cast_unchecked::() } else { return Err(DowncastError::new(obj, "Sequence").into()); } @@ -152,7 +152,7 @@ mod tests { 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(); + let obj = obj.cast_into::().unwrap(); assert_eq!(obj.as_bytes(), &*bytes); let nums: SmallVec<[u16; 8]> = [1, 2, 3, 4, 5].iter().cloned().collect(); diff --git a/src/conversions/std/array.rs b/src/conversions/std/array.rs index 44a5a5fcb32..a7f2d953284 100644 --- a/src/conversions/std/array.rs +++ b/src/conversions/std/array.rs @@ -54,7 +54,7 @@ where // to support this function and if not, we will only fail extraction safely. let seq = unsafe { if ffi::PySequence_Check(obj.as_ptr()) != 0 { - obj.downcast_unchecked::() + obj.cast_unchecked::() } else { return Err(DowncastError::new(obj, "Sequence").into()); } @@ -181,7 +181,7 @@ mod tests { Python::attach(|py| { let array: [f32; 4] = [0.0, -16.0, 16.0, 42.0]; let pyobject = array.into_pyobject(py).unwrap(); - let pylist = pyobject.downcast::().unwrap(); + let pylist = pyobject.cast::().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); @@ -210,7 +210,7 @@ mod tests { let pylist = array .into_pyobject(py) .unwrap() - .downcast_into::() + .cast_into::() .unwrap(); assert_eq!(pylist.get_item(0).unwrap().extract::().unwrap(), 0.0); @@ -226,7 +226,7 @@ mod tests { let bytes: [u8; 6] = *b"foobar"; let obj = bytes.into_pyobject(py).unwrap(); assert!(obj.is_instance_of::()); - let obj = obj.downcast_into::().unwrap(); + let obj = obj.cast_into::().unwrap(); assert_eq!(obj.as_bytes(), &bytes); let nums: [u16; 4] = [0, 1, 2, 3]; @@ -255,9 +255,9 @@ mod tests { let list = array .into_pyobject(py) .unwrap() - .downcast_into::() + .cast_into::() .unwrap(); - let _bound = list.get_item(4).unwrap().downcast::().unwrap(); + let _bound = list.get_item(4).unwrap().cast::().unwrap(); }); } diff --git a/src/conversions/std/map.rs b/src/conversions/std/map.rs index 937570b7a59..fc96d211dc3 100644 --- a/src/conversions/std/map.rs +++ b/src/conversions/std/map.rs @@ -114,7 +114,7 @@ where S: hash::BuildHasher + Default, { fn extract_bound(ob: &Bound<'py, PyAny>) -> Result { - let dict = ob.downcast::()?; + let dict = ob.cast::()?; let mut ret = collections::HashMap::with_capacity_and_hasher(dict.len(), S::default()); for (k, v) in dict { ret.insert(k.extract()?, v.extract()?); @@ -134,7 +134,7 @@ where V: FromPyObject<'py>, { fn extract_bound(ob: &Bound<'py, PyAny>) -> Result { - let dict = ob.downcast::()?; + let dict = ob.cast::()?; let mut ret = collections::BTreeMap::new(); for (k, v) in dict { ret.insert(k.extract()?, v.extract()?); diff --git a/src/conversions/std/num.rs b/src/conversions/std/num.rs index 3366606b81b..f41c4ca567b 100644 --- a/src/conversions/std/num.rs +++ b/src/conversions/std/num.rs @@ -82,7 +82,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.cast::() { // 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 { @@ -108,7 +108,7 @@ macro_rules! int_convert_u64_or_i64 { unsafe { Ok($pylong_from_ll_or_ull(self) .assume_owned(py) - .downcast_into_unchecked()) + .cast_into_unchecked()) } } @@ -165,7 +165,7 @@ macro_rules! int_fits_c_long { unsafe { Ok(ffi::PyLong_FromLong(self as c_long) .assume_owned(py) - .downcast_into_unchecked()) + .cast_into_unchecked()) } } @@ -224,7 +224,7 @@ impl<'py> IntoPyObject<'py> for u8 { unsafe { Ok(ffi::PyLong_FromLong(self as c_long) .assume_owned(py) - .downcast_into_unchecked()) + .cast_into_unchecked()) } } @@ -352,7 +352,7 @@ mod fast_128bit_int_conversion { $is_signed.into(), ) .assume_owned(py) - .downcast_into_unchecked()) + .cast_into_unchecked()) } } #[cfg(Py_3_13)] @@ -367,7 +367,7 @@ mod fast_128bit_int_conversion { ffi::Py_ASNATIVEBYTES_NATIVE_ENDIAN, ) .assume_owned(py) - .downcast_into_unchecked()) + .cast_into_unchecked()) } } else { unsafe { @@ -377,7 +377,7 @@ mod fast_128bit_int_conversion { ffi::Py_ASNATIVEBYTES_NATIVE_ENDIAN, ) .assume_owned(py) - .downcast_into_unchecked()) + .cast_into_unchecked()) } } } @@ -497,7 +497,7 @@ mod slow_128bit_int_conversion { Ok(ffi::PyNumber_Or(shifted.as_ptr(), lower.as_ptr()) .assume_owned(py) - .downcast_into_unchecked()) + .cast_into_unchecked()) } } diff --git a/src/conversions/std/osstr.rs b/src/conversions/std/osstr.rs index d9ee24226f6..acc6f2bab7f 100644 --- a/src/conversions/std/osstr.rs +++ b/src/conversions/std/osstr.rs @@ -1,7 +1,6 @@ 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, PyAny, PyResult, Python}; use std::borrow::Cow; @@ -35,7 +34,7 @@ impl<'py> IntoPyObject<'py> for &OsStr { // parse os strings losslessly (i.e. surrogateescape most of the time) Ok(ffi::PyUnicode_DecodeFSDefaultAndSize(ptr, len) .assume_owned(py) - .downcast_into_unchecked::()) + .cast_into_unchecked::()) } } @@ -50,7 +49,7 @@ impl<'py> IntoPyObject<'py> for &OsStr { Ok( ffi::PyUnicode_FromWideChar(wstr.as_ptr(), wstr.len() as ffi::Py_ssize_t) .assume_owned(py) - .downcast_into_unchecked::(), + .cast_into_unchecked::(), ) } } @@ -73,7 +72,7 @@ impl<'py> IntoPyObject<'py> for &&OsStr { impl FromPyObject<'_> for OsString { fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { - let pystring = ob.downcast::()?; + let pystring = ob.cast::()?; #[cfg(not(windows))] { @@ -207,7 +206,7 @@ mod tests { T::Error: Debug, { let pyobject = obj.clone().into_pyobject(py).unwrap().into_any(); - let pystring = pyobject.as_borrowed().downcast::().unwrap(); + let pystring = pyobject.as_borrowed().cast::().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/set.rs b/src/conversions/std/set.rs index 16c76032f16..4d4073101a0 100644 --- a/src/conversions/std/set.rs +++ b/src/conversions/std/set.rs @@ -59,10 +59,10 @@ where S: hash::BuildHasher + Default, { fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { - match ob.downcast::() { + match ob.cast::() { Ok(set) => set.iter().map(|any| any.extract()).collect(), Err(err) => { - if let Ok(frozen_set) = ob.downcast::() { + if let Ok(frozen_set) = ob.cast::() { frozen_set.iter().map(|any| any.extract()).collect() } else { Err(PyErr::from(err)) @@ -119,10 +119,10 @@ where K: FromPyObject<'py> + cmp::Ord, { fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { - match ob.downcast::() { + match ob.cast::() { Ok(set) => set.iter().map(|any| any.extract()).collect(), Err(err) => { - if let Ok(frozen_set) = ob.downcast::() { + if let Ok(frozen_set) = ob.cast::() { frozen_set.iter().map(|any| any.extract()).collect() } else { Err(PyErr::from(err)) diff --git a/src/conversions/std/slice.rs b/src/conversions/std/slice.rs index 8060f1e9ee8..dca72b8fdf7 100644 --- a/src/conversions/std/slice.rs +++ b/src/conversions/std/slice.rs @@ -37,7 +37,7 @@ where 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()) + Ok(obj.cast::()?.as_bytes()) } #[cfg(feature = "experimental-inspect")] @@ -53,11 +53,11 @@ impl<'a> crate::conversion::FromPyObjectBound<'a, '_> for &'a [u8] { /// If it is a `bytearray`, its contents will be copied to an owned `Cow`. 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::() { + if let Ok(bytes) = ob.cast::() { return Ok(Cow::Borrowed(bytes.as_bytes())); } - let byte_array = ob.downcast::()?; + let byte_array = ob.cast::()?; Ok(Cow::Owned(byte_array.to_vec())) } @@ -140,7 +140,7 @@ mod tests { let bytes: &[u8] = b"foobar"; let obj = bytes.into_pyobject(py).unwrap(); assert!(obj.is_instance_of::()); - let obj = obj.downcast_into::().unwrap(); + let obj = obj.cast_into::().unwrap(); assert_eq!(obj.as_bytes(), bytes); let nums: &[u16] = &[0, 1, 2, 3]; @@ -155,13 +155,13 @@ mod tests { 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(); + let obj = obj.cast_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(); + let obj = obj.cast_into::().unwrap(); assert_eq!(obj.as_bytes(), &*owned_bytes); let borrowed_nums = Cow::<[u16]>::Borrowed(&[0, 1, 2, 3]); diff --git a/src/conversions/std/string.rs b/src/conversions/std/string.rs index 2aa7e1a6de1..402028d39be 100644 --- a/src/conversions/std/string.rs +++ b/src/conversions/std/string.rs @@ -5,7 +5,7 @@ use crate::inspect::types::TypeInfo; use crate::{ conversion::IntoPyObject, instance::Bound, - types::{any::PyAnyMethods, string::PyStringMethods, PyString}, + types::{string::PyStringMethods, PyString}, FromPyObject, PyAny, PyResult, Python, }; @@ -142,7 +142,7 @@ impl<'a> crate::conversion::FromPyObjectBound<'a, '_> for &'a str { const INPUT_TYPE: &'static str = "str"; fn from_py_object_bound(ob: crate::Borrowed<'a, '_, PyAny>) -> PyResult { - ob.downcast::()?.to_str() + ob.cast::()?.to_str() } #[cfg(feature = "experimental-inspect")] @@ -156,7 +156,7 @@ impl<'a> crate::conversion::FromPyObjectBound<'a, '_> for Cow<'a, str> { const INPUT_TYPE: &'static str = "str"; fn from_py_object_bound(ob: crate::Borrowed<'a, '_, PyAny>) -> PyResult { - ob.downcast::()?.to_cow() + ob.cast::()?.to_cow() } #[cfg(feature = "experimental-inspect")] @@ -172,7 +172,7 @@ impl FromPyObject<'_> for String { const INPUT_TYPE: &'static str = "str"; fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { - obj.downcast::()?.to_cow().map(Cow::into_owned) + obj.cast::()?.to_cow().map(Cow::into_owned) } #[cfg(feature = "experimental-inspect")] @@ -186,7 +186,7 @@ impl FromPyObject<'_> for char { const INPUT_TYPE: &'static str = "str"; fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { - let s = obj.downcast::()?.to_cow()?; + let s = obj.cast::()?.to_cow()?; let mut iter = s.chars(); if let (Some(ch), None) = (iter.next(), iter.next()) { Ok(ch) diff --git a/src/conversions/std/time.rs b/src/conversions/std/time.rs index e46facbb6b8..62ee9693dc3 100644 --- a/src/conversions/std/time.rs +++ b/src/conversions/std/time.rs @@ -14,7 +14,7 @@ const SECONDS_PER_DAY: u64 = 24 * 60 * 60; impl FromPyObject<'_> for Duration { fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { - let delta = obj.downcast::()?; + let delta = obj.cast::()?; #[cfg(not(Py_LIMITED_API))] let (days, seconds, microseconds) = { ( @@ -108,7 +108,7 @@ impl<'py> IntoPyObject<'py> for SystemTime { self.duration_since(UNIX_EPOCH).unwrap().into_pyobject(py)?; unix_epoch_py(py)? .add(duration_since_unix_epoch)? - .downcast_into() + .cast_into() .map_err(Into::into) } } @@ -343,7 +343,7 @@ mod tests { naive_max .call_method("replace", (), Some(&kargs)) .unwrap() - .downcast_into() + .cast_into() .unwrap() } diff --git a/src/conversions/std/vec.rs b/src/conversions/std/vec.rs index 02f82439cfe..6ed71e44840 100644 --- a/src/conversions/std/vec.rs +++ b/src/conversions/std/vec.rs @@ -61,7 +61,7 @@ mod tests { 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(); + let obj = obj.cast_into::().unwrap(); assert_eq!(obj.as_bytes(), &bytes); let nums: Vec = vec![0, 1, 2, 3]; @@ -76,7 +76,7 @@ mod tests { 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(); + let obj = obj.cast_into::().unwrap(); assert_eq!(obj.as_bytes(), &bytes); let nums: Vec = vec![0, 1, 2, 3]; diff --git a/src/conversions/time.rs b/src/conversions/time.rs index 448a199a19b..d9161f9553a 100644 --- a/src/conversions/time.rs +++ b/src/conversions/time.rs @@ -105,7 +105,7 @@ macro_rules! month_from_number { fn extract_date_time(dt: &Bound<'_, PyAny>) -> PyResult<(Date, Time)> { #[cfg(not(Py_LIMITED_API))] { - let dt = dt.downcast::()?; + let dt = dt.cast::()?; let date = Date::from_calendar_date( dt.get_year(), month_from_number!(dt.get_month()), @@ -184,7 +184,7 @@ impl FromPyObject<'_> for Duration { fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { #[cfg(not(Py_LIMITED_API))] let (days, seconds, microseconds) = { - let delta = ob.downcast::()?; + let delta = ob.cast::()?; ( delta.get_days().into(), delta.get_seconds().into(), @@ -228,7 +228,7 @@ impl FromPyObject<'_> for Date { let (year, month, day) = { #[cfg(not(Py_LIMITED_API))] { - let date = ob.downcast::()?; + let date = ob.cast::()?; (date.get_year(), date.get_month(), date.get_day()) } @@ -269,7 +269,7 @@ impl FromPyObject<'_> for Time { let (hour, minute, second, microsecond) = { #[cfg(not(Py_LIMITED_API))] { - let time = ob.downcast::()?; + let time = ob.cast::()?; let hour: u8 = time.get_hour(); let minute: u8 = time.get_minute(); let second: u8 = time.get_second(); @@ -328,7 +328,7 @@ impl FromPyObject<'_> for PrimitiveDateTime { let has_tzinfo = { #[cfg(not(Py_LIMITED_API))] { - let dt = dt.downcast::()?; + let dt = dt.cast::()?; dt.get_tzinfo().is_some() } #[cfg(Py_LIMITED_API)] @@ -363,7 +363,7 @@ impl<'py> IntoPyObject<'py> for UtcOffset { impl FromPyObject<'_> for UtcOffset { fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { #[cfg(not(Py_LIMITED_API))] - let ob = ob.downcast::()?; + let ob = ob.cast::()?; // Get the offset in seconds from the Python tzinfo let py_timedelta = ob.call_method1("utcoffset", (PyNone::get(ob.py()),))?; @@ -412,7 +412,7 @@ impl<'py> IntoPyObject<'py> for OffsetDateTime { minute, second, microsecond, - Some(py_tzinfo.downcast()?), + Some(py_tzinfo.cast()?), ) } } @@ -422,7 +422,7 @@ impl FromPyObject<'_> for OffsetDateTime { let offset: UtcOffset = { #[cfg(not(Py_LIMITED_API))] { - let dt = ob.downcast::()?; + let dt = ob.cast::()?; let tzinfo = dt.get_tzinfo().ok_or_else(|| { PyTypeError::new_err("expected a datetime with non-None tzinfo") })?; @@ -485,7 +485,7 @@ impl FromPyObject<'_> for UtcDateTime { let tzinfo = { #[cfg(not(Py_LIMITED_API))] { - let dt = ob.downcast::()?; + let dt = ob.cast::()?; dt.get_tzinfo().ok_or_else(|| { PyTypeError::new_err("expected a datetime with non-None tzinfo") })? diff --git a/src/err/err_state.rs b/src/err/err_state.rs index 1a8a54186cd..bb1fe1a465f 100644 --- a/src/err/err_state.rs +++ b/src/err/err_state.rs @@ -178,7 +178,7 @@ impl PyErrStateNormalized { unsafe { ffi::PyException_GetTraceback(self.pvalue.as_ptr()) .assume_owned_or_opt(py) - .map(|b| b.downcast_into_unchecked()) + .map(|b| b.cast_into_unchecked()) } } @@ -190,7 +190,7 @@ impl PyErrStateNormalized { 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(), + pvalue: unsafe { pvalue.cast_into_unchecked() }.unbind(), } }) } @@ -214,13 +214,13 @@ impl PyErrStateNormalized { ( ptype .assume_owned_or_opt(py) - .map(|b| b.downcast_into_unchecked()), + .map(|b| b.cast_into_unchecked()), pvalue .assume_owned_or_opt(py) - .map(|b| b.downcast_into_unchecked()), + .map(|b| b.cast_into_unchecked()), ptraceback .assume_owned_or_opt(py) - .map(|b| b.downcast_into_unchecked()), + .map(|b| b.cast_into_unchecked()), ) }; diff --git a/src/err/mod.rs b/src/err/mod.rs index 19c51d25203..929ecaf511a 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -89,7 +89,7 @@ impl<'py> DowncastIntoError<'py> { /// Consumes this `DowncastIntoError` and returns the original object, allowing continued /// use of it after a failed conversion. /// - /// See [`downcast_into`][PyAnyMethods::downcast_into] for an example. + /// See [`cast_into`][Bound::cast_into] for an example. pub fn into_inner(self) -> Bound<'py, PyAny> { self.from } @@ -231,7 +231,7 @@ impl PyErr { /// }); /// ``` pub fn from_value(obj: Bound<'_, PyAny>) -> PyErr { - let state = match obj.downcast_into::() { + let state = match obj.cast_into::() { Ok(obj) => PyErrState::normalized(PyErrStateNormalized::new(obj)), Err(err) => { // Assume obj is Type[Exception]; let later normalization handle if this diff --git a/src/exceptions.rs b/src/exceptions.rs index 721dd33dbdd..d3b9f994dd5 100644 --- a/src/exceptions.rs +++ b/src/exceptions.rs @@ -650,7 +650,7 @@ impl PyUnicodeDecodeError { ) .assume_owned_or_err(py) } - .downcast_into() + .cast_into() } /// Creates a Python `UnicodeDecodeError` from a Rust UTF-8 decoding error. @@ -754,7 +754,6 @@ macro_rules! test_exception { use super::$exc_ty; $crate::Python::attach(|py| { - use $crate::types::PyAnyMethods; let err: $crate::PyErr = { None $( @@ -765,7 +764,7 @@ macro_rules! test_exception { assert!(err.is_instance_of::<$exc_ty>(py)); - let value = err.value(py).as_any().downcast::<$exc_ty>().unwrap(); + let value = err.value(py).as_any().cast::<$exc_ty>().unwrap(); assert!($crate::PyErr::from(value.clone()).is_instance_of::<$exc_ty>(py)); }) diff --git a/src/impl_/coroutine.rs b/src/impl_/coroutine.rs index e624b4ee6a7..ad3a19d721f 100644 --- a/src/impl_/coroutine.rs +++ b/src/impl_/coroutine.rs @@ -8,7 +8,7 @@ use crate::{ instance::Bound, pycell::impl_::PyClassBorrowChecker, pyclass::boolean_struct::False, - types::{PyAnyMethods, PyString}, + types::PyString, IntoPyObject, Py, PyAny, PyClass, PyErr, PyResult, Python, }; @@ -34,7 +34,7 @@ pub struct RefGuard(Py); impl RefGuard { pub fn new(obj: &Bound<'_, PyAny>) -> PyResult { - let bound = obj.downcast::()?; + let bound = obj.cast::()?; bound.get_class_object().borrow_checker().try_borrow()?; Ok(RefGuard(bound.clone().unbind())) } @@ -64,7 +64,7 @@ pub struct RefMutGuard>(Py); impl> RefMutGuard { pub fn new(obj: &Bound<'_, PyAny>) -> PyResult { - let bound = obj.downcast::()?; + let bound = obj.cast::()?; bound.get_class_object().borrow_checker().try_borrow_mut()?; Ok(RefMutGuard(bound.clone().unbind())) } diff --git a/src/impl_/extract_argument.rs b/src/impl_/extract_argument.rs index 77c875c41c0..0bca69ce3af 100644 --- a/src/impl_/extract_argument.rs +++ b/src/impl_/extract_argument.rs @@ -57,7 +57,7 @@ where #[inline] fn extract(obj: &'a Bound<'py, PyAny>, _: &'holder mut ()) -> PyResult { - obj.downcast().map_err(Into::into) + obj.cast().map_err(Into::into) } } @@ -331,7 +331,7 @@ 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> = unsafe { - Borrowed::from_ptr_or_opt(py, kwnames).map(|kwnames| kwnames.downcast_unchecked()) + Borrowed::from_ptr_or_opt(py, kwnames).map(|kwnames| kwnames.cast_unchecked()) }; if let Some(kwnames) = kwnames { let kwargs = unsafe { @@ -386,10 +386,9 @@ 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> = - unsafe { Borrowed::from_ptr(py, args).downcast_unchecked::() }; - let kwargs: Option> = unsafe { - Borrowed::from_ptr_or_opt(py, kwargs).map(|kwargs| kwargs.downcast_unchecked()) - }; + unsafe { Borrowed::from_ptr(py, args).cast_unchecked::() }; + let kwargs: Option> = + unsafe { Borrowed::from_ptr_or_opt(py, kwargs).map(|kwargs| kwargs.cast_unchecked()) }; let num_positional_parameters = self.positional_parameter_names.len(); @@ -457,7 +456,7 @@ impl FunctionDescription { // will return an error anyway. #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] let kwarg_name = - unsafe { kwarg_name_py.downcast_unchecked::() }.to_str(); + unsafe { kwarg_name_py.cast_unchecked::() }.to_str(); #[cfg(all(not(Py_3_10), Py_LIMITED_API))] let kwarg_name = kwarg_name_py.extract::(); diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index bbde7a0f0fe..830091e4d98 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -7,7 +7,6 @@ use crate::{ pyclass_init::PyObjectInit, pymethods::{PyGetterDef, PyMethodDefType}, }, - internal_tricks::ptr_from_ref, pycell::PyBorrowError, types::{any::PyAnyMethods, PyBool}, Borrowed, BoundObject, IntoPyObject, IntoPyObjectExt, Py, PyAny, PyClass, PyClassGuard, PyErr, @@ -1373,7 +1372,7 @@ unsafe fn ensure_no_mutable_alias<'a, ClassT: PyClass>( _py: Python<'_>, obj: &'a *mut ffi::PyObject, ) -> Result, PyBorrowError> { - unsafe { PyClassGuard::try_borrow(&*ptr_from_ref(obj).cast::>()) } + unsafe { PyClassGuard::try_borrow(NonNull::from(obj).cast::>().as_ref()) } } /// calculates the field pointer from an PyObject pointer diff --git a/src/impl_/pyclass_init.rs b/src/impl_/pyclass_init.rs index c6bea31a743..c3a112d3db8 100644 --- a/src/impl_/pyclass_init.rs +++ b/src/impl_/pyclass_init.rs @@ -44,7 +44,7 @@ impl PyObjectInit for PyNativeTypeInitializer { subtype .cast::() .assume_borrowed_unchecked(py) - .downcast_unchecked() + .cast_unchecked() }; if is_base_object { diff --git a/src/impl_/pymethods.rs b/src/impl_/pymethods.rs index 1087517c4d5..30029365ab0 100644 --- a/src/impl_/pymethods.rs +++ b/src/impl_/pymethods.rs @@ -7,7 +7,6 @@ use crate::internal::state::ForbidAttaching; 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, PyClassGuard, PyClassGuardMut, @@ -651,11 +650,11 @@ impl<'a, 'py> BoundRef<'a, 'py, PyAny> { } pub fn downcast(self) -> Result, DowncastError<'a, 'py>> { - self.0.downcast::().map(BoundRef) + self.0.cast::().map(BoundRef) } pub unsafe fn downcast_unchecked(self) -> BoundRef<'a, 'py, T> { - unsafe { BoundRef(self.0.downcast_unchecked::()) } + unsafe { BoundRef(self.0.cast_unchecked::()) } } } diff --git a/src/instance.rs b/src/instance.rs index 510f21c3d3f..29bac257fee 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -8,8 +8,8 @@ use crate::pyclass::boolean_struct::{False, True}; use crate::types::{any::PyAnyMethods, string::PyStringMethods, typeobject::PyTypeMethods}; use crate::types::{DerefToPyAny, PyDict, PyString}; use crate::{ - ffi, DowncastError, FromPyObject, PyAny, PyClass, PyClassInitializer, PyRef, PyRefMut, - PyTypeInfo, Python, + ffi, DowncastError, DowncastIntoError, FromPyObject, PyAny, PyClass, PyClassInitializer, PyRef, + PyRefMut, PyTypeInfo, Python, }; use crate::{internal::state, PyTypeCheck}; use std::marker::PhantomData; @@ -97,6 +97,223 @@ where } } +impl<'py, T> Bound<'py, T> { + /// Cast this to a concrete Python type or pyclass. + /// + /// Note that you can often avoid casting yourself by just specifying the desired type in + /// function or method signatures. However, manual casting is sometimes necessary. + /// + /// For extracting a Rust-only type, see [`extract`](PyAnyMethods::extract). + /// + /// This performs a runtime type check using the equivalent of Python's + /// `isinstance(self, U)`. + /// + /// # Example: Casting to a specific Python object + /// + /// ```rust + /// use pyo3::prelude::*; + /// use pyo3::types::{PyDict, PyList}; + /// + /// Python::attach(|py| { + /// let dict = PyDict::new(py); + /// assert!(dict.is_instance_of::()); + /// let any = dict.as_any(); + /// + /// assert!(any.cast::().is_ok()); + /// assert!(any.cast::().is_err()); + /// }); + /// ``` + /// + /// # Example: Getting a reference to a pyclass + /// + /// This is useful if you want to mutate a `Py` that might actually be a pyclass. + /// + /// ```rust + /// # fn main() -> Result<(), pyo3::PyErr> { + /// use pyo3::prelude::*; + /// + /// #[pyclass] + /// struct Class { + /// i: i32, + /// } + /// + /// Python::attach(|py| { + /// let class = Bound::new(py, Class { i: 0 })?.into_any(); + /// + /// let class_bound: &Bound<'_, Class> = class.cast()?; + /// + /// 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 cast(&self) -> Result<&Bound<'py, U>, DowncastError<'_, 'py>> + where + U: PyTypeCheck, + { + #[inline] + fn inner<'a, 'py, U>( + any: &'a Bound<'py, PyAny>, + ) -> Result<&'a Bound<'py, U>, DowncastError<'a, 'py>> + where + U: PyTypeCheck, + { + if U::type_check(any) { + // Safety: type_check is responsible for ensuring that the type is correct + Ok(unsafe { any.cast_unchecked() }) + } else { + Err(DowncastError::new(any, U::NAME)) + } + } + + inner(self.as_any()) + } + + /// Like [`cast`](Self::cast) but takes ownership of `self`. + /// + /// In case of an error, it is possible to retrieve `self` again via + /// [`DowncastIntoError::into_inner`]. + /// + /// # Example + /// + /// ```rust + /// use pyo3::prelude::*; + /// use pyo3::types::{PyDict, PyList}; + /// + /// Python::attach(|py| { + /// let obj: Bound<'_, PyAny> = PyDict::new(py).into_any(); + /// + /// let obj: Bound<'_, PyAny> = match obj.cast_into::() { + /// Ok(_) => panic!("obj should not be a list"), + /// Err(err) => err.into_inner(), + /// }; + /// + /// // obj is a dictionary + /// assert!(obj.cast_into::().is_ok()); + /// }) + /// ``` + #[inline] + pub fn cast_into(self) -> Result, DowncastIntoError<'py>> + where + U: PyTypeCheck, + { + #[inline] + fn inner(any: Bound<'_, PyAny>) -> Result, DowncastIntoError<'_>> + where + U: PyTypeCheck, + { + if U::type_check(&any) { + // Safety: type_check is responsible for ensuring that the type is correct + Ok(unsafe { any.cast_into_unchecked() }) + } else { + Err(DowncastIntoError::new(any, U::NAME)) + } + } + + inner(self.into_any()) + } + + /// Cast this to a concrete Python type or pyclass (but not a subclass of it). + /// + /// It is almost always better to use [`cast`](Self::cast) because it accounts for Python + /// subtyping. Use this method only when you do not want to allow subtypes. + /// + /// The advantage of this method over [`cast`](Self::cast) is that it is faster. The + /// implementation of `cast_exact` uses the equivalent of the Python expression `type(self) is + /// U`, whereas `cast` uses `isinstance(self, U)`. + /// + /// For extracting a Rust-only type, see [`extract`](PyAnyMethods::extract). + /// + /// # Example: Casting to a specific Python object but not a subtype + /// + /// ```rust + /// use pyo3::prelude::*; + /// use pyo3::types::{PyBool, PyInt}; + /// + /// Python::attach(|py| { + /// let b = PyBool::new(py, true); + /// assert!(b.is_instance_of::()); + /// let any: &Bound<'_, PyAny> = b.as_any(); + /// + /// // `bool` is a subtype of `int`, so `cast` will accept a `bool` as an `int` + /// // but `cast_exact` will not. + /// assert!(any.cast::().is_ok()); + /// assert!(any.cast_exact::().is_err()); + /// + /// assert!(any.cast_exact::().is_ok()); + /// }); + /// ``` + #[inline] + pub fn cast_exact(&self) -> Result<&Bound<'py, U>, DowncastError<'_, 'py>> + where + U: PyTypeInfo, + { + #[inline] + fn inner<'a, 'py, U>( + any: &'a Bound<'py, PyAny>, + ) -> Result<&'a Bound<'py, U>, DowncastError<'a, 'py>> + where + U: PyTypeInfo, + { + if any.is_exact_instance_of::() { + // Safety: is_exact_instance_of is responsible for ensuring that the type is correct + Ok(unsafe { any.cast_unchecked() }) + } else { + Err(DowncastError::new(any, U::NAME)) + } + } + + inner(self.as_any()) + } + + /// Like [`cast_exact`](Self::cast_exact) but takes ownership of `self`. + #[inline] + pub fn cast_into_exact(self) -> Result, DowncastIntoError<'py>> + where + U: PyTypeInfo, + { + #[inline] + fn inner(any: Bound<'_, PyAny>) -> Result, DowncastIntoError<'_>> + where + U: PyTypeInfo, + { + if any.is_exact_instance_of::() { + // Safety: is_exact_instance_of is responsible for ensuring that the type is correct + Ok(unsafe { any.cast_into_unchecked() }) + } else { + Err(DowncastIntoError::new(any, U::NAME)) + } + } + + inner(self.into_any()) + } + + /// Converts this 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 cast_unchecked(&self) -> &Bound<'py, U> { + unsafe { NonNull::from(self).cast().as_ref() } + } + + /// Like [`cast_unchecked`](Self::cast_unchecked) but takes ownership of `self`. + /// + /// # Safety + /// + /// Callers must ensure that the type is valid or risk type confusion. + #[inline] + pub unsafe fn cast_into_unchecked(self) -> Bound<'py, U> { + unsafe { std::mem::transmute(self) } + } +} + impl<'py> Bound<'py, PyAny> { /// Constructs a new `Bound<'py, PyAny>` from a pointer. Panics if `ptr` is null. /// @@ -428,8 +645,8 @@ where /// [`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() } + // a pyclass can always be safely "cast" to its base type + unsafe { self.as_any().cast_unchecked() } } /// Upcast this `Bound` to its base type by value. @@ -480,8 +697,8 @@ where /// [`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() } + // a pyclass can always be safely "cast" to its base type + unsafe { self.cast_into_unchecked() } } #[inline] @@ -809,13 +1026,13 @@ impl<'a, 'py> Borrowed<'a, 'py, PyAny> { } #[inline] - pub(crate) fn downcast(self) -> Result, DowncastError<'a, 'py>> + pub(crate) fn cast(self) -> Result, DowncastError<'a, 'py>> where T: PyTypeCheck, { if T::type_check(&self) { // Safety: type_check is responsible for ensuring that the type is correct - Ok(unsafe { self.downcast_unchecked() }) + Ok(unsafe { self.cast_unchecked() }) } else { Err(DowncastError::new_from_borrowed(self, T::NAME)) } @@ -826,7 +1043,7 @@ impl<'a, 'py> Borrowed<'a, 'py, PyAny> { /// # Safety /// Callers must ensure that the type is valid or risk type confusion. #[inline] - pub(crate) unsafe fn downcast_unchecked(self) -> Borrowed<'a, 'py, T> { + pub(crate) unsafe fn cast_unchecked(self) -> Borrowed<'a, 'py, T> { Borrowed(self.0, PhantomData, self.2) } } @@ -981,7 +1198,7 @@ impl<'a, 'py, T> BoundObject<'py, T> for Borrowed<'a, 'py, T> { /// # let m = pyo3::types::PyModule::new(py, "test")?; /// # m.add_class::()?; /// # -/// # let foo: Bound<'_, Foo> = m.getattr("Foo")?.call0()?.downcast_into()?; +/// # let foo: Bound<'_, Foo> = m.getattr("Foo")?.call0()?.cast_into()?; /// # let dict = &foo.borrow().inner; /// # let dict: &Bound<'_, PyDict> = dict.bind(py); /// # @@ -1018,7 +1235,7 @@ impl<'a, 'py, T> BoundObject<'py, T> for Borrowed<'a, 'py, T> { /// # let m = pyo3::types::PyModule::new(py, "test")?; /// # m.add_class::()?; /// # -/// # let foo: Bound<'_, Foo> = m.getattr("Foo")?.call0()?.downcast_into()?; +/// # let foo: Bound<'_, Foo> = m.getattr("Foo")?.call0()?.cast_into()?; /// # let bar = &foo.borrow().inner; /// # let bar: &Bar = &*bar.borrow(py); /// # @@ -1823,7 +2040,7 @@ where { /// Extracts `Self` from the source `PyObject`. fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { - ob.downcast().cloned().map_err(Into::into) + ob.cast().cloned().map_err(Into::into) } } @@ -1851,32 +2068,43 @@ impl std::fmt::Debug for Py { pub type PyObject = Py; impl PyObject { - /// Downcast this `PyObject` to a concrete Python type or pyclass. + /// Deprecated version of [`PyObject::cast_bound`] + #[inline] + #[deprecated(since = "0.26.0", note = "use `Py::cast_bound_unchecked` instead")] + pub fn downcast_bound<'py, T>( + &self, + py: Python<'py>, + ) -> Result<&Bound<'py, T>, DowncastError<'_, 'py>> + where + T: PyTypeCheck, + { + self.cast_bound(py) + } + + /// Cast this `Py` 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. + /// Note that you can often avoid casting yourself by just specifying the desired type in + /// function or method signatures. However, manual casting is sometimes necessary. /// - /// For extracting a Rust-only type, see [`Py::extract`](struct.Py.html#method.extract). + /// For extracting a Rust-only type, see [`Py::extract`]. /// - /// # Example: Downcasting to a specific Python object + /// # Example: Casting to a specific Python object /// /// ```rust /// use pyo3::prelude::*; /// use pyo3::types::{PyDict, PyList}; /// /// Python::attach(|py| { - /// let any: PyObject = PyDict::new(py).into(); + /// let any = PyDict::new(py).into_any().unbind(); /// - /// assert!(any.downcast_bound::(py).is_ok()); - /// assert!(any.downcast_bound::(py).is_err()); + /// assert!(any.cast_bound::(py).is_ok()); + /// assert!(any.cast_bound::(py).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. + /// This is useful if you want to mutate a `Py` that might actually be a pyclass. /// /// ```rust /// # fn main() -> Result<(), pyo3::PyErr> { @@ -1888,9 +2116,9 @@ impl PyObject { /// } /// /// Python::attach(|py| { - /// let class: PyObject = Py::new(py, Class { i: 0 })?.into_any(); + /// let class = Py::new(py, Class { i: 0 })?.into_any(); /// - /// let class_bound = class.downcast_bound::(py)?; + /// let class_bound = class.cast_bound::(py)?; /// /// class_bound.borrow_mut().i += 1; /// @@ -1901,15 +2129,14 @@ impl PyObject { /// }) /// # } /// ``` - #[inline] - pub fn downcast_bound<'py, T>( + pub fn cast_bound<'py, T>( &self, py: Python<'py>, ) -> Result<&Bound<'py, T>, DowncastError<'_, 'py>> where T: PyTypeCheck, { - self.bind(py).downcast() + self.bind(py).cast() } /// Casts the PyObject to a concrete Python object type without checking validity. @@ -1918,8 +2145,19 @@ impl PyObject { /// /// Callers must ensure that the type is valid or risk type confusion. #[inline] + #[deprecated(since = "0.26.0", note = "use `Py::cast_bound_unchecked` instead")] pub unsafe fn downcast_bound_unchecked<'py, T>(&self, py: Python<'py>) -> &Bound<'py, T> { - unsafe { self.bind(py).downcast_unchecked() } + unsafe { self.cast_bound_unchecked(py) } + } + + /// Casts the PyObject to a concrete Python object type without checking validity. + /// + /// # Safety + /// + /// Callers must ensure that the type is valid or risk type confusion. + #[inline] + pub unsafe fn cast_bound_unchecked<'py, T>(&self, py: Python<'py>) -> &Bound<'py, T> { + unsafe { self.bind(py).cast_unchecked() } } } diff --git a/src/marker.rs b/src/marker.rs index c36cd7111e8..bf0a2fad3a1 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -645,7 +645,7 @@ impl<'py> Python<'py> { /// ) /// .unwrap(); /// let ret = locals.get_item("ret").unwrap().unwrap(); - /// let b64 = ret.downcast::().unwrap(); + /// let b64 = ret.cast::().unwrap(); /// assert_eq!(b64.as_bytes(), b"SGVsbG8gUnVzdCE="); /// }); /// ``` diff --git a/src/marshal.rs b/src/marshal.rs index 0cb82573b2d..abe5e5ed96f 100644 --- a/src/marshal.rs +++ b/src/marshal.rs @@ -36,7 +36,7 @@ pub fn dumps<'py>(object: &Bound<'py, PyAny>, version: i32) -> PyResult: crate::sealed::Sealed { - fn downcast_into(self) -> PyResult>; - unsafe fn downcast_into_unchecked(self) -> PyResult>; + fn cast_into(self) -> PyResult>; + unsafe fn cast_into_unchecked(self) -> PyResult>; } impl<'py> PyResultExt<'py> for PyResult> { #[inline] - fn downcast_into(self) -> PyResult> where { - self.and_then(|instance| instance.downcast_into().map_err(Into::into)) + fn cast_into(self) -> PyResult> where { + self.and_then(|instance| instance.cast_into().map_err(Into::into)) } #[inline] - unsafe fn downcast_into_unchecked(self) -> PyResult> { - self.map(|instance| unsafe { instance.downcast_into_unchecked() }) + unsafe fn cast_into_unchecked(self) -> PyResult> { + self.map(|instance| unsafe { instance.cast_into_unchecked() }) } } diff --git a/src/pybacked.rs b/src/pybacked.rs index b1ee692003d..aab2b066745 100644 --- a/src/pybacked.rs +++ b/src/pybacked.rs @@ -4,8 +4,8 @@ use std::{convert::Infallible, ops::Deref, ptr::NonNull, sync::Arc}; use crate::{ types::{ - any::PyAnyMethods, bytearray::PyByteArrayMethods, bytes::PyBytesMethods, - string::PyStringMethods, PyByteArray, PyBytes, PyString, + bytearray::PyByteArrayMethods, bytes::PyBytesMethods, string::PyStringMethods, PyByteArray, + PyBytes, PyString, }, Bound, DowncastError, FromPyObject, IntoPyObject, Py, PyAny, PyErr, PyResult, Python, }; @@ -80,7 +80,7 @@ impl TryFrom> for PyBackedStr { impl FromPyObject<'_> for PyBackedStr { fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { - let py_string = obj.downcast::()?.to_owned(); + let py_string = obj.cast::()?.to_owned(); Self::try_from(py_string) } } @@ -203,9 +203,9 @@ impl From> for PyBackedBytes { impl FromPyObject<'_> for PyBackedBytes { fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { - if let Ok(bytes) = obj.downcast::() { + if let Ok(bytes) = obj.cast::() { Ok(Self::from(bytes.to_owned())) - } else if let Ok(bytearray) = obj.downcast::() { + } else if let Ok(bytearray) = obj.cast::() { Ok(Self::from(bytearray.to_owned())) } else { Err(DowncastError::new(obj, "`bytes` or `bytearray`").into()) @@ -315,6 +315,7 @@ use impl_traits; #[cfg(test)] mod test { use super::*; + use crate::types::PyAnyMethods as _; use crate::{IntoPyObject, Python}; use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; diff --git a/src/pycell.rs b/src/pycell.rs index 660019687bf..de21c39b203 100644 --- a/src/pycell.rs +++ b/src/pycell.rs @@ -65,7 +65,7 @@ //! # #[allow(deprecated)] //! let _cell = py //! .from_borrowed_ptr::<_pyo3::PyAny>(_slf) -//! .downcast::<_pyo3::PyCell>()?; +//! .cast::<_pyo3::PyCell>()?; //! let mut _ref = _cell.try_borrow_mut()?; //! let _slf: &mut Number = &mut *_ref; //! _pyo3::impl_::callback::convert(py, Number::increment(_slf)) @@ -198,7 +198,6 @@ 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, Borrowed, Bound, PyErr, Python}; use std::convert::Infallible; use std::fmt; @@ -396,7 +395,7 @@ where ManuallyDrop::new(self) .as_ptr() .assume_owned_unchecked(py) - .downcast_into_unchecked() + .cast_into_unchecked() }, } } @@ -595,7 +594,7 @@ where ManuallyDrop::new(self) .as_ptr() .assume_owned_unchecked(py) - .downcast_into_unchecked() + .cast_into_unchecked() }, } } diff --git a/src/pyclass/guard.rs b/src/pyclass/guard.rs index a14e7f9b7b0..91f4c95e387 100644 --- a/src/pyclass/guard.rs +++ b/src/pyclass/guard.rs @@ -247,7 +247,7 @@ impl Deref for PyClassGuard<'_, T> { impl<'a, 'py, T: PyClass> FromPyObjectBound<'a, 'py> for PyClassGuard<'a, T> { fn from_py_object_bound(obj: Borrowed<'a, 'py, crate::PyAny>) -> crate::PyResult { - Self::try_from_class_object(obj.downcast()?.get_class_object()).map_err(Into::into) + Self::try_from_class_object(obj.cast()?.get_class_object()).map_err(Into::into) } } @@ -271,7 +271,7 @@ impl<'a, 'py, T: PyClass> IntoPyObject<'py> for &PyClassGuard<'a, T> { fn into_pyobject(self, py: crate::Python<'py>) -> Result { // SAFETY: `ptr` is guaranteed to be valid for 'a and points to an // object of type T - unsafe { Ok(Borrowed::from_non_null(py, self.ptr).downcast_unchecked()) } + unsafe { Ok(Borrowed::from_non_null(py, self.ptr).cast_unchecked()) } } } @@ -575,7 +575,7 @@ impl> DerefMut for PyClassGuardMut<'_, T> { impl<'a, 'py, T: PyClass> FromPyObjectBound<'a, 'py> for PyClassGuardMut<'a, T> { fn from_py_object_bound(obj: Borrowed<'a, 'py, crate::PyAny>) -> crate::PyResult { - Self::try_from_class_object(obj.downcast()?.get_class_object()).map_err(Into::into) + Self::try_from_class_object(obj.cast()?.get_class_object()).map_err(Into::into) } } @@ -599,7 +599,7 @@ impl<'a, 'py, T: PyClass> IntoPyObject<'py> for &PyClassGuardMut fn into_pyobject(self, py: crate::Python<'py>) -> Result { // SAFETY: `ptr` is guaranteed to be valid for 'a and points to an // object of type T - unsafe { Ok(Borrowed::from_non_null(py, self.ptr).downcast_unchecked()) } + unsafe { Ok(Borrowed::from_non_null(py, self.ptr).cast_unchecked()) } } } diff --git a/src/pyclass_init.rs b/src/pyclass_init.rs index 310fa77fb9c..507fa24c2be 100644 --- a/src/pyclass_init.rs +++ b/src/pyclass_init.rs @@ -3,7 +3,6 @@ use crate::ffi_ptr_ext::FfiPtrExt; use crate::impl_::callback::IntoPyCallbackOutput; use crate::impl_::pyclass::{PyClassBaseType, PyClassDict, PyClassThreadChecker, PyClassWeakRef}; use crate::impl_::pyclass_init::{PyNativeTypeInitializer, PyObjectInit}; -use crate::types::PyAnyMethods; use crate::{ffi, Bound, Py, PyClass, PyResult, Python}; use crate::{ ffi::PyTypeObject, @@ -196,7 +195,7 @@ impl PyClassInitializer { // Safety: obj is a valid pointer to an object of type `target_type`, which` is a known // subclass of `T` - Ok(unsafe { obj.assume_owned(py).downcast_into_unchecked() }) + Ok(unsafe { obj.assume_owned(py).cast_into_unchecked() }) } } diff --git a/src/sync.rs b/src/sync.rs index 7e71ca6a533..54cafe3c1e4 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -357,10 +357,7 @@ where attr_name: &str, ) -> PyResult<&Bound<'py, T>> { self.get_or_try_init(py, || { - let type_object = py - .import(module_name)? - .getattr(attr_name)? - .downcast_into()?; + let type_object = py.import(module_name)?.getattr(attr_name)?.cast_into()?; Ok(type_object.unbind()) }) .map(|ty| ty.bind(py)) diff --git a/src/tests/common.rs b/src/tests/common.rs index 74da5376be9..683d68b1bf1 100644 --- a/src/tests/common.rs +++ b/src/tests/common.rs @@ -182,7 +182,7 @@ with warnings.catch_warnings(record=True) as warning_record: let catch_warnings = warnings .getattr("catch_warnings")? .call((), Some(&kwargs))?; - let list = catch_warnings.call_method0("__enter__")?.downcast_into()?; + let list = catch_warnings.call_method0("__enter__")?.cast_into()?; let _guard = Self { catch_warnings }; f(&list) } diff --git a/src/type_object.rs b/src/type_object.rs index 2d7c0f9ef2c..f552ef5034e 100644 --- a/src/type_object.rs +++ b/src/type_object.rs @@ -1,7 +1,6 @@ //! Python type object information use crate::ffi_ptr_ext::FfiPtrExt; -use crate::types::any::PyAnyMethods; use crate::types::{PyAny, PyType}; use crate::{ffi, Bound, Python}; use std::ptr; @@ -63,7 +62,7 @@ pub unsafe trait PyTypeInfo: Sized { .cast::() .assume_borrowed_unchecked(py) .to_owned() - .downcast_into_unchecked() + .cast_into_unchecked() } } diff --git a/src/types/any.rs b/src/types/any.rs index 58ff89b56b8..cfc41ed5d29 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -6,7 +6,6 @@ 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}; #[cfg(not(any(PyPy, GraalPy)))] @@ -730,6 +729,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// # Example: Downcasting to a specific Python object /// /// ```rust + /// # #![allow(deprecated)] /// use pyo3::prelude::*; /// use pyo3::types::{PyDict, PyList}; /// @@ -749,6 +749,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// might actually be a pyclass. /// /// ```rust + /// # #![allow(deprecated)] /// # fn main() -> Result<(), pyo3::PyErr> { /// use pyo3::prelude::*; /// @@ -771,6 +772,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// }) /// # } /// ``` + // FIXME(icxolu) deprecate in favor of `Bound::cast` fn downcast(&self) -> Result<&Bound<'py, T>, DowncastError<'_, 'py>> where T: PyTypeCheck; @@ -782,6 +784,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// # Example /// /// ```rust + /// # #![allow(deprecated)] /// use pyo3::prelude::*; /// use pyo3::types::{PyDict, PyList}; /// @@ -797,6 +800,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// assert!(obj.downcast_into::().is_ok()); /// }) /// ``` + // FIXME(icxolu) deprecate in favor of `Bound::cast_into` fn downcast_into(self) -> Result, DowncastIntoError<'py>> where T: PyTypeCheck; @@ -815,6 +819,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// # Example: Downcasting to a specific Python object but not a subtype /// /// ```rust + /// # #![allow(deprecated)] /// use pyo3::prelude::*; /// use pyo3::types::{PyBool, PyInt}; /// @@ -831,11 +836,13 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// assert!(any.downcast_exact::().is_ok()); /// }); /// ``` + // FIXME(icxolu) deprecate in favor of `Bound::cast_exact` fn downcast_exact(&self) -> Result<&Bound<'py, T>, DowncastError<'_, 'py>> where T: PyTypeInfo; /// Like `downcast_exact` but takes ownership of `self`. + // FIXME(icxolu) deprecate in favor of `Bound::cast_into_exact` fn downcast_into_exact(self) -> Result, DowncastIntoError<'py>> where T: PyTypeInfo; @@ -845,6 +852,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// # Safety /// /// Callers must ensure that the type is valid or risk type confusion. + // FIXME(icxolu) deprecate in favor of `Bound::cast_unchecked` unsafe fn downcast_unchecked(&self) -> &Bound<'py, T>; /// Like `downcast_unchecked` but takes ownership of `self`. @@ -852,6 +860,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// # Safety /// /// Callers must ensure that the type is valid or risk type confusion. + // FIXME(icxolu) deprecate in favor of `Bound::cast_into_unchecked` unsafe fn downcast_into_unchecked(self) -> Bound<'py, T>; /// Extracts some type from the Python object. @@ -1441,12 +1450,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { where T: PyTypeCheck, { - if T::type_check(self) { - // Safety: type_check is responsible for ensuring that the type is correct - Ok(unsafe { self.downcast_unchecked() }) - } else { - Err(DowncastError::new(self, T::NAME)) - } + self.cast() } #[inline] @@ -1454,12 +1458,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { where T: PyTypeCheck, { - if T::type_check(&self) { - // Safety: type_check is responsible for ensuring that the type is correct - Ok(unsafe { self.downcast_into_unchecked() }) - } else { - Err(DowncastIntoError::new(self, T::NAME)) - } + self.cast_into() } #[inline] @@ -1467,12 +1466,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { where T: PyTypeInfo, { - if self.is_exact_instance_of::() { - // Safety: is_exact_instance_of is responsible for ensuring that the type is correct - Ok(unsafe { self.downcast_unchecked() }) - } else { - Err(DowncastError::new(self, T::NAME)) - } + self.cast_exact() } #[inline] @@ -1480,22 +1474,17 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { where T: PyTypeInfo, { - if self.is_exact_instance_of::() { - // Safety: is_exact_instance_of is responsible for ensuring that the type is correct - Ok(unsafe { self.downcast_into_unchecked() }) - } else { - Err(DowncastIntoError::new(self, T::NAME)) - } + self.cast_into_exact() } #[inline] unsafe fn downcast_unchecked(&self) -> &Bound<'py, T> { - unsafe { &*ptr_from_ref(self).cast() } + unsafe { self.cast_unchecked() } } #[inline] unsafe fn downcast_into_unchecked(self) -> Bound<'py, T> { - unsafe { std::mem::transmute(self) } + unsafe { self.cast_into_unchecked() } } fn extract<'a, T>(&'a self) -> PyResult @@ -1513,7 +1502,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { unsafe { ffi::PyObject_Repr(self.as_ptr()) .assume_owned_or_err(self.py()) - .downcast_into_unchecked() + .cast_into_unchecked() } } @@ -1521,7 +1510,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { unsafe { ffi::PyObject_Str(self.as_ptr()) .assume_owned_or_err(self.py()) - .downcast_into_unchecked() + .cast_into_unchecked() } } @@ -1541,7 +1530,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { unsafe { ffi::PyObject_Dir(self.as_ptr()) .assume_owned_or_err(self.py()) - .downcast_into_unchecked() + .cast_into_unchecked() } } @@ -1826,7 +1815,7 @@ class SimpleClass: let dir = py .eval(ffi::c_str!("dir(42)"), None, None) .unwrap() - .downcast_into::() + .cast_into::() .unwrap(); let a = obj .dir() diff --git a/src/types/boolobject.rs b/src/types/boolobject.rs index c6fca362d64..51a0dbb346a 100644 --- a/src/types/boolobject.rs +++ b/src/types/boolobject.rs @@ -33,7 +33,7 @@ impl PyBool { unsafe { if val { ffi::Py_True() } else { ffi::Py_False() } .assume_borrowed(py) - .downcast_unchecked() + .cast_unchecked() } } } @@ -182,7 +182,7 @@ impl FromPyObject<'_> for bool { const INPUT_TYPE: &'static str = "bool"; fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { - let err = match obj.downcast::() { + let err = match obj.cast::() { Ok(obj) => return Ok(obj.is_true()), Err(err) => err, }; @@ -226,7 +226,7 @@ impl FromPyObject<'_> for bool { .lookup_special(crate::intern!(obj.py(), "__bool__"))? .ok_or_else(|| missing_conversion(obj))?; - let obj = meth.call0()?.downcast_into::()?; + let obj = meth.call0()?.cast_into::()?; return Ok(obj.is_true()); } } diff --git a/src/types/bytearray.rs b/src/types/bytearray.rs index da3eb3b2b20..11f9e37137f 100644 --- a/src/types/bytearray.rs +++ b/src/types/bytearray.rs @@ -3,7 +3,6 @@ use crate::ffi_ptr_ext::FfiPtrExt; use crate::instance::{Borrowed, Bound}; use crate::py_result_ext::PyResultExt; use crate::sync::with_critical_section; -use crate::types::any::PyAnyMethods; use crate::{ffi, PyAny, Python}; use std::slice; @@ -29,7 +28,7 @@ impl PyByteArray { unsafe { ffi::PyByteArray_FromStringAndSize(ptr, len) .assume_owned(py) - .downcast_into_unchecked() + .cast_into_unchecked() } } @@ -66,7 +65,7 @@ impl PyByteArray { let pybytearray: Bound<'_, Self> = ffi::PyByteArray_FromStringAndSize(std::ptr::null(), len as ffi::Py_ssize_t) .assume_owned_or_err(py)? - .downcast_into_unchecked(); + .cast_into_unchecked(); let buffer: *mut u8 = ffi::PyByteArray_AsString(pybytearray.as_ptr()).cast(); debug_assert!(!buffer.is_null()); @@ -84,7 +83,7 @@ impl PyByteArray { unsafe { ffi::PyByteArray_FromObject(src.as_ptr()) .assume_owned_or_err(src.py()) - .downcast_into_unchecked() + .cast_into_unchecked() } } } diff --git a/src/types/bytes.rs b/src/types/bytes.rs index 8e89e007487..f88aa5bb3b8 100644 --- a/src/types/bytes.rs +++ b/src/types/bytes.rs @@ -1,6 +1,5 @@ use crate::ffi_ptr_ext::FfiPtrExt; use crate::instance::{Borrowed, Bound}; -use crate::types::any::PyAnyMethods; use crate::{ffi, Py, PyAny, PyResult, Python}; use std::ops::Index; use std::slice::SliceIndex; @@ -22,8 +21,9 @@ use std::str; /// 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 -/// relevant, use [`PyAnyMethods::eq`], at cost of the additional overhead of a Python method call. +/// may have different equality semantics. In situations where subclasses overriding equality might +/// be relevant, use [`PyAnyMethods::eq`](crate::types::any::PyAnyMethods::eq), at cost of the +/// additional overhead of a Python method call. /// /// ```rust /// # use pyo3::prelude::*; @@ -60,7 +60,7 @@ impl PyBytes { unsafe { ffi::PyBytes_FromStringAndSize(ptr, len) .assume_owned(py) - .downcast_into_unchecked() + .cast_into_unchecked() } } @@ -96,7 +96,7 @@ impl PyBytes { unsafe { let pyptr = ffi::PyBytes_FromStringAndSize(std::ptr::null(), len as ffi::Py_ssize_t); // Check for an allocation error and return it - let pybytes = pyptr.assume_owned_or_err(py)?.downcast_into_unchecked(); + let pybytes = pyptr.assume_owned_or_err(py)?.cast_into_unchecked(); let buffer: *mut u8 = ffi::PyBytes_AsString(pyptr).cast(); debug_assert!(!buffer.is_null()); // Zero-initialise the uninitialised bytestring @@ -121,7 +121,7 @@ impl PyBytes { unsafe { ffi::PyBytes_FromStringAndSize(ptr.cast(), len as isize) .assume_owned(py) - .downcast_into_unchecked() + .cast_into_unchecked() } } } @@ -278,6 +278,7 @@ impl PartialEq> for &'_ [u8] { #[cfg(test)] mod tests { use super::*; + use crate::types::PyAnyMethods as _; #[test] fn test_bytes_index() { diff --git a/src/types/capsule.rs b/src/types/capsule.rs index 1cc05e799fb..22300c22d16 100644 --- a/src/types/capsule.rs +++ b/src/types/capsule.rs @@ -124,7 +124,7 @@ impl PyCapsule { Some(capsule_destructor::), ) .assume_owned_or_err(py) - .downcast_into_unchecked() + .cast_into_unchecked() } } diff --git a/src/types/code.rs b/src/types/code.rs index 15fea7ef9a2..450e586abc9 100644 --- a/src/types/code.rs +++ b/src/types/code.rs @@ -66,7 +66,7 @@ impl PyCode { unsafe { ffi::Py_CompileString(code.as_ptr(), filename.as_ptr(), start) .assume_owned_or_err(py) - .downcast_into_unchecked() + .cast_into_unchecked() } } } @@ -101,7 +101,7 @@ impl<'py> PyCodeMethods<'py> for Bound<'py, PyCode> { let attr = mptr.getattr(crate::intern!(self.py(), "__dict__"))?; let globals = match globals { Some(globals) => globals, - None => attr.downcast::()?, + None => attr.cast::()?, }; let locals = locals.unwrap_or(globals); diff --git a/src/types/complex.rs b/src/types/complex.rs index 0da7546c247..8f03a7cd1fd 100644 --- a/src/types/complex.rs +++ b/src/types/complex.rs @@ -1,6 +1,8 @@ #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] use crate::py_result_ext::PyResultExt; -use crate::{ffi, types::any::PyAnyMethods, Bound, PyAny, Python}; +#[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] +use crate::types::any::PyAnyMethods; +use crate::{ffi, Bound, PyAny, Python}; use std::os::raw::c_double; /// Represents a Python [`complex`](https://docs.python.org/3/library/functions.html#complex) object. @@ -35,7 +37,7 @@ impl PyComplex { unsafe { ffi::PyComplex_FromDoubles(real, imag) .assume_owned(py) - .downcast_into_unchecked() + .cast_into_unchecked() } } } @@ -53,7 +55,7 @@ mod not_limited_impls { type Output = Bound<'py, PyComplex>; fn $fn(self, other: Self) -> Self::Output { PyAnyMethods::$fn(self.as_any(), other) - .downcast_into().expect( + .cast_into().expect( concat!("Complex method ", stringify!($fn), " failed.") @@ -100,7 +102,7 @@ mod not_limited_impls { type Output = Bound<'py, PyComplex>; fn neg(self) -> Self::Output { PyAnyMethods::neg(self.as_any()) - .downcast_into() + .cast_into() .expect("Complex method __neg__ failed.") } } @@ -231,7 +233,7 @@ impl<'py> PyComplexMethods<'py> for Bound<'py, PyComplex> { #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] fn abs(&self) -> c_double { PyAnyMethods::abs(self.as_any()) - .downcast_into() + .cast_into() .expect("Complex method __abs__ failed.") .extract() .expect("Failed to extract to c double.") @@ -241,7 +243,7 @@ impl<'py> PyComplexMethods<'py> for Bound<'py, PyComplex> { fn pow(&self, other: &Bound<'py, PyComplex>) -> Bound<'py, PyComplex> { Python::attach(|py| { PyAnyMethods::pow(self.as_any(), other, py.None()) - .downcast_into() + .cast_into() .expect("Complex method __pow__ failed.") }) } diff --git a/src/types/datetime.rs b/src/types/datetime.rs index c45a7e64098..f5d51567262 100644 --- a/src/types/datetime.rs +++ b/src/types/datetime.rs @@ -67,12 +67,12 @@ impl DatetimeTypes { TYPES.get_or_try_init(py, || { let datetime = py.import("datetime")?; Ok::<_, PyErr>(Self { - date: datetime.getattr("date")?.downcast_into()?.into(), - datetime: datetime.getattr("datetime")?.downcast_into()?.into(), - time: datetime.getattr("time")?.downcast_into()?.into(), - timedelta: datetime.getattr("timedelta")?.downcast_into()?.into(), - timezone: datetime.getattr("timezone")?.downcast_into()?.into(), - tzinfo: datetime.getattr("tzinfo")?.downcast_into()?.into(), + date: datetime.getattr("date")?.cast_into()?.into(), + datetime: datetime.getattr("datetime")?.cast_into()?.into(), + time: datetime.getattr("time")?.cast_into()?.into(), + timedelta: datetime.getattr("timedelta")?.cast_into()?.into(), + timezone: datetime.getattr("timezone")?.cast_into()?.into(), + tzinfo: datetime.getattr("tzinfo")?.cast_into()?.into(), }) }) } @@ -259,7 +259,7 @@ impl PyDate { unsafe { (api.Date_FromDate)(year, c_int::from(month), c_int::from(day), api.DateType) .assume_owned_or_err(py) - .downcast_into_unchecked() + .cast_into_unchecked() } } #[cfg(Py_LIMITED_API)] @@ -268,7 +268,7 @@ impl PyDate { .date .bind(py) .call((year, month, day), None)? - .downcast_into_unchecked()) + .cast_into_unchecked()) } } @@ -286,7 +286,7 @@ impl PyDate { unsafe { PyDate_FromTimestamp(time_tuple.as_ptr()) .assume_owned_or_err(py) - .downcast_into_unchecked() + .cast_into_unchecked() } } @@ -296,7 +296,7 @@ impl PyDate { .date .bind(py) .call_method1("fromtimestamp", (timestamp,))? - .downcast_into_unchecked()) + .cast_into_unchecked()) } } } @@ -381,7 +381,7 @@ impl PyDateTime { api.DateTimeType, ) .assume_owned_or_err(py) - .downcast_into_unchecked() + .cast_into_unchecked() } } @@ -394,7 +394,7 @@ impl PyDateTime { (year, month, day, hour, minute, second, microsecond, tzinfo), None, )? - .downcast_into_unchecked()) + .cast_into_unchecked()) } } @@ -435,7 +435,7 @@ impl PyDateTime { api.DateTimeType, ) .assume_owned_or_err(py) - .downcast_into_unchecked() + .cast_into_unchecked() } } @@ -448,7 +448,7 @@ impl PyDateTime { (year, month, day, hour, minute, second, microsecond, tzinfo), Some(&[("fold", fold)].into_py_dict(py)?), )? - .downcast_into_unchecked()) + .cast_into_unchecked()) } } @@ -470,7 +470,7 @@ impl PyDateTime { unsafe { PyDateTime_FromTimestamp(args.as_ptr()) .assume_owned_or_err(py) - .downcast_into_unchecked() + .cast_into_unchecked() } } @@ -480,7 +480,7 @@ impl PyDateTime { .datetime .bind(py) .call_method1("fromtimestamp", (timestamp, tzinfo))? - .downcast_into_unchecked()) + .cast_into_unchecked()) } } } @@ -534,7 +534,7 @@ impl<'py> PyTzInfoAccess<'py> for Bound<'py, PyDateTime> { .tzinfo .assume_borrowed(self.py()) .to_owned() - .downcast_into_unchecked(), + .cast_into_unchecked(), ) } else { None @@ -550,7 +550,7 @@ impl<'py> PyTzInfoAccess<'py> for Bound<'py, PyDateTime> { Some( res.assume_borrowed(self.py()) .to_owned() - .downcast_into_unchecked(), + .cast_into_unchecked(), ) } } @@ -561,7 +561,7 @@ impl<'py> PyTzInfoAccess<'py> for Bound<'py, PyDateTime> { if tzinfo.is_none() { None } else { - Some(tzinfo.downcast_into_unchecked()) + Some(tzinfo.cast_into_unchecked()) } } } @@ -625,7 +625,7 @@ impl PyTime { api.TimeType, ) .assume_owned_or_err(py) - .downcast_into_unchecked() + .cast_into_unchecked() } } @@ -635,7 +635,7 @@ impl PyTime { .time .bind(py) .call((hour, minute, second, microsecond, tzinfo), None)? - .downcast_into_unchecked()) + .cast_into_unchecked()) } } @@ -663,7 +663,7 @@ impl PyTime { api.TimeType, ) .assume_owned_or_err(py) - .downcast_into_unchecked() + .cast_into_unchecked() } } @@ -677,7 +677,7 @@ impl PyTime { (hour, minute, second, microsecond, tzinfo), Some(&[("fold", fold)].into_py_dict(py)?), )? - .downcast_into_unchecked()) + .cast_into_unchecked()) } } } @@ -716,7 +716,7 @@ impl<'py> PyTzInfoAccess<'py> for Bound<'py, PyTime> { .tzinfo .assume_borrowed(self.py()) .to_owned() - .downcast_into_unchecked(), + .cast_into_unchecked(), ) } else { None @@ -732,7 +732,7 @@ impl<'py> PyTzInfoAccess<'py> for Bound<'py, PyTime> { Some( res.assume_borrowed(self.py()) .to_owned() - .downcast_into_unchecked(), + .cast_into_unchecked(), ) } } @@ -743,7 +743,7 @@ impl<'py> PyTzInfoAccess<'py> for Bound<'py, PyTime> { if tzinfo.is_none() { None } else { - Some(tzinfo.downcast_into_unchecked()) + Some(tzinfo.cast_into_unchecked()) } } } @@ -796,7 +796,7 @@ impl PyTzInfo { Ok(ensure_datetime_api(py)? .TimeZone_UTC .assume_borrowed(py) - .downcast_unchecked()) + .cast_unchecked()) } #[cfg(Py_LIMITED_API)] @@ -807,7 +807,7 @@ impl PyTzInfo { .timezone .bind(py) .getattr("utc")? - .downcast_into()? + .cast_into()? .unbind()) }) .map(|utc| utc.bind_borrowed(py)) @@ -830,7 +830,7 @@ impl PyTzInfo { zoneinfo? .call1((iana_name,))? - .downcast_into() + .cast_into() .map_err(Into::into) } @@ -846,7 +846,7 @@ impl PyTzInfo { unsafe { (api.TimeZone_FromTimeZone)(delta.as_ptr(), std::ptr::null_mut()) .assume_owned_or_err(py) - .downcast_into_unchecked() + .cast_into_unchecked() } } @@ -856,7 +856,7 @@ impl PyTzInfo { .timezone .bind(py) .call1((offset,))? - .downcast_into_unchecked()) + .cast_into_unchecked()) } } } @@ -925,7 +925,7 @@ impl PyDelta { api.DeltaType, ) .assume_owned_or_err(py) - .downcast_into_unchecked() + .cast_into_unchecked() } } @@ -936,7 +936,7 @@ impl PyDelta { .timedelta .bind(py) .call1((days, seconds, microseconds))? - .downcast_into_unchecked()) + .cast_into_unchecked()) } } } @@ -1057,7 +1057,7 @@ mod tests { .unwrap() .call_method1("utcoffset", (PyNone::get(py),)) .unwrap() - .downcast_into::() + .cast_into::() .unwrap() .eq(PyDelta::new(py, 0, -3600, 0, true).unwrap()) .unwrap() @@ -1068,7 +1068,7 @@ mod tests { .unwrap() .call_method1("utcoffset", (PyNone::get(py),)) .unwrap() - .downcast_into::() + .cast_into::() .unwrap() .eq(PyDelta::new(py, 0, 3600, 0, true).unwrap()) .unwrap() diff --git a/src/types/dict.rs b/src/types/dict.rs index 46db32c65e6..8d01d9a5ab3 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -3,7 +3,7 @@ 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::{PyAny, PyAnyMethods, PyList, PyMapping}; +use crate::types::{PyAny, PyList, PyMapping}; use crate::{ffi, BoundObject, IntoPyObject, IntoPyObjectExt, Python}; /// Represents a Python `dict`. @@ -64,7 +64,7 @@ pyobject_native_type_core!( impl PyDict { /// Creates a new empty dictionary. pub fn new(py: Python<'_>) -> Bound<'_, PyDict> { - unsafe { ffi::PyDict_New().assume_owned(py).downcast_into_unchecked() } + unsafe { ffi::PyDict_New().assume_owned(py).cast_into_unchecked() } } /// Creates a new dictionary from the sequence given. @@ -205,7 +205,7 @@ impl<'py> PyDictMethods<'py> for Bound<'py, PyDict> { unsafe { ffi::PyDict_Copy(self.as_ptr()) .assume_owned_or_err(self.py()) - .downcast_into_unchecked() + .cast_into_unchecked() } } @@ -314,7 +314,7 @@ impl<'py> PyDictMethods<'py> for Bound<'py, PyDict> { unsafe { ffi::PyDict_Keys(self.as_ptr()) .assume_owned(self.py()) - .downcast_into_unchecked() + .cast_into_unchecked() } } @@ -322,7 +322,7 @@ impl<'py> PyDictMethods<'py> for Bound<'py, PyDict> { unsafe { ffi::PyDict_Values(self.as_ptr()) .assume_owned(self.py()) - .downcast_into_unchecked() + .cast_into_unchecked() } } @@ -330,7 +330,7 @@ impl<'py> PyDictMethods<'py> for Bound<'py, PyDict> { unsafe { ffi::PyDict_Items(self.as_ptr()) .assume_owned(self.py()) - .downcast_into_unchecked() + .cast_into_unchecked() } } @@ -358,11 +358,11 @@ impl<'py> PyDictMethods<'py> for Bound<'py, PyDict> { } fn as_mapping(&self) -> &Bound<'py, PyMapping> { - unsafe { self.downcast_unchecked() } + unsafe { self.cast_unchecked() } } fn into_mapping(self) -> Bound<'py, PyMapping> { - unsafe { self.into_any().downcast_into_unchecked() } + unsafe { self.cast_into_unchecked() } } fn update(&self, other: &Bound<'_, PyMapping>) -> PyResult<()> { @@ -814,7 +814,7 @@ where #[cfg(test)] mod tests { use super::*; - use crate::types::PyTuple; + use crate::types::{PyAnyMethods as _, PyTuple}; use std::collections::{BTreeMap, HashMap}; #[test] @@ -1063,7 +1063,7 @@ mod tests { let mut key_sum = 0; let mut value_sum = 0; for el in dict.items() { - let tuple = el.downcast::().unwrap(); + let tuple = el.cast::().unwrap(); key_sum += tuple.get_item(0).unwrap().extract::().unwrap(); value_sum += tuple.get_item(1).unwrap().extract::().unwrap(); } diff --git a/src/types/ellipsis.rs b/src/types/ellipsis.rs index 86403be358a..5aaa19a8c4b 100644 --- a/src/types/ellipsis.rs +++ b/src/types/ellipsis.rs @@ -16,7 +16,7 @@ impl PyEllipsis { /// Returns the `Ellipsis` object. #[inline] pub fn get(py: Python<'_>) -> Borrowed<'_, '_, PyEllipsis> { - unsafe { ffi::Py_Ellipsis().assume_borrowed(py).downcast_unchecked() } + unsafe { ffi::Py_Ellipsis().assume_borrowed(py).cast_unchecked() } } } @@ -67,7 +67,7 @@ mod tests { #[test] fn test_dict_is_not_ellipsis() { Python::attach(|py| { - assert!(PyDict::new(py).downcast::().is_err()); + assert!(PyDict::new(py).cast::().is_err()); }) } } diff --git a/src/types/float.rs b/src/types/float.rs index 920a99a09c5..ba131026ad8 100644 --- a/src/types/float.rs +++ b/src/types/float.rs @@ -38,7 +38,7 @@ impl PyFloat { unsafe { ffi::PyFloat_FromDouble(val) .assume_owned(py) - .downcast_into_unchecked() + .cast_into_unchecked() } } } @@ -119,7 +119,7 @@ impl<'py> FromPyObject<'py> for f64 { // we have exactly a `float` object (it's not worth going through // `isinstance` machinery for subclasses). #[cfg(not(Py_LIMITED_API))] - if let Ok(float) = obj.downcast_exact::() { + if let Ok(float) = obj.cast_exact::() { return Ok(float.value()); } diff --git a/src/types/frozenset.rs b/src/types/frozenset.rs index 69528c10725..45787a13b63 100644 --- a/src/types/frozenset.rs +++ b/src/types/frozenset.rs @@ -4,7 +4,6 @@ use crate::{ ffi, ffi_ptr_ext::FfiPtrExt, py_result_ext::PyResultExt, - types::any::PyAnyMethods, Bound, PyAny, Python, }; use crate::{Borrowed, BoundObject, IntoPyObject, IntoPyObjectExt}; @@ -98,7 +97,7 @@ impl PyFrozenSet { unsafe { ffi::PyFrozenSet_New(ptr::null_mut()) .assume_owned_or_err(py) - .downcast_into_unchecked() + .cast_into_unchecked() } } } @@ -240,7 +239,7 @@ where // 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() + .cast_into_unchecked() }; let ptr = set.as_ptr(); @@ -255,6 +254,7 @@ where #[cfg(test)] mod tests { use super::*; + use crate::types::PyAnyMethods as _; #[test] fn test_frozenset_new_and_len() { diff --git a/src/types/function.rs b/src/types/function.rs index 9df21203743..fa822b5faa0 100644 --- a/src/types/function.rs +++ b/src/types/function.rs @@ -101,7 +101,7 @@ impl PyCFunction { unsafe { ffi::PyCFunction_NewEx(data.def.get(), capsule.as_ptr(), std::ptr::null_mut()) .assume_owned_or_err(py) - .downcast_into_unchecked() + .cast_into_unchecked() } } @@ -129,7 +129,7 @@ impl PyCFunction { unsafe { ffi::PyCFunction_NewEx(def, mod_ptr, module_name_ptr) .assume_owned_or_err(py) - .downcast_into_unchecked() + .cast_into_unchecked() } } } @@ -145,8 +145,6 @@ where F: Fn(&Bound<'_, PyTuple>, Option<&Bound<'_, PyDict>>) -> R + Send + 'static, for<'py> R: crate::impl_::callback::IntoPyCallbackOutput<'py, *mut ffi::PyObject>, { - use crate::types::any::PyAnyMethods; - unsafe { crate::impl_::trampoline::cfunction_with_keywords( capsule_ptr, @@ -156,10 +154,10 @@ where 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 args = Bound::ref_from_ptr(py, &args).cast_unchecked::(); let kwargs = Bound::ref_from_ptr_or_opt(py, &kwargs) .as_ref() - .map(|b| b.downcast_unchecked::()); + .map(|b| b.cast_unchecked::()); let result = (boxed_fn.closure)(args, kwargs); crate::impl_::callback::convert(py, result) }, diff --git a/src/types/genericalias.rs b/src/types/genericalias.rs index 227b50aa9e8..04e0407784e 100644 --- a/src/types/genericalias.rs +++ b/src/types/genericalias.rs @@ -33,7 +33,7 @@ impl PyGenericAlias { unsafe { ffi::Py_GenericAlias(origin.as_ptr(), args.as_ptr()) .assume_owned_or_err(py) - .downcast_into_unchecked() + .cast_into_unchecked() } } } diff --git a/src/types/iterator.rs b/src/types/iterator.rs index 8a7a74751af..5424c8849c4 100644 --- a/src/types/iterator.rs +++ b/src/types/iterator.rs @@ -40,7 +40,7 @@ impl PyIterator { unsafe { ffi::PyObject_GetIter(obj.as_ptr()) .assume_owned_or_err(obj.py()) - .downcast_into_unchecked() + .cast_into_unchecked() } } } @@ -288,7 +288,7 @@ def fibonacci(target): let generator: Bound<'_, PyIterator> = py .eval(ffi::c_str!("fibonacci(5)"), None, Some(&context)) .unwrap() - .downcast_into() + .cast_into() .unwrap(); let mut items = vec![]; for actual in &generator { @@ -322,7 +322,7 @@ def fibonacci(target): #[crate::pymethods(crate = "crate")] impl Downcaster { fn downcast_iterator(&mut self, obj: &crate::Bound<'_, crate::PyAny>) { - self.failed = Some(obj.downcast::().unwrap_err().into()); + self.failed = Some(obj.cast::().unwrap_err().into()); } } @@ -361,7 +361,7 @@ def fibonacci(target): fn python_class_iterator() { #[crate::pyfunction(crate = "crate")] fn assert_iterator(obj: &crate::Bound<'_, crate::PyAny>) { - assert!(obj.downcast::().is_ok()) + assert!(obj.cast::().is_ok()) } // Regression test for 2913 diff --git a/src/types/list.rs b/src/types/list.rs index c41457a1a9a..0a2a32a3760 100644 --- a/src/types/list.rs +++ b/src/types/list.rs @@ -2,7 +2,6 @@ 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, Python}; @@ -40,7 +39,7 @@ pub(crate) fn try_new_from_iter<'py>( // We create the `Bound` pointer here for two reasons: // - panics if the ptr is null // - its Drop cleans up the list if user code or the asserts panic. - let list = ptr.assume_owned(py).downcast_into_unchecked(); + let list = ptr.assume_owned(py).cast_into_unchecked(); let count = (&mut elements) .take(len as usize) @@ -101,11 +100,7 @@ impl PyList { /// Constructs a new empty list. pub fn empty(py: Python<'_>) -> Bound<'_, PyList> { - unsafe { - ffi::PyList_New(0) - .assume_owned(py) - .downcast_into_unchecked() - } + unsafe { ffi::PyList_New(0).assume_owned(py).cast_into_unchecked() } } } @@ -251,12 +246,12 @@ impl<'py> PyListMethods<'py> for Bound<'py, PyList> { /// Returns `self` cast as a `PySequence`. fn as_sequence(&self) -> &Bound<'py, PySequence> { - unsafe { self.downcast_unchecked() } + unsafe { self.cast_unchecked() } } /// Returns `self` cast as a `PySequence`. fn into_sequence(self) -> Bound<'py, PySequence> { - unsafe { self.into_any().downcast_into_unchecked() } + unsafe { self.cast_into_unchecked() } } /// Gets the list item at the specified index. @@ -299,7 +294,7 @@ impl<'py> PyListMethods<'py> for Bound<'py, PyList> { unsafe { ffi::PyList_GetSlice(self.as_ptr(), get_ssize_index(low), get_ssize_index(high)) .assume_owned(self.py()) - .downcast_into_unchecked() + .cast_into_unchecked() } } @@ -446,7 +441,7 @@ impl<'py> PyListMethods<'py> for Bound<'py, PyList> { unsafe { ffi::PyList_AsTuple(self.as_ptr()) .assume_owned(self.py()) - .downcast_into_unchecked() + .cast_into_unchecked() } } } @@ -1010,7 +1005,7 @@ mod tests { { let v = vec![2]; let ob = v.into_pyobject(py).unwrap(); - let list = ob.downcast::().unwrap(); + let list = ob.cast::().unwrap(); cnt = obj.get_refcnt(); list.set_item(0, &obj).unwrap(); } @@ -1094,7 +1089,7 @@ mod tests { Python::attach(|py| { let v = vec![2, 3, 5, 7]; let ob = (&v).into_pyobject(py).unwrap(); - let list = ob.downcast::().unwrap(); + let list = ob.cast::().unwrap(); let mut iter = list.iter(); assert_eq!(iter.size_hint(), (v.len(), Some(v.len()))); @@ -1113,7 +1108,7 @@ mod tests { Python::attach(|py| { let v = vec![2, 3, 5, 7]; let ob = v.into_pyobject(py).unwrap(); - let list = ob.downcast::().unwrap(); + let list = ob.cast::().unwrap(); let mut iter = list.iter().rev(); @@ -1373,7 +1368,7 @@ mod tests { fn test_array_into_pyobject() { Python::attach(|py| { let array = [1, 2].into_pyobject(py).unwrap(); - let list = array.downcast::().unwrap(); + let list = array.cast::().unwrap(); assert_eq!(1, list.get_item(0).unwrap().extract::().unwrap()); assert_eq!(2, list.get_item(1).unwrap().extract::().unwrap()); }); @@ -1616,7 +1611,7 @@ mod tests { Python::attach(|py| { let v = vec![6, 7, 8, 9, 10]; let ob = (&v).into_pyobject(py).unwrap(); - let list = ob.downcast::().unwrap(); + let list = ob.cast::().unwrap(); let mut iter = list.iter(); iter.next(); @@ -1626,7 +1621,7 @@ mod tests { let v: Vec = vec![]; let ob = (&v).into_pyobject(py).unwrap(); - let list = ob.downcast::().unwrap(); + let list = ob.cast::().unwrap(); let mut iter = list.iter(); iter.next(); @@ -1634,14 +1629,14 @@ mod tests { let v = vec![1, 2, 3]; let ob = (&v).into_pyobject(py).unwrap(); - let list = ob.downcast::().unwrap(); + let list = ob.cast::().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 list = ob.cast::().unwrap(); let mut iter = list.iter(); assert_eq!(iter.next().unwrap().extract::().unwrap(), 6); assert_eq!(iter.nth(2).unwrap().extract::().unwrap(), 9); @@ -1659,7 +1654,7 @@ mod tests { Python::attach(|py| { let v = vec![1, 2, 3, 4, 5]; let ob = (&v).into_pyobject(py).unwrap(); - let list = ob.downcast::().unwrap(); + let list = ob.cast::().unwrap(); let mut iter = list.iter(); assert_eq!(iter.nth_back(0).unwrap().extract::().unwrap(), 5); @@ -1668,7 +1663,7 @@ mod tests { let v: Vec = vec![]; let ob = (&v).into_pyobject(py).unwrap(); - let list = ob.downcast::().unwrap(); + let list = ob.cast::().unwrap(); let mut iter = list.iter(); assert!(iter.nth_back(0).is_none()); @@ -1676,14 +1671,14 @@ mod tests { let v = vec![1, 2, 3]; let ob = (&v).into_pyobject(py).unwrap(); - let list = ob.downcast::().unwrap(); + let list = ob.cast::().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 list = ob.cast::().unwrap(); let mut iter = list.iter(); iter.next_back(); // Consume the last element @@ -1693,7 +1688,7 @@ mod tests { let v = vec![1, 2, 3, 4, 5]; let ob = (&v).into_pyobject(py).unwrap(); - let list = ob.downcast::().unwrap(); + let list = ob.cast::().unwrap(); let mut iter = list.iter(); assert_eq!(iter.nth_back(1).unwrap().extract::().unwrap(), 4); @@ -1717,7 +1712,7 @@ mod tests { Python::attach(|py| { let v = vec![1, 2, 3, 4, 5]; let ob = (&v).into_pyobject(py).unwrap(); - let list = ob.downcast::().unwrap(); + let list = ob.cast::().unwrap(); let mut iter = list.iter(); assert_eq!(iter.advance_by(2), Ok(())); @@ -1743,7 +1738,7 @@ mod tests { Python::attach(|py| { let v = vec![1, 2, 3, 4, 5]; let ob = (&v).into_pyobject(py).unwrap(); - let list = ob.downcast::().unwrap(); + let list = ob.cast::().unwrap(); let mut iter = list.iter(); assert_eq!(iter.advance_back_by(2), Ok(())); diff --git a/src/types/mapping.rs b/src/types/mapping.rs index 6ecda738d4a..f0d039d4028 100644 --- a/src/types/mapping.rs +++ b/src/types/mapping.rs @@ -23,7 +23,7 @@ pyobject_native_type_named!(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`. + /// This registration is required for a pyclass to be castable from `PyAny` to `PyMapping`. pub fn register(py: Python<'_>) -> PyResult<()> { let ty = T::type_object(py); get_mapping_abc(py)?.call_method1("register", (ty,))?; @@ -137,7 +137,7 @@ impl<'py> PyMappingMethods<'py> for Bound<'py, PyMapping> { unsafe { ffi::PyMapping_Keys(self.as_ptr()) .assume_owned_or_err(self.py()) - .downcast_into_unchecked() + .cast_into_unchecked() } } @@ -146,7 +146,7 @@ impl<'py> PyMappingMethods<'py> for Bound<'py, PyMapping> { unsafe { ffi::PyMapping_Values(self.as_ptr()) .assume_owned_or_err(self.py()) - .downcast_into_unchecked() + .cast_into_unchecked() } } @@ -155,7 +155,7 @@ impl<'py> PyMappingMethods<'py> for Bound<'py, PyMapping> { unsafe { ffi::PyMapping_Items(self.as_ptr()) .assume_owned_or_err(self.py()) - .downcast_into_unchecked() + .cast_into_unchecked() } } } @@ -199,13 +199,13 @@ mod tests { Python::attach(|py| { let mut v = HashMap::::new(); let ob = (&v).into_pyobject(py).unwrap(); - let mapping = ob.downcast::().unwrap(); + let mapping = ob.cast::().unwrap(); assert_eq!(0, mapping.len().unwrap()); assert!(mapping.is_empty().unwrap()); v.insert(7, 32); let ob = v.into_pyobject(py).unwrap(); - let mapping2 = ob.downcast::().unwrap(); + let mapping2 = ob.cast::().unwrap(); assert_eq!(1, mapping2.len().unwrap()); assert!(!mapping2.is_empty().unwrap()); }); @@ -217,7 +217,7 @@ mod tests { let mut v = HashMap::new(); v.insert("key0", 1234); let ob = v.into_pyobject(py).unwrap(); - let mapping = ob.downcast::().unwrap(); + let mapping = ob.cast::().unwrap(); mapping.set_item("key1", "foo").unwrap(); assert!(mapping.contains("key0").unwrap()); @@ -232,7 +232,7 @@ mod tests { let mut v = HashMap::new(); v.insert(7, 32); let ob = v.into_pyobject(py).unwrap(); - let mapping = ob.downcast::().unwrap(); + let mapping = ob.cast::().unwrap(); assert_eq!( 32, mapping.get_item(7i32).unwrap().extract::().unwrap() @@ -250,7 +250,7 @@ mod tests { let mut v = HashMap::new(); v.insert(7, 32); let ob = v.into_pyobject(py).unwrap(); - let mapping = ob.downcast::().unwrap(); + let mapping = ob.cast::().unwrap(); assert!(mapping.set_item(7i32, 42i32).is_ok()); // change assert!(mapping.set_item(8i32, 123i32).is_ok()); // insert assert_eq!( @@ -270,7 +270,7 @@ mod tests { let mut v = HashMap::new(); v.insert(7, 32); let ob = v.into_pyobject(py).unwrap(); - let mapping = ob.downcast::().unwrap(); + let mapping = ob.cast::().unwrap(); assert!(mapping.del_item(7i32).is_ok()); assert_eq!(0, mapping.len().unwrap()); assert!(mapping @@ -288,12 +288,12 @@ mod tests { v.insert(8, 42); v.insert(9, 123); let ob = v.into_pyobject(py).unwrap(); - let mapping = ob.downcast::().unwrap(); + let mapping = ob.cast::().unwrap(); // Can't just compare against a vector of tuples since we don't have a guaranteed ordering. let mut key_sum = 0; let mut value_sum = 0; for el in mapping.items().unwrap().try_iter().unwrap() { - let tuple = el.unwrap().downcast_into::().unwrap(); + let tuple = el.unwrap().cast_into::().unwrap(); key_sum += tuple.get_item(0).unwrap().extract::().unwrap(); value_sum += tuple.get_item(1).unwrap().extract::().unwrap(); } @@ -310,7 +310,7 @@ mod tests { v.insert(8, 42); v.insert(9, 123); let ob = v.into_pyobject(py).unwrap(); - let mapping = ob.downcast::().unwrap(); + let mapping = ob.cast::().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().try_iter().unwrap() { @@ -328,7 +328,7 @@ mod tests { v.insert(8, 42); v.insert(9, 123); let ob = v.into_pyobject(py).unwrap(); - let mapping = ob.downcast::().unwrap(); + let mapping = ob.cast::().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().try_iter().unwrap() { diff --git a/src/types/mappingproxy.rs b/src/types/mappingproxy.rs index 0de1f4b246d..1f900d2af9d 100644 --- a/src/types/mappingproxy.rs +++ b/src/types/mappingproxy.rs @@ -34,7 +34,7 @@ impl PyMappingProxy { unsafe { ffi::PyDictProxy_New(elements.as_ptr()) .assume_owned(py) - .downcast_into_unchecked() + .cast_into_unchecked() } } } @@ -76,7 +76,7 @@ impl<'py, 'a> PyMappingProxyMethods<'py, 'a> for Bound<'py, PyMappingProxy> { unsafe { Ok(ffi::PyMapping_Keys(self.as_ptr()) .assume_owned_or_err(self.py())? - .downcast_into_unchecked()) + .cast_into_unchecked()) } } @@ -85,7 +85,7 @@ impl<'py, 'a> PyMappingProxyMethods<'py, 'a> for Bound<'py, PyMappingProxy> { unsafe { Ok(ffi::PyMapping_Values(self.as_ptr()) .assume_owned_or_err(self.py())? - .downcast_into_unchecked()) + .cast_into_unchecked()) } } @@ -94,12 +94,12 @@ impl<'py, 'a> PyMappingProxyMethods<'py, 'a> for Bound<'py, PyMappingProxy> { unsafe { Ok(ffi::PyMapping_Items(self.as_ptr()) .assume_owned_or_err(self.py())? - .downcast_into_unchecked()) + .cast_into_unchecked()) } } fn as_mapping(&self) -> &Bound<'py, PyMapping> { - unsafe { self.downcast_unchecked() } + unsafe { self.cast_unchecked() } } fn try_iter(&'a self) -> PyResult> { @@ -287,7 +287,7 @@ mod tests { let mut value_sum = 0; for res in mappingproxy.items().unwrap().try_iter().unwrap() { let el = res.unwrap(); - let tuple = el.downcast::().unwrap(); + let tuple = el.cast::().unwrap(); key_sum += tuple.get_item(0).unwrap().extract::().unwrap(); value_sum += tuple.get_item(1).unwrap().extract::().unwrap(); } @@ -506,18 +506,8 @@ mod tests { .map(|object| { let tuple = object.unwrap(); ( - tuple - .0 - .downcast::() - .unwrap() - .extract::() - .unwrap(), - tuple - .1 - .downcast::() - .unwrap() - .extract::() - .unwrap(), + tuple.0.cast::().unwrap().extract::().unwrap(), + tuple.1.cast::().unwrap().extract::().unwrap(), ) }) .collect::>() diff --git a/src/types/memoryview.rs b/src/types/memoryview.rs index 31bdcd47d65..cdb252e72d2 100644 --- a/src/types/memoryview.rs +++ b/src/types/memoryview.rs @@ -19,7 +19,7 @@ impl PyMemoryView { unsafe { ffi::PyMemoryView_FromObject(src.as_ptr()) .assume_owned_or_err(src.py()) - .downcast_into_unchecked() + .cast_into_unchecked() } } } diff --git a/src/types/mod.rs b/src/types/mod.rs index da02cda71fe..666262d75f9 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -64,7 +64,7 @@ pub use self::weakref::{PyWeakref, PyWeakrefMethods, PyWeakrefProxy, PyWeakrefRe /// /// # pub fn main() -> PyResult<()> { /// Python::attach(|py| { -/// let dict = py.eval(c_str!("{'a':'b', 'c':'d'}"), None, None)?.downcast_into::()?; +/// let dict = py.eval(c_str!("{'a':'b', 'c':'d'}"), None, None)?.cast_into::()?; /// /// for (key, value) in &dict { /// println!("key: {}, value: {}", key, value); diff --git a/src/types/module.rs b/src/types/module.rs index 2c7064d86d5..a56f6dd06cc 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -56,7 +56,7 @@ impl PyModule { unsafe { ffi::PyModule_NewObject(name.as_ptr()) .assume_owned_or_err(py) - .downcast_into_unchecked() + .cast_into_unchecked() } } @@ -89,7 +89,7 @@ impl PyModule { unsafe { ffi::PyImport_Import(name.as_ptr()) .assume_owned_or_err(py) - .downcast_into_unchecked() + .cast_into_unchecked() } } @@ -172,7 +172,7 @@ impl PyModule { ffi::PyImport_ExecCodeModuleEx(module_name.as_ptr(), code.as_ptr(), file_name.as_ptr()) .assume_owned_or_err(py) - .downcast_into() + .cast_into() } } } @@ -409,14 +409,14 @@ impl<'py> PyModuleMethods<'py> for Bound<'py, PyModule> { ffi::PyModule_GetDict(self.as_ptr()) .assume_borrowed(self.py()) .to_owned() - .downcast_into_unchecked() + .cast_into_unchecked() } } fn index(&self) -> PyResult> { let __all__ = __all__(self.py()); match self.getattr(__all__) { - Ok(idx) => idx.downcast_into().map_err(PyErr::from), + Ok(idx) => idx.cast_into().map_err(PyErr::from), Err(err) => { if err.is_instance_of::(self.py()) { let l = PyList::empty(self.py()); @@ -435,7 +435,7 @@ impl<'py> PyModuleMethods<'py> for Bound<'py, PyModule> { unsafe { ffi::PyModule_GetNameObject(self.as_ptr()) .assume_owned_or_err(self.py()) - .downcast_into_unchecked() + .cast_into_unchecked() } } @@ -444,7 +444,7 @@ impl<'py> PyModuleMethods<'py> for Bound<'py, PyModule> { self.dict() .get_item("__name__") .map_err(|_| exceptions::PyAttributeError::new_err("__name__"))? - .downcast_into() + .cast_into() .map_err(PyErr::from) } } @@ -454,7 +454,7 @@ impl<'py> PyModuleMethods<'py> for Bound<'py, PyModule> { unsafe { ffi::PyModule_GetFilenameObject(self.as_ptr()) .assume_owned_or_err(self.py()) - .downcast_into_unchecked() + .cast_into_unchecked() } #[cfg(PyPy)] @@ -462,7 +462,7 @@ impl<'py> PyModuleMethods<'py> for Bound<'py, PyModule> { self.dict() .get_item("__file__") .map_err(|_| exceptions::PyAttributeError::new_err("__file__"))? - .downcast_into() + .cast_into() .map_err(PyErr::from) } } @@ -506,7 +506,7 @@ impl<'py> PyModuleMethods<'py> for Bound<'py, PyModule> { { fn inner(module: &Bound<'_, PyModule>, object: Bound<'_, PyAny>) -> PyResult<()> { let name = object.getattr(__name__(module.py()))?; - module.add(name.downcast_into::()?, object) + module.add(name.cast_into::()?, object) } let py = self.py(); @@ -520,7 +520,7 @@ impl<'py> PyModuleMethods<'py> for Bound<'py, PyModule> { fn add_function(&self, fun: Bound<'_, PyCFunction>) -> PyResult<()> { let name = fun.getattr(__name__(self.py()))?; - self.add(name.downcast_into::()?, fun) + self.add(name.cast_into::()?, fun) } #[cfg_attr(any(Py_LIMITED_API, not(Py_GIL_DISABLED)), allow(unused_variables))] diff --git a/src/types/none.rs b/src/types/none.rs index 4b864237958..23f58883ab4 100644 --- a/src/types/none.rs +++ b/src/types/none.rs @@ -14,7 +14,7 @@ impl PyNone { /// Returns the `None` object. #[inline] pub fn get(py: Python<'_>) -> Borrowed<'_, '_, PyNone> { - unsafe { ffi::Py_None().assume_borrowed(py).downcast_unchecked() } + unsafe { ffi::Py_None().assume_borrowed(py).cast_unchecked() } } } @@ -63,14 +63,14 @@ mod tests { #[test] fn test_none_is_none() { Python::attach(|py| { - assert!(PyNone::get(py).downcast::().unwrap().is_none()); + assert!(PyNone::get(py).cast::().unwrap().is_none()); }) } #[test] fn test_dict_is_not_none() { Python::attach(|py| { - assert!(PyDict::new(py).downcast::().is_err()); + assert!(PyDict::new(py).cast::().is_err()); }) } } diff --git a/src/types/notimplemented.rs b/src/types/notimplemented.rs index ebdf3aced6d..36a8edb2e8c 100644 --- a/src/types/notimplemented.rs +++ b/src/types/notimplemented.rs @@ -19,7 +19,7 @@ impl PyNotImplemented { unsafe { ffi::Py_NotImplemented() .assume_borrowed(py) - .downcast_unchecked() + .cast_unchecked() } } } @@ -70,7 +70,7 @@ mod tests { #[test] fn test_dict_is_not_notimplemented() { Python::attach(|py| { - assert!(PyDict::new(py).downcast::().is_err()); + assert!(PyDict::new(py).cast::().is_err()); }) } } diff --git a/src/types/pysuper.rs b/src/types/pysuper.rs index 3093bcf6192..b8f3cef2d06 100644 --- a/src/types/pysuper.rs +++ b/src/types/pysuper.rs @@ -63,7 +63,7 @@ impl PySuper { ) -> PyResult> { PySuper::type_object(ty.py()).call1((ty, obj)).map(|any| { // Safety: super() always returns instance of super - unsafe { any.downcast_into_unchecked() } + unsafe { any.cast_into_unchecked() } }) } } diff --git a/src/types/range.rs b/src/types/range.rs index fcf90c2cc7a..00c7ce2a325 100644 --- a/src/types/range.rs +++ b/src/types/range.rs @@ -30,7 +30,7 @@ impl<'py> PyRange { unsafe { Ok(Self::type_object(py) .call1((start, stop, step))? - .downcast_into_unchecked()) + .cast_into_unchecked()) } } } diff --git a/src/types/sequence.rs b/src/types/sequence.rs index dfdfd8e8856..d510f36552b 100644 --- a/src/types/sequence.rs +++ b/src/types/sequence.rs @@ -28,7 +28,7 @@ pyobject_native_type_named!(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`. + /// This registration is required for a pyclass to be castable from `PyAny` to `PySequence`. pub fn register(py: Python<'_>) -> PyResult<()> { let ty = T::type_object(py); get_sequence_abc(py)?.call_method1("register", (ty,))?; @@ -157,7 +157,7 @@ impl<'py> PySequenceMethods<'py> for Bound<'py, PySequence> { unsafe { ffi::PySequence_Concat(self.as_ptr(), other.as_ptr()) .assume_owned_or_err(self.py()) - .downcast_into_unchecked() + .cast_into_unchecked() } } @@ -166,7 +166,7 @@ impl<'py> PySequenceMethods<'py> for Bound<'py, PySequence> { unsafe { ffi::PySequence_Repeat(self.as_ptr(), get_ssize_index(count)) .assume_owned_or_err(self.py()) - .downcast_into_unchecked() + .cast_into_unchecked() } } @@ -175,7 +175,7 @@ impl<'py> PySequenceMethods<'py> for Bound<'py, PySequence> { unsafe { ffi::PySequence_InPlaceConcat(self.as_ptr(), other.as_ptr()) .assume_owned_or_err(self.py()) - .downcast_into_unchecked() + .cast_into_unchecked() } } @@ -184,7 +184,7 @@ impl<'py> PySequenceMethods<'py> for Bound<'py, PySequence> { unsafe { ffi::PySequence_InPlaceRepeat(self.as_ptr(), get_ssize_index(count)) .assume_owned_or_err(self.py()) - .downcast_into_unchecked() + .cast_into_unchecked() } } @@ -201,7 +201,7 @@ impl<'py> PySequenceMethods<'py> for Bound<'py, PySequence> { unsafe { ffi::PySequence_GetSlice(self.as_ptr(), get_ssize_index(begin), get_ssize_index(end)) .assume_owned_or_err(self.py()) - .downcast_into_unchecked() + .cast_into_unchecked() } } @@ -317,7 +317,7 @@ impl<'py> PySequenceMethods<'py> for Bound<'py, PySequence> { unsafe { ffi::PySequence_List(self.as_ptr()) .assume_owned_or_err(self.py()) - .downcast_into_unchecked() + .cast_into_unchecked() } } @@ -326,7 +326,7 @@ impl<'py> PySequenceMethods<'py> for Bound<'py, PySequence> { unsafe { ffi::PySequence_Tuple(self.as_ptr()) .assume_owned_or_err(self.py()) - .downcast_into_unchecked() + .cast_into_unchecked() } } } @@ -356,7 +356,7 @@ where // to support this function and if not, we will only fail extraction safely. let seq = unsafe { if ffi::PySequence_Check(obj.as_ptr()) != 0 { - obj.downcast_unchecked::() + obj.cast_unchecked::() } else { return Err(DowncastError::new(obj, "Sequence").into()); } @@ -414,11 +414,7 @@ mod tests { fn test_numbers_are_not_sequences() { Python::attach(|py| { let v = 42i32; - assert!(v - .into_pyobject(py) - .unwrap() - .downcast::() - .is_err()); + assert!(v.into_pyobject(py).unwrap().cast::().is_err()); }); } @@ -426,11 +422,7 @@ mod tests { fn test_strings_are_sequences() { Python::attach(|py| { let v = "London Calling"; - assert!(v - .into_pyobject(py) - .unwrap() - .downcast::() - .is_ok()); + assert!(v.into_pyobject(py).unwrap().cast::().is_ok()); }); } @@ -450,7 +442,7 @@ mod tests { Python::attach(|py| { let v: Vec = vec![]; let ob = v.into_pyobject(py).unwrap(); - let seq = ob.downcast::().unwrap(); + let seq = ob.cast::().unwrap(); assert_eq!(0, seq.len().unwrap()); let needle = 7i32.into_pyobject(py).unwrap(); @@ -462,11 +454,11 @@ mod tests { fn test_seq_is_empty() { Python::attach(|py| { let list = vec![1].into_pyobject(py).unwrap(); - let seq = list.downcast::().unwrap(); + let seq = list.cast::().unwrap(); assert!(!seq.is_empty().unwrap()); let vec: Vec = Vec::new(); let empty_list = vec.into_pyobject(py).unwrap(); - let empty_seq = empty_list.downcast::().unwrap(); + let empty_seq = empty_list.cast::().unwrap(); assert!(empty_seq.is_empty().unwrap()); }); } @@ -476,7 +468,7 @@ mod tests { Python::attach(|py| { let v: Vec = vec![1, 1, 2, 3, 5, 8]; let ob = v.into_pyobject(py).unwrap(); - let seq = ob.downcast::().unwrap(); + let seq = ob.cast::().unwrap(); assert_eq!(6, seq.len().unwrap()); let bad_needle = 7i32.into_pyobject(py).unwrap(); @@ -495,7 +487,7 @@ mod tests { Python::attach(|py| { let v: Vec = vec![1, 1, 2, 3, 5, 8]; let ob = v.into_pyobject(py).unwrap(); - let seq = ob.downcast::().unwrap(); + let seq = ob.cast::().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()); @@ -511,7 +503,7 @@ mod tests { Python::attach(|py| { let v: Vec = vec![1, 1, 2, 3, 5, 8]; let ob = v.into_pyobject(py).unwrap(); - let seq = ob.downcast::().unwrap(); + let seq = ob.cast::().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()); @@ -535,7 +527,7 @@ mod tests { Python::attach(|py| { let v: Vec = vec![1, 2]; let ob = v.into_pyobject(py).unwrap(); - let seq = ob.downcast::().unwrap(); + let seq = ob.cast::().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()); @@ -549,7 +541,7 @@ mod tests { Python::attach(|py| { let v: Vec = vec![1, 2]; let ob = v.into_pyobject(py).unwrap(); - let seq = ob.downcast::().unwrap(); + let seq = ob.cast::().unwrap(); assert!(seq.set_item(1, &obj).is_ok()); assert!(ptr::eq(seq.get_item(1).unwrap().as_ptr(), obj.as_ptr())); }); @@ -564,7 +556,7 @@ mod tests { Python::attach(|py| { let v: Vec = vec![1, 1, 2, 3, 5, 8]; let ob = v.into_pyobject(py).unwrap(); - let seq = ob.downcast::().unwrap(); + let seq = ob.cast::().unwrap(); assert_eq!( [1, 2, 3], seq.get_slice(1, 4).unwrap().extract::<[i32; 3]>().unwrap() @@ -585,7 +577,7 @@ mod tests { let v: Vec = vec![1, 1, 2, 3, 5, 8]; let w: Vec = vec![7, 4]; let ob = v.into_pyobject(py).unwrap(); - let seq = ob.downcast::().unwrap(); + let seq = ob.cast::().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()); @@ -599,7 +591,7 @@ mod tests { Python::attach(|py| { let v: Vec = vec![1, 1, 2, 3, 5, 8]; let ob = v.into_pyobject(py).unwrap(); - let seq = ob.downcast::().unwrap(); + let seq = ob.cast::().unwrap(); seq.del_slice(1, 4).unwrap(); assert_eq!([1, 5, 8], seq.extract::<[i32; 3]>().unwrap()); seq.del_slice(1, 100).unwrap(); @@ -612,7 +604,7 @@ mod tests { Python::attach(|py| { let v: Vec = vec![1, 1, 2, 3, 5, 8]; let ob = v.into_pyobject(py).unwrap(); - let seq = ob.downcast::().unwrap(); + let seq = ob.cast::().unwrap(); assert_eq!(0, seq.index(1i32).unwrap()); assert_eq!(2, seq.index(2i32).unwrap()); assert_eq!(3, seq.index(3i32).unwrap()); @@ -628,7 +620,7 @@ mod tests { Python::attach(|py| { let v: Vec = vec![1, 1, 2, 3, 5, 8]; let ob = v.into_pyobject(py).unwrap(); - let seq = ob.downcast::().unwrap(); + let seq = ob.cast::().unwrap(); assert_eq!(2, seq.count(1i32).unwrap()); assert_eq!(1, seq.count(2i32).unwrap()); assert_eq!(1, seq.count(3i32).unwrap()); @@ -643,7 +635,7 @@ mod tests { Python::attach(|py| { let v: Vec = vec![1, 1, 2, 3, 5, 8]; let ob = (&v).into_pyobject(py).unwrap(); - let seq = ob.downcast::().unwrap(); + let seq = ob.cast::().unwrap(); let mut idx = 0; for el in seq.try_iter().unwrap() { assert_eq!(v[idx], el.unwrap().extract::().unwrap()); @@ -658,7 +650,7 @@ mod tests { Python::attach(|py| { let v = vec!["It", "was", "the", "worst", "of", "times"]; let ob = v.into_pyobject(py).unwrap(); - let seq = ob.downcast::().unwrap(); + let seq = ob.cast::().unwrap(); let bad_needle = "blurst".into_pyobject(py).unwrap(); assert!(!seq.contains(bad_needle).unwrap()); @@ -673,7 +665,7 @@ mod tests { Python::attach(|py| { let v: Vec = vec![1, 2, 3]; let ob = v.into_pyobject(py).unwrap(); - let seq = ob.downcast::().unwrap(); + let seq = ob.cast::().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]; @@ -688,7 +680,7 @@ mod tests { Python::attach(|py| { let v = "string"; let ob = v.into_pyobject(py).unwrap(); - let seq = ob.downcast::().unwrap(); + let seq = ob.cast::().unwrap(); let concat_seq = seq.concat(seq).unwrap(); assert_eq!(12, concat_seq.len().unwrap()); let concat_v = "stringstring".to_owned(); @@ -703,7 +695,7 @@ mod tests { Python::attach(|py| { let v = vec!["foo", "bar"]; let ob = v.into_pyobject(py).unwrap(); - let seq = ob.downcast::().unwrap(); + let seq = ob.cast::().unwrap(); let repeat_seq = seq.repeat(3).unwrap(); assert_eq!(6, repeat_seq.len().unwrap()); let repeated = ["foo", "bar", "foo", "bar", "foo", "bar"]; @@ -718,7 +710,7 @@ mod tests { Python::attach(|py| { let v = vec!["foo", "bar"]; let ob = v.into_pyobject(py).unwrap(); - let seq = ob.downcast::().unwrap(); + let seq = ob.cast::().unwrap(); let rep_seq = seq.in_place_repeat(3).unwrap(); assert_eq!(6, seq.len().unwrap()); assert!(seq.is(&rep_seq)); @@ -734,7 +726,7 @@ mod tests { Python::attach(|py| { let v = vec!["foo", "bar"]; let ob = (&v).into_pyobject(py).unwrap(); - let seq = ob.downcast::().unwrap(); + let seq = ob.cast::().unwrap(); assert!(seq .to_list() .unwrap() @@ -748,7 +740,7 @@ mod tests { Python::attach(|py| { let v = "foo"; let ob = v.into_pyobject(py).unwrap(); - let seq = ob.downcast::().unwrap(); + let seq = ob.cast::().unwrap(); assert!(seq .to_list() .unwrap() @@ -762,7 +754,7 @@ mod tests { Python::attach(|py| { let v = ("foo", "bar"); let ob = v.into_pyobject(py).unwrap(); - let seq = ob.downcast::().unwrap(); + let seq = ob.cast::().unwrap(); assert!(seq .to_tuple() .unwrap() @@ -776,7 +768,7 @@ mod tests { Python::attach(|py| { let v = vec!["foo", "bar"]; let ob = (&v).into_pyobject(py).unwrap(); - let seq = ob.downcast::().unwrap(); + let seq = ob.cast::().unwrap(); assert!(seq .to_tuple() .unwrap() @@ -822,13 +814,13 @@ mod tests { } #[test] - fn test_seq_downcast_unchecked() { + fn test_seq_cast_unchecked() { Python::attach(|py| { let v = vec!["foo", "bar"]; let ob = v.into_pyobject(py).unwrap(); - let seq = ob.downcast::().unwrap(); + let seq = ob.cast::().unwrap(); let type_ptr = seq.as_any(); - let seq_from = unsafe { type_ptr.downcast_unchecked::() }; + let seq_from = unsafe { type_ptr.cast_unchecked::() }; assert!(seq_from.to_list().is_ok()); }); } diff --git a/src/types/set.rs b/src/types/set.rs index acc41340ae1..cdc920b0c12 100644 --- a/src/types/set.rs +++ b/src/types/set.rs @@ -4,7 +4,6 @@ use crate::{ ffi_ptr_ext::FfiPtrExt, instance::Bound, py_result_ext::PyResultExt, - types::any::PyAnyMethods, }; use crate::{ffi, Borrowed, BoundObject, IntoPyObject, IntoPyObjectExt, PyAny, Python}; use std::ptr; @@ -57,7 +56,7 @@ impl PySet { unsafe { ffi::PySet_New(ptr::null_mut()) .assume_owned_or_err(py) - .downcast_into_unchecked() + .cast_into_unchecked() } } } @@ -276,7 +275,7 @@ where // user code errors or panics. ffi::PySet_New(std::ptr::null_mut()) .assume_owned_or_err(py)? - .downcast_into_unchecked() + .cast_into_unchecked() }; let ptr = set.as_ptr(); @@ -324,11 +323,11 @@ mod tests { Python::attach(|py| { let mut v = HashSet::::new(); let ob = (&v).into_pyobject(py).unwrap(); - let set = ob.downcast::().unwrap(); + let set = ob.cast::().unwrap(); assert_eq!(0, set.len()); v.insert(7); let ob = v.into_pyobject(py).unwrap(); - let set2 = ob.downcast::().unwrap(); + let set2 = ob.cast::().unwrap(); assert_eq!(1, set2.len()); }); } diff --git a/src/types/slice.rs b/src/types/slice.rs index f6b9c378af0..78248d54c40 100644 --- a/src/types/slice.rs +++ b/src/types/slice.rs @@ -1,7 +1,6 @@ use crate::err::{PyErr, PyResult}; use crate::ffi; use crate::ffi_ptr_ext::FfiPtrExt; -use crate::types::any::PyAnyMethods; use crate::types::{PyRange, PyRangeMethods}; use crate::{Bound, IntoPyObject, PyAny, Python}; use std::convert::Infallible; @@ -64,7 +63,7 @@ impl PySlice { ffi::PyLong_FromSsize_t(step), ) .assume_owned(py) - .downcast_into_unchecked() + .cast_into_unchecked() } } @@ -73,7 +72,7 @@ impl PySlice { unsafe { ffi::PySlice_New(ffi::Py_None(), ffi::Py_None(), ffi::Py_None()) .assume_owned(py) - .downcast_into_unchecked() + .cast_into_unchecked() } } } @@ -157,6 +156,7 @@ impl<'py> TryFrom> for Bound<'py, PySlice> { #[cfg(test)] mod tests { use super::*; + use crate::types::PyAnyMethods as _; #[test] fn test_py_slice_new() { diff --git a/src/types/string.rs b/src/types/string.rs index c92c58146e6..c30c2da4bf4 100644 --- a/src/types/string.rs +++ b/src/types/string.rs @@ -3,7 +3,6 @@ use crate::exceptions::PyUnicodeDecodeError; use crate::ffi_ptr_ext::FfiPtrExt; use crate::instance::Borrowed; use crate::py_result_ext::PyResultExt; -use crate::types::any::PyAnyMethods; use crate::types::bytes::PyBytesMethods; use crate::types::PyBytes; use crate::{ffi, Bound, Py, PyAny, PyResult, Python}; @@ -132,9 +131,10 @@ impl<'a> PyStringData<'a> { /// 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. +/// 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`](crate::types::any::PyAnyMethods::eq), at +/// cost of the additional overhead of a Python method call. /// /// ```rust /// # use pyo3::prelude::*; @@ -164,7 +164,7 @@ impl PyString { unsafe { ffi::PyUnicode_FromStringAndSize(ptr, len) .assume_owned(py) - .downcast_into_unchecked() + .cast_into_unchecked() } } @@ -184,7 +184,7 @@ impl PyString { if !ob.is_null() { ffi::PyUnicode_InternInPlace(&mut ob); } - ob.assume_owned(py).downcast_into_unchecked() + ob.assume_owned(py).cast_into_unchecked() } } @@ -213,7 +213,7 @@ impl PyString { unsafe { ffi::PyUnicode_FromEncodedObject(src.as_ptr(), encoding, errors) .assume_owned_or_err(src.py()) - .downcast_into_unchecked() + .cast_into_unchecked() } } @@ -301,7 +301,7 @@ impl<'py> PyStringMethods<'py> for Bound<'py, PyString> { unsafe { ffi::PyUnicode_AsUTF8String(self.as_ptr()) .assume_owned_or_err(self.py()) - .downcast_into_unchecked::() + .cast_into_unchecked::() } } @@ -363,7 +363,7 @@ impl<'a> Borrowed<'a, '_, PyString> { ffi::c_str!("surrogatepass").as_ptr(), ) .assume_owned(py) - .downcast_into_unchecked::() + .cast_into_unchecked::() }; Cow::Owned(String::from_utf8_lossy(bytes.as_bytes()).into_owned()) } @@ -562,7 +562,7 @@ mod tests { use pyo3_ffi::c_str; use super::*; - use crate::{exceptions::PyLookupError, IntoPyObject, PyObject}; + use crate::{exceptions::PyLookupError, types::PyAnyMethods as _, IntoPyObject, PyObject}; #[test] fn test_to_cow_utf8() { @@ -579,7 +579,7 @@ mod tests { let py_string = py .eval(ffi::c_str!(r"'\ud800'"), None, None) .unwrap() - .downcast_into::() + .cast_into::() .unwrap(); assert!(py_string.to_cow().is_err()); }) @@ -612,7 +612,7 @@ mod tests { .into(); assert!(obj .bind(py) - .downcast::() + .cast::() .unwrap() .encode_utf8() .is_err()); @@ -625,7 +625,7 @@ mod tests { let py_string = py .eval(ffi::c_str!(r"'🐈 Hello \ud800World'"), None, None) .unwrap() - .downcast_into::() + .cast_into::() .unwrap(); assert_eq!(py_string.to_string_lossy(), "🐈 Hello ���World"); @@ -735,7 +735,7 @@ mod tests { ) }; assert!(!ptr.is_null()); - let s = unsafe { ptr.assume_owned(py).downcast_into_unchecked::() }; + let s = unsafe { ptr.assume_owned(py).cast_into_unchecked::() }; let data = unsafe { s.data().unwrap() }; assert_eq!(data, PyStringData::Ucs1(b"f\xfe")); let err = data.to_string(py).unwrap_err(); @@ -752,7 +752,7 @@ mod tests { fn test_string_data_ucs2() { Python::attach(|py| { let s = py.eval(ffi::c_str!("'foo\\ud800'"), None, None).unwrap(); - let py_string = s.downcast::().unwrap(); + let py_string = s.cast::().unwrap(); let data = unsafe { py_string.data().unwrap() }; assert_eq!(data, PyStringData::Ucs2(&[102, 111, 111, 0xd800])); @@ -777,7 +777,7 @@ mod tests { ) }; assert!(!ptr.is_null()); - let s = unsafe { ptr.assume_owned(py).downcast_into_unchecked::() }; + let s = unsafe { ptr.assume_owned(py).cast_into_unchecked::() }; let data = unsafe { s.data().unwrap() }; assert_eq!(data, PyStringData::Ucs2(&[0xff22, 0xd800])); let err = data.to_string(py).unwrap_err(); @@ -816,7 +816,7 @@ mod tests { ) }; assert!(!ptr.is_null()); - let s = unsafe { ptr.assume_owned(py).downcast_into_unchecked::() }; + let s = unsafe { ptr.assume_owned(py).cast_into_unchecked::() }; let data = unsafe { s.data().unwrap() }; assert_eq!(data, PyStringData::Ucs4(&[0x20000, 0xd800])); let err = data.to_string(py).unwrap_err(); diff --git a/src/types/traceback.rs b/src/types/traceback.rs index c37b943e51e..1d6f88d1afe 100644 --- a/src/types/traceback.rs +++ b/src/types/traceback.rs @@ -71,7 +71,7 @@ impl<'py> PyTracebackMethods<'py> for Bound<'py, PyTraceback> { let formatted = string_io .getattr(intern!(py, "getvalue"))? .call0()? - .downcast::()? + .cast::()? .to_cow()? .into_owned(); Ok(formatted) diff --git a/src/types/tuple.rs b/src/types/tuple.rs index 21667159f91..0669b6e345f 100644 --- a/src/types/tuple.rs +++ b/src/types/tuple.rs @@ -29,7 +29,7 @@ fn try_new_from_iter<'py>( // - Panics if the ptr is null // - Cleans up the tuple if `convert` or the asserts panic - let tup = ptr.assume_owned(py).downcast_into_unchecked(); + let tup = ptr.assume_owned(py).cast_into_unchecked(); let mut counter: Py_ssize_t = 0; @@ -110,11 +110,7 @@ impl PyTuple { /// Constructs an empty tuple (on the Python side, a singleton object). pub fn empty(py: Python<'_>) -> Bound<'_, PyTuple> { - unsafe { - ffi::PyTuple_New(0) - .assume_owned(py) - .downcast_into_unchecked() - } + unsafe { ffi::PyTuple_New(0).assume_owned(py).cast_into_unchecked() } } } @@ -228,18 +224,18 @@ impl<'py> PyTupleMethods<'py> for Bound<'py, PyTuple> { } fn as_sequence(&self) -> &Bound<'py, PySequence> { - unsafe { self.downcast_unchecked() } + unsafe { self.cast_unchecked() } } fn into_sequence(self) -> Bound<'py, PySequence> { - unsafe { self.into_any().downcast_into_unchecked() } + unsafe { self.cast_into_unchecked() } } fn get_slice(&self, low: usize, high: usize) -> Bound<'py, PyTuple> { unsafe { ffi::PyTuple_GetSlice(self.as_ptr(), get_ssize_index(low), get_ssize_index(high)) .assume_owned(self.py()) - .downcast_into_unchecked() + .cast_into_unchecked() } } @@ -895,7 +891,7 @@ macro_rules! tuple_conversion ({$length:expr,$(($refN:ident, $n:tt, $T:ident)),+ impl<'py, $($T: FromPyObject<'py>),+> FromPyObject<'py> for ($($T,)+) { fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { - let t = obj.downcast::()?; + let t = obj.cast::()?; if t.len() == $length { #[cfg(any(Py_LIMITED_API, PyPy, GraalPy))] return Ok(($(t.get_borrowed_item($n)?.extract::<$T>()?,)+)); @@ -920,7 +916,7 @@ fn array_into_tuple<'py, const N: usize>( ) -> Bound<'py, PyTuple> { unsafe { let ptr = ffi::PyTuple_New(N.try_into().expect("0 < N <= 12")); - let tup = ptr.assume_owned(py).downcast_into_unchecked(); + let tup = ptr.assume_owned(py).cast_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()); @@ -1062,7 +1058,7 @@ mod tests { fn test_len() { Python::attach(|py| { let ob = (1, 2, 3).into_pyobject(py).unwrap(); - let tuple = ob.downcast::().unwrap(); + let tuple = ob.cast::().unwrap(); assert_eq!(3, tuple.len()); assert!(!tuple.is_empty()); let ob = tuple.as_any(); @@ -1094,7 +1090,7 @@ mod tests { fn test_iter() { Python::attach(|py| { let ob = (1, 2, 3).into_pyobject(py).unwrap(); - let tuple = ob.downcast::().unwrap(); + let tuple = ob.cast::().unwrap(); assert_eq!(3, tuple.len()); let mut iter = tuple.iter(); @@ -1118,7 +1114,7 @@ mod tests { fn test_iter_rev() { Python::attach(|py| { let ob = (1, 2, 3).into_pyobject(py).unwrap(); - let tuple = ob.downcast::().unwrap(); + let tuple = ob.cast::().unwrap(); assert_eq!(3, tuple.len()); let mut iter = tuple.iter().rev(); @@ -1188,7 +1184,7 @@ mod tests { fn test_into_iter() { Python::attach(|py| { let ob = (1, 2, 3).into_pyobject(py).unwrap(); - let tuple = ob.downcast::().unwrap(); + let tuple = ob.cast::().unwrap(); assert_eq!(3, tuple.len()); for (i, item) in tuple.iter().enumerate() { @@ -1216,7 +1212,7 @@ mod tests { fn test_as_slice() { Python::attach(|py| { let ob = (1, 2, 3).into_pyobject(py).unwrap(); - let tuple = ob.downcast::().unwrap(); + let tuple = ob.cast::().unwrap(); let slice = tuple.as_slice(); assert_eq!(3, slice.len()); @@ -1298,7 +1294,7 @@ mod tests { fn test_tuple_get_item_invalid_index() { Python::attach(|py| { let ob = (1, 2, 3).into_pyobject(py).unwrap(); - let tuple = ob.downcast::().unwrap(); + let tuple = ob.cast::().unwrap(); let obj = tuple.get_item(5); assert!(obj.is_err()); assert_eq!( @@ -1312,7 +1308,7 @@ mod tests { fn test_tuple_get_item_sanity() { Python::attach(|py| { let ob = (1, 2, 3).into_pyobject(py).unwrap(); - let tuple = ob.downcast::().unwrap(); + let tuple = ob.cast::().unwrap(); let obj = tuple.get_item(0); assert_eq!(obj.unwrap().extract::().unwrap(), 1); }); @@ -1323,7 +1319,7 @@ mod tests { fn test_tuple_get_item_unchecked_sanity() { Python::attach(|py| { let ob = (1, 2, 3).into_pyobject(py).unwrap(); - let tuple = ob.downcast::().unwrap(); + let tuple = ob.cast::().unwrap(); let obj = unsafe { tuple.get_item_unchecked(0) }; assert_eq!(obj.extract::().unwrap(), 1); }); @@ -1333,7 +1329,7 @@ mod tests { fn test_tuple_contains() { Python::attach(|py| { let ob = (1, 1, 2, 3, 5, 8).into_pyobject(py).unwrap(); - let tuple = ob.downcast::().unwrap(); + let tuple = ob.cast::().unwrap(); assert_eq!(6, tuple.len()); let bad_needle = 7i32.into_pyobject(py).unwrap(); @@ -1351,7 +1347,7 @@ mod tests { fn test_tuple_index() { Python::attach(|py| { let ob = (1, 1, 2, 3, 5, 8).into_pyobject(py).unwrap(); - let tuple = ob.downcast::().unwrap(); + let tuple = ob.cast::().unwrap(); assert_eq!(0, tuple.index(1i32).unwrap()); assert_eq!(2, tuple.index(2i32).unwrap()); assert_eq!(3, tuple.index(3i32).unwrap()); diff --git a/src/types/typeobject.rs b/src/types/typeobject.rs index 142a4d1f0be..58c94d46cf4 100644 --- a/src/types/typeobject.rs +++ b/src/types/typeobject.rs @@ -2,6 +2,7 @@ use crate::err::{self, PyResult}; use crate::instance::Borrowed; #[cfg(not(Py_3_13))] use crate::pybacked::PyBackedStr; +#[cfg(any(Py_LIMITED_API, PyPy, not(Py_3_13)))] use crate::types::any::PyAnyMethods; use crate::types::PyTuple; use crate::{ffi, Bound, PyAny, PyTypeInfo, Python}; @@ -41,7 +42,7 @@ impl PyType { ) -> Bound<'_, PyType> { unsafe { Borrowed::from_ptr_unchecked(py, p.cast()) - .downcast_unchecked() + .cast_unchecked() .to_owned() } } @@ -104,9 +105,7 @@ impl<'py> PyTypeMethods<'py> for Bound<'py, PyType> { /// Gets the name of the `PyType`. fn name(&self) -> PyResult> { #[cfg(not(Py_3_11))] - let name = self - .getattr(intern!(self.py(), "__name__"))? - .downcast_into()?; + let name = self.getattr(intern!(self.py(), "__name__"))?.cast_into()?; #[cfg(Py_3_11)] let name = unsafe { @@ -114,7 +113,7 @@ impl<'py> PyTypeMethods<'py> for Bound<'py, PyType> { 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() + .cast_into_unchecked() }; Ok(name) @@ -125,7 +124,7 @@ impl<'py> PyTypeMethods<'py> for Bound<'py, PyType> { #[cfg(not(Py_3_11))] let name = self .getattr(intern!(self.py(), "__qualname__"))? - .downcast_into()?; + .cast_into()?; #[cfg(Py_3_11)] let name = unsafe { @@ -133,7 +132,7 @@ impl<'py> PyTypeMethods<'py> for Bound<'py, PyType> { 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() + .cast_into_unchecked() }; Ok(name) @@ -151,7 +150,7 @@ impl<'py> PyTypeMethods<'py> for Bound<'py, PyType> { }; // `__module__` is never guaranteed to be a `str` - name.downcast_into().map_err(Into::into) + name.cast_into().map_err(Into::into) } /// Gets the [fully qualified name](https://docs.python.org/3/glossary.html#term-qualified-name) of the `PyType`. @@ -163,7 +162,7 @@ impl<'py> PyTypeMethods<'py> for Bound<'py, PyType> { let module_str = module.extract::()?; if module_str == "builtins" || module_str == "__main__" { - qualname.downcast_into()? + qualname.cast_into()? } else { PyString::new(self.py(), &format!("{module}.{qualname}")) } @@ -174,7 +173,7 @@ impl<'py> PyTypeMethods<'py> for Bound<'py, PyType> { use crate::ffi_ptr_ext::FfiPtrExt; ffi::PyType_GetFullyQualifiedName(self.as_type_ptr()) .assume_owned_or_err(self.py())? - .downcast_into_unchecked() + .cast_into_unchecked() }; Ok(name) @@ -215,7 +214,7 @@ impl<'py> PyTypeMethods<'py> for Bound<'py, PyType> { .tp_mro .assume_borrowed(self.py()) .to_owned() - .downcast_into_unchecked() + .cast_into_unchecked() }; mro @@ -236,7 +235,7 @@ impl<'py> PyTypeMethods<'py> for Bound<'py, PyType> { .tp_bases .assume_borrowed(self.py()) .to_owned() - .downcast_into_unchecked() + .cast_into_unchecked() }; bases @@ -326,7 +325,7 @@ class MyClass: .expect("module create failed"); let my_class = module.getattr("MyClass").unwrap(); - let my_class_type = my_class.downcast_into::().unwrap(); + let my_class_type = my_class.cast_into::().unwrap(); assert_eq!(my_class_type.name().unwrap(), "MyClass"); assert_eq!(my_class_type.qualname().unwrap(), "MyClass"); let module_name = module_name.to_str().unwrap(); @@ -370,7 +369,7 @@ class OuterClass: let outer_class = module.getattr("OuterClass").unwrap(); let inner_class = outer_class.getattr("InnerClass").unwrap(); - let inner_class_type = inner_class.downcast_into::().unwrap(); + let inner_class_type = inner_class.cast_into::().unwrap(); assert_eq!(inner_class_type.name().unwrap(), "InnerClass"); assert_eq!( inner_class_type.qualname().unwrap(), diff --git a/src/types/weakref/anyref.rs b/src/types/weakref/anyref.rs index 04220e0dc59..967acb5a404 100644 --- a/src/types/weakref/anyref.rs +++ b/src/types/weakref/anyref.rs @@ -1,7 +1,7 @@ use crate::err::PyResult; use crate::ffi_ptr_ext::FfiPtrExt; use crate::type_object::{PyTypeCheck, PyTypeInfo}; -use crate::types::any::{PyAny, PyAnyMethods}; +use crate::types::any::PyAny; use crate::{ffi, Bound}; /// Represents any Python `weakref` reference. @@ -104,12 +104,12 @@ pub trait PyWeakrefMethods<'py>: crate::sealed::Sealed { T: PyTypeCheck, { self.upgrade() - .map(Bound::downcast_into::) + .map(Bound::cast_into::) .transpose() .map_err(Into::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`. + /// Upgrade the weakref to a direct Bound object reference unchecked. The type of the recovered object is not checked before casting, 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_GetRef`]. @@ -180,7 +180,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(unsafe { self.upgrade()?.downcast_into_unchecked() }) + Some(unsafe { self.upgrade()?.cast_into_unchecked() }) } /// Upgrade the weakref to a exact direct Bound object reference. @@ -254,7 +254,7 @@ pub trait PyWeakrefMethods<'py>: crate::sealed::Sealed { T: PyTypeInfo, { self.upgrade() - .map(Bound::downcast_into_exact) + .map(Bound::cast_into_exact) .transpose() .map_err(Into::into) } @@ -341,12 +341,12 @@ mod tests { fn new_reference<'py>(object: &Bound<'py, PyAny>) -> PyResult> { let reference = PyWeakrefReference::new(object)?; - reference.into_any().downcast_into().map_err(Into::into) + reference.cast_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) + reference.cast_into().map_err(Into::into) } mod python_class { @@ -357,8 +357,7 @@ mod tests { 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::() + py.eval(ffi::c_str!("A"), None, None).cast_into::() } #[test] diff --git a/src/types/weakref/proxy.rs b/src/types/weakref/proxy.rs index b8dedd99ac6..f786f817c88 100644 --- a/src/types/weakref/proxy.rs +++ b/src/types/weakref/proxy.rs @@ -77,7 +77,7 @@ impl PyWeakrefProxy { object.py(), ffi::PyWeakref_NewProxy(object.as_ptr(), ffi::Py_None()), ) - .downcast_into_unchecked() + .cast_into_unchecked() } } @@ -152,7 +152,7 @@ impl PyWeakrefProxy { object.py(), ffi::PyWeakref_NewProxy(object.as_ptr(), callback.as_ptr()), ) - .downcast_into_unchecked() + .cast_into_unchecked() } } @@ -249,7 +249,7 @@ mod tests { 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::() + .cast_into::() } #[test] @@ -585,7 +585,7 @@ mod tests { None, )?; py.eval(ffi::c_str!("A"), Some(&globals), None) - .downcast_into::() + .cast_into::() } #[test] diff --git a/src/types/weakref/reference.rs b/src/types/weakref/reference.rs index 9fbd275d01b..cca4858f1e3 100644 --- a/src/types/weakref/reference.rs +++ b/src/types/weakref/reference.rs @@ -88,7 +88,7 @@ impl PyWeakrefReference { object.py(), ffi::PyWeakref_NewRef(object.as_ptr(), ffi::Py_None()), ) - .downcast_into_unchecked() + .cast_into_unchecked() } } @@ -162,7 +162,7 @@ impl PyWeakrefReference { object.py(), ffi::PyWeakref_NewRef(object.as_ptr(), callback.as_ptr()), ) - .downcast_into_unchecked() + .cast_into_unchecked() } } @@ -244,8 +244,7 @@ mod tests { 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::() + py.eval(ffi::c_str!("A"), None, None).cast_into::() } #[test] diff --git a/tests/test_class_attributes.rs b/tests/test_class_attributes.rs index 97a3cc75dd9..5cad559cf8b 100644 --- a/tests/test_class_attributes.rs +++ b/tests/test_class_attributes.rs @@ -178,7 +178,7 @@ fn test_fallible_class_attribute() { .string_io .getattr("getvalue")? .call0()? - .downcast::()? + .cast::()? .to_cow()? .into_owned(); let sys = py.import("sys")?; diff --git a/tests/test_class_new.rs b/tests/test_class_new.rs index 37a1044b8c5..51db45f4774 100644 --- a/tests/test_class_new.rs +++ b/tests/test_class_new.rs @@ -23,7 +23,7 @@ fn empty_class_with_new() { assert!(typeobj .call((), None) .unwrap() - .downcast::() + .cast::() .is_ok()); // Calling with arbitrary args or kwargs is not ok @@ -52,7 +52,7 @@ fn unit_class_with_new() { assert!(typeobj .call((), None) .unwrap() - .downcast::() + .cast::() .is_ok()); }); } @@ -73,7 +73,7 @@ fn tuple_class_with_new() { Python::attach(|py| { let typeobj = py.get_type::(); let wrp = typeobj.call((42,), None).unwrap(); - let obj = wrp.downcast::().unwrap(); + let obj = wrp.cast::().unwrap(); let obj_ref = obj.borrow(); assert_eq!(obj_ref.0, 42); }); @@ -98,7 +98,7 @@ fn new_with_one_arg() { Python::attach(|py| { let typeobj = py.get_type::(); let wrp = typeobj.call((42,), None).unwrap(); - let obj = wrp.downcast::().unwrap(); + let obj = wrp.cast::().unwrap(); let obj_ref = obj.borrow(); assert_eq!(obj_ref.data, 42); }); @@ -129,7 +129,7 @@ fn new_with_two_args() { .call((10, 20), None) .map_err(|e| e.display(py)) .unwrap(); - let obj = wrp.downcast::().unwrap(); + let obj = wrp.cast::().unwrap(); let obj_ref = obj.borrow(); assert_eq!(obj_ref.data1, 10); assert_eq!(obj_ref.data2, 20); diff --git a/tests/test_enum.rs b/tests/test_enum.rs index 2e588f9266a..f1a2ab3eff6 100644 --- a/tests/test_enum.rs +++ b/tests/test_enum.rs @@ -337,9 +337,9 @@ fn custom_eq() { #[pymethods] impl CustomPyEq { fn __eq__(&self, other: &Bound<'_, PyAny>) -> bool { - if let Ok(rhs) = other.downcast::() { + if let Ok(rhs) = other.cast::() { rhs.to_cow().is_ok_and(|rhs| self.__str__() == rhs) - } else if let Ok(rhs) = other.downcast::() { + } else if let Ok(rhs) = other.cast::() { self == rhs.get() } else { false diff --git a/tests/test_frompy_intopy_roundtrip.rs b/tests/test_frompy_intopy_roundtrip.rs index 35501447422..e8e41c14ef2 100644 --- a/tests/test_frompy_intopy_roundtrip.rs +++ b/tests/test_frompy_intopy_roundtrip.rs @@ -249,7 +249,7 @@ fn test_enum() { let foo = (&struct_var) .into_pyobject(py) .unwrap() - .downcast_into::() + .cast_into::() .unwrap(); assert_eq!(struct_var, foo.extract::().unwrap()); @@ -257,7 +257,7 @@ fn test_enum() { .clone() .into_pyobject(py) .unwrap() - .downcast_into::() + .cast_into::() .unwrap(); assert_eq!(struct_var, foo.extract::().unwrap()); diff --git a/tests/test_gc.rs b/tests/test_gc.rs index 284b1eae863..2a90ab5a1eb 100644 --- a/tests/test_gc.rs +++ b/tests/test_gc.rs @@ -275,7 +275,7 @@ fn inheritance_with_new_methods_with_drop() { let inst = typeobj .call((), None) .unwrap() - .downcast_into::() + .cast_into::() .unwrap(); inst.as_super().borrow_mut().guard = Some(guard_base); diff --git a/tests/test_intopyobject.rs b/tests/test_intopyobject.rs index 2e824739f59..58795d7c7f6 100644 --- a/tests/test_intopyobject.rs +++ b/tests/test_intopyobject.rs @@ -29,7 +29,7 @@ fn test_named_fields_struct() { pya.get_item("s") .unwrap() .unwrap() - .downcast::() + .cast::() .unwrap(), "Hello" ); @@ -37,7 +37,7 @@ fn test_named_fields_struct() { pya.get_item("t") .unwrap() .unwrap() - .downcast::() + .cast::() .unwrap(), "World" ); @@ -191,14 +191,14 @@ fn test_enum() { } .into_pyobject(py) .unwrap() - .downcast_into::() + .cast_into::() .unwrap(); assert_eq!( foo.get_item("test") .unwrap() .unwrap() - .downcast_into::() + .cast_into::() .unwrap(), "test" ); diff --git a/tests/test_mapping.rs b/tests/test_mapping.rs index cb6e9f49f10..8e958fc5c2f 100644 --- a/tests/test_mapping.rs +++ b/tests/test_mapping.rs @@ -132,7 +132,7 @@ fn mapping_is_not_sequence() { PyMapping::register::(py).unwrap(); - assert!(m.bind(py).downcast::().is_ok()); - assert!(m.bind(py).downcast::().is_err()); + assert!(m.bind(py).cast::().is_ok()); + assert!(m.bind(py).cast::().is_err()); }); } diff --git a/tests/test_proto_methods.rs b/tests/test_proto_methods.rs index f1af2b64f18..e5e36939c53 100644 --- a/tests/test_proto_methods.rs +++ b/tests/test_proto_methods.rs @@ -218,7 +218,7 @@ fn mapping() { ) .unwrap(); - let mapping: &Bound<'_, PyMapping> = inst.bind(py).downcast().unwrap(); + let mapping: &Bound<'_, PyMapping> = inst.bind(py).cast().unwrap(); py_assert!(py, inst, "len(inst) == 0"); @@ -320,7 +320,7 @@ fn sequence() { let inst = Py::new(py, Sequence { values: vec![] }).unwrap(); - let sequence: &Bound<'_, PySequence> = inst.bind(py).downcast().unwrap(); + let sequence: &Bound<'_, PySequence> = inst.bind(py).cast().unwrap(); py_assert!(py, inst, "len(inst) == 0"); @@ -527,7 +527,7 @@ struct GetItem {} #[pymethods] impl GetItem { fn __getitem__(&self, idx: &Bound<'_, PyAny>) -> PyResult<&'static str> { - if let Ok(slice) = idx.downcast::() { + if let Ok(slice) = idx.cast::() { let indices = slice.indices(1000)?; if indices.start == 100 && indices.stop == 200 && indices.step == 1 { return Ok("slice"); diff --git a/tests/test_pyfunction.rs b/tests/test_pyfunction.rs index 8411017e574..3a2e2baeb3e 100644 --- a/tests/test_pyfunction.rs +++ b/tests/test_pyfunction.rs @@ -179,7 +179,7 @@ fn test_functions_with_function_args() { #[cfg(not(Py_LIMITED_API))] fn datetime_to_timestamp(dt: &Bound<'_, PyAny>) -> PyResult { - let dt = dt.downcast::()?; + let dt = dt.cast::()?; let ts: f64 = dt.call_method0("timestamp")?.extract()?; Ok(ts as i64) diff --git a/tests/test_sequence.rs b/tests/test_sequence.rs index 3f8a6bd816f..2d0344d6d58 100644 --- a/tests/test_sequence.rs +++ b/tests/test_sequence.rs @@ -343,8 +343,8 @@ fn sequence_is_not_mapping() { PySequence::register::(py).unwrap(); - assert!(list.downcast::().is_err()); - assert!(list.downcast::().is_ok()); + assert!(list.cast::().is_err()); + assert!(list.cast::().is_ok()); }) } diff --git a/tests/test_various.rs b/tests/test_various.rs index f525fdb77a5..c0ed072f18e 100644 --- a/tests/test_various.rs +++ b/tests/test_various.rs @@ -145,7 +145,7 @@ fn test_pickle() { .get_item("modules") .unwrap() .unwrap() - .downcast::()? + .cast::()? .set_item(module.name()?, module) } diff --git a/tests/ui/forbid_unsafe.rs b/tests/ui/forbid_unsafe.rs index d9a51c52895..1966ab99a8f 100644 --- a/tests/ui/forbid_unsafe.rs +++ b/tests/ui/forbid_unsafe.rs @@ -33,7 +33,7 @@ mod from_py_with { use pyo3::types::PyBytes; fn bytes_from_py(bytes: &Bound<'_, PyAny>) -> PyResult> { - Ok(bytes.downcast::()?.as_bytes().to_vec()) + Ok(bytes.cast::()?.as_bytes().to_vec()) } #[pyfunction] From 54536b80159886a3bcba5632ef6c3cc9a9b8bfbc Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 8 Aug 2025 22:04:58 +0100 Subject: [PATCH 784/936] ci: remove allow for fixed clippy false positive (#5307) --- src/impl_/pyclass/probes.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/impl_/pyclass/probes.rs b/src/impl_/pyclass/probes.rs index 001b85f5023..185e76773ac 100644 --- a/src/impl_/pyclass/probes.rs +++ b/src/impl_/pyclass/probes.rs @@ -30,9 +30,6 @@ impl IsPyT> { 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 4671c901f9daa0500b384c14c440e2ff76e4e931 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sat, 9 Aug 2025 06:06:02 +0100 Subject: [PATCH 785/936] ci: run all clippy jobs on linux arm (#5306) * ci: run all clippy jobs on linux arm * smoke test * Revert "smoke test" This reverts commit 8c64731098b04a6448ec241063d42985b559686c. --- .github/workflows/ci.yml | 86 ++++++++-------------------------------- 1 file changed, 17 insertions(+), 69 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cf57ebd3751..20b96b654cb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,93 +87,41 @@ jobs: # Don't run clippy on `main` because it's already run in the merge queue. if: github.ref != 'refs/heads/main' needs: [fmt] - runs-on: ${{ matrix.platform.os }} + runs-on: ubuntu-24.04-arm 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: rust: [stable] - platform: + target: [ - { - 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-22.04-arm", - python-architecture: "arm64", - rust-target: "aarch64-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-wasip1", - }, - { - 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", - }, - { - os: "windows-11-arm", - python-architecture: "arm64", - rust-target: "aarch64-pc-windows-msvc", - }, + "aarch64-apple-darwin", + "x86_64-unknown-linux-gnu", + "aarch64-unknown-linux-gnu", + "powerpc64le-unknown-linux-gnu", + "s390x-unknown-linux-gnu", + "wasm32-wasip1", + "x86_64-pc-windows-msvc", + "i686-pc-windows-msvc", + "aarch64-pc-windows-msvc", ] include: # Run beta clippy as a way to detect any incoming lints which may affect downstream users - rust: beta - platform: - { - os: "ubuntu-latest", - python-architecture: "x64", - rust-target: "x86_64-unknown-linux-gnu", - } - name: clippy/${{ matrix.platform.rust-target }}/${{ matrix.rust }} + target: "x86_64-unknown-linux-gnu" + name: clippy/${{ matrix.target }}/${{ matrix.rust }} continue-on-error: ${{ matrix.rust != 'stable' }} steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@master with: toolchain: ${{ matrix.rust }} - targets: ${{ matrix.platform.rust-target }} + targets: ${{ matrix.target }} components: clippy,rust-src - - uses: actions/setup-python@v5 - with: - python-version: "3.13" - architecture: ${{ matrix.platform.python-architecture }} - # windows on arm image contains x86-64 libclang - - name: Install LLVM and Clang - if: matrix.platform.os == 'windows-11-arm' - uses: KyleMayes/install-llvm-action@v2 - with: - # to match windows-2022 images - version: "18" - - run: python -m pip install --upgrade pip && pip install nox - - run: nox -s clippy-all + - uses: astral-sh/setup-uv@v6 + - run: uvx nox -s clippy-all env: - CARGO_BUILD_TARGET: ${{ matrix.platform.rust-target }} + CARGO_BUILD_TARGET: ${{ matrix.target }} build-pr: if: ${{ !contains(github.event.pull_request.labels.*.name, 'CI-build-full') && github.event_name == 'pull_request' }} From d265254d1172e67ef5a60b4e147a6fbe666df5ed Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sat, 9 Aug 2025 18:19:51 +0100 Subject: [PATCH 786/936] ci: tidy up testing for send / sync w. probes (#5308) * ci: tidy up testing for send / sync w. probes * simplify value_of macro --- pyo3-macros-backend/src/pyclass.rs | 4 ++-- pyo3-macros-backend/src/pymethod.rs | 2 +- src/err/mod.rs | 12 +++++------- src/impl_/pyclass.rs | 1 + src/impl_/pyclass/probes.rs | 18 ++++++++++++++++++ src/lib.rs | 7 +++++-- src/pybacked.rs | 12 +++++------- src/test_utils.rs | 2 -- 8 files changed, 37 insertions(+), 21 deletions(-) delete mode 100644 src/test_utils.rs diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 419af70c4d8..83935e71998 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -1229,7 +1229,7 @@ fn impl_complex_enum_struct_variant_cls( let field_getter_impl = quote! { fn #field_name(slf: #pyo3_path::PyClassGuard<'_, Self>, py: #pyo3_path::Python<'_>) -> #pyo3_path::PyResult<#pyo3_path::PyObject> { #[allow(unused_imports)] - use #pyo3_path::impl_::pyclass::Probe; + use #pyo3_path::impl_::pyclass::Probe as _; match &*slf.into_super() { #enum_name::#variant_ident { #field_name, .. } => #pyo3_path::impl_::pyclass::ConvertField::< @@ -1305,7 +1305,7 @@ fn impl_complex_enum_tuple_variant_field_getters( let field_getter_impl: syn::ImplItemFn = parse_quote! { fn #field_name(slf: #pyo3_path::PyClassGuard<'_, Self>, py: #pyo3_path::Python<'_>) -> #pyo3_path::PyResult<#pyo3_path::PyObject> { #[allow(unused_imports)] - use #pyo3_path::impl_::pyclass::Probe; + use #pyo3_path::impl_::pyclass::Probe as _; match &*slf.into_super() { #enum_name::#variant_ident ( #(#field_access_tokens), *) => #pyo3_path::impl_::pyclass::ConvertField::< diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index fe9e347dc09..d91b78c4c81 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -853,7 +853,7 @@ pub fn impl_py_getter_def( #cfg_attrs { #[allow(unused_imports)] // might not be used if all probes are positve - use #pyo3_path::impl_::pyclass::Probe; + use #pyo3_path::impl_::pyclass::Probe as _; struct Offset; unsafe impl #pyo3_path::impl_::pyclass::OffsetCalculator<#cls, #ty> for Offset { diff --git a/src/err/mod.rs b/src/err/mod.rs index 929ecaf511a..9c48e30325e 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -852,6 +852,7 @@ impl_signed_integer!(isize); mod tests { use super::PyErrState; use crate::exceptions::{self, PyTypeError, PyValueError}; + use crate::impl_::pyclass::{value_of, IsSend, IsSync}; use crate::{ffi, PyErr, PyTypeInfo, Python}; #[test] @@ -977,14 +978,11 @@ mod tests { #[test] fn test_pyerr_send_sync() { - fn is_send() {} - fn is_sync() {} + assert!(value_of!(IsSend, PyErr)); + assert!(value_of!(IsSync, PyErr)); - is_send::(); - is_sync::(); - - is_send::(); - is_sync::(); + assert!(value_of!(IsSend, PyErrState)); + assert!(value_of!(IsSync, PyErrState)); } #[test] diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index 830091e4d98..ddaeec96ab1 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -25,6 +25,7 @@ use std::{ mod assertions; mod lazy_type_object; +#[macro_use] mod probes; pub use assertions::*; diff --git a/src/impl_/pyclass/probes.rs b/src/impl_/pyclass/probes.rs index 185e76773ac..bfdae766129 100644 --- a/src/impl_/pyclass/probes.rs +++ b/src/impl_/pyclass/probes.rs @@ -46,6 +46,12 @@ where pub const VALUE: bool = true; } +probe!(IsSend); + +impl IsSend { + pub const VALUE: bool = true; +} + probe!(IsSync); impl IsSync { @@ -57,3 +63,15 @@ probe!(IsOption); impl IsOption> { pub const VALUE: bool = true; } + +#[cfg(test)] +macro_rules! value_of { + ($probe:ident, $ty:ty) => {{ + #[allow(unused_imports)] // probe trait not used if VALUE is true + use crate::impl_::pyclass::Probe as _; + $probe::<$ty>::VALUE + }}; +} + +#[cfg(test)] +pub(crate) use value_of; diff --git a/src/lib.rs b/src/lib.rs index cda313c2c91..55a17d9f0bd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -411,6 +411,11 @@ pub use inventory; // Re-exported for `#[pyclass]` and `#[pymethods]` with `mult #[macro_use] mod tests; +// Macro dependencies, also contains macros exported for use across the codebase and +// in expanded macros. +#[doc(hidden)] +pub mod impl_; + #[macro_use] mod internal_tricks; mod internal; @@ -424,8 +429,6 @@ pub mod coroutine; mod err; pub mod exceptions; pub mod ffi; -#[doc(hidden)] -pub mod impl_; mod instance; mod interpreter_lifecycle; pub mod marker; diff --git a/src/pybacked.rs b/src/pybacked.rs index aab2b066745..8a91fad756e 100644 --- a/src/pybacked.rs +++ b/src/pybacked.rs @@ -315,6 +315,7 @@ use impl_traits; #[cfg(test)] mod test { use super::*; + use crate::impl_::pyclass::{value_of, IsSend, IsSync}; use crate::types::PyAnyMethods as _; use crate::{IntoPyObject, Python}; use std::collections::hash_map::DefaultHasher; @@ -424,14 +425,11 @@ mod test { #[test] fn test_backed_types_send_sync() { - fn is_send() {} - fn is_sync() {} + assert!(value_of!(IsSend, PyBackedStr)); + assert!(value_of!(IsSync, PyBackedStr)); - is_send::(); - is_sync::(); - - is_send::(); - is_sync::(); + assert!(value_of!(IsSend, PyBackedBytes)); + assert!(value_of!(IsSync, PyBackedBytes)); } #[cfg(feature = "py-clone")] diff --git a/src/test_utils.rs b/src/test_utils.rs deleted file mode 100644 index a007483fad4..00000000000 --- a/src/test_utils.rs +++ /dev/null @@ -1,2 +0,0 @@ -use crate as pyo3; -include!("../tests/common.rs"); From c366646372e84c44af661f29fdaac4e2356033f4 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sat, 9 Aug 2025 20:27:01 +0100 Subject: [PATCH 787/936] move `#[pyclass]` docstring generation to compile time (#5286) * move `#[pyclass]` docstring generation to compile time * improve error message * clean up to use cstr all the way through * fixup msrv * fixup final tests, newsfragment * runtime test coverage * use "copy_from_slice" (with a note for the future) * fix clippy beta --- guide/src/class.md | 12 +- newsfragments/5286.changed.md | 1 + pyo3-build-config/src/lib.rs | 2 + pyo3-macros-backend/src/method.rs | 2 +- pyo3-macros-backend/src/module.rs | 4 +- pyo3-macros-backend/src/pyclass.rs | 29 ++- pyo3-macros-backend/src/pyfunction.rs | 2 +- pyo3-macros-backend/src/pymethod.rs | 41 ++-- pyo3-macros-backend/src/utils.rs | 35 ++- src/impl_.rs | 1 - src/impl_/concat.rs | 225 +++++++++++++++++-- src/impl_/pyclass.rs | 53 ++--- src/impl_/pyclass/doc.rs | 123 ++++++++++ src/impl_/pyclass/probes.rs | 6 + src/pyclass/create_type_object.rs | 2 +- tests/test_compile_error.rs | 1 + tests/ui/invalid_cancel_handle.stderr | 52 ++--- tests/ui/invalid_pyclass_doc.rs | 7 + tests/ui/invalid_pyclass_doc.stderr | 5 + tests/ui/invalid_pymethods_duplicates.stderr | 4 +- 20 files changed, 464 insertions(+), 143 deletions(-) create mode 100644 newsfragments/5286.changed.md create mode 100644 src/impl_/pyclass/doc.rs create mode 100644 tests/ui/invalid_pyclass_doc.rs create mode 100644 tests/ui/invalid_pyclass_doc.stderr diff --git a/guide/src/class.md b/guide/src/class.md index 30a0cf68b0f..7d10dce9494 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -1430,6 +1430,9 @@ impl pyo3::impl_::pyclass::PyClassImpl for MyClass { type WeakRef = pyo3::impl_::pyclass::PyClassDummySlot; type BaseNativeType = pyo3::PyAny; + const RAW_DOC: &'static std::ffi::CStr = pyo3::ffi::c_str!("..."); + const DOC: &'static std::ffi::CStr = pyo3::ffi::c_str!("..."); + fn items_iter() -> pyo3::impl_::pyclass::PyClassItemsIter { use pyo3::impl_::pyclass::*; let collector = PyClassImplCollector::::new(); @@ -1442,15 +1445,6 @@ impl pyo3::impl_::pyclass::PyClassImpl for MyClass { static TYPE_OBJECT: LazyTypeObject = LazyTypeObject::new(); &TYPE_OBJECT } - - fn doc(py: Python<'_>) -> pyo3::PyResult<&'static ::std::ffi::CStr> { - use pyo3::impl_::pyclass::*; - static DOC: pyo3::sync::GILOnceCell<::std::borrow::Cow<'static, ::std::ffi::CStr>> = pyo3::sync::GILOnceCell::new(); - DOC.get_or_try_init(py, || { - let collector = PyClassImplCollector::::new(); - build_pyclass_doc(::NAME, pyo3::ffi::c_str!(""), collector.new_text_signature()) - }).map(::std::ops::Deref::deref) - } } # Python::attach(|py| { diff --git a/newsfragments/5286.changed.md b/newsfragments/5286.changed.md new file mode 100644 index 00000000000..1bb754dd0c1 --- /dev/null +++ b/newsfragments/5286.changed.md @@ -0,0 +1 @@ +Move `#[pyclass]` docstring formatting from import time to compile time. diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index 22f0d333522..534725ac9cc 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -186,7 +186,9 @@ pub fn print_feature_cfgs() { // https://github.com/rust-lang/rust/issues/124651 just in case print_feature_cfg(79, "diagnostic_namespace"); print_feature_cfg(83, "io_error_more"); + print_feature_cfg(83, "mut_ref_in_const_fn"); print_feature_cfg(85, "fn_ptr_eq"); + print_feature_cfg(86, "from_bytes_with_nul_error"); } /// Registers `pyo3`s config names as reachable cfg expressions diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index ed28aff1a1c..0e8977db101 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -976,7 +976,7 @@ impl<'a> FnSpec<'a> { } /// Forwards to [utils::get_doc] with the text signature of this spec. - pub fn get_doc(&self, attrs: &[syn::Attribute], ctx: &Ctx) -> PythonDoc { + pub fn get_doc(&self, attrs: &[syn::Attribute], ctx: &Ctx) -> syn::Result { let text_signature = self .text_signature_call_signature() .map(|sig| format!("{}{}", self.python_name, sig)); diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index 44363570e70..1888f564df5 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -115,7 +115,7 @@ pub fn pymodule_module_impl( 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 doc = get_doc(attrs, None, ctx)?; let name = options .name .map_or_else(|| ident.unraw(), |name| name.value.0); @@ -453,7 +453,7 @@ pub fn pymodule_function_impl( .name .map_or_else(|| ident.unraw(), |name| name.value.0); let vis = &function.vis; - let doc = get_doc(&function.attrs, None, ctx); + let doc = get_doc(&function.attrs, None, ctx)?; let initialization = module_initialization( &name, diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 83935e71998..31e398ef08b 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -250,7 +250,7 @@ pub fn build_py_class( args.options.take_pyo3_options(&mut class.attrs)?; let ctx = &Ctx::new(&args.options.krate, None); - let doc = utils::get_doc(&class.attrs, None, ctx); + let doc = utils::get_doc(&class.attrs, None, ctx)?; if let Some(lt) = class.generics.lifetimes().next() { bail_spanned!( @@ -521,7 +521,7 @@ pub fn build_py_enum( bail_spanned!(generic.span() => "enums do not support #[pyclass(generic)]"); } - let doc = utils::get_doc(&enum_.attrs, None, ctx); + let doc = utils::get_doc(&enum_.attrs, None, ctx)?; let enum_ = PyClassEnum::new(enum_)?; impl_enum(enum_, &args, doc, method_type, ctx) } @@ -1758,7 +1758,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, ctx), + doc: crate::get_doc(&[], None, ctx)?, }; let getter = crate::pymethod::impl_py_getter_def(variant_cls_type, property_type, ctx)?; @@ -2056,7 +2056,7 @@ fn pyclass_class_geitem( let class_geitem_method = crate::pymethod::impl_py_method_def( cls, &spec, - &spec.get_doc(&class_geitem_impl.attrs, ctx), + &spec.get_doc(&class_geitem_impl.attrs, ctx)?, Some(quote!(#pyo3_path::ffi::METH_CLASS)), ctx, )?; @@ -2387,14 +2387,19 @@ impl<'a> PyClassImplsBuilder<'a> { PyClassItemsIter::new(&INTRINSIC_ITEMS, #pymethods_items) } - fn doc(py: #pyo3_path::Python<'_>) -> #pyo3_path::PyResult<&'static ::std::ffi::CStr> { - use #pyo3_path::impl_::pyclass::*; - static DOC: #pyo3_path::sync::GILOnceCell<::std::borrow::Cow<'static, ::std::ffi::CStr>> = #pyo3_path::sync::GILOnceCell::new(); - DOC.get_or_try_init(py, || { - let collector = PyClassImplCollector::::new(); - build_pyclass_doc(::NAME, #doc, collector.new_text_signature()) - }).map(::std::ops::Deref::deref) - } + const RAW_DOC: &'static ::std::ffi::CStr = #doc; + + const DOC: &'static ::std::ffi::CStr = { + use #pyo3_path::impl_ as impl_; + use impl_::pyclass::Probe as _; + const DOC_PIECES: &'static [&'static [u8]] = impl_::pyclass::doc::PyClassDocGenerator::< + #cls, + { impl_::pyclass::HasNewTextSignature::<#cls>::VALUE } + >::DOC_PIECES; + const LEN: usize = impl_::concat::combined_len_bytes(DOC_PIECES); + const DOC: &'static [u8] = &impl_::concat::combine_bytes_to_array::(DOC_PIECES); + impl_::pyclass::doc::doc_bytes_as_cstr(DOC) + }; #dict_offset diff --git a/pyo3-macros-backend/src/pyfunction.rs b/pyo3-macros-backend/src/pyfunction.rs index 337a868d412..303af155acf 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -425,7 +425,7 @@ pub fn impl_wrap_pyfunction( ); } let wrapper = spec.get_wrapper_function(&wrapper_ident, None, ctx)?; - let methoddef = spec.get_methoddef(wrapper_ident, &spec.get_doc(&func.attrs, ctx), ctx); + let methoddef = spec.get_methoddef(wrapper_ident, &spec.get_doc(&func.attrs, ctx)?, ctx); let wrapped_pyfunction = quote! { // Create a module with the same name as the `#[pyfunction]` - this way `use ` diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index d91b78c4c81..33b7af657b3 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -236,21 +236,21 @@ pub fn gen_py_method( (_, FnType::Fn(_)) => GeneratedPyMethod::Method(impl_py_method_def( cls, spec, - &spec.get_doc(meth_attrs, ctx), + &spec.get_doc(meth_attrs, ctx)?, None, ctx, )?), (_, FnType::FnClass(_)) => GeneratedPyMethod::Method(impl_py_method_def( cls, spec, - &spec.get_doc(meth_attrs, ctx), + &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, ctx), + &spec.get_doc(meth_attrs, ctx)?, Some(quote!(#pyo3_path::ffi::METH_STATIC)), ctx, )?), @@ -264,7 +264,7 @@ pub fn gen_py_method( PropertyType::Function { self_type, spec, - doc: spec.get_doc(meth_attrs, ctx), + doc: spec.get_doc(meth_attrs, ctx)?, }, ctx, )?), @@ -273,7 +273,7 @@ pub fn gen_py_method( PropertyType::Function { self_type, spec, - doc: spec.get_doc(meth_attrs, ctx), + doc: spec.get_doc(meth_attrs, ctx)?, }, ctx, )?), @@ -360,10 +360,14 @@ pub fn impl_py_method_def_new( // Use just the text_signature_call_signature() because the class' Python name // isn't known to `#[pymethods]` - that has to be attached at runtime from the PyClassImpl // trait implementation created by `#[pyclass]`. - let text_signature_body = spec.text_signature_call_signature().map_or_else( - || quote!(::std::option::Option::None), - |text_signature| quote!(::std::option::Option::Some(#text_signature)), - ); + let text_signature_impl = spec.text_signature_call_signature().map(|text_signature| { + quote! { + #[allow(unknown_lints, non_local_definitions)] + impl #pyo3_path::impl_::pyclass::doc::PyClassNewTextSignature for #cls { + const TEXT_SIGNATURE: &'static str = #text_signature; + } + } + }); let slot_def = quote! { #pyo3_path::ffi::PyType_Slot { slot: #pyo3_path::ffi::Py_tp_new, @@ -373,13 +377,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 { - #[allow(unknown_lints, non_local_definitions)] - 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 - } - } + + #text_signature_impl #pyo3_path::impl_::trampoline::newfunc( subtype, @@ -627,7 +626,7 @@ pub fn impl_py_setter_def( ) -> Result { let Ctx { pyo3_path, .. } = ctx; let python_name = property_type.null_terminated_python_name(ctx)?; - let doc = property_type.doc(ctx); + let doc = property_type.doc(ctx)?; let mut holders = Holders::new(); let setter_impl = match property_type { PropertyType::Descriptor { @@ -815,7 +814,7 @@ pub fn impl_py_getter_def( ) -> Result { let Ctx { pyo3_path, .. } = ctx; let python_name = property_type.null_terminated_python_name(ctx)?; - let doc = property_type.doc(ctx); + let doc = property_type.doc(ctx)?; let mut cfg_attrs = TokenStream::new(); if let PropertyType::Descriptor { field, .. } = &property_type { @@ -978,12 +977,12 @@ impl PropertyType<'_> { } } - fn doc(&self, ctx: &Ctx) -> Cow<'_, PythonDoc> { + fn doc(&self, ctx: &Ctx) -> Result> { match self { PropertyType::Descriptor { field, .. } => { - Cow::Owned(utils::get_doc(&field.attrs, None, ctx)) + utils::get_doc(&field.attrs, None, ctx).map(Cow::Owned) } - PropertyType::Function { doc, .. } => Cow::Borrowed(doc), + PropertyType::Function { doc, .. } => Ok(Cow::Borrowed(doc)), } } } diff --git a/pyo3-macros-backend/src/utils.rs b/pyo3-macros-backend/src/utils.rs index 1954e79c858..0df0b75e431 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 proc_macro2::{Span, TokenStream}; -use quote::{quote, ToTokens}; +use quote::{quote, quote_spanned, ToTokens}; use std::ffi::CString; use syn::spanned::Spanned; use syn::visit_mut::VisitMut; @@ -132,7 +132,7 @@ pub fn get_doc( attrs: &[syn::Attribute], mut text_signature: Option, ctx: &Ctx, -) -> PythonDoc { +) -> syn::Result { let Ctx { pyo3_path, .. } = ctx; // insert special divider between `__text_signature__` and doc // (assume text_signature is itself well-formed) @@ -143,10 +143,15 @@ pub fn get_doc( let mut parts = Punctuated::::new(); let mut first = true; let mut current_part = text_signature.unwrap_or_default(); + let mut current_part_span = None; for attr in attrs { if attr.path().is_ident("doc") { if let Ok(nv) = attr.meta.require_name_value() { + current_part_span = match current_part_span { + None => Some(nv.value.span()), + Some(span) => span.join(nv.value.span()), + }; if !first { current_part.push('\n'); } else { @@ -164,7 +169,7 @@ pub fn get_doc( } else { // This is probably a macro doc from Rust 1.54, e.g. #[doc = include_str!(...)] // Reset the string buffer, write that part, and then push this macro part too. - parts.push(current_part.to_token_stream()); + parts.push(quote_spanned!(current_part_span.unwrap_or(Span::call_site()) => #current_part)); current_part.clear(); parts.push(nv.value.to_token_stream()); } @@ -175,7 +180,9 @@ pub fn get_doc( if !parts.is_empty() { // Doc contained macro pieces - return as `concat!` expression if !current_part.is_empty() { - parts.push(current_part.to_token_stream()); + parts.push( + quote_spanned!(current_part_span.unwrap_or(Span::call_site()) => #current_part), + ); } let mut tokens = TokenStream::new(); @@ -187,17 +194,25 @@ pub fn get_doc( syn::token::Comma(Span::call_site()).to_tokens(tokens); }); - PythonDoc(PythonDocKind::Tokens( + Ok(PythonDoc(PythonDocKind::Tokens( quote!(#pyo3_path::ffi::c_str!(#tokens)), - )) + ))) } else { // Just a string doc - return directly with nul terminator - let docs = CString::new(current_part).unwrap(); - PythonDoc(PythonDocKind::LitCStr(LitCStr::new( + let docs = CString::new(current_part).map_err(|e| { + syn::Error::new( + current_part_span.unwrap_or(Span::call_site()), + format!( + "Python doc may not contain nul byte, found nul at position {}", + e.nul_position() + ), + ) + })?; + Ok(PythonDoc(PythonDocKind::LitCStr(LitCStr::new( docs, - Span::call_site(), + current_part_span.unwrap_or(Span::call_site()), ctx, - ))) + )))) } } diff --git a/src/impl_.rs b/src/impl_.rs index 8026312ed5c..d72a084e48f 100644 --- a/src/impl_.rs +++ b/src/impl_.rs @@ -7,7 +7,6 @@ //! breaking semver guarantees. pub mod callback; -#[cfg(feature = "experimental-inspect")] pub mod concat; #[cfg(feature = "experimental-async")] pub mod coroutine; diff --git a/src/impl_/concat.rs b/src/impl_/concat.rs index 4993eb45bc5..942e406cf9f 100644 --- a/src/impl_/concat.rs +++ b/src/impl_/concat.rs @@ -6,25 +6,216 @@ macro_rules! const_concat { $e }}; ($l:expr, $($r:expr),+ $(,)?) => {{ - const L: &'static str = $l; - const R: &'static str = $crate::impl_::concat::const_concat!($($r),*); - const LEN: usize = L.len() + R.len(); - const fn combine(l: &'static [u8], r: &'static [u8]) -> [u8; LEN] { - let mut out = [0u8; LEN]; - let mut i = 0; - while i < l.len() { - out[i] = l[i]; - i += 1; + const PIECES: &[&str] = &[$l, $($r),+]; + const RAW_BYTES: &[u8] = &$crate::impl_::concat::combine_to_array::<{ + $crate::impl_::concat::combined_len(PIECES) + }>(PIECES); + // Safety: `RAW_BYTES` is combined from valid &str pieces + unsafe { ::std::str::from_utf8_unchecked(RAW_BYTES) } + }} +} + +pub use const_concat; + +/// Calculates the total byte length of all string pieces in the array. +/// +/// This is a useful utility in order to determine the size needed for the constant +/// `combine` function. +pub const fn combined_len(pieces: &[&str]) -> usize { + let mut len = 0; + let mut pieces_idx = 0; + while pieces_idx < pieces.len() { + len += pieces[pieces_idx].len(); + pieces_idx += 1; + } + len +} + +/// Combines all string pieces into a single byte array. +/// +/// `out` should be a buffer at the correct size of `combined_len(pieces)`, else this will panic. +#[cfg(mut_ref_in_const_fn)] // requires MSRV 1.83 +#[allow(clippy::incompatible_msrv)] // `split_at_mut` also requires MSRV 1.83 +pub const fn combine(pieces: &[&str], mut out: &mut [u8]) { + let mut pieces_idx = 0; + while pieces_idx < pieces.len() { + let piece = pieces[pieces_idx].as_bytes(); + slice_copy_from_slice(out, piece); + // using split_at_mut because range indexing not yet supported in const fn + out = out.split_at_mut(piece.len()).1; + pieces_idx += 1; + } + // should be no trailing buffer + assert!(out.is_empty(), "output buffer too large"); +} + +/// Wrapper around combine which has a const generic parameter, this is going to be more codegen +/// at compile time (?) +/// +/// Unfortunately the `&mut [u8]` buffer needs MSRV 1.83 +pub const fn combine_to_array(pieces: &[&str]) -> [u8; LEN] { + let mut out: [u8; LEN] = [0u8; LEN]; + #[cfg(mut_ref_in_const_fn)] + combine(pieces, &mut out); + #[cfg(not(mut_ref_in_const_fn))] // inlined here for higher code + { + let mut out_idx = 0; + let mut pieces_idx = 0; + while pieces_idx < pieces.len() { + let piece = pieces[pieces_idx].as_bytes(); + let mut piece_idx = 0; + while piece_idx < piece.len() { + out[out_idx] = piece[piece_idx]; + out_idx += 1; + piece_idx += 1; } - while i < LEN { - out[i] = r[i - l.len()]; - i += 1; + pieces_idx += 1; + } + assert!(out_idx == out.len(), "output buffer too large"); + } + out +} + +/// Calculates the total byte length of all byte pieces in the array. +/// +/// This is a useful utility in order to determine the size needed for the constant +/// `combine_bytes` function. +pub const fn combined_len_bytes(pieces: &[&[u8]]) -> usize { + let mut len = 0; + let mut pieces_idx = 0; + while pieces_idx < pieces.len() { + len += pieces[pieces_idx].len(); + pieces_idx += 1; + } + len +} + +/// Combines all bytes pieces into a single byte array. +/// +/// `out` should be a buffer at the correct size of `combined_len(pieces)`, else this will panic. +#[cfg(mut_ref_in_const_fn)] // requires MSRV 1.83 +#[allow(clippy::incompatible_msrv)] // `split_at_mut` also requires MSRV 1.83 +pub const fn combine_bytes(pieces: &[&[u8]], mut out: &mut [u8]) { + let mut pieces_idx = 0; + while pieces_idx < pieces.len() { + let piece = pieces[pieces_idx]; + slice_copy_from_slice(out, piece); + // using split_at_mut because range indexing not yet supported in const fn + out = out.split_at_mut(piece.len()).1; + pieces_idx += 1; + } + // should be no trailing buffer + assert!(out.is_empty(), "output buffer too large"); +} + +/// Wrapper around `combine_bytes` which has a const generic parameter, this is going to be more codegen +/// at compile time (?) +/// +/// Unfortunately the `&mut [u8]` buffer needs MSRV 1.83 +pub const fn combine_bytes_to_array(pieces: &[&[u8]]) -> [u8; LEN] { + let mut out: [u8; LEN] = [0u8; LEN]; + #[cfg(mut_ref_in_const_fn)] + combine_bytes(pieces, &mut out); + #[cfg(not(mut_ref_in_const_fn))] // inlined here for higher code + { + let mut out_idx = 0; + let mut pieces_idx = 0; + while pieces_idx < pieces.len() { + let piece = pieces[pieces_idx]; + let mut piece_idx = 0; + while piece_idx < piece.len() { + out[out_idx] = piece[piece_idx]; + out_idx += 1; + piece_idx += 1; } - out + pieces_idx += 1; } - #[allow(unsafe_code)] - unsafe { ::std::str::from_utf8_unchecked(&combine(L.as_bytes(), R.as_bytes())) } - }} + assert!(out_idx == out.len(), "output buffer too large"); + } + out } -pub use const_concat; +/// Replacement for `slice::copy_from_slice`, which is const from 1.87 +#[cfg(mut_ref_in_const_fn)] // requires MSRV 1.83 +const fn slice_copy_from_slice(out: &mut [u8], src: &[u8]) { + let mut i = 0; + while i < src.len() { + out[i] = src[i]; + i += 1; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_combined_len() { + let pieces = ["foo", "bar", "baz"]; + assert_eq!(combined_len(&pieces), 9); + let empty: [&str; 0] = []; + assert_eq!(combined_len(&empty), 0); + } + + #[test] + fn test_combine_to_array() { + let pieces = ["foo", "bar"]; + let combined = combine_to_array::<6>(&pieces); + assert_eq!(&combined, b"foobar"); + } + + #[test] + fn test_const_concat_macro() { + const RESULT: &str = const_concat!("foo", "bar", "baz"); + assert_eq!(RESULT, "foobarbaz"); + const SINGLE: &str = const_concat!("abc"); + assert_eq!(SINGLE, "abc"); + } + + #[test] + fn test_combined_len_bytes() { + let pieces: [&[u8]; 3] = [b"foo", b"bar", b"baz"]; + assert_eq!(combined_len_bytes(&pieces), 9); + let empty: [&[u8]; 0] = []; + assert_eq!(combined_len_bytes(&empty), 0); + } + + #[test] + fn test_combine_bytes_to_array() { + let pieces: [&[u8]; 2] = [b"foo", b"bar"]; + let combined = combine_bytes_to_array::<6>(&pieces); + assert_eq!(&combined, b"foobar"); + } + + #[test] + #[should_panic(expected = "index out of bounds")] + fn test_combine_to_array_buffer_too_small() { + let pieces = ["foo", "bar"]; + // Intentionally wrong length + let _ = combine_to_array::<5>(&pieces); + } + + #[test] + #[should_panic(expected = "output buffer too large")] + fn test_combine_to_array_buffer_too_big() { + let pieces = ["foo", "bar"]; + // Intentionally wrong length + let _ = combine_to_array::<10>(&pieces); + } + + #[test] + #[should_panic(expected = "index out of bounds")] + fn test_combine_bytes_to_array_buffer_too_small() { + let pieces: [&[u8]; 2] = [b"foo", b"bar"]; + // Intentionally wrong length + let _ = combine_bytes_to_array::<5>(&pieces); + } + + #[test] + #[should_panic(expected = "output buffer too large")] + fn test_combine_bytes_to_array_buffer_too_big() { + let pieces: [&[u8]; 2] = [b"foo", b"bar"]; + // Intentionally wrong length + let _ = combine_bytes_to_array::<10>(&pieces); + } +} diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index ddaeec96ab1..2535ca7ed47 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -1,5 +1,5 @@ use crate::{ - exceptions::{PyAttributeError, PyNotImplementedError, PyRuntimeError, PyValueError}, + exceptions::{PyAttributeError, PyNotImplementedError, PyRuntimeError}, ffi, impl_::{ freelist::PyObjectFreeList, @@ -13,8 +13,7 @@ use crate::{ PyResult, PyTypeInfo, Python, }; use std::{ - borrow::Cow, - ffi::{CStr, CString}, + ffi::CStr, marker::PhantomData, os::raw::{c_int, c_void}, ptr, @@ -24,6 +23,7 @@ use std::{ }; mod assertions; +pub mod doc; mod lazy_type_object; #[macro_use] mod probes; @@ -208,8 +208,16 @@ pub trait PyClassImpl: Sized + 'static { #[cfg(feature = "multiple-pymethods")] type Inventory: PyClassInventory; - /// Rendered class doc - fn doc(py: Python<'_>) -> PyResult<&'static CStr>; + /// Docstring for the class provided on the struct or enum. + /// + /// This is exposed for `PyClassDocGenerator` to use as a docstring piece. + const RAW_DOC: &'static CStr; + + /// Fully rendered class doc, including the `text_signature` if a constructor is defined. + /// + /// This is constructed at compile-time with const specialization via the proc macros with help + /// from the PyClassDocGenerator` type. + const DOC: &'static CStr; fn items_iter() -> PyClassItemsIter; @@ -226,29 +234,6 @@ pub trait PyClassImpl: Sized + 'static { fn lazy_type_object() -> &'static LazyTypeObject; } -/// Runtime helper to build a class docstring from the `doc` and `text_signature`. -/// -/// This is done at runtime because the class text signature is collected via dtolnay -/// specialization in to the `#[pyclass]` macro from the `#[pymethods]` macro. -pub fn build_pyclass_doc( - class_name: &'static str, - doc: &'static CStr, - text_signature: Option<&'static str>, -) -> PyResult> { - if let Some(text_signature) = text_signature { - let doc = CString::new(format!( - "{}{}\n--\n\n{}", - class_name, - text_signature, - doc.to_str().unwrap(), - )) - .map_err(|_| PyValueError::new_err("class doc cannot contain nul bytes"))?; - Ok(Cow::Owned(doc)) - } else { - Ok(Cow::Borrowed(doc)) - } -} - /// Iterator used to process all class items during type instantiation. pub struct PyClassItemsIter { /// Iteration state @@ -1053,18 +1038,6 @@ impl PyMethods for &'_ PyClassImplCollector { } } -// Text signature for __new__ -pub trait PyClassNewTextSignature { - fn new_text_signature(self) -> Option<&'static str>; -} - -impl PyClassNewTextSignature for &'_ PyClassImplCollector { - #[inline] - fn new_text_signature(self) -> Option<&'static str> { - None - } -} - // Thread checkers #[doc(hidden)] diff --git a/src/impl_/pyclass/doc.rs b/src/impl_/pyclass/doc.rs new file mode 100644 index 00000000000..5d6d8120692 --- /dev/null +++ b/src/impl_/pyclass/doc.rs @@ -0,0 +1,123 @@ +use std::{ffi::CStr, marker::PhantomData}; + +use crate::{impl_::pyclass::PyClassImpl, PyClass, PyTypeInfo}; + +/// Trait implemented by classes with a known text signature for instantiation. +/// +/// This is implemented by the `#[pymethods]` macro when handling expansion for a +/// `#[new]` method. +pub trait PyClassNewTextSignature { + const TEXT_SIGNATURE: &'static str; +} + +/// Type which uses specialization on impl blocks to facilitate generating the documentation for a +/// `#[pyclass]` type. +/// +/// At the moment, this is only used to help lift the `TEXT_SIGNATURE` constant to compile time +/// providing a base case and a specialized implementation when the signature is known at compile time. +/// +/// In the future when const eval is more advanced, it will probably be possible to format the whole +/// class docstring at compile time as part of this type instead of in macro expansion. +pub struct PyClassDocGenerator< + ClassT: PyClass, + // switch to determine if a signature for class instantiation is known + const HAS_NEW_TEXT_SIGNATURE: bool, +>(PhantomData); + +impl PyClassDocGenerator { + pub const DOC_PIECES: &'static [&'static [u8]] = &[ + ::NAME.as_bytes(), + ClassT::TEXT_SIGNATURE.as_bytes(), + b"\n--\n\n", + ::RAW_DOC.to_bytes_with_nul(), + ]; +} + +impl PyClassDocGenerator { + pub const DOC_PIECES: &'static [&'static [u8]] = + &[::RAW_DOC.to_bytes_with_nul()]; +} + +/// Casts bytes to a CStr, ensuring they are valid. +pub const fn doc_bytes_as_cstr(bytes: &'static [u8]) -> &'static ::std::ffi::CStr { + match CStr::from_bytes_with_nul(bytes) { + Ok(cstr) => cstr, + #[cfg(not(from_bytes_with_nul_error))] // MSRV 1.86 + Err(_) => panic!("invalid pyclass doc"), + #[cfg(from_bytes_with_nul_error)] + // This case may happen if the user provides an invalid docstring + Err(std::ffi::FromBytesWithNulError::InteriorNul { .. }) => { + panic!("pyclass doc contains nul bytes") + } + // This case shouldn't happen using the macro machinery as long as `PyClassDocGenerator` + // uses the RAW_DOC as the final piece, which is nul terminated. + #[cfg(from_bytes_with_nul_error)] + Err(std::ffi::FromBytesWithNulError::NotNulTerminated) => { + panic!("pyclass doc expected to be nul terminated") + } + } +} + +#[cfg(test)] +mod tests { + + use crate::ffi; + + use super::*; + + #[test] + #[cfg(feature = "macros")] + fn test_doc_generator() { + use crate::impl_::concat::{combine_bytes_to_array, combined_len_bytes}; + + /// A dummy class with signature. + #[crate::pyclass(crate = "crate")] + struct MyClass; + + #[crate::pymethods(crate = "crate")] + impl MyClass { + #[new] + fn new(x: i32, y: i32) -> Self { + let _ = (x, y); // suppress unused variable warnings + MyClass + } + } + + // simulate what the macro is doing + const PIECES: &[&[u8]] = PyClassDocGenerator::::DOC_PIECES; + assert_eq!( + &combine_bytes_to_array::<{ combined_len_bytes(PIECES) }>(PIECES), + b"MyClass(x, y)\n--\n\nA dummy class with signature.\0" + ); + + // simulate if the macro detected no text signature + const PIECES_WITHOUT_SIGNATURE: &[&[u8]] = + PyClassDocGenerator::::DOC_PIECES; + assert_eq!( + &combine_bytes_to_array::<{ combined_len_bytes(PIECES_WITHOUT_SIGNATURE) }>( + PIECES_WITHOUT_SIGNATURE + ), + b"A dummy class with signature.\0" + ); + } + + #[test] + fn test_doc_bytes_as_cstr() { + let cstr = doc_bytes_as_cstr(b"MyClass\0"); + assert_eq!(cstr, ffi::c_str!("MyClass")); + } + + #[test] + #[cfg(from_bytes_with_nul_error)] + #[should_panic(expected = "pyclass doc contains nul bytes")] + fn test_doc_bytes_as_cstr_central_nul() { + doc_bytes_as_cstr(b"MyClass\0Foo"); + } + + #[test] + #[cfg(from_bytes_with_nul_error)] + #[should_panic(expected = "pyclass doc expected to be nul terminated")] + fn test_doc_bytes_as_cstr_not_nul_terminated() { + doc_bytes_as_cstr(b"MyClass"); + } +} diff --git a/src/impl_/pyclass/probes.rs b/src/impl_/pyclass/probes.rs index bfdae766129..6e3804aaec8 100644 --- a/src/impl_/pyclass/probes.rs +++ b/src/impl_/pyclass/probes.rs @@ -64,6 +64,12 @@ impl IsOption> { pub const VALUE: bool = true; } +probe!(HasNewTextSignature); + +impl HasNewTextSignature { + pub const VALUE: bool = true; +} + #[cfg(test)] macro_rules! value_of { ($probe:ident, $ty:ty) => {{ diff --git a/src/pyclass/create_type_object.rs b/src/pyclass/create_type_object.rs index 3b43488cad7..1a6dd032925 100644 --- a/src/pyclass/create_type_object.rs +++ b/src/pyclass/create_type_object.rs @@ -92,7 +92,7 @@ where T::IS_MAPPING, T::IS_SEQUENCE, T::IS_IMMUTABLE_TYPE, - T::doc(py)?, + T::DOC, T::dict_offset(), T::weaklist_offset(), T::IS_BASETYPE, diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index b7ed01a510b..c3b8570d6d3 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -8,6 +8,7 @@ fn test_compile_errors() { t.compile_fail("tests/ui/invalid_property_args.rs"); t.compile_fail("tests/ui/invalid_proto_pymethods.rs"); t.compile_fail("tests/ui/invalid_pyclass_args.rs"); + t.compile_fail("tests/ui/invalid_pyclass_doc.rs"); t.compile_fail("tests/ui/invalid_pyclass_enum.rs"); t.compile_fail("tests/ui/invalid_pyclass_item.rs"); #[cfg(Py_3_9)] diff --git a/tests/ui/invalid_cancel_handle.stderr b/tests/ui/invalid_cancel_handle.stderr index 930a9e487c0..a0f773aad90 100644 --- a/tests/ui/invalid_cancel_handle.stderr +++ b/tests/ui/invalid_cancel_handle.stderr @@ -22,32 +22,6 @@ error: `from_py_with` and `cancel_handle` cannot be specified together 24 | #[pyo3(cancel_handle, from_py_with = cancel_handle)] _param: pyo3::coroutine::CancelHandle, | ^^^^^^^^^^^^^ -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 `PyClass` is not implemented for `CancelHandle` - | - = 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<'_, '_, '_, false>` - -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, 'holder, 'py, IS_OPTION>`: - `&'a pyo3::Bound<'py, T>` implements `PyFunctionArgument<'a, 'holder, 'py, false>` - `&'holder mut pyo3::coroutine::Coroutine` implements `PyFunctionArgument<'a, 'holder, 'py, false>` - `&'holder pyo3::coroutine::Coroutine` implements `PyFunctionArgument<'a, 'holder, 'py, false>` - `Option` implements `PyFunctionArgument<'a, 'holder, 'py, true>` - = note: required for `CancelHandle` to implement `FromPyObject<'_>` - = note: required for `CancelHandle` to implement `FromPyObjectBound<'_, '_>` - = note: required for `CancelHandle` to implement `PyFunctionArgument<'_, '_, '_, false>` - error[E0308]: mismatched types --> tests/ui/invalid_cancel_handle.rs:16:1 | @@ -105,3 +79,29 @@ note: required by a bound in `extract_argument` ... | T: PyFunctionArgument<'a, 'holder, 'py, IS_OPTION>, | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `extract_argument` + +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 `PyClass` is not implemented for `CancelHandle` + | + = 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<'_, '_, '_, false>` + +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, 'holder, 'py, IS_OPTION>`: + `&'a pyo3::Bound<'py, T>` implements `PyFunctionArgument<'a, 'holder, 'py, false>` + `&'holder mut pyo3::coroutine::Coroutine` implements `PyFunctionArgument<'a, 'holder, 'py, false>` + `&'holder pyo3::coroutine::Coroutine` implements `PyFunctionArgument<'a, 'holder, 'py, false>` + `Option` implements `PyFunctionArgument<'a, 'holder, 'py, true>` + = note: required for `CancelHandle` to implement `FromPyObject<'_>` + = note: required for `CancelHandle` to implement `FromPyObjectBound<'_, '_>` + = note: required for `CancelHandle` to implement `PyFunctionArgument<'_, '_, '_, false>` diff --git a/tests/ui/invalid_pyclass_doc.rs b/tests/ui/invalid_pyclass_doc.rs new file mode 100644 index 00000000000..55122aa29db --- /dev/null +++ b/tests/ui/invalid_pyclass_doc.rs @@ -0,0 +1,7 @@ +use pyo3::prelude::*; + +#[doc = "This \0 contains a nul byte!"] +#[pyclass] +struct InvalidDocWithNulByte {} + +fn main() {} diff --git a/tests/ui/invalid_pyclass_doc.stderr b/tests/ui/invalid_pyclass_doc.stderr new file mode 100644 index 00000000000..7341125f85b --- /dev/null +++ b/tests/ui/invalid_pyclass_doc.stderr @@ -0,0 +1,5 @@ +error: Python doc may not contain nul byte, found nul at position 5 + --> tests/ui/invalid_pyclass_doc.rs:3:9 + | +3 | #[doc = "This \0 contains a nul byte!"] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/ui/invalid_pymethods_duplicates.stderr b/tests/ui/invalid_pymethods_duplicates.stderr index a68d01cf5fb..215bc0640cd 100644 --- a/tests/ui/invalid_pymethods_duplicates.stderr +++ b/tests/ui/invalid_pymethods_duplicates.stderr @@ -1,11 +1,11 @@ -error[E0119]: conflicting implementations of trait `pyo3::impl_::pyclass::PyClassNewTextSignature` for type `pyo3::impl_::pyclass::PyClassImplCollector` +error[E0119]: conflicting implementations of trait `PyClassNewTextSignature` for type `TwoNew` --> tests/ui/invalid_pymethods_duplicates.rs:9:1 | 9 | #[pymethods] | ^^^^^^^^^^^^ | | | first implementation here - | conflicting implementation for `pyo3::impl_::pyclass::PyClassImplCollector` + | conflicting implementation for `TwoNew` | = note: this error originates in the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info) From 1c40611086b27c8610bd268fd14a4cc5f2325495 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Aug 2025 06:24:32 +0100 Subject: [PATCH 788/936] build(deps): bump actions/download-artifact from 4 to 5 (#5314) Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 5. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-version: '5' 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/netlify-deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/netlify-deploy.yml b/.github/workflows/netlify-deploy.yml index 27141d16f90..c7e2b97daf5 100644 --- a/.github/workflows/netlify-deploy.yml +++ b/.github/workflows/netlify-deploy.yml @@ -22,7 +22,7 @@ jobs: steps: - name: Download Build Artifact - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v5 with: name: site github-token: ${{ secrets.GITHUB_TOKEN }} From a202d8df9c595820aa4c8c347cee7e513f8bed81 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 12 Aug 2025 06:24:50 +0100 Subject: [PATCH 789/936] build(deps): bump actions/checkout from 4 to 5 (#5313) Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: '5' 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 +- .github/workflows/build.yml | 2 +- .github/workflows/changelog.yml | 2 +- .github/workflows/ci.yml | 30 +++++++++++++------------- .github/workflows/coverage-pr-base.yml | 2 +- .github/workflows/gh-pages.yml | 2 +- .github/workflows/netlify-build.yml | 2 +- .github/workflows/release.yaml | 2 +- 8 files changed, 22 insertions(+), 22 deletions(-) diff --git a/.github/workflows/benches.yml b/.github/workflows/benches.yml index 73005715515..6cc07900ee3 100644 --- a/.github/workflows/benches.yml +++ b/.github/workflows/benches.yml @@ -17,7 +17,7 @@ jobs: benchmarks: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-python@v5 with: python-version: '3.13' diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e99126fa6c5..2dda8fb57be 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,7 +29,7 @@ jobs: runs-on: ${{ inputs.os }} if: ${{ !(startsWith(inputs.python-version, 'graalpy') && startsWith(inputs.os, 'windows')) }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 with: # For PRs, we need to run on the real PR head, not the resultant merge of the PR into the target branch. # diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index 798a8dd06f9..c7cd9a6699a 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -9,7 +9,7 @@ jobs: name: Check changelog entry runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-python@v5 with: python-version: '3.13' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 20b96b654cb..c2d86de775e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: fmt: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-python@v5 with: python-version: "3.13" @@ -39,7 +39,7 @@ jobs: MSRV: ${{ steps.resolve-msrv.outputs.MSRV }} verbose: ${{ runner.debug == '1' }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-python@v5 with: python-version: "3.13" @@ -52,7 +52,7 @@ jobs: needs: [fmt] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-python@v5 with: python-version: "3.13" @@ -62,7 +62,7 @@ jobs: needs: [fmt, resolve] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@master with: toolchain: ${{ needs.resolve.outputs.MSRV }} @@ -112,7 +112,7 @@ jobs: name: clippy/${{ matrix.target }}/${{ matrix.rust }} continue-on-error: ${{ matrix.rust != 'stable' }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@master with: toolchain: ${{ matrix.rust }} @@ -387,7 +387,7 @@ jobs: needs: [fmt] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-python@v5 with: python-version: "3.13" @@ -408,7 +408,7 @@ jobs: needs: [fmt] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-python@v5 with: python-version: "3.13" @@ -430,7 +430,7 @@ jobs: needs: [fmt] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-python@v5 with: python-version: "3.13" @@ -448,7 +448,7 @@ jobs: needs: [fmt] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-python@v5 with: # TODO bump emscripten builds to test on 3.13 @@ -489,7 +489,7 @@ jobs: needs: [fmt] runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'CI-save-pr-cache') }} @@ -533,7 +533,7 @@ jobs: if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || github.event_name != 'pull_request' }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-python@v5 with: python-version: "3.13" @@ -557,7 +557,7 @@ jobs: include: - rust: ${{ needs.resolve.outputs.MSRV }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-python@v5 with: python-version: "3.13" @@ -608,7 +608,7 @@ jobs: target: "aarch64-pc-windows-msvc" flags: "-i python3.13 --features generate-import-lib" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-python@v5 with: python-version: "3.13" @@ -638,7 +638,7 @@ jobs: if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || github.event_name != 'pull_request' }} runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-python@v5 with: python-version: "3.13" @@ -702,7 +702,7 @@ jobs: ] runs-on: ${{ matrix.platform.os }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.platform.rust-target }} diff --git a/.github/workflows/coverage-pr-base.yml b/.github/workflows/coverage-pr-base.yml index 907baa01ed2..38417ab8e55 100644 --- a/.github/workflows/coverage-pr-base.yml +++ b/.github/workflows/coverage-pr-base.yml @@ -12,7 +12,7 @@ jobs: coverage-pr-base: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-python@v5 with: python-version: '3.13' diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index 397b2f36a0d..2e96ea8eb40 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -21,7 +21,7 @@ jobs: outputs: tag_name: ${{ steps.prepare_tag.outputs.tag_name }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-python@v5 with: python-version: '3.13' diff --git a/.github/workflows/netlify-build.yml b/.github/workflows/netlify-build.yml index fa815fc10e0..f166352c882 100644 --- a/.github/workflows/netlify-build.yml +++ b/.github/workflows/netlify-build.yml @@ -21,7 +21,7 @@ jobs: outputs: tag_name: ${{ steps.prepare_tag.outputs.tag_name }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-python@v5 with: python-version: "3.13" diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 3da11abdf7f..dd4cf1fd196 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -11,7 +11,7 @@ jobs: environment: release steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@v5 - uses: actions/setup-python@v5 with: From 584b8741ced5ff3546cb4fbe05db14d4769601de Mon Sep 17 00:00:00 2001 From: Thomas Tanon Date: Wed, 13 Aug 2025 20:51:46 +0200 Subject: [PATCH 790/936] Drop const_concat! macro and str-focused const combine functions (#5315) * Drop const_concat! macro and str-focused const combine functions Follow up to #5286 * Makes combine not `pub` --- pyo3-macros-backend/src/introspection.rs | 11 +- pyo3-macros-backend/src/pyclass.rs | 4 +- src/impl_/concat.rs | 139 ++--------------------- src/impl_/pyclass/doc.rs | 6 +- 4 files changed, 24 insertions(+), 136 deletions(-) diff --git a/pyo3-macros-backend/src/introspection.rs b/pyo3-macros-backend/src/introspection.rs index 4d7507129a6..7e0348eba32 100644 --- a/pyo3-macros-backend/src/introspection.rs +++ b/pyo3-macros-backend/src/introspection.rs @@ -363,7 +363,7 @@ impl IntrospectionNode<'_> { const _: () = { #[used] #[no_mangle] - static #static_name: &'static str = #content; + static #static_name: &'static [u8] = #content; }; } } @@ -485,11 +485,16 @@ impl ConcatenationBuilder { if let [ConcatenationBuilderElement::String(string)] = elements.as_slice() { // We avoid the const_concat! macro if there is only a single string - return string.to_token_stream(); + return quote! { #string.as_bytes() }; } quote! { - #pyo3_crate_path::impl_::concat::const_concat!(#(#elements , )*) + { + const PIECES: &[&[u8]] = &[#(#elements.as_bytes() , )*]; + &#pyo3_crate_path::impl_::concat::combine_to_array::<{ + #pyo3_crate_path::impl_::concat::combined_len(PIECES) + }>(PIECES) + } } } } diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 31e398ef08b..f0803172ddb 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -2396,8 +2396,8 @@ impl<'a> PyClassImplsBuilder<'a> { #cls, { impl_::pyclass::HasNewTextSignature::<#cls>::VALUE } >::DOC_PIECES; - const LEN: usize = impl_::concat::combined_len_bytes(DOC_PIECES); - const DOC: &'static [u8] = &impl_::concat::combine_bytes_to_array::(DOC_PIECES); + const LEN: usize = impl_::concat::combined_len(DOC_PIECES); + const DOC: &'static [u8] = &impl_::concat::combine_to_array::(DOC_PIECES); impl_::pyclass::doc::doc_bytes_as_cstr(DOC) }; diff --git a/src/impl_/concat.rs b/src/impl_/concat.rs index 942e406cf9f..245a8b63aeb 100644 --- a/src/impl_/concat.rs +++ b/src/impl_/concat.rs @@ -1,86 +1,8 @@ -/// `concat!` but working with constants -#[macro_export] -#[doc(hidden)] -macro_rules! const_concat { - ($e:expr) => {{ - $e - }}; - ($l:expr, $($r:expr),+ $(,)?) => {{ - const PIECES: &[&str] = &[$l, $($r),+]; - const RAW_BYTES: &[u8] = &$crate::impl_::concat::combine_to_array::<{ - $crate::impl_::concat::combined_len(PIECES) - }>(PIECES); - // Safety: `RAW_BYTES` is combined from valid &str pieces - unsafe { ::std::str::from_utf8_unchecked(RAW_BYTES) } - }} -} - -pub use const_concat; - -/// Calculates the total byte length of all string pieces in the array. -/// -/// This is a useful utility in order to determine the size needed for the constant -/// `combine` function. -pub const fn combined_len(pieces: &[&str]) -> usize { - let mut len = 0; - let mut pieces_idx = 0; - while pieces_idx < pieces.len() { - len += pieces[pieces_idx].len(); - pieces_idx += 1; - } - len -} - -/// Combines all string pieces into a single byte array. -/// -/// `out` should be a buffer at the correct size of `combined_len(pieces)`, else this will panic. -#[cfg(mut_ref_in_const_fn)] // requires MSRV 1.83 -#[allow(clippy::incompatible_msrv)] // `split_at_mut` also requires MSRV 1.83 -pub const fn combine(pieces: &[&str], mut out: &mut [u8]) { - let mut pieces_idx = 0; - while pieces_idx < pieces.len() { - let piece = pieces[pieces_idx].as_bytes(); - slice_copy_from_slice(out, piece); - // using split_at_mut because range indexing not yet supported in const fn - out = out.split_at_mut(piece.len()).1; - pieces_idx += 1; - } - // should be no trailing buffer - assert!(out.is_empty(), "output buffer too large"); -} - -/// Wrapper around combine which has a const generic parameter, this is going to be more codegen -/// at compile time (?) -/// -/// Unfortunately the `&mut [u8]` buffer needs MSRV 1.83 -pub const fn combine_to_array(pieces: &[&str]) -> [u8; LEN] { - let mut out: [u8; LEN] = [0u8; LEN]; - #[cfg(mut_ref_in_const_fn)] - combine(pieces, &mut out); - #[cfg(not(mut_ref_in_const_fn))] // inlined here for higher code - { - let mut out_idx = 0; - let mut pieces_idx = 0; - while pieces_idx < pieces.len() { - let piece = pieces[pieces_idx].as_bytes(); - let mut piece_idx = 0; - while piece_idx < piece.len() { - out[out_idx] = piece[piece_idx]; - out_idx += 1; - piece_idx += 1; - } - pieces_idx += 1; - } - assert!(out_idx == out.len(), "output buffer too large"); - } - out -} - /// Calculates the total byte length of all byte pieces in the array. /// /// This is a useful utility in order to determine the size needed for the constant -/// `combine_bytes` function. -pub const fn combined_len_bytes(pieces: &[&[u8]]) -> usize { +/// `combine` function. +pub const fn combined_len(pieces: &[&[u8]]) -> usize { let mut len = 0; let mut pieces_idx = 0; while pieces_idx < pieces.len() { @@ -95,7 +17,7 @@ pub const fn combined_len_bytes(pieces: &[&[u8]]) -> usize { /// `out` should be a buffer at the correct size of `combined_len(pieces)`, else this will panic. #[cfg(mut_ref_in_const_fn)] // requires MSRV 1.83 #[allow(clippy::incompatible_msrv)] // `split_at_mut` also requires MSRV 1.83 -pub const fn combine_bytes(pieces: &[&[u8]], mut out: &mut [u8]) { +const fn combine(pieces: &[&[u8]], mut out: &mut [u8]) { let mut pieces_idx = 0; while pieces_idx < pieces.len() { let piece = pieces[pieces_idx]; @@ -108,14 +30,14 @@ pub const fn combine_bytes(pieces: &[&[u8]], mut out: &mut [u8]) { assert!(out.is_empty(), "output buffer too large"); } -/// Wrapper around `combine_bytes` which has a const generic parameter, this is going to be more codegen +/// Wrapper around `combine` which has a const generic parameter, this is going to be more codegen /// at compile time (?) /// /// Unfortunately the `&mut [u8]` buffer needs MSRV 1.83 -pub const fn combine_bytes_to_array(pieces: &[&[u8]]) -> [u8; LEN] { +pub const fn combine_to_array(pieces: &[&[u8]]) -> [u8; LEN] { let mut out: [u8; LEN] = [0u8; LEN]; #[cfg(mut_ref_in_const_fn)] - combine_bytes(pieces, &mut out); + combine(pieces, &mut out); #[cfg(not(mut_ref_in_const_fn))] // inlined here for higher code { let mut out_idx = 0; @@ -151,46 +73,23 @@ mod tests { #[test] fn test_combined_len() { - let pieces = ["foo", "bar", "baz"]; + let pieces: [&[u8]; 3] = [b"foo", b"bar", b"baz"]; assert_eq!(combined_len(&pieces), 9); - let empty: [&str; 0] = []; + let empty: [&[u8]; 0] = []; assert_eq!(combined_len(&empty), 0); } #[test] fn test_combine_to_array() { - let pieces = ["foo", "bar"]; - let combined = combine_to_array::<6>(&pieces); - assert_eq!(&combined, b"foobar"); - } - - #[test] - fn test_const_concat_macro() { - const RESULT: &str = const_concat!("foo", "bar", "baz"); - assert_eq!(RESULT, "foobarbaz"); - const SINGLE: &str = const_concat!("abc"); - assert_eq!(SINGLE, "abc"); - } - - #[test] - fn test_combined_len_bytes() { - let pieces: [&[u8]; 3] = [b"foo", b"bar", b"baz"]; - assert_eq!(combined_len_bytes(&pieces), 9); - let empty: [&[u8]; 0] = []; - assert_eq!(combined_len_bytes(&empty), 0); - } - - #[test] - fn test_combine_bytes_to_array() { let pieces: [&[u8]; 2] = [b"foo", b"bar"]; - let combined = combine_bytes_to_array::<6>(&pieces); + let combined = combine_to_array::<6>(&pieces); assert_eq!(&combined, b"foobar"); } #[test] #[should_panic(expected = "index out of bounds")] fn test_combine_to_array_buffer_too_small() { - let pieces = ["foo", "bar"]; + let pieces: [&[u8]; 2] = [b"foo", b"bar"]; // Intentionally wrong length let _ = combine_to_array::<5>(&pieces); } @@ -198,24 +97,8 @@ mod tests { #[test] #[should_panic(expected = "output buffer too large")] fn test_combine_to_array_buffer_too_big() { - let pieces = ["foo", "bar"]; - // Intentionally wrong length - let _ = combine_to_array::<10>(&pieces); - } - - #[test] - #[should_panic(expected = "index out of bounds")] - fn test_combine_bytes_to_array_buffer_too_small() { - let pieces: [&[u8]; 2] = [b"foo", b"bar"]; - // Intentionally wrong length - let _ = combine_bytes_to_array::<5>(&pieces); - } - - #[test] - #[should_panic(expected = "output buffer too large")] - fn test_combine_bytes_to_array_buffer_too_big() { let pieces: [&[u8]; 2] = [b"foo", b"bar"]; // Intentionally wrong length - let _ = combine_bytes_to_array::<10>(&pieces); + let _ = combine_to_array::<10>(&pieces); } } diff --git a/src/impl_/pyclass/doc.rs b/src/impl_/pyclass/doc.rs index 5d6d8120692..616800e3fdd 100644 --- a/src/impl_/pyclass/doc.rs +++ b/src/impl_/pyclass/doc.rs @@ -68,7 +68,7 @@ mod tests { #[test] #[cfg(feature = "macros")] fn test_doc_generator() { - use crate::impl_::concat::{combine_bytes_to_array, combined_len_bytes}; + use crate::impl_::concat::{combine_to_array, combined_len}; /// A dummy class with signature. #[crate::pyclass(crate = "crate")] @@ -86,7 +86,7 @@ mod tests { // simulate what the macro is doing const PIECES: &[&[u8]] = PyClassDocGenerator::::DOC_PIECES; assert_eq!( - &combine_bytes_to_array::<{ combined_len_bytes(PIECES) }>(PIECES), + &combine_to_array::<{ combined_len(PIECES) }>(PIECES), b"MyClass(x, y)\n--\n\nA dummy class with signature.\0" ); @@ -94,7 +94,7 @@ mod tests { const PIECES_WITHOUT_SIGNATURE: &[&[u8]] = PyClassDocGenerator::::DOC_PIECES; assert_eq!( - &combine_bytes_to_array::<{ combined_len_bytes(PIECES_WITHOUT_SIGNATURE) }>( + &combine_to_array::<{ combined_len(PIECES_WITHOUT_SIGNATURE) }>( PIECES_WITHOUT_SIGNATURE ), b"A dummy class with signature.\0" From daa8575e80c35e699aed931de48b6708303d8c00 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 15 Aug 2025 19:49:44 +0100 Subject: [PATCH 791/936] ci: pin nightly to 2025-08-14 (#5321) --- .github/workflows/ci.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c2d86de775e..8aecae55b64 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -415,9 +415,11 @@ jobs: - uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'CI-save-pr-cache') }} - - uses: dtolnay/rust-toolchain@nightly + - uses: dtolnay/rust-toolchain@master with: components: rust-src + # FIXME: unpin once https://github.com/rust-lang/rust/issues/145437 is resolved + toolchain: nightly-2025-08-14 - 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 @@ -437,9 +439,11 @@ jobs: - uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'CI-save-pr-cache') }} - - uses: dtolnay/rust-toolchain@nightly + - uses: dtolnay/rust-toolchain@master with: components: rust-src + # FIXME: unpin once https://github.com/rust-lang/rust/issues/145437 is resolved + toolchain: nightly-2025-08-14 - run: cargo rustdoc --lib --no-default-features --features full,jiff-02 -Zunstable-options --config "build.rustdocflags=[\"--cfg\", \"docsrs\"]" emscripten: From 2034bf3614e8a6071d3e91acc198be7f69127948 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 15 Aug 2025 20:40:40 +0100 Subject: [PATCH 792/936] ci: merge `gh-pages` job into `netlify-build` job (#5322) --- .github/workflows/gh-pages.yml | 66 ----------------------------- .github/workflows/netlify-build.yml | 39 +++++++++++------ 2 files changed, 27 insertions(+), 78 deletions(-) delete mode 100644 .github/workflows/gh-pages.yml diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml deleted file mode 100644 index 2e96ea8eb40..00000000000 --- a/.github/workflows/gh-pages.yml +++ /dev/null @@ -1,66 +0,0 @@ -name: gh-pages - -on: - push: - branches: - - main - pull_request: - release: - types: [published] - -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} - cancel-in-progress: true - -env: - CARGO_TERM_COLOR: always - -jobs: - guide-build: - runs-on: ubuntu-latest - outputs: - tag_name: ${{ steps.prepare_tag.outputs.tag_name }} - steps: - - uses: actions/checkout@v5 - - uses: actions/setup-python@v5 - with: - python-version: '3.13' - - uses: dtolnay/rust-toolchain@nightly - - - name: Setup mdBook - uses: taiki-e/install-action@v2 - with: - tool: mdbook,mdbook-tabs - - - name: Link Checker - id: lychee - uses: lycheeverse/lychee-action@v2 - with: - # setup lychee but don't run it for now - args: --version - lycheeVersion: nightly - - - name: Prepare tag - id: prepare_tag - run: | - TAG_NAME="${GITHUB_REF##*/}" - echo "::set-output name=tag_name::${TAG_NAME}" - - # This builds the book in target/guide/. - - name: Build the guide - run: | - python -m pip install --upgrade pip && pip install nox - nox -s ${{ github.event_name == 'release' && 'build-guide' || 'check-guide' }} - env: - PYO3_VERSION_TAG: ${{ steps.prepare_tag.outputs.tag_name }} - # allows lychee to get better rate limits from github - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Deploy docs and the guide - if: ${{ github.event_name == 'release' }} - uses: peaceiris/actions-gh-pages@v4 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./target/guide/ - destination_dir: ${{ steps.prepare_tag.outputs.tag_name }} - full_commit_message: "Upload documentation for ${{ steps.prepare_tag.outputs.tag_name }}" diff --git a/.github/workflows/netlify-build.yml b/.github/workflows/netlify-build.yml index f166352c882..19411af3ed5 100644 --- a/.github/workflows/netlify-build.yml +++ b/.github/workflows/netlify-build.yml @@ -28,11 +28,6 @@ jobs: - uses: dtolnay/rust-toolchain@nightly - - name: Use nightly - run: | - rustup update nightly - rustup default nightly - - name: Setup mdBook uses: taiki-e/install-action@v2 with: @@ -46,19 +41,39 @@ jobs: args: --version lycheeVersion: nightly - - name: Get current PyO3 version - run: | - PYO3_VERSION=$(cargo search pyo3 --limit 1 | head -1 | tr -s ' ' | cut -d ' ' -f 3 | tr -d '"') - echo "PYO3_VERSION=${PYO3_VERSION}" >> $GITHUB_ENV - - name: Prepare tag id: prepare_tag run: | TAG_NAME="${GITHUB_REF##*/}" echo "tag_name=${TAG_NAME}" >> $GITHUB_OUTPUT - # Build the site - - name: Prepare the versioned guide entries + # This builds the book in target/guide/. + - name: Build the guide + run: | + python -m pip install --upgrade pip && pip install nox + nox -s ${{ github.event_name == 'release' && 'build-guide' || 'check-guide' }} + env: + PYO3_VERSION_TAG: ${{ steps.prepare_tag.outputs.tag_name }} + # allows lychee to get better rate limits from github + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # We store the versioned guides on GitHub's gh-pages branch for convenience + # (the full gh-pages branch is pulled in the build-netlify-site step) + - name: Deploy the guide + if: ${{ github.event_name == 'release' }} + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./target/guide/ + destination_dir: ${{ steps.prepare_tag.outputs.tag_name }} + full_commit_message: "Upload documentation for ${{ steps.prepare_tag.outputs.tag_name }}" + + - name: Get current PyO3 version + run: | + PYO3_VERSION=$(cargo search pyo3 --limit 1 | head -1 | tr -s ' ' | cut -d ' ' -f 3 | tr -d '"') + echo "PYO3_VERSION=${PYO3_VERSION}" >> $GITHUB_ENV + + - name: Build the site run: | python -m pip install --upgrade pip && pip install nox towncrier requests nox -s build-netlify-site -- ${{ (github.ref != 'refs/heads/main' && '--preview') || '' }} From c218a97f139929c59c752fa29344ff9db4811d92 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 15 Aug 2025 22:00:59 +0100 Subject: [PATCH 793/936] use types from `std::ffi` instead of `std::os::raw` (#5323) --- guide/src/class/numeric.md | 2 +- guide/src/migration.md | 4 +- pyo3-ffi/README.md | 2 +- pyo3-ffi/examples/sequential/src/id.rs | 2 +- pyo3-ffi/examples/sequential/src/module.rs | 2 +- pyo3-ffi/examples/string-sum/src/lib.rs | 2 +- pyo3-ffi/src/abstract_.rs | 6 +- pyo3-ffi/src/boolobject.rs | 2 +- pyo3-ffi/src/bytearrayobject.rs | 2 +- pyo3-ffi/src/bytesobject.rs | 2 +- pyo3-ffi/src/ceval.rs | 2 +- pyo3-ffi/src/codecs.rs | 2 +- pyo3-ffi/src/compat/py_3_10.rs | 4 +- pyo3-ffi/src/compat/py_3_13.rs | 14 ++-- pyo3-ffi/src/compat/py_3_14.rs | 2 +- pyo3-ffi/src/compile.rs | 2 +- pyo3-ffi/src/complexobject.rs | 2 +- pyo3-ffi/src/context.rs | 2 +- pyo3-ffi/src/cpython/abstract_.rs | 12 ++-- pyo3-ffi/src/cpython/bytesobject.rs | 4 +- pyo3-ffi/src/cpython/ceval.rs | 2 +- pyo3-ffi/src/cpython/code.rs | 4 +- pyo3-ffi/src/cpython/compile.rs | 4 +- pyo3-ffi/src/cpython/complexobject.rs | 2 +- pyo3-ffi/src/cpython/descrobject.rs | 2 +- pyo3-ffi/src/cpython/dictobject.rs | 2 +- pyo3-ffi/src/cpython/floatobject.rs | 2 +- pyo3-ffi/src/cpython/frameobject.rs | 4 +- pyo3-ffi/src/cpython/funcobject.rs | 2 +- pyo3-ffi/src/cpython/genobject.rs | 4 +- pyo3-ffi/src/cpython/import.rs | 4 +- pyo3-ffi/src/cpython/initconfig.rs | 2 +- pyo3-ffi/src/cpython/longobject.rs | 4 +- pyo3-ffi/src/cpython/methodobject.rs | 2 +- pyo3-ffi/src/cpython/object.rs | 8 +-- pyo3-ffi/src/cpython/objimpl.rs | 4 +- pyo3-ffi/src/cpython/pydebug.rs | 2 +- pyo3-ffi/src/cpython/pyframe.rs | 4 +- pyo3-ffi/src/cpython/pyhash.rs | 4 +- pyo3-ffi/src/cpython/pylifecycle.rs | 2 +- pyo3-ffi/src/cpython/pymem.rs | 2 +- pyo3-ffi/src/cpython/pystate.rs | 2 +- pyo3-ffi/src/cpython/pythonrun.rs | 2 +- pyo3-ffi/src/cpython/unicodeobject.rs | 2 +- pyo3-ffi/src/datetime.rs | 6 +- pyo3-ffi/src/descrobject.rs | 2 +- pyo3-ffi/src/dictobject.rs | 2 +- pyo3-ffi/src/fileobject.rs | 2 +- pyo3-ffi/src/fileutils.rs | 2 +- pyo3-ffi/src/floatobject.rs | 2 +- pyo3-ffi/src/impl_/mod.rs | 2 +- pyo3-ffi/src/import.rs | 2 +- pyo3-ffi/src/intrcheck.rs | 2 +- pyo3-ffi/src/iterobject.rs | 2 +- pyo3-ffi/src/lib.rs | 2 +- pyo3-ffi/src/listobject.rs | 2 +- pyo3-ffi/src/longobject.rs | 2 +- pyo3-ffi/src/marshal.rs | 2 +- pyo3-ffi/src/memoryobject.rs | 2 +- pyo3-ffi/src/methodobject.rs | 2 +- pyo3-ffi/src/modsupport.rs | 2 +- pyo3-ffi/src/moduleobject.rs | 2 +- pyo3-ffi/src/object.rs | 2 +- pyo3-ffi/src/objimpl.rs | 2 +- pyo3-ffi/src/pybuffer.rs | 2 +- pyo3-ffi/src/pycapsule.rs | 2 +- pyo3-ffi/src/pyerrors.rs | 2 +- pyo3-ffi/src/pyframe.rs | 2 +- pyo3-ffi/src/pyhash.rs | 4 +- pyo3-ffi/src/pylifecycle.rs | 2 +- pyo3-ffi/src/pymem.rs | 2 +- pyo3-ffi/src/pyport.rs | 2 +- pyo3-ffi/src/pystate.rs | 4 +- pyo3-ffi/src/pystrtod.rs | 2 +- pyo3-ffi/src/pythonrun.rs | 6 +- pyo3-ffi/src/rangeobject.rs | 2 +- pyo3-ffi/src/refcount.rs | 10 +-- pyo3-ffi/src/setobject.rs | 2 +- pyo3-ffi/src/sliceobject.rs | 2 +- pyo3-ffi/src/structmember.rs | 2 +- pyo3-ffi/src/structseq.rs | 2 +- pyo3-ffi/src/sysmodule.rs | 2 +- pyo3-ffi/src/traceback.rs | 2 +- pyo3-ffi/src/tupleobject.rs | 2 +- pyo3-ffi/src/typeslots.rs | 2 +- pyo3-ffi/src/unicodeobject.rs | 2 +- pyo3-ffi/src/warnings.rs | 2 +- pyo3-ffi/src/weakrefobject.rs | 2 +- pyo3-macros-backend/src/pymethod.rs | 10 +-- src/buffer.rs | 74 +++++++++++----------- src/conversions/num_complex.rs | 2 +- src/conversions/std/num.rs | 2 +- src/impl_/callback.rs | 12 ++-- src/impl_/pyclass.rs | 4 +- src/impl_/pymethods.rs | 2 +- src/internal/get_slot.rs | 22 +++---- src/marshal.rs | 2 +- src/pyclass/gc.rs | 2 +- src/types/any.rs | 2 +- src/types/bytes.rs | 4 +- src/types/capsule.rs | 6 +- src/types/complex.rs | 2 +- src/types/datetime.rs | 2 +- src/types/dict.rs | 4 +- src/types/float.rs | 2 +- src/types/mappingproxy.rs | 2 +- src/types/module.rs | 4 +- src/types/weakref/anyref.rs | 4 +- src/types/weakref/proxy.rs | 4 +- src/types/weakref/reference.rs | 4 +- tests/test_buffer_protocol.rs | 2 +- tests/test_gc.rs | 4 +- tests/test_pyfunction.rs | 2 +- tests/ui/invalid_pymethods_buffer.rs | 2 +- tests/ui/invalid_pymethods_buffer.stderr | 2 +- tests/ui/pyclass_send.rs | 2 +- 116 files changed, 220 insertions(+), 218 deletions(-) diff --git a/guide/src/class/numeric.md b/guide/src/class/numeric.md index c326e93b23e..b1c3cdf9954 100644 --- a/guide/src/class/numeric.md +++ b/guide/src/class/numeric.md @@ -417,7 +417,7 @@ Let's create that helper function. The signature has to be `fn(&Bound<'_, PyAny> ```rust,no_run # #![allow(dead_code)] -use std::os::raw::c_ulong; +use std::ffi::c_ulong; use pyo3::prelude::*; use pyo3::ffi; diff --git a/guide/src/migration.md b/guide/src/migration.md index 2085c58aca3..9306525cf11 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -1930,7 +1930,7 @@ There can be two fixes: #[pyclass] struct Unsendable { - pointers: Vec<*mut std::os::raw::c_char>, + pointers: Vec<*mut std::ffi::c_char>, } ``` @@ -1941,7 +1941,7 @@ There can be two fixes: #[pyclass(unsendable)] struct Unsendable { - pointers: Vec<*mut std::os::raw::c_char>, + pointers: Vec<*mut std::ffi::c_char>, } ```
diff --git a/pyo3-ffi/README.md b/pyo3-ffi/README.md index dc5e12827af..30623cc50f1 100644 --- a/pyo3-ffi/README.md +++ b/pyo3-ffi/README.md @@ -66,7 +66,7 @@ fn main() { **`src/lib.rs`** ```rust,no_run -use std::os::raw::{c_char, c_long}; +use std::ffi::{c_char, c_long}; use std::ptr; use pyo3_ffi::*; diff --git a/pyo3-ffi/examples/sequential/src/id.rs b/pyo3-ffi/examples/sequential/src/id.rs index fa72bb091c7..a20cc54b3b2 100644 --- a/pyo3-ffi/examples/sequential/src/id.rs +++ b/pyo3-ffi/examples/sequential/src/id.rs @@ -1,7 +1,7 @@ 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 std::ffi::{c_char, c_int, c_uint, c_ulonglong, c_void}; use pyo3_ffi::*; diff --git a/pyo3-ffi/examples/sequential/src/module.rs b/pyo3-ffi/examples/sequential/src/module.rs index baa7c66f206..3f95f8e8783 100644 --- a/pyo3-ffi/examples/sequential/src/module.rs +++ b/pyo3-ffi/examples/sequential/src/module.rs @@ -1,6 +1,6 @@ use core::{mem, ptr}; use pyo3_ffi::*; -use std::os::raw::{c_int, c_void}; +use std::ffi::{c_int, c_void}; pub static mut MODULE_DEF: PyModuleDef = PyModuleDef { m_base: PyModuleDef_HEAD_INIT, diff --git a/pyo3-ffi/examples/string-sum/src/lib.rs b/pyo3-ffi/examples/string-sum/src/lib.rs index 7af80bc08d7..94d2445af1d 100644 --- a/pyo3-ffi/examples/string-sum/src/lib.rs +++ b/pyo3-ffi/examples/string-sum/src/lib.rs @@ -1,4 +1,4 @@ -use std::os::raw::{c_char, c_long}; +use std::ffi::{c_char, c_long}; use std::ptr; use pyo3_ffi::*; diff --git a/pyo3-ffi/src/abstract_.rs b/pyo3-ffi/src/abstract_.rs index 59639d14de8..bf80ce2e56b 100644 --- a/pyo3-ffi/src/abstract_.rs +++ b/pyo3-ffi/src/abstract_.rs @@ -2,11 +2,11 @@ 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}; +use std::ffi::{c_char, c_int}; #[inline] #[cfg(all( - not(Py_3_13), // CPython exposed as a function in 3.13, in object.h + 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 { @@ -15,7 +15,7 @@ pub unsafe fn PyObject_DelAttrString(o: *mut PyObject, attr_name: *const c_char) #[inline] #[cfg(all( - not(Py_3_13), // CPython exposed as a function in 3.13, in object.h + 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 { diff --git a/pyo3-ffi/src/boolobject.rs b/pyo3-ffi/src/boolobject.rs index eec9da707a1..4d2ce70a600 100644 --- a/pyo3-ffi/src/boolobject.rs +++ b/pyo3-ffi/src/boolobject.rs @@ -1,7 +1,7 @@ #[cfg(not(GraalPy))] use crate::longobject::PyLongObject; use crate::object::*; -use std::os::raw::{c_int, c_long}; +use std::ffi::{c_int, c_long}; use std::ptr::addr_of_mut; #[inline] diff --git a/pyo3-ffi/src/bytearrayobject.rs b/pyo3-ffi/src/bytearrayobject.rs index d27dfa8b0ec..6729f6167c6 100644 --- a/pyo3-ffi/src/bytearrayobject.rs +++ b/pyo3-ffi/src/bytearrayobject.rs @@ -1,6 +1,6 @@ use crate::object::*; use crate::pyport::Py_ssize_t; -use std::os::raw::{c_char, c_int}; +use std::ffi::{c_char, c_int}; use std::ptr::addr_of_mut; #[cfg(not(any(PyPy, GraalPy, Py_LIMITED_API)))] diff --git a/pyo3-ffi/src/bytesobject.rs b/pyo3-ffi/src/bytesobject.rs index 0fd327b6fca..08351d18daa 100644 --- a/pyo3-ffi/src/bytesobject.rs +++ b/pyo3-ffi/src/bytesobject.rs @@ -1,6 +1,6 @@ use crate::object::*; use crate::pyport::Py_ssize_t; -use std::os::raw::{c_char, c_int}; +use std::ffi::{c_char, c_int}; use std::ptr::addr_of_mut; #[cfg_attr(windows, link(name = "pythonXY"))] diff --git a/pyo3-ffi/src/ceval.rs b/pyo3-ffi/src/ceval.rs index d1839a108cb..5746d574346 100644 --- a/pyo3-ffi/src/ceval.rs +++ b/pyo3-ffi/src/ceval.rs @@ -1,6 +1,6 @@ use crate::object::PyObject; use crate::pystate::PyThreadState; -use std::os::raw::{c_char, c_int, c_void}; +use std::ffi::{c_char, c_int, c_void}; extern "C" { #[cfg_attr(PyPy, link_name = "PyPyEval_EvalCode")] diff --git a/pyo3-ffi/src/codecs.rs b/pyo3-ffi/src/codecs.rs index 2fd214cbbfe..9e4c52c9348 100644 --- a/pyo3-ffi/src/codecs.rs +++ b/pyo3-ffi/src/codecs.rs @@ -1,5 +1,5 @@ use crate::object::PyObject; -use std::os::raw::{c_char, c_int}; +use std::ffi::{c_char, c_int}; extern "C" { pub fn PyCodec_Register(search_function: *mut PyObject) -> c_int; diff --git a/pyo3-ffi/src/compat/py_3_10.rs b/pyo3-ffi/src/compat/py_3_10.rs index d962fb3bc7e..b59eb8ee8e4 100644 --- a/pyo3-ffi/src/compat/py_3_10.rs +++ b/pyo3-ffi/src/compat/py_3_10.rs @@ -24,9 +24,9 @@ compat_function!( #[inline] pub unsafe fn PyModule_AddObjectRef( module: *mut crate::PyObject, - name: *const std::os::raw::c_char, + name: *const std::ffi::c_char, value: *mut crate::PyObject, - ) -> std::os::raw::c_int { + ) -> std::ffi::c_int { if value.is_null() && crate::PyErr_Occurred().is_null() { crate::PyErr_SetString( crate::PyExc_SystemError, diff --git a/pyo3-ffi/src/compat/py_3_13.rs b/pyo3-ffi/src/compat/py_3_13.rs index 96c90e7eb66..f158c843606 100644 --- a/pyo3-ffi/src/compat/py_3_13.rs +++ b/pyo3-ffi/src/compat/py_3_13.rs @@ -6,7 +6,7 @@ compat_function!( dp: *mut crate::PyObject, key: *mut crate::PyObject, result: *mut *mut crate::PyObject, - ) -> std::os::raw::c_int { + ) -> std::ffi::c_int { use crate::{compat::Py_NewRef, PyDict_GetItemWithError, PyErr_Occurred}; let item = PyDict_GetItemWithError(dp, key); @@ -43,7 +43,7 @@ compat_function!( #[inline] pub unsafe fn PyImport_AddModuleRef( - name: *const std::os::raw::c_char, + name: *const std::ffi::c_char, ) -> *mut crate::PyObject { use crate::{compat::Py_XNewRef, PyImport_AddModule}; @@ -58,7 +58,7 @@ compat_function!( pub unsafe fn PyWeakref_GetRef( reference: *mut crate::PyObject, pobj: *mut *mut crate::PyObject, - ) -> std::os::raw::c_int { + ) -> std::ffi::c_int { use crate::{ compat::Py_NewRef, PyErr_SetString, PyExc_TypeError, PyWeakref_Check, PyWeakref_GetObject, Py_None, @@ -91,7 +91,7 @@ compat_function!( pub unsafe fn PyList_Extend( list: *mut crate::PyObject, iterable: *mut crate::PyObject, - ) -> std::os::raw::c_int { + ) -> std::ffi::c_int { crate::PyList_SetSlice(list, crate::PY_SSIZE_T_MAX, crate::PY_SSIZE_T_MAX, iterable) } ); @@ -100,7 +100,7 @@ compat_function!( originally_defined_for(Py_3_13); #[inline] - pub unsafe fn PyList_Clear(list: *mut crate::PyObject) -> std::os::raw::c_int { + pub unsafe fn PyList_Clear(list: *mut crate::PyObject) -> std::ffi::c_int { crate::PyList_SetSlice(list, 0, crate::PY_SSIZE_T_MAX, std::ptr::null_mut()) } ); @@ -111,9 +111,9 @@ compat_function!( #[inline] pub unsafe fn PyModule_Add( module: *mut crate::PyObject, - name: *const std::os::raw::c_char, + name: *const std::ffi::c_char, value: *mut crate::PyObject, - ) -> std::os::raw::c_int { + ) -> std::ffi::c_int { let result = crate::compat::PyModule_AddObjectRef(module, name, value); crate::Py_XDECREF(value); result diff --git a/pyo3-ffi/src/compat/py_3_14.rs b/pyo3-ffi/src/compat/py_3_14.rs index 6fdaef17488..859d26ea777 100644 --- a/pyo3-ffi/src/compat/py_3_14.rs +++ b/pyo3-ffi/src/compat/py_3_14.rs @@ -13,7 +13,7 @@ compat_function!( #[cfg(any(Py_LIMITED_API, PyPy))] { - let bytes = crate::PyBytes_FromStringAndSize(ptr as *const std::os::raw::c_char, len); + let bytes = crate::PyBytes_FromStringAndSize(ptr as *const std::ffi::c_char, len); if bytes.is_null() { -1 } else { diff --git a/pyo3-ffi/src/compile.rs b/pyo3-ffi/src/compile.rs index 189a1a8b8a4..50128a815d0 100644 --- a/pyo3-ffi/src/compile.rs +++ b/pyo3-ffi/src/compile.rs @@ -1,4 +1,4 @@ -use std::os::raw::c_int; +use std::ffi::c_int; pub const Py_single_input: c_int = 256; pub const Py_file_input: c_int = 257; diff --git a/pyo3-ffi/src/complexobject.rs b/pyo3-ffi/src/complexobject.rs index 283bacf6e84..a8920765424 100644 --- a/pyo3-ffi/src/complexobject.rs +++ b/pyo3-ffi/src/complexobject.rs @@ -1,5 +1,5 @@ use crate::object::*; -use std::os::raw::{c_double, c_int}; +use std::ffi::{c_double, c_int}; use std::ptr::addr_of_mut; #[cfg_attr(windows, link(name = "pythonXY"))] diff --git a/pyo3-ffi/src/context.rs b/pyo3-ffi/src/context.rs index 210d6e21f04..8f2f1576bb1 100644 --- a/pyo3-ffi/src/context.rs +++ b/pyo3-ffi/src/context.rs @@ -1,5 +1,5 @@ use crate::object::{PyObject, PyTypeObject, Py_TYPE}; -use std::os::raw::{c_char, c_int}; +use std::ffi::{c_char, c_int}; use std::ptr::addr_of_mut; extern "C" { diff --git a/pyo3-ffi/src/cpython/abstract_.rs b/pyo3-ffi/src/cpython/abstract_.rs index b9d9dd47dc9..53ef71e0710 100644 --- a/pyo3-ffi/src/cpython/abstract_.rs +++ b/pyo3-ffi/src/cpython/abstract_.rs @@ -1,7 +1,7 @@ use crate::{PyObject, Py_ssize_t}; #[cfg(any(all(Py_3_8, not(PyPy)), not(Py_3_11)))] -use std::os::raw::c_char; -use std::os::raw::c_int; +use std::ffi::c_char; +use std::ffi::c_int; #[cfg(not(Py_3_11))] use crate::Py_buffer; @@ -238,12 +238,12 @@ extern "C" { pub fn PyBuffer_GetPointer( view: *mut Py_buffer, indices: *mut Py_ssize_t, - ) -> *mut std::os::raw::c_void; + ) -> *mut std::ffi::c_void; #[cfg_attr(PyPy, link_name = "PyPyBuffer_SizeFromFormat")] pub fn PyBuffer_SizeFromFormat(format: *const c_char) -> Py_ssize_t; #[cfg_attr(PyPy, link_name = "PyPyBuffer_ToContiguous")] pub fn PyBuffer_ToContiguous( - buf: *mut std::os::raw::c_void, + buf: *mut std::ffi::c_void, view: *mut Py_buffer, len: Py_ssize_t, order: c_char, @@ -251,7 +251,7 @@ extern "C" { #[cfg_attr(PyPy, link_name = "PyPyBuffer_FromContiguous")] pub fn PyBuffer_FromContiguous( view: *mut Py_buffer, - buf: *mut std::os::raw::c_void, + buf: *mut std::ffi::c_void, len: Py_ssize_t, order: c_char, ) -> c_int; @@ -269,7 +269,7 @@ extern "C" { pub fn PyBuffer_FillInfo( view: *mut Py_buffer, o: *mut PyObject, - buf: *mut std::os::raw::c_void, + buf: *mut std::ffi::c_void, len: Py_ssize_t, readonly: c_int, flags: c_int, diff --git a/pyo3-ffi/src/cpython/bytesobject.rs b/pyo3-ffi/src/cpython/bytesobject.rs index 9e233311ac1..56344d35dd9 100644 --- a/pyo3-ffi/src/cpython/bytesobject.rs +++ b/pyo3-ffi/src/cpython/bytesobject.rs @@ -1,8 +1,8 @@ use crate::object::*; use crate::Py_ssize_t; #[cfg(not(Py_LIMITED_API))] -use std::os::raw::c_char; -use std::os::raw::c_int; +use std::ffi::c_char; +use std::ffi::c_int; #[cfg(not(any(PyPy, GraalPy, Py_LIMITED_API)))] #[repr(C)] diff --git a/pyo3-ffi/src/cpython/ceval.rs b/pyo3-ffi/src/cpython/ceval.rs index 6df10627d2e..cbec47195ec 100644 --- a/pyo3-ffi/src/cpython/ceval.rs +++ b/pyo3-ffi/src/cpython/ceval.rs @@ -1,6 +1,6 @@ use crate::cpython::pystate::Py_tracefunc; use crate::object::{freefunc, PyObject}; -use std::os::raw::c_int; +use std::ffi::c_int; extern "C" { // skipped non-limited _PyEval_CallTracing diff --git a/pyo3-ffi/src/cpython/code.rs b/pyo3-ffi/src/cpython/code.rs index 56ae0c5083d..aa02a80adb9 100644 --- a/pyo3-ffi/src/cpython/code.rs +++ b/pyo3-ffi/src/cpython/code.rs @@ -2,8 +2,8 @@ use crate::object::*; use crate::pyport::Py_ssize_t; #[cfg(not(GraalPy))] -use std::os::raw::c_char; -use std::os::raw::{c_int, c_void}; +use std::ffi::c_char; +use std::ffi::{c_int, c_void}; #[cfg(not(PyPy))] use std::ptr::addr_of_mut; diff --git a/pyo3-ffi/src/cpython/compile.rs b/pyo3-ffi/src/cpython/compile.rs index 078b5e0d675..df0b982b147 100644 --- a/pyo3-ffi/src/cpython/compile.rs +++ b/pyo3-ffi/src/cpython/compile.rs @@ -8,8 +8,8 @@ use crate::pythonrun::*; use crate::PyCodeObject; use crate::INT_MAX; #[cfg(not(any(PyPy, Py_3_10)))] -use std::os::raw::c_char; -use std::os::raw::c_int; +use std::ffi::c_char; +use std::ffi::c_int; // skipped PyCF_MASK // skipped PyCF_MASK_OBSOLETE diff --git a/pyo3-ffi/src/cpython/complexobject.rs b/pyo3-ffi/src/cpython/complexobject.rs index 4cc86db5667..3283fc4e52f 100644 --- a/pyo3-ffi/src/cpython/complexobject.rs +++ b/pyo3-ffi/src/cpython/complexobject.rs @@ -1,5 +1,5 @@ use crate::PyObject; -use std::os::raw::c_double; +use std::ffi::c_double; #[repr(C)] #[derive(Copy, Clone)] diff --git a/pyo3-ffi/src/cpython/descrobject.rs b/pyo3-ffi/src/cpython/descrobject.rs index 7cef9bdbf42..03d64789fa7 100644 --- a/pyo3-ffi/src/cpython/descrobject.rs +++ b/pyo3-ffi/src/cpython/descrobject.rs @@ -1,5 +1,5 @@ use crate::{PyGetSetDef, PyMethodDef, PyObject, PyTypeObject}; -use std::os::raw::{c_char, c_int, c_void}; +use std::ffi::{c_char, c_int, c_void}; pub type wrapperfunc = Option< unsafe extern "C" fn( diff --git a/pyo3-ffi/src/cpython/dictobject.rs b/pyo3-ffi/src/cpython/dictobject.rs index 34b66a9699d..0ee90b53fc6 100644 --- a/pyo3-ffi/src/cpython/dictobject.rs +++ b/pyo3-ffi/src/cpython/dictobject.rs @@ -1,6 +1,6 @@ use crate::object::*; use crate::pyport::Py_ssize_t; -use std::os::raw::c_int; +use std::ffi::c_int; opaque_struct!(pub PyDictKeysObject); diff --git a/pyo3-ffi/src/cpython/floatobject.rs b/pyo3-ffi/src/cpython/floatobject.rs index e7caa441c5d..4b9ef1a2484 100644 --- a/pyo3-ffi/src/cpython/floatobject.rs +++ b/pyo3-ffi/src/cpython/floatobject.rs @@ -1,7 +1,7 @@ #[cfg(GraalPy)] use crate::PyFloat_AsDouble; use crate::{PyFloat_Check, PyObject}; -use std::os::raw::c_double; +use std::ffi::c_double; #[repr(C)] pub struct PyFloatObject { diff --git a/pyo3-ffi/src/cpython/frameobject.rs b/pyo3-ffi/src/cpython/frameobject.rs index 99ab63b9254..5bd9a620f19 100644 --- a/pyo3-ffi/src/cpython/frameobject.rs +++ b/pyo3-ffi/src/cpython/frameobject.rs @@ -6,8 +6,8 @@ use crate::object::*; use crate::pystate::PyThreadState; use crate::PyFrameObject; #[cfg(not(any(PyPy, GraalPy, Py_3_11)))] -use std::os::raw::c_char; -use std::os::raw::c_int; +use std::ffi::c_char; +use std::ffi::c_int; #[cfg(not(any(PyPy, GraalPy, Py_3_11)))] pub type PyFrameState = c_char; diff --git a/pyo3-ffi/src/cpython/funcobject.rs b/pyo3-ffi/src/cpython/funcobject.rs index cd2052de174..7a242e85abd 100644 --- a/pyo3-ffi/src/cpython/funcobject.rs +++ b/pyo3-ffi/src/cpython/funcobject.rs @@ -1,4 +1,4 @@ -use std::os::raw::c_int; +use std::ffi::c_int; #[cfg(not(all(PyPy, not(Py_3_8))))] use std::ptr::addr_of_mut; diff --git a/pyo3-ffi/src/cpython/genobject.rs b/pyo3-ffi/src/cpython/genobject.rs index 05ee6f3f652..680a261c0de 100644 --- a/pyo3-ffi/src/cpython/genobject.rs +++ b/pyo3-ffi/src/cpython/genobject.rs @@ -1,8 +1,8 @@ use crate::object::*; use crate::PyFrameObject; #[cfg(all(Py_3_11, not(any(PyPy, GraalPy, Py_3_14))))] -use std::os::raw::c_char; -use std::os::raw::c_int; +use std::ffi::c_char; +use std::ffi::c_int; use std::ptr::addr_of_mut; #[cfg(not(any(PyPy, GraalPy, Py_3_14)))] diff --git a/pyo3-ffi/src/cpython/import.rs b/pyo3-ffi/src/cpython/import.rs index c8ef5ab487e..3b9374f68e3 100644 --- a/pyo3-ffi/src/cpython/import.rs +++ b/pyo3-ffi/src/cpython/import.rs @@ -1,7 +1,7 @@ use crate::{PyInterpreterState, PyObject}; #[cfg(not(PyPy))] -use std::os::raw::c_uchar; -use std::os::raw::{c_char, c_int}; +use std::ffi::c_uchar; +use std::ffi::{c_char, c_int}; // skipped PyInit__imp diff --git a/pyo3-ffi/src/cpython/initconfig.rs b/pyo3-ffi/src/cpython/initconfig.rs index 076981f1485..6b0ae2e5dec 100644 --- a/pyo3-ffi/src/cpython/initconfig.rs +++ b/pyo3-ffi/src/cpython/initconfig.rs @@ -2,7 +2,7 @@ use crate::Py_ssize_t; use libc::wchar_t; -use std::os::raw::{c_char, c_int, c_ulong}; +use std::ffi::{c_char, c_int, c_ulong}; #[repr(C)] #[derive(Copy, Clone, Debug, PartialEq, Eq)] diff --git a/pyo3-ffi/src/cpython/longobject.rs b/pyo3-ffi/src/cpython/longobject.rs index 45acaae577d..a778d8c26d2 100644 --- a/pyo3-ffi/src/cpython/longobject.rs +++ b/pyo3-ffi/src/cpython/longobject.rs @@ -4,8 +4,8 @@ use crate::object::*; 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}; +use std::ffi::c_void; +use std::ffi::{c_int, c_uchar}; #[cfg(Py_3_13)] extern "C" { diff --git a/pyo3-ffi/src/cpython/methodobject.rs b/pyo3-ffi/src/cpython/methodobject.rs index 97ad9ce35f0..096bb02ae13 100644 --- a/pyo3-ffi/src/cpython/methodobject.rs +++ b/pyo3-ffi/src/cpython/methodobject.rs @@ -1,7 +1,7 @@ use crate::object::*; #[cfg(not(GraalPy))] use crate::{PyCFunctionObject, PyMethodDefPointer, METH_METHOD, METH_STATIC}; -use std::os::raw::c_int; +use std::ffi::c_int; use std::ptr::addr_of_mut; #[cfg(not(GraalPy))] diff --git a/pyo3-ffi/src/cpython/object.rs b/pyo3-ffi/src/cpython/object.rs index 26ef784dde1..698cb3fe4bd 100644 --- a/pyo3-ffi/src/cpython/object.rs +++ b/pyo3-ffi/src/cpython/object.rs @@ -1,8 +1,8 @@ #[cfg(Py_3_8)] use crate::vectorcallfunc; use crate::{object, PyGetSetDef, PyMemberDef, PyMethodDef, PyObject, Py_ssize_t}; +use std::ffi::{c_char, c_int, c_uint, c_void}; use std::mem; -use std::os::raw::{c_char, c_int, c_uint, c_void}; // skipped private _Py_NewReference // skipped private _Py_NewReferenceNoTotal @@ -22,7 +22,7 @@ use std::os::raw::{c_char, c_int, c_uint, c_void}; #[cfg(not(Py_3_11))] // moved to src/buffer.rs from Python mod bufferinfo { use crate::Py_ssize_t; - use std::os::raw::{c_char, c_int, c_void}; + use std::ffi::{c_char, c_int, c_void}; use std::ptr; #[repr(C)] @@ -233,7 +233,7 @@ pub struct PyTypeObject { pub tp_setattro: Option, pub tp_as_buffer: *mut PyBufferProcs, #[cfg(not(Py_GIL_DISABLED))] - pub tp_flags: std::os::raw::c_ulong, + pub tp_flags: std::ffi::c_ulong, #[cfg(Py_GIL_DISABLED)] pub tp_flags: crate::impl_::AtomicCULong, pub tp_doc: *const c_char, @@ -271,7 +271,7 @@ pub struct PyTypeObject { #[cfg(any(all(PyPy, Py_3_8, not(Py_3_10)), all(not(PyPy), Py_3_8, not(Py_3_9))))] pub tp_print: Option, #[cfg(all(PyPy, not(Py_3_10)))] - pub tp_pypy_flags: std::os::raw::c_long, + pub tp_pypy_flags: std::ffi::c_long, #[cfg(py_sys_config = "COUNT_ALLOCS")] pub tp_allocs: Py_ssize_t, #[cfg(py_sys_config = "COUNT_ALLOCS")] diff --git a/pyo3-ffi/src/cpython/objimpl.rs b/pyo3-ffi/src/cpython/objimpl.rs index 14f7121a202..71087d28d2c 100644 --- a/pyo3-ffi/src/cpython/objimpl.rs +++ b/pyo3-ffi/src/cpython/objimpl.rs @@ -1,9 +1,9 @@ #[cfg(not(all(Py_3_11, any(PyPy, GraalPy))))] use libc::size_t; -use std::os::raw::c_int; +use std::ffi::c_int; #[cfg(not(any(PyPy, GraalPy)))] -use std::os::raw::c_void; +use std::ffi::c_void; use crate::object::*; diff --git a/pyo3-ffi/src/cpython/pydebug.rs b/pyo3-ffi/src/cpython/pydebug.rs index a42848e8fdb..6878554aead 100644 --- a/pyo3-ffi/src/cpython/pydebug.rs +++ b/pyo3-ffi/src/cpython/pydebug.rs @@ -1,4 +1,4 @@ -use std::os::raw::{c_char, c_int}; +use std::ffi::{c_char, c_int}; #[cfg(not(Py_LIMITED_API))] #[cfg_attr(windows, link(name = "pythonXY"))] diff --git a/pyo3-ffi/src/cpython/pyframe.rs b/pyo3-ffi/src/cpython/pyframe.rs index a0479b50b6b..6da9ecd5b53 100644 --- a/pyo3-ffi/src/cpython/pyframe.rs +++ b/pyo3-ffi/src/cpython/pyframe.rs @@ -2,8 +2,8 @@ use crate::PyFrameObject; use crate::{PyObject, PyTypeObject, Py_TYPE}; #[cfg(Py_3_12)] -use std::os::raw::c_char; -use std::os::raw::c_int; +use std::ffi::c_char; +use std::ffi::c_int; use std::ptr::addr_of_mut; // NB used in `_PyEval_EvalFrameDefault`, maybe we remove this too. diff --git a/pyo3-ffi/src/cpython/pyhash.rs b/pyo3-ffi/src/cpython/pyhash.rs index b746018a38a..f5c98c23256 100644 --- a/pyo3-ffi/src/cpython/pyhash.rs +++ b/pyo3-ffi/src/cpython/pyhash.rs @@ -3,9 +3,9 @@ use crate::Py_ssize_t; #[cfg(Py_3_13)] use crate::{PyObject, Py_hash_t}; #[cfg(any(Py_3_13, not(PyPy)))] -use std::os::raw::c_void; +use std::ffi::c_void; #[cfg(not(PyPy))] -use std::os::raw::{c_char, c_int}; +use std::ffi::{c_char, c_int}; #[cfg(not(PyPy))] #[repr(C)] diff --git a/pyo3-ffi/src/cpython/pylifecycle.rs b/pyo3-ffi/src/cpython/pylifecycle.rs index c259c369efd..1609c6ab7e4 100644 --- a/pyo3-ffi/src/cpython/pylifecycle.rs +++ b/pyo3-ffi/src/cpython/pylifecycle.rs @@ -1,6 +1,6 @@ use crate::{PyConfig, PyPreConfig, PyStatus, Py_ssize_t}; use libc::wchar_t; -use std::os::raw::{c_char, c_int}; +use std::ffi::{c_char, c_int}; // "private" functions in cpython/pylifecycle.h accepted in PEP 587 extern "C" { diff --git a/pyo3-ffi/src/cpython/pymem.rs b/pyo3-ffi/src/cpython/pymem.rs index c400fa30b05..762e850820f 100644 --- a/pyo3-ffi/src/cpython/pymem.rs +++ b/pyo3-ffi/src/cpython/pymem.rs @@ -1,5 +1,5 @@ use libc::size_t; -use std::os::raw::c_void; +use std::ffi::c_void; extern "C" { #[cfg_attr(PyPy, link_name = "PyPyMem_RawMalloc")] diff --git a/pyo3-ffi/src/cpython/pystate.rs b/pyo3-ffi/src/cpython/pystate.rs index 707d4945f49..96e2635a157 100644 --- a/pyo3-ffi/src/cpython/pystate.rs +++ b/pyo3-ffi/src/cpython/pystate.rs @@ -1,7 +1,7 @@ #[cfg(not(PyPy))] use crate::PyThreadState; use crate::{PyFrameObject, PyInterpreterState, PyObject}; -use std::os::raw::c_int; +use std::ffi::c_int; // skipped _PyInterpreterState_RequiresIDRef // skipped _PyInterpreterState_RequireIDRef diff --git a/pyo3-ffi/src/cpython/pythonrun.rs b/pyo3-ffi/src/cpython/pythonrun.rs index fe78f55ca07..db3767cb60c 100644 --- a/pyo3-ffi/src/cpython/pythonrun.rs +++ b/pyo3-ffi/src/cpython/pythonrun.rs @@ -5,7 +5,7 @@ use crate::PyCompilerFlags; #[cfg(not(any(PyPy, GraalPy, Py_3_10)))] use crate::{_mod, _node}; use libc::FILE; -use std::os::raw::{c_char, c_int}; +use std::ffi::{c_char, c_int}; extern "C" { pub fn PyRun_SimpleStringFlags(arg1: *const c_char, arg2: *mut PyCompilerFlags) -> c_int; diff --git a/pyo3-ffi/src/cpython/unicodeobject.rs b/pyo3-ffi/src/cpython/unicodeobject.rs index 452c82e4c4b..cf0ef54484e 100644 --- a/pyo3-ffi/src/cpython/unicodeobject.rs +++ b/pyo3-ffi/src/cpython/unicodeobject.rs @@ -2,7 +2,7 @@ use crate::Py_hash_t; 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}; +use std::ffi::{c_char, c_int, c_uint, c_void}; // skipped Py_UNICODE_ISSPACE() // skipped Py_UNICODE_ISLOWER() diff --git a/pyo3-ffi/src/datetime.rs b/pyo3-ffi/src/datetime.rs index 4db1bdd16d1..143de98e676 100644 --- a/pyo3-ffi/src/datetime.rs +++ b/pyo3-ffi/src/datetime.rs @@ -9,13 +9,13 @@ 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::os::raw::c_char; -use std::os::raw::c_int; +use std::ffi::c_char; +use std::ffi::c_int; use std::ptr; use std::sync::Once; use std::{cell::UnsafeCell, ffi::CStr}; #[cfg(not(PyPy))] -use {crate::Py_hash_t, std::os::raw::c_uchar}; +use {crate::Py_hash_t, std::ffi::c_uchar}; // Type struct wrappers const _PyDateTime_DATE_DATASIZE: usize = 4; const _PyDateTime_TIME_DATASIZE: usize = 6; diff --git a/pyo3-ffi/src/descrobject.rs b/pyo3-ffi/src/descrobject.rs index 0b75f2961f2..8a4d5839a37 100644 --- a/pyo3-ffi/src/descrobject.rs +++ b/pyo3-ffi/src/descrobject.rs @@ -1,7 +1,7 @@ use crate::methodobject::PyMethodDef; use crate::object::{PyObject, PyTypeObject}; use crate::Py_ssize_t; -use std::os::raw::{c_char, c_int, c_void}; +use std::ffi::{c_char, c_int, c_void}; use std::ptr; pub type getter = unsafe extern "C" fn(slf: *mut PyObject, closure: *mut c_void) -> *mut PyObject; diff --git a/pyo3-ffi/src/dictobject.rs b/pyo3-ffi/src/dictobject.rs index e609352ac1b..d8419ba4317 100644 --- a/pyo3-ffi/src/dictobject.rs +++ b/pyo3-ffi/src/dictobject.rs @@ -1,6 +1,6 @@ use crate::object::*; use crate::pyport::Py_ssize_t; -use std::os::raw::{c_char, c_int}; +use std::ffi::{c_char, c_int}; use std::ptr::addr_of_mut; #[cfg_attr(windows, link(name = "pythonXY"))] diff --git a/pyo3-ffi/src/fileobject.rs b/pyo3-ffi/src/fileobject.rs index 91618ab7fd2..ec3a0409930 100644 --- a/pyo3-ffi/src/fileobject.rs +++ b/pyo3-ffi/src/fileobject.rs @@ -1,5 +1,5 @@ use crate::object::PyObject; -use std::os::raw::{c_char, c_int}; +use std::ffi::{c_char, c_int}; pub const PY_STDIOTEXTMODE: &str = "b"; diff --git a/pyo3-ffi/src/fileutils.rs b/pyo3-ffi/src/fileutils.rs index 3f053b72b51..649751babf5 100644 --- a/pyo3-ffi/src/fileutils.rs +++ b/pyo3-ffi/src/fileutils.rs @@ -1,6 +1,6 @@ use crate::pyport::Py_ssize_t; use libc::wchar_t; -use std::os::raw::c_char; +use std::ffi::c_char; extern "C" { pub fn Py_DecodeLocale(arg1: *const c_char, size: *mut Py_ssize_t) -> *mut wchar_t; diff --git a/pyo3-ffi/src/floatobject.rs b/pyo3-ffi/src/floatobject.rs index 4e1d6476f58..18bbc8bece0 100644 --- a/pyo3-ffi/src/floatobject.rs +++ b/pyo3-ffi/src/floatobject.rs @@ -1,5 +1,5 @@ use crate::object::*; -use std::os::raw::{c_double, c_int}; +use std::ffi::{c_double, c_int}; use std::ptr::addr_of_mut; #[cfg(Py_LIMITED_API)] diff --git a/pyo3-ffi/src/impl_/mod.rs b/pyo3-ffi/src/impl_/mod.rs index 3058e852e6f..064df213ba6 100644 --- a/pyo3-ffi/src/impl_/mod.rs +++ b/pyo3-ffi/src/impl_/mod.rs @@ -13,7 +13,7 @@ mod atomic_c_ulong { } pub type TYPE = - () * 8 }> as AtomicCULongType>::Type; + () * 8 }> as AtomicCULongType>::Type; } /// Typedef for an atomic integer to match the platform-dependent c_ulong type. diff --git a/pyo3-ffi/src/import.rs b/pyo3-ffi/src/import.rs index e15a37b0a72..ce224af28e1 100644 --- a/pyo3-ffi/src/import.rs +++ b/pyo3-ffi/src/import.rs @@ -1,5 +1,5 @@ use crate::object::PyObject; -use std::os::raw::{c_char, c_int, c_long}; +use std::ffi::{c_char, c_int, c_long}; extern "C" { pub fn PyImport_GetMagicNumber() -> c_long; diff --git a/pyo3-ffi/src/intrcheck.rs b/pyo3-ffi/src/intrcheck.rs index 3d8a58f7f2f..05400557d5a 100644 --- a/pyo3-ffi/src/intrcheck.rs +++ b/pyo3-ffi/src/intrcheck.rs @@ -1,4 +1,4 @@ -use std::os::raw::c_int; +use std::ffi::c_int; extern "C" { #[cfg_attr(PyPy, link_name = "PyPyOS_InterruptOccurred")] diff --git a/pyo3-ffi/src/iterobject.rs b/pyo3-ffi/src/iterobject.rs index aa0c7b26db1..04a28d4826f 100644 --- a/pyo3-ffi/src/iterobject.rs +++ b/pyo3-ffi/src/iterobject.rs @@ -1,5 +1,5 @@ use crate::object::*; -use std::os::raw::c_int; +use std::ffi::c_int; use std::ptr::addr_of_mut; #[cfg_attr(windows, link(name = "pythonXY"))] diff --git a/pyo3-ffi/src/lib.rs b/pyo3-ffi/src/lib.rs index cd24fd86221..2ee446598f5 100644 --- a/pyo3-ffi/src/lib.rs +++ b/pyo3-ffi/src/lib.rs @@ -130,7 +130,7 @@ //! //! **`src/lib.rs`** //! ```rust,no_run -//! use std::os::raw::{c_char, c_long}; +//! use std::ffi::{c_char, c_long}; //! use std::ptr; //! //! use pyo3_ffi::*; diff --git a/pyo3-ffi/src/listobject.rs b/pyo3-ffi/src/listobject.rs index 881a8a8707b..6fd28ff0c99 100644 --- a/pyo3-ffi/src/listobject.rs +++ b/pyo3-ffi/src/listobject.rs @@ -1,6 +1,6 @@ use crate::object::*; use crate::pyport::Py_ssize_t; -use std::os::raw::c_int; +use std::ffi::c_int; use std::ptr::addr_of_mut; #[cfg_attr(windows, link(name = "pythonXY"))] diff --git a/pyo3-ffi/src/longobject.rs b/pyo3-ffi/src/longobject.rs index eca0af3d0a5..6927fcbef5a 100644 --- a/pyo3-ffi/src/longobject.rs +++ b/pyo3-ffi/src/longobject.rs @@ -1,7 +1,7 @@ use crate::object::*; use crate::pyport::Py_ssize_t; use libc::size_t; -use std::os::raw::{c_char, c_double, c_int, c_long, c_longlong, c_ulong, c_ulonglong, c_void}; +use std::ffi::{c_char, c_double, c_int, c_long, c_longlong, c_ulong, c_ulonglong, c_void}; use std::ptr::addr_of_mut; opaque_struct!(pub PyLongObject); diff --git a/pyo3-ffi/src/marshal.rs b/pyo3-ffi/src/marshal.rs index 562f6f91620..bba8f5ece50 100644 --- a/pyo3-ffi/src/marshal.rs +++ b/pyo3-ffi/src/marshal.rs @@ -1,5 +1,5 @@ use super::{PyObject, Py_ssize_t}; -use std::os::raw::{c_char, c_int}; +use std::ffi::{c_char, c_int}; // skipped Py_MARSHAL_VERSION // skipped PyMarshal_WriteLongToFile diff --git a/pyo3-ffi/src/memoryobject.rs b/pyo3-ffi/src/memoryobject.rs index 4e1e50c6c82..e11055f3743 100644 --- a/pyo3-ffi/src/memoryobject.rs +++ b/pyo3-ffi/src/memoryobject.rs @@ -1,6 +1,6 @@ use crate::object::*; use crate::pyport::Py_ssize_t; -use std::os::raw::{c_char, c_int}; +use std::ffi::{c_char, c_int}; use std::ptr::addr_of_mut; // skipped _PyManagedBuffer_Type diff --git a/pyo3-ffi/src/methodobject.rs b/pyo3-ffi/src/methodobject.rs index 3dfbbb5a208..5f367c9bd48 100644 --- a/pyo3-ffi/src/methodobject.rs +++ b/pyo3-ffi/src/methodobject.rs @@ -1,7 +1,7 @@ use crate::object::{PyObject, PyTypeObject, Py_TYPE}; #[cfg(Py_3_9)] use crate::PyObject_TypeCheck; -use std::os::raw::{c_char, c_int, c_void}; +use std::ffi::{c_char, c_int, c_void}; use std::{mem, ptr}; #[cfg(all(Py_3_9, not(Py_LIMITED_API), not(GraalPy)))] diff --git a/pyo3-ffi/src/modsupport.rs b/pyo3-ffi/src/modsupport.rs index 56a68fe2613..344e0abb979 100644 --- a/pyo3-ffi/src/modsupport.rs +++ b/pyo3-ffi/src/modsupport.rs @@ -2,7 +2,7 @@ use crate::methodobject::PyMethodDef; use crate::moduleobject::PyModuleDef; use crate::object::PyObject; use crate::pyport::Py_ssize_t; -use std::os::raw::{c_char, c_int, c_long}; +use std::ffi::{c_char, c_int, c_long}; extern "C" { #[cfg_attr(PyPy, link_name = "PyPyArg_Parse")] diff --git a/pyo3-ffi/src/moduleobject.rs b/pyo3-ffi/src/moduleobject.rs index 1eff7a39491..0d46b6d3130 100644 --- a/pyo3-ffi/src/moduleobject.rs +++ b/pyo3-ffi/src/moduleobject.rs @@ -1,7 +1,7 @@ use crate::methodobject::PyMethodDef; use crate::object::*; use crate::pyport::Py_ssize_t; -use std::os::raw::{c_char, c_int, c_void}; +use std::ffi::{c_char, c_int, c_void}; use std::ptr::addr_of_mut; #[cfg_attr(windows, link(name = "pythonXY"))] diff --git a/pyo3-ffi/src/object.rs b/pyo3-ffi/src/object.rs index 6d5dc781287..0e3aafe4b65 100644 --- a/pyo3-ffi/src/object.rs +++ b/pyo3-ffi/src/object.rs @@ -3,10 +3,10 @@ use crate::pyport::{Py_hash_t, Py_ssize_t}; use crate::refcount; #[cfg(Py_GIL_DISABLED)] use crate::PyMutex; +use std::ffi::{c_char, c_int, c_uint, c_ulong, c_void}; #[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}; diff --git a/pyo3-ffi/src/objimpl.rs b/pyo3-ffi/src/objimpl.rs index 76835a6d158..338228a53ea 100644 --- a/pyo3-ffi/src/objimpl.rs +++ b/pyo3-ffi/src/objimpl.rs @@ -1,5 +1,5 @@ use libc::size_t; -use std::os::raw::{c_int, c_void}; +use std::ffi::{c_int, c_void}; use crate::object::*; use crate::pyport::Py_ssize_t; diff --git a/pyo3-ffi/src/pybuffer.rs b/pyo3-ffi/src/pybuffer.rs index de7067599ff..48559c0bb32 100644 --- a/pyo3-ffi/src/pybuffer.rs +++ b/pyo3-ffi/src/pybuffer.rs @@ -1,6 +1,6 @@ use crate::object::PyObject; use crate::pyport::Py_ssize_t; -use std::os::raw::{c_char, c_int, c_void}; +use std::ffi::{c_char, c_int, c_void}; use std::ptr; #[repr(C)] diff --git a/pyo3-ffi/src/pycapsule.rs b/pyo3-ffi/src/pycapsule.rs index 5b77841c23c..65df8837063 100644 --- a/pyo3-ffi/src/pycapsule.rs +++ b/pyo3-ffi/src/pycapsule.rs @@ -1,5 +1,5 @@ use crate::object::*; -use std::os::raw::{c_char, c_int, c_void}; +use std::ffi::{c_char, c_int, c_void}; use std::ptr::addr_of_mut; #[cfg_attr(windows, link(name = "pythonXY"))] diff --git a/pyo3-ffi/src/pyerrors.rs b/pyo3-ffi/src/pyerrors.rs index d341239a07b..836491bb6e6 100644 --- a/pyo3-ffi/src/pyerrors.rs +++ b/pyo3-ffi/src/pyerrors.rs @@ -1,6 +1,6 @@ use crate::object::*; use crate::pyport::Py_ssize_t; -use std::os::raw::{c_char, c_int}; +use std::ffi::{c_char, c_int}; extern "C" { #[cfg_attr(PyPy, link_name = "PyPyErr_SetNone")] diff --git a/pyo3-ffi/src/pyframe.rs b/pyo3-ffi/src/pyframe.rs index 8ab5abce37f..f91069929ed 100644 --- a/pyo3-ffi/src/pyframe.rs +++ b/pyo3-ffi/src/pyframe.rs @@ -4,7 +4,7 @@ use crate::object::PyObject; #[cfg(any(Py_3_10, all(Py_3_9, not(Py_LIMITED_API))))] use crate::PyCodeObject; use crate::PyFrameObject; -use std::os::raw::c_int; +use std::ffi::c_int; extern "C" { pub fn PyFrame_GetLineNumber(frame: *mut PyFrameObject) -> c_int; diff --git a/pyo3-ffi/src/pyhash.rs b/pyo3-ffi/src/pyhash.rs index 074278ddf3c..4fab07d00d4 100644 --- a/pyo3-ffi/src/pyhash.rs +++ b/pyo3-ffi/src/pyhash.rs @@ -1,9 +1,9 @@ #[cfg(not(any(Py_LIMITED_API, PyPy)))] use crate::pyport::{Py_hash_t, Py_ssize_t}; #[cfg(not(any(Py_LIMITED_API, PyPy)))] -use std::os::raw::c_void; +use std::ffi::c_void; -use std::os::raw::{c_int, c_ulong}; +use std::ffi::{c_int, c_ulong}; extern "C" { // skipped non-limited _Py_HashDouble diff --git a/pyo3-ffi/src/pylifecycle.rs b/pyo3-ffi/src/pylifecycle.rs index 3f051c54f7c..53b82a55b91 100644 --- a/pyo3-ffi/src/pylifecycle.rs +++ b/pyo3-ffi/src/pylifecycle.rs @@ -1,7 +1,7 @@ use crate::pystate::PyThreadState; use libc::wchar_t; -use std::os::raw::{c_char, c_int}; +use std::ffi::{c_char, c_int}; extern "C" { pub fn Py_Initialize(); diff --git a/pyo3-ffi/src/pymem.rs b/pyo3-ffi/src/pymem.rs index ab0c3d74af9..aaa5a4f6e8d 100644 --- a/pyo3-ffi/src/pymem.rs +++ b/pyo3-ffi/src/pymem.rs @@ -1,5 +1,5 @@ use libc::size_t; -use std::os::raw::c_void; +use std::ffi::c_void; extern "C" { #[cfg_attr(PyPy, link_name = "PyPyMem_Malloc")] diff --git a/pyo3-ffi/src/pyport.rs b/pyo3-ffi/src/pyport.rs index e524831d80d..3a066353ca4 100644 --- a/pyo3-ffi/src/pyport.rs +++ b/pyo3-ffi/src/pyport.rs @@ -1,7 +1,7 @@ // NB libc does not define this constant on all platforms, so we hard code it // like CPython does. // https://github.com/python/cpython/blob/d8b9011702443bb57579f8834f3effe58e290dfc/Include/pyport.h#L372 -pub const INT_MAX: std::os::raw::c_int = 2147483647; +pub const INT_MAX: std::ffi::c_int = 2147483647; pub type PY_UINT32_T = u32; pub type PY_UINT64_T = u64; diff --git a/pyo3-ffi/src/pystate.rs b/pyo3-ffi/src/pystate.rs index c9dc2b5ceda..1ba6a84289e 100644 --- a/pyo3-ffi/src/pystate.rs +++ b/pyo3-ffi/src/pystate.rs @@ -1,12 +1,12 @@ use crate::moduleobject::PyModuleDef; use crate::object::PyObject; -use std::os::raw::c_int; +use std::ffi::c_int; #[cfg(all(Py_3_10, not(PyPy), not(Py_LIMITED_API)))] use crate::PyFrameObject; #[cfg(not(PyPy))] -use std::os::raw::c_long; +use std::ffi::c_long; pub const MAX_CO_EXTRA_USERS: c_int = 255; diff --git a/pyo3-ffi/src/pystrtod.rs b/pyo3-ffi/src/pystrtod.rs index 1f027686bd9..20d5a24387b 100644 --- a/pyo3-ffi/src/pystrtod.rs +++ b/pyo3-ffi/src/pystrtod.rs @@ -1,5 +1,5 @@ use crate::object::PyObject; -use std::os::raw::{c_char, c_double, c_int}; +use std::ffi::{c_char, c_double, c_int}; extern "C" { #[cfg_attr(PyPy, link_name = "PyPyOS_string_to_double")] diff --git a/pyo3-ffi/src/pythonrun.rs b/pyo3-ffi/src/pythonrun.rs index 80209b5875a..090c964d665 100644 --- a/pyo3-ffi/src/pythonrun.rs +++ b/pyo3-ffi/src/pythonrun.rs @@ -2,8 +2,8 @@ use crate::object::*; #[cfg(not(any(PyPy, Py_LIMITED_API, Py_3_10)))] use libc::FILE; #[cfg(any(Py_LIMITED_API, not(Py_3_10), PyPy, GraalPy))] -use std::os::raw::c_char; -use std::os::raw::c_int; +use std::ffi::c_char; +use std::ffi::c_int; extern "C" { #[cfg(any(all(Py_LIMITED_API, not(PyPy)), GraalPy))] @@ -33,7 +33,7 @@ pub unsafe fn Py_CompileString(string: *const c_char, p: *const c_char, s: c_int 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 + f: *mut std::ffi::c_void, // Actually *mut Py_CompilerFlags in the real definition ) -> *mut PyObject; } #[cfg(not(Py_LIMITED_API))] diff --git a/pyo3-ffi/src/rangeobject.rs b/pyo3-ffi/src/rangeobject.rs index 408b5cdc5a4..35667fed8c2 100644 --- a/pyo3-ffi/src/rangeobject.rs +++ b/pyo3-ffi/src/rangeobject.rs @@ -1,5 +1,5 @@ use crate::object::*; -use std::os::raw::c_int; +use std::ffi::c_int; use std::ptr::addr_of_mut; #[cfg_attr(windows, link(name = "pythonXY"))] diff --git a/pyo3-ffi/src/refcount.rs b/pyo3-ffi/src/refcount.rs index fcb5f45be6a..22a6268c729 100644 --- a/pyo3-ffi/src/refcount.rs +++ b/pyo3-ffi/src/refcount.rs @@ -1,15 +1,15 @@ use crate::pyport::Py_ssize_t; use crate::PyObject; #[cfg(py_sys_config = "Py_REF_DEBUG")] -use std::os::raw::c_char; +use std::ffi::c_char; #[cfg(Py_3_12)] -use std::os::raw::c_int; +use std::ffi::c_int; #[cfg(all(Py_3_14, any(not(Py_GIL_DISABLED), target_pointer_width = "32")))] -use std::os::raw::c_long; +use std::ffi::c_long; #[cfg(any(Py_GIL_DISABLED, all(Py_3_12, not(Py_3_14))))] -use std::os::raw::c_uint; +use std::ffi::c_uint; #[cfg(all(Py_3_14, not(Py_GIL_DISABLED)))] -use std::os::raw::c_ulong; +use std::ffi::c_ulong; use std::ptr; #[cfg(Py_GIL_DISABLED)] use std::sync::atomic::Ordering::Relaxed; diff --git a/pyo3-ffi/src/setobject.rs b/pyo3-ffi/src/setobject.rs index 87e33e803f4..4c32952238c 100644 --- a/pyo3-ffi/src/setobject.rs +++ b/pyo3-ffi/src/setobject.rs @@ -2,7 +2,7 @@ use crate::object::*; #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] use crate::pyport::Py_hash_t; use crate::pyport::Py_ssize_t; -use std::os::raw::c_int; +use std::ffi::c_int; use std::ptr::addr_of_mut; pub const PySet_MINSIZE: usize = 8; diff --git a/pyo3-ffi/src/sliceobject.rs b/pyo3-ffi/src/sliceobject.rs index a3ea153987c..f753f441ee5 100644 --- a/pyo3-ffi/src/sliceobject.rs +++ b/pyo3-ffi/src/sliceobject.rs @@ -1,6 +1,6 @@ use crate::object::*; use crate::pyport::Py_ssize_t; -use std::os::raw::c_int; +use std::ffi::c_int; use std::ptr::addr_of_mut; #[cfg_attr(windows, link(name = "pythonXY"))] diff --git a/pyo3-ffi/src/structmember.rs b/pyo3-ffi/src/structmember.rs index afae165c5b3..2d9dd5a4a1f 100644 --- a/pyo3-ffi/src/structmember.rs +++ b/pyo3-ffi/src/structmember.rs @@ -1,4 +1,4 @@ -use std::os::raw::c_int; +use std::ffi::c_int; pub use crate::PyMemberDef; diff --git a/pyo3-ffi/src/structseq.rs b/pyo3-ffi/src/structseq.rs index f8566787b51..e5cfee1a876 100644 --- a/pyo3-ffi/src/structseq.rs +++ b/pyo3-ffi/src/structseq.rs @@ -1,7 +1,7 @@ use crate::object::{PyObject, PyTypeObject}; #[cfg(not(PyPy))] use crate::pyport::Py_ssize_t; -use std::os::raw::{c_char, c_int}; +use std::ffi::{c_char, c_int}; #[repr(C)] #[derive(Copy, Clone)] diff --git a/pyo3-ffi/src/sysmodule.rs b/pyo3-ffi/src/sysmodule.rs index 6f402197ece..c0a3f176228 100644 --- a/pyo3-ffi/src/sysmodule.rs +++ b/pyo3-ffi/src/sysmodule.rs @@ -1,6 +1,6 @@ use crate::object::PyObject; use libc::wchar_t; -use std::os::raw::{c_char, c_int}; +use std::ffi::{c_char, c_int}; extern "C" { #[cfg_attr(PyPy, link_name = "PyPySys_GetObject")] diff --git a/pyo3-ffi/src/traceback.rs b/pyo3-ffi/src/traceback.rs index 432b6980ef5..8b1ac216bea 100644 --- a/pyo3-ffi/src/traceback.rs +++ b/pyo3-ffi/src/traceback.rs @@ -1,5 +1,5 @@ use crate::object::*; -use std::os::raw::c_int; +use std::ffi::c_int; #[cfg(not(PyPy))] use std::ptr::addr_of_mut; diff --git a/pyo3-ffi/src/tupleobject.rs b/pyo3-ffi/src/tupleobject.rs index d265c91a4b1..1bd34081bce 100644 --- a/pyo3-ffi/src/tupleobject.rs +++ b/pyo3-ffi/src/tupleobject.rs @@ -1,6 +1,6 @@ use crate::object::*; use crate::pyport::Py_ssize_t; -use std::os::raw::c_int; +use std::ffi::c_int; use std::ptr::addr_of_mut; #[cfg_attr(windows, link(name = "pythonXY"))] diff --git a/pyo3-ffi/src/typeslots.rs b/pyo3-ffi/src/typeslots.rs index da7c60b310c..fd0c3b77a74 100644 --- a/pyo3-ffi/src/typeslots.rs +++ b/pyo3-ffi/src/typeslots.rs @@ -1,4 +1,4 @@ -use std::os::raw::c_int; +use std::ffi::c_int; pub const Py_bf_getbuffer: c_int = 1; pub const Py_bf_releasebuffer: c_int = 2; diff --git a/pyo3-ffi/src/unicodeobject.rs b/pyo3-ffi/src/unicodeobject.rs index 1e0425ce2a2..8ba645bfa05 100644 --- a/pyo3-ffi/src/unicodeobject.rs +++ b/pyo3-ffi/src/unicodeobject.rs @@ -1,7 +1,7 @@ use crate::object::*; use crate::pyport::Py_ssize_t; use libc::wchar_t; -use std::os::raw::{c_char, c_int, c_void}; +use std::ffi::{c_char, c_int, c_void}; #[cfg(not(PyPy))] use std::ptr::addr_of_mut; diff --git a/pyo3-ffi/src/warnings.rs b/pyo3-ffi/src/warnings.rs index d58f37435de..f2cd8adbf9b 100644 --- a/pyo3-ffi/src/warnings.rs +++ b/pyo3-ffi/src/warnings.rs @@ -1,6 +1,6 @@ use crate::object::PyObject; use crate::pyport::Py_ssize_t; -use std::os::raw::{c_char, c_int}; +use std::ffi::{c_char, c_int}; extern "C" { #[cfg_attr(PyPy, link_name = "PyPyErr_WarnEx")] diff --git a/pyo3-ffi/src/weakrefobject.rs b/pyo3-ffi/src/weakrefobject.rs index 88a1bf90314..3921310cee8 100644 --- a/pyo3-ffi/src/weakrefobject.rs +++ b/pyo3-ffi/src/weakrefobject.rs @@ -1,5 +1,5 @@ use crate::object::*; -use std::os::raw::c_int; +use std::ffi::c_int; #[cfg(not(PyPy))] use std::ptr::addr_of_mut; diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index 33b7af657b3..6d7b455988f 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -472,8 +472,8 @@ fn impl_traverse_slot( pub unsafe extern "C" fn __pymethod_traverse__( slf: *mut #pyo3_path::ffi::PyObject, visit: #pyo3_path::ffi::visitproc, - arg: *mut ::std::os::raw::c_void, - ) -> ::std::os::raw::c_int { + arg: *mut ::std::ffi::c_void, + ) -> ::std::ffi::c_int { #pyo3_path::impl_::pymethods::_call_traverse::<#cls>(slf, #cls::#rust_fn_ident, visit, arg, #cls::__pymethod_traverse__) } }; @@ -514,7 +514,7 @@ fn impl_clear_slot(cls: &syn::Type, spec: &FnSpec<'_>, ctx: &Ctx) -> syn::Result let associated_method = quote! { pub unsafe extern "C" fn __pymethod___clear____( _slf: *mut #pyo3_path::ffi::PyObject, - ) -> ::std::os::raw::c_int { + ) -> ::std::ffi::c_int { #pyo3_path::impl_::pymethods::_call_clear(_slf, |py, _slf| { #holders let result = #fncall; @@ -749,7 +749,7 @@ pub fn impl_py_setter_def( py: #pyo3_path::Python<'_>, _slf: *mut #pyo3_path::ffi::PyObject, _value: *mut #pyo3_path::ffi::PyObject, - ) -> #pyo3_path::PyResult<::std::os::raw::c_int> { + ) -> #pyo3_path::PyResult<::std::ffi::c_int> { use ::std::convert::Into; let _value = #pyo3_path::impl_::pymethods::BoundRef::ref_from_ptr_or_opt(py, &_value) .ok_or_else(|| { @@ -1124,7 +1124,7 @@ impl Ty { Ty::Object | Ty::MaybeNullObject => quote! { *mut #pyo3_path::ffi::PyObject }, Ty::NonNullObject => quote! { ::std::ptr::NonNull<#pyo3_path::ffi::PyObject> }, Ty::IPowModulo => quote! { #pyo3_path::impl_::pymethods::IPowModulo }, - Ty::Int | Ty::CompareOp => quote! { ::std::os::raw::c_int }, + Ty::Int | Ty::CompareOp => quote! { ::std::ffi::c_int }, Ty::PyHashT => quote! { #pyo3_path::ffi::Py_hash_t }, Ty::PySsizeT => quote! { #pyo3_path::ffi::Py_ssize_t }, Ty::Void => quote! { () }, diff --git a/src/buffer.rs b/src/buffer.rs index fe824b69450..6cd5ff03ad7 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -20,8 +20,11 @@ //! `PyBuffer` implementation use crate::Bound; use crate::{err, exceptions::PyBufferError, ffi, FromPyObject, PyAny, PyResult, Python}; +use std::ffi::{ + c_char, c_int, c_long, c_longlong, c_schar, c_short, c_uchar, c_uint, c_ulong, c_ulonglong, + c_ushort, c_void, +}; use std::marker::PhantomData; -use std::os::raw; use std::pin::Pin; use std::{cell, mem, ptr, slice}; use std::{ffi::CStr, fmt::Debug}; @@ -96,38 +99,38 @@ fn native_element_type_from_type_char(type_char: u8) -> ElementType { use self::ElementType::*; match type_char { b'c' => UnsignedInteger { - bytes: mem::size_of::(), + bytes: mem::size_of::(), }, b'b' => SignedInteger { - bytes: mem::size_of::(), + bytes: mem::size_of::(), }, b'B' => UnsignedInteger { - bytes: mem::size_of::(), + bytes: mem::size_of::(), }, b'?' => Bool, b'h' => SignedInteger { - bytes: mem::size_of::(), + bytes: mem::size_of::(), }, b'H' => UnsignedInteger { - bytes: mem::size_of::(), + bytes: mem::size_of::(), }, b'i' => SignedInteger { - bytes: mem::size_of::(), + bytes: mem::size_of::(), }, b'I' => UnsignedInteger { - bytes: mem::size_of::(), + bytes: mem::size_of::(), }, b'l' => SignedInteger { - bytes: mem::size_of::(), + bytes: mem::size_of::(), }, b'L' => UnsignedInteger { - bytes: mem::size_of::(), + bytes: mem::size_of::(), }, b'q' => SignedInteger { - bytes: mem::size_of::(), + bytes: mem::size_of::(), }, b'Q' => UnsignedInteger { - bytes: mem::size_of::(), + bytes: mem::size_of::(), }, b'n' => SignedInteger { bytes: mem::size_of::(), @@ -229,14 +232,14 @@ impl PyBuffer { /// Warning: the buffer memory might be mutated by other Python functions, /// and thus may only be accessed while the GIL is held. #[inline] - pub fn buf_ptr(&self) -> *mut raw::c_void { + pub fn buf_ptr(&self) -> *mut c_void { self.0.buf } /// Gets a pointer to the specified item. /// /// If `indices.len() < self.dimensions()`, returns the start address of the sub-array at the specified dimension. - pub fn get_ptr(&self, indices: &[usize]) -> *mut raw::c_void { + pub fn get_ptr(&self, indices: &[usize]) -> *mut c_void { let shape = &self.shape()[..indices.len()]; for i in 0..indices.len() { assert!(indices[i] < shape[i]); @@ -349,13 +352,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, b'C' as std::os::raw::c_char) != 0 } + unsafe { ffi::PyBuffer_IsContiguous(&*self.0, b'C' as std::ffi::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, b'F' as std::os::raw::c_char) != 0 } + unsafe { ffi::PyBuffer_IsContiguous(&*self.0, b'F' as std::ffi::c_char) != 0 } } /// Gets the buffer memory as a slice. @@ -493,7 +496,7 @@ impl PyBuffer { &*self.0 as *const ffi::Py_buffer as *mut ffi::Py_buffer }, self.0.len, - fort as std::os::raw::c_char, + fort as std::ffi::c_char, ) }) } @@ -522,7 +525,7 @@ impl PyBuffer { // Due to T:Copy, we don't need to be concerned with Drop impls. err::error_on_minusone(py, unsafe { ffi::PyBuffer_ToContiguous( - vec.as_ptr() as *mut raw::c_void, + vec.as_ptr() as *mut c_void, #[cfg(Py_3_11)] &*self.0, #[cfg(not(Py_3_11))] @@ -530,7 +533,7 @@ impl PyBuffer { &*self.0 as *const ffi::Py_buffer as *mut ffi::Py_buffer }, self.0.len, - fort as std::os::raw::c_char, + fort as std::ffi::c_char, ) })?; // set vector length to mark the now-initialized space as usable @@ -591,10 +594,10 @@ impl PyBuffer { }, #[cfg(not(Py_3_11))] { - source.as_ptr() as *mut raw::c_void + source.as_ptr() as *mut c_void }, self.0.len, - fort as std::os::raw::c_char, + fort as std::ffi::c_char, ) }) } @@ -689,7 +692,8 @@ impl_element!(f64, Float); #[cfg(test)] mod tests { - use super::PyBuffer; + use super::*; + use crate::ffi; use crate::types::any::PyAnyMethods; use crate::Python; @@ -721,84 +725,82 @@ mod tests { #[test] fn test_element_type_from_format() { - use super::ElementType; use super::ElementType::*; use std::mem::size_of; - use std::os::raw; for (cstr, expected) in [ // @ prefix goes to native_element_type_from_type_char ( ffi::c_str!("@b"), SignedInteger { - bytes: size_of::(), + bytes: size_of::(), }, ), ( ffi::c_str!("@c"), UnsignedInteger { - bytes: size_of::(), + bytes: size_of::(), }, ), ( ffi::c_str!("@b"), SignedInteger { - bytes: size_of::(), + bytes: size_of::(), }, ), ( ffi::c_str!("@B"), UnsignedInteger { - bytes: size_of::(), + bytes: size_of::(), }, ), (ffi::c_str!("@?"), Bool), ( ffi::c_str!("@h"), SignedInteger { - bytes: size_of::(), + bytes: size_of::(), }, ), ( ffi::c_str!("@H"), UnsignedInteger { - bytes: size_of::(), + bytes: size_of::(), }, ), ( ffi::c_str!("@i"), SignedInteger { - bytes: size_of::(), + bytes: size_of::(), }, ), ( ffi::c_str!("@I"), UnsignedInteger { - bytes: size_of::(), + bytes: size_of::(), }, ), ( ffi::c_str!("@l"), SignedInteger { - bytes: size_of::(), + bytes: size_of::(), }, ), ( ffi::c_str!("@L"), UnsignedInteger { - bytes: size_of::(), + bytes: size_of::(), }, ), ( ffi::c_str!("@q"), SignedInteger { - bytes: size_of::(), + bytes: size_of::(), }, ), ( ffi::c_str!("@Q"), UnsignedInteger { - bytes: size_of::(), + bytes: size_of::(), }, ), ( diff --git a/src/conversions/num_complex.rs b/src/conversions/num_complex.rs index 1ed689c81c5..edcc0ef2d54 100644 --- a/src/conversions/num_complex.rs +++ b/src/conversions/num_complex.rs @@ -98,7 +98,7 @@ use crate::{ Python, }; use num_complex::Complex; -use std::os::raw::c_double; +use std::ffi::c_double; impl PyComplex { /// Creates a new Python `PyComplex` object from `num_complex`'s [`Complex`]. diff --git a/src/conversions/std/num.rs b/src/conversions/std/num.rs index f41c4ca567b..029219b39bc 100644 --- a/src/conversions/std/num.rs +++ b/src/conversions/std/num.rs @@ -7,11 +7,11 @@ use crate::types::any::PyAnyMethods; use crate::types::{PyBytes, PyInt}; use crate::{exceptions, ffi, Bound, FromPyObject, PyAny, PyErr, PyResult, Python}; use std::convert::Infallible; +use std::ffi::c_long; use std::num::{ NonZeroI128, NonZeroI16, NonZeroI32, NonZeroI64, NonZeroI8, NonZeroIsize, NonZeroU128, NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU8, NonZeroUsize, }; -use std::os::raw::c_long; macro_rules! int_fits_larger_int { ($rust_type:ty, $larger_type:ty) => { diff --git a/src/impl_/callback.rs b/src/impl_/callback.rs index 9f06340bc59..e8340c829a1 100644 --- a/src/impl_/callback.rs +++ b/src/impl_/callback.rs @@ -4,7 +4,7 @@ use crate::err::{PyErr, PyResult}; use crate::exceptions::PyOverflowError; use crate::ffi::{self, Py_hash_t}; use crate::{BoundObject, IntoPyObject, PyObject, Python}; -use std::os::raw::c_int; +use std::ffi::c_int; /// A type which can be the return type of a python C-API callback pub trait PyCallbackOutput: Copy { @@ -16,7 +16,7 @@ impl PyCallbackOutput for *mut ffi::PyObject { const ERR_VALUE: Self = std::ptr::null_mut(); } -impl PyCallbackOutput for std::os::raw::c_int { +impl PyCallbackOutput for std::ffi::c_int { const ERR_VALUE: Self = -1; } @@ -62,16 +62,16 @@ impl IntoPyCallbackOutput<'_, Self> for *mut ffi::PyObject { } } -impl IntoPyCallbackOutput<'_, std::os::raw::c_int> for () { +impl IntoPyCallbackOutput<'_, std::ffi::c_int> for () { #[inline] - fn convert(self, _: Python<'_>) -> PyResult { + fn convert(self, _: Python<'_>) -> PyResult { Ok(0) } } -impl IntoPyCallbackOutput<'_, std::os::raw::c_int> for bool { +impl IntoPyCallbackOutput<'_, std::ffi::c_int> for bool { #[inline] - fn convert(self, _: Python<'_>) -> PyResult { + fn convert(self, _: Python<'_>) -> PyResult { Ok(self as c_int) } } diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index 2535ca7ed47..a158d5a1a59 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -439,7 +439,7 @@ macro_rules! define_pyclass_setattr_slot { _slf: *mut $crate::ffi::PyObject, attr: *mut $crate::ffi::PyObject, value: *mut $crate::ffi::PyObject, - ) -> ::std::os::raw::c_int { + ) -> ::std::ffi::c_int { unsafe { $crate::impl_::trampoline::setattrofunc( _slf, @@ -882,7 +882,7 @@ macro_rules! generate_pyclass_richcompare_slot { unsafe extern "C" fn __pymethod___richcmp____( slf: *mut $crate::ffi::PyObject, other: *mut $crate::ffi::PyObject, - op: ::std::os::raw::c_int, + op: ::std::ffi::c_int, ) -> *mut $crate::ffi::PyObject { unsafe { $crate::impl_::trampoline::richcmpfunc(slf, other, op, |py, slf, other, op| { diff --git a/src/impl_/pymethods.rs b/src/impl_/pymethods.rs index 30029365ab0..50d02b6a645 100644 --- a/src/impl_/pymethods.rs +++ b/src/impl_/pymethods.rs @@ -14,9 +14,9 @@ use crate::{ PyVisit, Python, }; use std::ffi::CStr; +use std::ffi::{c_int, c_void}; 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, NonNull}; diff --git a/src/internal/get_slot.rs b/src/internal/get_slot.rs index 611644cb6bc..f938fc8a6ac 100644 --- a/src/internal/get_slot.rs +++ b/src/internal/get_slot.rs @@ -3,7 +3,7 @@ use crate::{ types::{PyType, PyTypeMethods}, Borrowed, Bound, }; -use std::os::raw::c_int; +use std::ffi::c_int; impl Bound<'_, PyType> { #[inline] @@ -160,7 +160,7 @@ pub struct PyNumberMethods39Snapshot { pub nb_xor: Option, pub nb_or: Option, pub nb_int: Option, - pub nb_reserved: *mut std::os::raw::c_void, + pub nb_reserved: *mut std::ffi::c_void, pub nb_float: Option, pub nb_inplace_add: Option, pub nb_inplace_subtract: Option, @@ -188,9 +188,9 @@ pub struct PySequenceMethods39Snapshot { pub sq_concat: Option, pub sq_repeat: Option, pub sq_item: Option, - pub was_sq_slice: *mut std::os::raw::c_void, + pub was_sq_slice: *mut std::ffi::c_void, pub sq_ass_item: Option, - pub was_sq_ass_slice: *mut std::os::raw::c_void, + pub was_sq_ass_slice: *mut std::ffi::c_void, pub sq_contains: Option, pub sq_inplace_concat: Option, pub sq_inplace_repeat: Option, @@ -216,8 +216,8 @@ pub struct PyAsyncMethods39Snapshot { #[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, + pub bf_getbuffer: *mut std::ffi::c_void, + pub bf_releasebuffer: *mut std::ffi::c_void, } /// Snapshot of the structure of PyTypeObject for Python 3.7 through 3.9. @@ -229,12 +229,12 @@ pub struct PyBufferProcs39Snapshot { #[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_name: *const std::ffi::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 + pub tp_print: *mut std::ffi::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, @@ -250,8 +250,8 @@ struct PyTypeObject39Snapshot { 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_flags: std::ffi::c_ulong, + pub tp_doc: *const std::ffi::c_char, pub tp_traverse: Option, pub tp_clear: Option, pub tp_richcompare: Option, @@ -277,7 +277,7 @@ struct PyTypeObject39Snapshot { 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_version_tag: std::ffi::c_uint, pub tp_finalize: Option, #[cfg(Py_3_8)] pub tp_vectorcall: Option, diff --git a/src/marshal.rs b/src/marshal.rs index abe5e5ed96f..6866a7cc1c0 100644 --- a/src/marshal.rs +++ b/src/marshal.rs @@ -7,7 +7,7 @@ use crate::py_result_ext::PyResultExt; use crate::types::{PyAny, PyBytes}; use crate::{ffi, Bound}; use crate::{PyResult, Python}; -use std::os::raw::c_int; +use std::ffi::c_int; /// The current version of the marshal binary format. pub const VERSION: i32 = 4; diff --git a/src/pyclass/gc.rs b/src/pyclass/gc.rs index b4c0c994de4..295e7e05894 100644 --- a/src/pyclass/gc.rs +++ b/src/pyclass/gc.rs @@ -63,7 +63,7 @@ mod get_nonzero_c_int { } pub type Type = - () * 8 }> as NonZeroCIntType>::Type; + () * 8 }> as NonZeroCIntType>::Type; } use get_nonzero_c_int::Type as NonZeroCInt; diff --git a/src/types/any.rs b/src/types/any.rs index cfc41ed5d29..4c3fc0b53fa 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -14,7 +14,7 @@ use crate::types::{PyDict, PyIterator, PyList, PyString, PyType}; use crate::{err, ffi, Borrowed, BoundObject, IntoPyObjectExt, Py, Python}; use std::cell::UnsafeCell; use std::cmp::Ordering; -use std::os::raw::c_int; +use std::ffi::c_int; use std::ptr; /// Represents any Python object. diff --git a/src/types/bytes.rs b/src/types/bytes.rs index f88aa5bb3b8..136f6361b27 100644 --- a/src/types/bytes.rs +++ b/src/types/bytes.rs @@ -375,8 +375,8 @@ mod tests { let py_bytes = PyBytes::new(py, b); unsafe { assert_eq!( - ffi::PyBytes_AsString(py_bytes.as_ptr()) as *const std::os::raw::c_char, - ffi::PyBytes_AS_STRING(py_bytes.as_ptr()) as *const std::os::raw::c_char + ffi::PyBytes_AsString(py_bytes.as_ptr()) as *const std::ffi::c_char, + ffi::PyBytes_AS_STRING(py_bytes.as_ptr()) as *const std::ffi::c_char ); } }) diff --git a/src/types/capsule.rs b/src/types/capsule.rs index 22300c22d16..0d8849535ed 100644 --- a/src/types/capsule.rs +++ b/src/types/capsule.rs @@ -3,8 +3,8 @@ use crate::py_result_ext::PyResultExt; use crate::{ffi, PyAny}; use crate::{Bound, Python}; use crate::{PyErr, PyResult}; +use std::ffi::{c_char, c_int, c_void}; 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 @@ -168,7 +168,7 @@ pub trait PyCapsuleMethods<'py>: crate::sealed::Sealed { /// # Example /// /// ``` - /// use std::os::raw::c_void; + /// use std::ffi::c_void; /// use std::sync::mpsc::{channel, Sender}; /// use pyo3::{prelude::*, types::PyCapsule}; /// @@ -337,8 +337,8 @@ mod tests { use crate::types::capsule::PyCapsuleMethods; use crate::types::module::PyModuleMethods; use crate::{types::PyCapsule, Py, PyResult, Python}; + use std::ffi::c_void; use std::ffi::CString; - use std::os::raw::c_void; use std::sync::mpsc::{channel, Sender}; #[test] diff --git a/src/types/complex.rs b/src/types/complex.rs index 8f03a7cd1fd..d2624875733 100644 --- a/src/types/complex.rs +++ b/src/types/complex.rs @@ -3,7 +3,7 @@ use crate::py_result_ext::PyResultExt; #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] use crate::types::any::PyAnyMethods; use crate::{ffi, Bound, PyAny, Python}; -use std::os::raw::c_double; +use std::ffi::c_double; /// Represents a Python [`complex`](https://docs.python.org/3/library/functions.html#complex) object. /// diff --git a/src/types/datetime.rs b/src/types/datetime.rs index f5d51567262..d519d8a69d0 100644 --- a/src/types/datetime.rs +++ b/src/types/datetime.rs @@ -26,7 +26,7 @@ use crate::{sync::GILOnceCell, Py}; use crate::{types::IntoPyDict, PyTypeCheck}; use crate::{Borrowed, Bound, IntoPyObject, PyAny, PyErr, Python}; #[cfg(not(Py_LIMITED_API))] -use std::os::raw::c_int; +use std::ffi::c_int; #[cfg(not(Py_LIMITED_API))] fn ensure_datetime_api(py: Python<'_>) -> PyResult<&'static PyDateTime_CAPI> { diff --git a/src/types/dict.rs b/src/types/dict.rs index 8d01d9a5ab3..15d31e06f49 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -253,9 +253,9 @@ impl<'py> PyDictMethods<'py> for Bound<'py, PyDict> { match unsafe { ffi::compat::PyDict_GetItemRef(dict.as_ptr(), key.as_ptr(), &mut result) } { - std::os::raw::c_int::MIN..=-1 => Err(PyErr::fetch(py)), + std::ffi::c_int::MIN..=-1 => Err(PyErr::fetch(py)), 0 => Ok(None), - 1..=std::os::raw::c_int::MAX => { + 1..=std::ffi::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/float.rs b/src/types/float.rs index ba131026ad8..62a7ae40bcb 100644 --- a/src/types/float.rs +++ b/src/types/float.rs @@ -7,7 +7,7 @@ use crate::{ Python, }; use std::convert::Infallible; -use std::os::raw::c_double; +use std::ffi::c_double; /// Represents a Python `float` object. /// diff --git a/src/types/mappingproxy.rs b/src/types/mappingproxy.rs index 1f900d2af9d..478621a7163 100644 --- a/src/types/mappingproxy.rs +++ b/src/types/mappingproxy.rs @@ -8,7 +8,7 @@ use crate::types::any::PyAnyMethods; use crate::types::{PyAny, PyIterator, PyList}; use crate::{ffi, Python}; -use std::os::raw::c_int; +use std::ffi::c_int; /// Represents a Python `mappingproxy`. #[repr(transparent)] diff --git a/src/types/module.rs b/src/types/module.rs index a56f6dd06cc..ec6e15a2f92 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -11,9 +11,9 @@ use crate::types::{ use crate::{ exceptions, ffi, Borrowed, Bound, BoundObject, IntoPyObject, IntoPyObjectExt, PyObject, Python, }; -use std::ffi::CStr; #[cfg(all(not(Py_LIMITED_API), Py_GIL_DISABLED))] -use std::os::raw::c_int; +use std::ffi::c_int; +use std::ffi::CStr; use std::str; /// Represents a Python [`module`][1] object. diff --git a/src/types/weakref/anyref.rs b/src/types/weakref/anyref.rs index 967acb5a404..25c627402e9 100644 --- a/src/types/weakref/anyref.rs +++ b/src/types/weakref/anyref.rs @@ -326,9 +326,9 @@ impl<'py> PyWeakrefMethods<'py> for Bound<'py, PyWeakref> { 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)"), + std::ffi::c_int::MIN..=-1 => panic!("The 'weakref' weak reference instance should be valid (non-null and actually a weakref reference)"), 0 => None, - 1..=std::os::raw::c_int::MAX => Some(unsafe { obj.assume_owned_unchecked(self.py()) }), + 1..=std::ffi::c_int::MAX => Some(unsafe { obj.assume_owned_unchecked(self.py()) }), } } } diff --git a/src/types/weakref/proxy.rs b/src/types/weakref/proxy.rs index f786f817c88..7bff0868184 100644 --- a/src/types/weakref/proxy.rs +++ b/src/types/weakref/proxy.rs @@ -171,9 +171,9 @@ impl<'py> PyWeakrefMethods<'py> for Bound<'py, PyWeakrefProxy> { 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)"), + std::ffi::c_int::MIN..=-1 => panic!("The 'weakref.ProxyType' (or `weakref.CallableProxyType`) instance should be valid (non-null and actually a weakref reference)"), 0 => None, - 1..=std::os::raw::c_int::MAX => Some(unsafe { obj.assume_owned_unchecked(self.py()) }), + 1..=std::ffi::c_int::MAX => Some(unsafe { obj.assume_owned_unchecked(self.py()) }), } } } diff --git a/src/types/weakref/reference.rs b/src/types/weakref/reference.rs index cca4858f1e3..f008b2cd252 100644 --- a/src/types/weakref/reference.rs +++ b/src/types/weakref/reference.rs @@ -181,9 +181,9 @@ impl<'py> PyWeakrefMethods<'py> for Bound<'py, PyWeakrefReference> { 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)"), + std::ffi::c_int::MIN..=-1 => panic!("The 'weakref.ReferenceType' instance should be valid (non-null and actually a weakref reference)"), 0 => None, - 1..=std::os::raw::c_int::MAX => Some(unsafe { obj.assume_owned_unchecked(self.py()) }), + 1..=std::ffi::c_int::MAX => Some(unsafe { obj.assume_owned_unchecked(self.py()) }), } } } diff --git a/tests/test_buffer_protocol.rs b/tests/test_buffer_protocol.rs index 2243a04f0f2..d5f50d02e5d 100644 --- a/tests/test_buffer_protocol.rs +++ b/tests/test_buffer_protocol.rs @@ -8,7 +8,7 @@ use pyo3::ffi; use pyo3::prelude::*; use pyo3::types::IntoPyDict; use std::ffi::CString; -use std::os::raw::{c_int, c_void}; +use std::ffi::{c_int, c_void}; use std::ptr; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; diff --git a/tests/test_gc.rs b/tests/test_gc.rs index 2a90ab5a1eb..368fd3871bc 100644 --- a/tests/test_gc.rs +++ b/tests/test_gc.rs @@ -728,7 +728,7 @@ unsafe fn get_type_traverse(tp: *mut pyo3::ffi::PyTypeObject) -> Option std::os::raw::c_int { +) -> std::ffi::c_int { 0 } @@ -736,7 +736,7 @@ extern "C" fn novisit( extern "C" fn visit_error( _object: *mut pyo3::ffi::PyObject, _arg: *mut core::ffi::c_void, -) -> std::os::raw::c_int { +) -> std::ffi::c_int { -1 } diff --git a/tests/test_pyfunction.rs b/tests/test_pyfunction.rs index 3a2e2baeb3e..54447b92940 100644 --- a/tests/test_pyfunction.rs +++ b/tests/test_pyfunction.rs @@ -405,7 +405,7 @@ fn test_pycfunction_new() { #[test] fn test_pycfunction_new_with_keywords() { use pyo3::ffi; - use std::os::raw::c_long; + use std::ffi::c_long; use std::ptr; Python::attach(|py| { diff --git a/tests/ui/invalid_pymethods_buffer.rs b/tests/ui/invalid_pymethods_buffer.rs index 4eb7c28e091..d16eddee37a 100644 --- a/tests/ui/invalid_pymethods_buffer.rs +++ b/tests/ui/invalid_pymethods_buffer.rs @@ -6,7 +6,7 @@ struct MyClass {} #[pymethods] impl MyClass { #[pyo3(name = "__getbuffer__")] - fn getbuffer_must_be_unsafe(&self, _view: *mut pyo3::ffi::Py_buffer, _flags: std::os::raw::c_int) {} + fn getbuffer_must_be_unsafe(&self, _view: *mut pyo3::ffi::Py_buffer, _flags: std::ffi::c_int) {} } #[pymethods] diff --git a/tests/ui/invalid_pymethods_buffer.stderr b/tests/ui/invalid_pymethods_buffer.stderr index 3480848f3cb..899d4a569f4 100644 --- a/tests/ui/invalid_pymethods_buffer.stderr +++ b/tests/ui/invalid_pymethods_buffer.stderr @@ -1,7 +1,7 @@ error: `__getbuffer__` must be `unsafe fn` --> tests/ui/invalid_pymethods_buffer.rs:9:8 | -9 | fn getbuffer_must_be_unsafe(&self, _view: *mut pyo3::ffi::Py_buffer, _flags: std::os::raw::c_int) {} +9 | fn getbuffer_must_be_unsafe(&self, _view: *mut pyo3::ffi::Py_buffer, _flags: std::ffi::c_int) {} | ^^^^^^^^^^^^^^^^^^^^^^^^ error: `__releasebuffer__` must be `unsafe fn` diff --git a/tests/ui/pyclass_send.rs b/tests/ui/pyclass_send.rs index 5faded9874f..1a764f621f0 100644 --- a/tests/ui/pyclass_send.rs +++ b/tests/ui/pyclass_send.rs @@ -1,5 +1,5 @@ use pyo3::prelude::*; -use std::os::raw::c_void; +use std::ffi::c_void; #[pyclass] struct NotSyncNotSend(*mut c_void); From 8b94de6218dae7149f07e0766945019fdcda865a Mon Sep 17 00:00:00 2001 From: Thomas Tanon Date: Fri, 15 Aug 2025 23:01:14 +0200 Subject: [PATCH 794/936] Introspection: Properly generate output type annotation for #[pyclass] (#5320) --- newsfragments/5320.added.md | 1 + pyo3-macros-backend/src/pyclass.rs | 25 ++++++++++++++++++------- pytests/stubs/consts.pyi | 3 ++- 3 files changed, 21 insertions(+), 8 deletions(-) create mode 100644 newsfragments/5320.added.md diff --git a/newsfragments/5320.added.md b/newsfragments/5320.added.md new file mode 100644 index 00000000000..bbea32386e3 --- /dev/null +++ b/newsfragments/5320.added.md @@ -0,0 +1 @@ +Introspection: Properly generate output type annotation for #[pyclass] \ No newline at end of file diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index f0803172ddb..c21373f8ed9 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -407,6 +407,16 @@ fn get_class_python_name<'a>(cls: &'a syn::Ident, args: &'a PyClassArgs) -> Cow< .unwrap_or_else(|| Cow::Owned(cls.unraw())) } +fn get_class_python_module_and_name<'a>(cls: &'a Ident, args: &'a PyClassArgs) -> String { + let name = get_class_python_name(cls, args); + if let Some(module) = &args.options.module { + let value = module.value.value(); + format!("{value}.{name}") + } else { + name.to_string() + } +} + fn impl_class( cls: &syn::Ident, args: &PyClassArgs, @@ -2140,13 +2150,7 @@ impl<'a> PyClassImplsBuilder<'a> { let cls = self.cls; let input_type = if cfg!(feature = "experimental-inspect") { - let cls_name = get_class_python_name(cls, self.attr).to_string(); - let full_name = if let Some(ModuleAttribute { value, .. }) = &self.attr.options.module { - let value = value.value(); - format!("{value}.{cls_name}") - } else { - cls_name - }; + let full_name = get_class_python_module_and_name(cls, self.attr); quote! { const INPUT_TYPE: &'static str = #full_name; } } else { quote! {} @@ -2200,11 +2204,18 @@ impl<'a> PyClassImplsBuilder<'a> { let attr = self.attr; // If #cls is not extended type, we allow Self->PyObject conversion if attr.options.extends.is_none() { + let output_type = if cfg!(feature = "experimental-inspect") { + let full_name = get_class_python_module_and_name(cls, self.attr); + quote! { const OUTPUT_TYPE: &'static str = #full_name; } + } else { + quote! {} + }; quote! { impl<'py> #pyo3_path::conversion::IntoPyObject<'py> for #cls { type Target = Self; type Output = #pyo3_path::Bound<'py, >::Target>; type Error = #pyo3_path::PyErr; + #output_type fn into_pyobject(self, py: #pyo3_path::Python<'py>) -> ::std::result::Result< ::Output, diff --git a/pytests/stubs/consts.pyi b/pytests/stubs/consts.pyi index ddbd6e7a09a..45c1d5fcbfb 100644 --- a/pytests/stubs/consts.pyi +++ b/pytests/stubs/consts.pyi @@ -1,7 +1,8 @@ +import consts import typing PI: typing.Final[float] SIMPLE: typing.Final = "SIMPLE" class ClassWithConst: - INSTANCE: typing.Final[typing.Any] + INSTANCE: typing.Final[consts.ClassWithConst] From fa0427cb959e4d3d147b4b0e5641fee0c71e3a4f Mon Sep 17 00:00:00 2001 From: Olga Pustovalova <162949+olp-cs@users.noreply.github.com> Date: Sat, 16 Aug 2025 00:01:23 +0300 Subject: [PATCH 795/936] docs: Add recent PyO3 videos to README; fix expired links (#5319) --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d7cf8421407..72af4eee9ff 100644 --- a/README.md +++ b/README.md @@ -236,9 +236,12 @@ about this topic. ## Articles and other media +- [(Video) Using Rust in Free-Threaded vs Regular Python 3.13](https://www.youtube.com/watch?v=J7phN_M4GLM) - Jun 4, 2025 +- [(Video) Techniques learned from five years finding the way for Rust in Python](https://www.youtube.com/watch?v=KTQn_PTHNCw) - Feb 26, 2025 +- [(Podcast) Bridging Python and Rust: An Interview with PyO3 Maintainer David Hewitt](https://www.youtube.com/watch?v=P47JUMSQagU) - Aug 30, 2024 - [(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) How Python Harnesses Rust through PyO3](https://www.youtube.com/watch?v=UilujdubqVU) - 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 @@ -250,7 +253,7 @@ about this topic. - [Calling Rust from Python using PyO3](https://saidvandeklundert.net/learn/2021-11-18-calling-rust-from-python-using-pyo3/) - Nov 18, 2021 - [davidhewitt's 2021 talk at Rust Manchester meetup](https://www.youtube.com/watch?v=-XyWG_klSAw&t=320s) - Aug 19, 2021 - [Incrementally porting a small Python project to Rust](https://blog.waleedkhan.name/port-python-to-rust/) - Apr 29, 2021 -- [Vortexa - Integrating Rust into Python](https://www.vortexa.com/insight/integrating-rust-into-python) - Apr 12, 2021 +- [Vortexa - Integrating Rust into Python](https://www.vortexa.com/blog/integrating-rust-into-python) - Apr 12, 2021 - [Writing and publishing a Python module in Rust](https://blog.yossarian.net/2020/08/02/Writing-and-publishing-a-python-module-in-rust) - Aug 2, 2020 ## Contributing From 4e24c89b6b8dfa150e4cc1ef963b31da3d6bcada Mon Sep 17 00:00:00 2001 From: Thomas Tanon Date: Fri, 15 Aug 2025 23:01:33 +0200 Subject: [PATCH 796/936] Introspection: expose module members gated behind #[cfg(...)] (#5318) --- newsfragments/5318.fixed.md | 1 + pyo3-macros-backend/src/introspection.rs | 75 ++++++++++++++++-------- pytests/stubs/pyclasses.pyi | 3 + 3 files changed, 54 insertions(+), 25 deletions(-) create mode 100644 newsfragments/5318.fixed.md diff --git a/newsfragments/5318.fixed.md b/newsfragments/5318.fixed.md new file mode 100644 index 00000000000..7d2a8770f3c --- /dev/null +++ b/newsfragments/5318.fixed.md @@ -0,0 +1 @@ +Introspection: properly include module members gated behind `#[cfg(...)]` attributes \ No newline at end of file diff --git a/pyo3-macros-backend/src/introspection.rs b/pyo3-macros-backend/src/introspection.rs index 7e0348eba32..868bfe18fb5 100644 --- a/pyo3-macros-backend/src/introspection.rs +++ b/pyo3-macros-backend/src/introspection.rs @@ -43,14 +43,9 @@ pub fn module_introspection_code<'a>( members .into_iter() .zip(members_cfg_attrs) - .filter_map(|(member, attributes)| { - if attributes.is_empty() { - Some(IntrospectionNode::IntrospectionId(Some(ident_to_type( - member, - )))) - } else { - None // TODO: properly interpret cfg attributes - } + .map(|(member, attributes)| AttributedIntrospectionNode { + node: IntrospectionNode::IntrospectionId(Some(ident_to_type(member))), + attributes, }) .collect(), ), @@ -138,7 +133,7 @@ pub fn function_introspection_code( } let decorators = decorators .into_iter() - .map(|d| IntrospectionNode::String(d.into())) + .map(|d| IntrospectionNode::String(d.into()).into()) .collect::>(); if !decorators.is_empty() { desc.insert("decorators", IntrospectionNode::List(decorators)); @@ -220,9 +215,12 @@ fn arguments_introspection_data<'a>( let mut kwarg = None; if let Some(first_argument) = first_argument { - posonlyargs.push(IntrospectionNode::Map( - [("name", IntrospectionNode::String(first_argument.into()))].into(), - )); + posonlyargs.push( + IntrospectionNode::Map( + [("name", IntrospectionNode::String(first_argument.into()))].into(), + ) + .into(), + ); } for (i, param) in signature @@ -296,7 +294,7 @@ fn argument_introspection_data<'a>( name: &'a str, desc: &'a RegularArg<'_>, class_type: Option<&Type>, -) -> IntrospectionNode<'a> { +) -> AttributedIntrospectionNode<'a> { let mut params: HashMap<_, _> = [("name", IntrospectionNode::String(name.into()))].into(); if desc.default_value.is_some() { params.insert( @@ -338,7 +336,7 @@ fn argument_introspection_data<'a>( ); } } - IntrospectionNode::Map(params) + IntrospectionNode::Map(params).into() } enum IntrospectionNode<'a> { @@ -348,7 +346,7 @@ enum IntrospectionNode<'a> { InputType { rust_type: Type, nullable: bool }, OutputType { rust_type: Type, is_final: bool }, Map(HashMap<&'static str, IntrospectionNode<'a>>), - List(Vec>), + List(Vec>), } impl IntrospectionNode<'_> { @@ -381,9 +379,9 @@ impl IntrospectionNode<'_> { Self::IntrospectionId(ident) => { content.push_str("\""); content.push_tokens(if let Some(ident) = ident { - quote! { #ident::_PYO3_INTROSPECTION_ID } + quote! { #ident::_PYO3_INTROSPECTION_ID.as_bytes() } } else { - quote! { _PYO3_INTROSPECTION_ID } + quote! { _PYO3_INTROSPECTION_ID.as_bytes() } }); content.push_str("\""); } @@ -392,7 +390,7 @@ impl IntrospectionNode<'_> { nullable, } => { content.push_str("\""); - content.push_tokens(quote! { <#rust_type as #pyo3_crate_path::impl_::extract_argument::PyFunctionArgument>::INPUT_TYPE }); + content.push_tokens(quote! { <#rust_type as #pyo3_crate_path::impl_::extract_argument::PyFunctionArgument>::INPUT_TYPE.as_bytes() }); if nullable { content.push_str(" | None"); } @@ -406,7 +404,7 @@ impl IntrospectionNode<'_> { if is_final { content.push_str("typing.Final["); } - content.push_tokens(quote! { <#rust_type as #pyo3_crate_path::impl_::introspection::PyReturnType>::OUTPUT_TYPE }); + content.push_tokens(quote! { <#rust_type as #pyo3_crate_path::impl_::introspection::PyReturnType>::OUTPUT_TYPE.as_bytes() }); if is_final { content.push_str("]"); } @@ -426,11 +424,24 @@ impl IntrospectionNode<'_> { } Self::List(list) => { content.push_str("["); - for (i, value) in list.into_iter().enumerate() { - if i > 0 { - content.push_str(","); + for (i, AttributedIntrospectionNode { node, attributes }) in + list.into_iter().enumerate() + { + if attributes.is_empty() { + if i > 0 { + content.push_str(","); + } + node.add_to_serialization(content, pyo3_crate_path); + } else { + // We serialize the element to easily gate it behind the attributes + let mut nested_builder = ConcatenationBuilder::default(); + if i > 0 { + nested_builder.push_str(","); + } + node.add_to_serialization(&mut nested_builder, pyo3_crate_path); + let nested_content = nested_builder.into_token_stream(pyo3_crate_path); + content.push_tokens(quote! { #(#attributes)* #nested_content }); } - value.add_to_serialization(content, pyo3_crate_path); } content.push_str("]"); } @@ -438,6 +449,20 @@ impl IntrospectionNode<'_> { } } +struct AttributedIntrospectionNode<'a> { + node: IntrospectionNode<'a>, + attributes: &'a [Attribute], +} + +impl<'a> From> for AttributedIntrospectionNode<'a> { + fn from(node: IntrospectionNode<'a>) -> Self { + Self { + node, + attributes: &[], + } + } +} + #[derive(Default)] struct ConcatenationBuilder { elements: Vec, @@ -490,7 +515,7 @@ impl ConcatenationBuilder { quote! { { - const PIECES: &[&[u8]] = &[#(#elements.as_bytes() , )*]; + const PIECES: &[&[u8]] = &[#(#elements , )*]; &#pyo3_crate_path::impl_::concat::combine_to_array::<{ #pyo3_crate_path::impl_::concat::combined_len(PIECES) }>(PIECES) @@ -507,7 +532,7 @@ enum ConcatenationBuilderElement { impl ToTokens for ConcatenationBuilderElement { fn to_tokens(&self, tokens: &mut TokenStream) { match self { - Self::String(s) => s.to_tokens(tokens), + Self::String(s) => quote! { #s.as_bytes() }.to_tokens(tokens), Self::TokenStream(ts) => ts.to_tokens(tokens), } } diff --git a/pytests/stubs/pyclasses.pyi b/pytests/stubs/pyclasses.pyi index 34486cada01..27c825d7d04 100644 --- a/pytests/stubs/pyclasses.pyi +++ b/pytests/stubs/pyclasses.pyi @@ -17,6 +17,9 @@ class ClassWithDecorators: @staticmethod def static_method() -> int: ... +class ClassWithDict: + def __new__(cls, /) -> None: ... + class ClassWithoutConstructor: ... class EmptyClass: From 700b4cca6aa7f14b6688a3523e8550d79c077dc8 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Fri, 15 Aug 2025 23:11:03 +0200 Subject: [PATCH 797/936] add `PyClassGuard(Mut)::map` (#5311) --- src/pycell/impl_.rs | 4 +- src/pyclass.rs | 2 +- src/pyclass/guard.rs | 152 ++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 155 insertions(+), 3 deletions(-) diff --git a/src/pycell/impl_.rs b/src/pycell/impl_.rs index c3d697d56d0..3401ed8a8b3 100644 --- a/src/pycell/impl_.rs +++ b/src/pycell/impl_.rs @@ -98,7 +98,9 @@ pub struct BorrowChecker(BorrowFlag); pub trait PyClassBorrowChecker { /// Initial value for self - fn new() -> Self; + fn new() -> Self + where + Self: Sized; /// Increments immutable borrow count, if possible fn try_borrow(&self) -> Result<(), PyBorrowError>; diff --git a/src/pyclass.rs b/src/pyclass.rs index 5d212a810f7..cd21f8876ed 100644 --- a/src/pyclass.rs +++ b/src/pyclass.rs @@ -9,7 +9,7 @@ mod guard; pub(crate) use self::create_type_object::{create_type_object, PyClassTypeObject}; pub use self::gc::{PyTraverseError, PyVisit}; -pub use self::guard::{PyClassGuard, PyClassGuardMut}; +pub use self::guard::{PyClassGuard, PyClassGuardMap, PyClassGuardMut}; /// Types that can be used as Python classes. /// diff --git a/src/pyclass/guard.rs b/src/pyclass/guard.rs index 91f4c95e387..9ee045c1ab6 100644 --- a/src/pyclass/guard.rs +++ b/src/pyclass/guard.rs @@ -106,6 +106,38 @@ impl<'a, T: PyClass> PyClassGuard<'a, T> { // valid for at least 'a unsafe { self.ptr.cast().as_ref() } } + + /// Consumes the [`PyClassGuard`] and returns a [`PyClassGuardMap`] for a component of the + /// borrowed data + /// + /// # Examples + /// + /// ``` + /// # use pyo3::prelude::*; + /// # use pyo3::PyClassGuard; + /// + /// #[pyclass] + /// pub struct MyClass { + /// msg: String, + /// } + /// + /// # Python::attach(|py| { + /// let obj = Bound::new(py, MyClass { msg: String::from("hello") })?; + /// let msg = obj.extract::>()?.map(|c| &c.msg); + /// assert_eq!(&*msg, "hello"); + /// # Ok::<_, PyErr>(()) + /// # }).unwrap(); + /// ``` + pub fn map(self, f: F) -> PyClassGuardMap<'a, U, false> + where + F: FnOnce(&T) -> &U, + { + let slf = std::mem::ManuallyDrop::new(self); // the borrow is released when dropping the `PyClassGuardMap` + PyClassGuardMap { + ptr: NonNull::from(f(&slf)), + checker: slf.as_class_object().borrow_checker(), + } + } } impl<'a, T, U> PyClassGuard<'a, T> @@ -521,6 +553,38 @@ impl<'a, T: PyClass> PyClassGuardMut<'a, T> { // valid for at least 'a unsafe { self.ptr.cast().as_ref() } } + + /// Consumes the [`PyClassGuardMut`] and returns a [`PyClassGuardMap`] for a component of the + /// borrowed data + /// + /// # Examples + /// + /// ``` + /// # use pyo3::prelude::*; + /// # use pyo3::PyClassGuardMut; + /// + /// #[pyclass] + /// pub struct MyClass { + /// data: [i32; 100], + /// } + /// + /// # Python::attach(|py| { + /// let obj = Bound::new(py, MyClass { data: [0; 100] })?; + /// let mut data = obj.extract::>()?.map(|c| c.data.as_mut_slice()); + /// data[0] = 42; + /// # Ok::<_, PyErr>(()) + /// # }).unwrap(); + /// ``` + pub fn map(self, f: F) -> PyClassGuardMap<'a, U, true> + where + F: FnOnce(&mut T) -> &mut U, + { + let mut slf = std::mem::ManuallyDrop::new(self); // the borrow is released when dropping the `PyClassGuardMap` + PyClassGuardMap { + ptr: NonNull::from(f(&mut slf)), + checker: slf.as_class_object().borrow_checker(), + } + } } impl<'a, T, U> PyClassGuardMut<'a, T> @@ -620,11 +684,45 @@ unsafe impl> crate::marker::Ungil for PyClassGuardMut unsafe impl + Send + Sync> Send for PyClassGuardMut<'_, T> {} unsafe impl + Sync> Sync for PyClassGuardMut<'_, T> {} +/// Wraps a borrowed reference `U` to a value stored inside of a pyclass `T` +/// +/// See [`PyClassGuard::map`] and [`PyClassGuardMut::map`] +pub struct PyClassGuardMap<'a, U: ?Sized, const MUT: bool> { + ptr: NonNull, + checker: &'a dyn PyClassBorrowChecker, +} + +impl Deref for PyClassGuardMap<'_, U, MUT> { + type Target = U; + + fn deref(&self) -> &U { + // SAFETY: `checker` guards our access to the `T` that `U` points into + unsafe { self.ptr.as_ref() } + } +} + +impl DerefMut for PyClassGuardMap<'_, U, true> { + fn deref_mut(&mut self) -> &mut Self::Target { + // SAFETY: `checker` guards our access to the `T` that `U` points into + unsafe { self.ptr.as_mut() } + } +} + +impl Drop for PyClassGuardMap<'_, U, MUT> { + fn drop(&mut self) { + if MUT { + self.checker.release_borrow_mut(); + } else { + self.checker.release_borrow(); + } + } +} + #[cfg(test)] #[cfg(feature = "macros")] mod tests { use super::{PyClassGuard, PyClassGuardMut}; - use crate::{types::PyAnyMethods as _, IntoPyObject as _, Py, PyErr, Python}; + use crate::{types::PyAnyMethods as _, Bound, IntoPyObject as _, Py, PyErr, Python}; #[test] fn test_into_frozen_super_released_borrow() { @@ -831,4 +929,56 @@ mod tests { crate::py_run!(py, obj, "assert obj.get_values() == (20, 30, 40)"); }); } + + #[crate::pyclass] + #[pyo3(crate = "crate")] + pub struct MyClass { + data: [i32; 100], + } + + #[test] + fn test_pyclassguard_map() { + Python::attach(|py| { + let obj = Bound::new(py, MyClass { data: [0; 100] })?; + let data = PyClassGuard::try_borrow(obj.as_unbound())?.map(|c| &c.data); + assert_eq!(data[0], 0); + assert!(obj.try_borrow_mut().is_err()); // obj is still protected + drop(data); + assert!(obj.try_borrow_mut().is_ok()); // drop released shared borrow + Ok::<_, PyErr>(()) + }) + .unwrap() + } + + #[test] + fn test_pyclassguardmut_map() { + Python::attach(|py| { + let obj = Bound::new(py, MyClass { data: [0; 100] })?; + let mut data = + PyClassGuardMut::try_borrow_mut(obj.as_unbound())?.map(|c| c.data.as_mut_slice()); + assert_eq!(data[0], 0); + data[0] = 5; + assert_eq!(data[0], 5); + assert!(obj.try_borrow_mut().is_err()); // obj is still protected + drop(data); + assert!(obj.try_borrow_mut().is_ok()); // drop released mutable borrow + Ok::<_, PyErr>(()) + }) + .unwrap() + } + + #[test] + fn test_pyclassguard_map_unrelated() { + use crate::types::{PyString, PyStringMethods}; + Python::attach(|py| { + let obj = Bound::new(py, MyClass { data: [0; 100] })?; + let string = PyString::new(py, "pyo3"); + // It is possible to return something not borrowing from the guard, but that shouldn't + // matter. `RefCell` has the same behaviour + let refmap = PyClassGuard::try_borrow(obj.as_unbound())?.map(|_| &string); + assert_eq!(refmap.to_cow()?, "pyo3"); + Ok::<_, PyErr>(()) + }) + .unwrap() + } } From dd9e404aedd9f2c34b48fcbe269d9c8d18e5a831 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Fri, 15 Aug 2025 21:34:13 -0400 Subject: [PATCH 798/936] Fix comment on PyBuffer::buf_ptr (#5327) --- src/buffer.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index 6cd5ff03ad7..722a2b82852 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -229,8 +229,13 @@ impl PyBuffer { /// Gets the pointer to the start of the buffer memory. /// - /// Warning: the buffer memory might be mutated by other Python functions, - /// and thus may only be accessed while the GIL is held. + /// Warning: the buffer memory can be mutated by other code (including + /// other Python functions, if the GIL is released, or other extension + /// modules even if the GIL is held). You must either access memory + /// atomically, or ensure there are no data races yourself. See + /// [this blog post] for more details. + /// + /// [this blog post]: https://alexgaynor.net/2022/oct/23/buffers-on-the-edge/ #[inline] pub fn buf_ptr(&self) -> *mut c_void { self.0.buf From 4bef3243388f66d45b8304cb2ace2dc43b18e039 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sun, 17 Aug 2025 00:31:40 +0200 Subject: [PATCH 799/936] deprecate `PyObject` type alias (#5325) * deprecate `PyObject` type alias * fix typo Co-authored-by: Lily Acorn --------- Co-authored-by: Lily Acorn --- guide/src/async-await.md | 2 +- guide/src/class.md | 4 +- guide/src/class/protocols.md | 10 +-- guide/src/conversions/tables.md | 3 +- guide/src/conversions/traits.md | 7 +-- guide/src/function/signature.md | 2 +- guide/src/migration.md | 9 +++ guide/src/python-from-rust/function-calls.md | 4 +- guide/src/python-typing-hints.md | 2 +- guide/src/trait-bounds.md | 2 +- guide/src/types.md | 8 +-- newsfragments/5325.changed.md | 1 + pyo3-macros-backend/src/pyclass.rs | 12 ++-- pyo3-macros-backend/src/pyimpl.rs | 2 +- pyo3-macros-backend/src/pymethod.rs | 2 +- pytests/src/awaitable.rs | 10 +-- pytests/src/objstore.rs | 2 +- src/conversions/std/path.rs | 4 +- src/coroutine.rs | 13 ++-- src/coroutine/cancel.rs | 10 +-- src/coroutine/waker.rs | 8 +-- src/err/err_state.rs | 12 ++-- src/err/impls.rs | 10 +-- src/err/mod.rs | 10 +-- src/impl_/callback.rs | 6 +- src/impl_/pyclass/lazy_type_object.rs | 7 +-- src/impl_/pymethods.rs | 6 +- src/impl_/wrap.rs | 8 ++- src/instance.rs | 35 ++++++----- src/internal/state.rs | 10 +-- src/lib.rs | 4 +- src/marker.rs | 8 +-- src/prelude.rs | 4 +- src/tests/common.rs | 4 +- src/types/any.rs | 4 +- src/types/bytearray.rs | 4 +- src/types/module.rs | 6 +- src/types/sequence.rs | 4 +- src/types/string.rs | 4 +- tests/test_arithmetics.rs | 4 +- tests/test_class_basics.rs | 8 +-- tests/test_class_new.rs | 2 +- tests/test_gc.rs | 8 +-- tests/test_mapping.rs | 4 +- tests/test_methods.rs | 34 +++++----- tests/test_module.rs | 4 +- tests/test_proto_methods.rs | 22 +++---- tests/test_pyself.rs | 2 +- tests/test_sequence.rs | 8 +-- tests/ui/ambiguous_associated_items.rs | 12 ++-- tests/ui/invalid_pyclass_args.rs | 2 +- tests/ui/invalid_pyclass_generic.rs | 5 +- tests/ui/invalid_pyclass_generic.stderr | 66 ++++++++++---------- 53 files changed, 229 insertions(+), 215 deletions(-) create mode 100644 newsfragments/5325.changed.md diff --git a/guide/src/async-await.md b/guide/src/async-await.md index 9e53ca8dc7a..81d57ef3341 100644 --- a/guide/src/async-await.md +++ b/guide/src/async-await.md @@ -13,7 +13,7 @@ use pyo3::prelude::*; #[pyfunction] #[pyo3(signature=(seconds, result=None))] -async fn sleep(seconds: f64, result: Option) -> Option { +async fn sleep(seconds: f64, result: Option>) -> Option> { let (tx, rx) = oneshot::channel(); thread::spawn(move || { thread::sleep(Duration::from_secs_f64(seconds)); diff --git a/guide/src/class.md b/guide/src/class.md index 7d10dce9494..8193b3bdfae 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -413,7 +413,7 @@ impl SubSubClass { } #[staticmethod] - fn factory_method(py: Python<'_>, val: usize) -> PyResult { + fn factory_method(py: Python<'_>, val: usize) -> PyResult> { let base = PyClassInitializer::from(BaseClass::new()); let sub = base.add_subclass(SubClass { val2: val }); if val % 2 == 0 { @@ -748,7 +748,7 @@ To create a constructor which takes a positional class argument, you can combine # use pyo3::prelude::*; # use pyo3::types::PyType; # #[pyclass] -# struct BaseClass(PyObject); +# struct BaseClass(Py); # #[pymethods] impl BaseClass { diff --git a/guide/src/class/protocols.md b/guide/src/class/protocols.md index e4ed8b6dae5..45a7126a9b2 100644 --- a/guide/src/class/protocols.md +++ b/guide/src/class/protocols.md @@ -59,7 +59,7 @@ given signatures should be interpreted as follows: #[pymethods] impl NotHashable { #[classattr] - const __hash__: Option = None; + const __hash__: Option> = None; } ```
@@ -162,7 +162,7 @@ use std::sync::Mutex; #[pyclass] struct MyIterator { - iter: Mutex + Send>>, + iter: Mutex> + Send>>, } #[pymethods] @@ -170,7 +170,7 @@ impl MyIterator { fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { slf } - fn __next__(slf: PyRefMut<'_, Self>) -> Option { + fn __next__(slf: PyRefMut<'_, Self>) -> Option> { slf.iter.lock().unwrap().next() } } @@ -283,7 +283,7 @@ Use the `#[pyclass(sequence)]` annotation to instruct PyO3 to fill the `sq_lengt #[pymethods] impl NoContains { #[classattr] - const __contains__: Option = None; + const __contains__: Option> = None; } ```
@@ -439,7 +439,7 @@ use pyo3::gc::PyVisit; #[pyclass] struct ClassWithGCSupport { - obj: Option, + obj: Option>, } #[pymethods] diff --git a/guide/src/conversions/tables.md b/guide/src/conversions/tables.md index 7fc09657aff..9c2bd0add1f 100644 --- a/guide/src/conversions/tables.md +++ b/guide/src/conversions/tables.md @@ -1,6 +1,6 @@ ## Mapping of Rust types to Python types -When writing functions callable from Python (such as a `#[pyfunction]` or in a `#[pymethods]` block), the trait `FromPyObject` is required for function arguments, and `IntoPy` is required for function return values. +When writing functions callable from Python (such as a `#[pyfunction]` or in a `#[pymethods]` block), the trait `FromPyObject` is required for function arguments, and `IntoPyObject` is required for function return values. Consult the tables in the following section to find the Rust types provided by PyO3 which implement these traits. @@ -54,7 +54,6 @@ It is also worth remembering the following special types: | `Python<'py>` | A GIL token, used to pass to PyO3 constructors to prove ownership of the GIL. | | `Bound<'py, T>` | A Python object connected to the GIL lifetime. This provides access to most of PyO3's APIs. | | `Py` | A Python object isolated from the GIL lifetime. This can be sent to other threads. | -| `PyObject` | An alias for `Py` | | `PyRef` | A `#[pyclass]` borrowed immutably. | | `PyRefMut` | A `#[pyclass]` borrowed mutably. | diff --git a/guide/src/conversions/traits.md b/guide/src/conversions/traits.md index a7c8d03bd4b..704a43d9aab 100644 --- a/guide/src/conversions/traits.md +++ b/guide/src/conversions/traits.md @@ -580,7 +580,7 @@ forward the implementation to the inner type. // newtype tuple structs are implicitly `transparent` #[derive(IntoPyObject)] -struct TransparentTuple(PyObject); +struct TransparentTuple(Py); #[derive(IntoPyObject)] #[pyo3(transparent)] @@ -599,7 +599,7 @@ For `enum`s each variant is converted according to the rules for `struct`s above #[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 + TransparentTuple(Py), // rules on the variants as the structs above #[pyo3(transparent)] TransparentStruct { inner: Bound<'py, PyAny> }, Tuple(&'a str, HashMap), @@ -645,7 +645,7 @@ demonstrated below. ```rust,no_run # use pyo3::prelude::*; # #[allow(dead_code)] -struct MyPyObjectWrapper(PyObject); +struct MyPyObjectWrapper(Py); impl<'py> IntoPyObject<'py> for MyPyObjectWrapper { type Target = PyAny; // the Python type @@ -741,7 +741,6 @@ In the example above we used `BoundObject::into_any` and `BoundObject::unbind` t [`FromPyObject`]: {{#PYO3_DOCS_URL}}/pyo3/conversion/trait.FromPyObject.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 diff --git a/guide/src/function/signature.md b/guide/src/function/signature.md index 913e9d4c3ec..2f578366782 100644 --- a/guide/src/function/signature.md +++ b/guide/src/function/signature.md @@ -82,7 +82,7 @@ Arguments of type `Python` must not be part of the signature: # use pyo3::prelude::*; #[pyfunction] #[pyo3(signature = (lambda))] -pub fn simple_python_bound_function(py: Python<'_>, lambda: PyObject) -> PyResult<()> { +pub fn simple_python_bound_function(py: Python<'_>, lambda: Py) -> PyResult<()> { Ok(()) } ``` diff --git a/guide/src/migration.md b/guide/src/migration.md index 9306525cf11..4a48dbfabef 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -16,6 +16,14 @@ For this reason we chose to rename these to more modern terminology introduced i - `pyo3::prepare_freethreaded_python` is now called `Python::initialize`.
+### Deprecation of `PyObject` type alias +
+Click to expand + +The type alias `PyObject` (aka `Py`) is often confused with the identically named FFI definition `pyo3::ffi::PyObject`. For this reason we are deprecating its usage. To migrate simply replace its usage by the target type `Py`. + +
+ ### Deprecation of `GILProtected`
Click to expand @@ -186,6 +194,7 @@ impl ToPyObject for MyPyObjectWrapper { After: ```rust,no_run +# #![allow(deprecated)] # use pyo3::prelude::*; # #[allow(dead_code)] # struct MyPyObjectWrapper(PyObject); diff --git a/guide/src/python-from-rust/function-calls.md b/guide/src/python-from-rust/function-calls.md index ed8454fe431..6fde4cda33e 100644 --- a/guide/src/python-from-rust/function-calls.md +++ b/guide/src/python-from-rust/function-calls.md @@ -12,9 +12,9 @@ Both of these APIs take `args` and `kwargs` arguments (for positional and keywor * [`call1`]({{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.call1) and [`call_method1`]({{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.call_method1) to call only with positional `args`. * [`call0`]({{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.call0) and [`call_method0`]({{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.call_method0) to call with no arguments. -For convenience the [`Py`](../types.md#pyt-and-pyobject) smart pointer also exposes these same six API methods, but needs a `Python` token as an additional first argument to prove the GIL is held. +For convenience the [`Py`](../types.md#pyt) smart pointer also exposes these same six API methods, but needs a `Python` token as an additional first argument to prove the GIL is held. -The example below calls a Python function behind a `PyObject` (aka `Py`) reference: +The example below calls a Python function behind a `Py` reference: ```rust use pyo3::prelude::*; diff --git a/guide/src/python-typing-hints.md b/guide/src/python-typing-hints.md index 76ad0b6aa21..d2c755b0712 100644 --- a/guide/src/python-typing-hints.md +++ b/guide/src/python-typing-hints.md @@ -230,7 +230,7 @@ impl MyClass { pub fn __class_getitem__( cls: &Bound<'_, PyType>, key: &Bound<'_, PyAny>, - ) -> PyResult { + ) -> PyResult> { /* implementation details */ } } diff --git a/guide/src/trait-bounds.md b/guide/src/trait-bounds.md index 0f14d8e3783..3ac56b68035 100644 --- a/guide/src/trait-bounds.md +++ b/guide/src/trait-bounds.md @@ -44,7 +44,7 @@ How could we expose this solver to Python thanks to PyO3 ? ## Implementation of the trait bounds for the Python class If a Python class implements the same three methods as the `Model` trait, it seems logical it could be adapted to use the solver. -However, it is not possible to pass a `PyObject` to it as it does not implement the Rust trait (even if the Python model has the required methods). +However, it is not possible to pass a `Py` to it as it does not implement the Rust trait (even if the Python model has the required methods). In order to implement the trait, we must write a wrapper around the calls in Rust to the Python model. The method signatures must be the same as the trait, keeping in mind that the Rust trait cannot be changed for the purpose of making the code available in Python. diff --git a/guide/src/types.md b/guide/src/types.md index 891d639e02e..50d98aac41e 100644 --- a/guide/src/types.md +++ b/guide/src/types.md @@ -22,9 +22,9 @@ The recommendation of when to use each of these smart pointers is as follows: The sections below also explain these smart pointers in a little more detail. -### `Py` (and `PyObject`) +### `Py` -[`Py`][Py] is the foundational smart pointer in PyO3's API. The type parameter `T` denotes the type of the Python object. Very frequently this is `PyAny`, meaning any Python object. This is so common that `Py` has a type alias `PyObject`. +[`Py`][Py] is the foundational smart pointer in PyO3's API. The type parameter `T` denotes the type of the Python object. Very frequently this is `PyAny`, meaning any Python object. Because `Py` is not bound to [the `'py` lifetime](./python-from-rust.md#the-py-lifetime), it is the type to use when storing a Python object inside a Rust `struct` or `enum` which do not want to have a lifetime parameter. In particular, [`#[pyclass]`][pyclass] types are not permitted to have a lifetime, so `Py` is the correct type to store Python objects inside them. @@ -117,11 +117,11 @@ fn add<'py>( # }) ``` -If naming the `'py` lifetime adds unwanted complexity to the function signature, it is also acceptable to return `PyObject` (aka `Py`), which has no lifetime. The cost is instead paid by a slight increase in implementation complexity, as seen by the introduction of a call to [`Bound::unbind`]: +If naming the `'py` lifetime adds unwanted complexity to the function signature, it is also acceptable to return `Py`, which has no lifetime. The cost is instead paid by a slight increase in implementation complexity, as seen by the introduction of a call to [`Bound::unbind`]: ```rust # use pyo3::prelude::*; -fn add(left: &Bound<'_, PyAny>, right: &Bound<'_, PyAny>) -> PyResult { +fn add(left: &Bound<'_, PyAny>, right: &Bound<'_, PyAny>) -> PyResult> { let output: Bound<'_, PyAny> = left.add(right)?; Ok(output.unbind()) } diff --git a/newsfragments/5325.changed.md b/newsfragments/5325.changed.md new file mode 100644 index 00000000000..aba0a81cc50 --- /dev/null +++ b/newsfragments/5325.changed.md @@ -0,0 +1 @@ +deprecated `PyObject` type alias for `Py` \ No newline at end of file diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index c21373f8ed9..714d56ed247 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -1237,7 +1237,7 @@ 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::PyClassGuard<'_, Self>, py: #pyo3_path::Python<'_>) -> #pyo3_path::PyResult<#pyo3_path::PyObject> { + fn #field_name(slf: #pyo3_path::PyClassGuard<'_, Self>, py: #pyo3_path::Python<'_>) -> #pyo3_path::PyResult<#pyo3_path::Py<#pyo3_path::PyAny>> { #[allow(unused_imports)] use #pyo3_path::impl_::pyclass::Probe as _; match &*slf.into_super() { @@ -1313,7 +1313,7 @@ fn impl_complex_enum_tuple_variant_field_getters( }) .collect(); let field_getter_impl: syn::ImplItemFn = parse_quote! { - fn #field_name(slf: #pyo3_path::PyClassGuard<'_, Self>, py: #pyo3_path::Python<'_>) -> #pyo3_path::PyResult<#pyo3_path::PyObject> { + fn #field_name(slf: #pyo3_path::PyClassGuard<'_, Self>, py: #pyo3_path::Python<'_>) -> #pyo3_path::PyResult<#pyo3_path::Py<#pyo3_path::PyAny>> { #[allow(unused_imports)] use #pyo3_path::impl_::pyclass::Probe as _; match &*slf.into_super() { @@ -1374,7 +1374,7 @@ fn impl_complex_enum_tuple_variant_getitem( .collect(); let mut get_item_method_impl: syn::ImplItemFn = parse_quote! { - fn __getitem__(slf: #pyo3_path::PyClassGuard<'_, Self>, py: #pyo3_path::Python<'_>, idx: usize) -> #pyo3_path::PyResult< #pyo3_path::PyObject> { + fn __getitem__(slf: #pyo3_path::PyClassGuard<'_, Self>, py: #pyo3_path::Python<'_>, idx: usize) -> #pyo3_path::PyResult< #pyo3_path::Py<#pyo3_path::PyAny>> { match idx { #( #match_arms, )* _ => ::std::result::Result::Err(#pyo3_path::exceptions::PyIndexError::new_err("tuple index out of range")), @@ -1582,7 +1582,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> { + fn #wrapper_ident(py: #pyo3_path::Python<'_>) -> #pyo3_path::PyResult<#pyo3_path::Py<#pyo3_path::PyAny>> { ::std::result::Result::Ok(py.get_type::<#variant_cls>().into_any().unbind()) } }; @@ -1953,7 +1953,7 @@ fn pyclass_richcmp_simple_enum( py: #pyo3_path::Python, other: &#pyo3_path::Bound<'_, #pyo3_path::PyAny>, op: #pyo3_path::pyclass::CompareOp - ) -> #pyo3_path::PyResult<#pyo3_path::PyObject> { + ) -> #pyo3_path::PyResult<#pyo3_path::Py<#pyo3_path::PyAny>> { #eq #eq_int @@ -1987,7 +1987,7 @@ fn pyclass_richcmp( py: #pyo3_path::Python, other: &#pyo3_path::Bound<'_, #pyo3_path::PyAny>, op: #pyo3_path::pyclass::CompareOp - ) -> #pyo3_path::PyResult<#pyo3_path::PyObject> { + ) -> #pyo3_path::PyResult<#pyo3_path::Py<#pyo3_path::PyAny>> { let self_val = self; if let ::std::result::Result::Ok(other) = other.cast::() { let other = &*other.borrow(); diff --git a/pyo3-macros-backend/src/pyimpl.rs b/pyo3-macros-backend/src/pyimpl.rs index 388e5184bdf..282a6f35d7b 100644 --- a/pyo3-macros-backend/src/pyimpl.rs +++ b/pyo3-macros-backend/src/pyimpl.rs @@ -236,7 +236,7 @@ pub fn gen_py_const(cls: &syn::Type, spec: &ConstSpec, ctx: &Ctx) -> MethodAndMe let Ctx { pyo3_path, .. } = ctx; let associated_method = quote! { - fn #wrapper_ident(py: #pyo3_path::Python<'_>) -> #pyo3_path::PyResult<#pyo3_path::PyObject> { + fn #wrapper_ident(py: #pyo3_path::Python<'_>) -> #pyo3_path::PyResult<#pyo3_path::Py<#pyo3_path::PyAny>> { #pyo3_path::IntoPyObjectExt::into_py_any(#cls::#member, py) } }; diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index 6d7b455988f..eede90f882f 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -565,7 +565,7 @@ pub(crate) fn impl_py_class_attribute( let body = quotes::ok_wrap(fncall, ctx); let associated_method = quote! { - fn #wrapper_ident(py: #pyo3_path::Python<'_>) -> #pyo3_path::PyResult<#pyo3_path::PyObject> { + fn #wrapper_ident(py: #pyo3_path::Python<'_>) -> #pyo3_path::PyResult<#pyo3_path::Py<#pyo3_path::PyAny>> { let function = #cls::#name; // Shadow the method name to avoid #3017 let result = #body; #pyo3_path::impl_::wrap::converter(&result).map_into_pyobject(py, result) diff --git a/pytests/src/awaitable.rs b/pytests/src/awaitable.rs index 01a93c70a0d..8481e2f6528 100644 --- a/pytests/src/awaitable.rs +++ b/pytests/src/awaitable.rs @@ -11,13 +11,13 @@ use pyo3::prelude::*; #[pyclass] #[derive(Debug)] pub(crate) struct IterAwaitable { - result: Option>, + result: Option>>, } #[pymethods] impl IterAwaitable { #[new] - fn new(result: PyObject) -> Self { + fn new(result: Py) -> Self { IterAwaitable { result: Some(Ok(result)), } @@ -31,7 +31,7 @@ impl IterAwaitable { pyself } - fn __next__(&mut self, py: Python<'_>) -> PyResult { + fn __next__(&mut self, py: Python<'_>) -> PyResult> { match self.result.take() { Some(res) => match res { Ok(v) => Err(PyStopIteration::new_err(v)), @@ -46,13 +46,13 @@ impl IterAwaitable { pub(crate) struct FutureAwaitable { #[pyo3(get, set, name = "_asyncio_future_blocking")] py_block: bool, - result: Option>, + result: Option>>, } #[pymethods] impl FutureAwaitable { #[new] - fn new(result: PyObject) -> Self { + fn new(result: Py) -> Self { FutureAwaitable { py_block: false, result: Some(Ok(result)), diff --git a/pytests/src/objstore.rs b/pytests/src/objstore.rs index 8e729052992..89643c3967d 100644 --- a/pytests/src/objstore.rs +++ b/pytests/src/objstore.rs @@ -3,7 +3,7 @@ use pyo3::prelude::*; #[pyclass] #[derive(Default)] pub struct ObjStore { - obj: Vec, + obj: Vec>, } #[pymethods] diff --git a/src/conversions/std/path.rs b/src/conversions/std/path.rs index 5a16aba1273..ef51367e03c 100644 --- a/src/conversions/std/path.rs +++ b/src/conversions/std/path.rs @@ -3,7 +3,7 @@ use crate::ffi_ptr_ext::FfiPtrExt; use crate::instance::Bound; use crate::sync::GILOnceCell; use crate::types::any::PyAnyMethods; -use crate::{ffi, FromPyObject, PyAny, PyErr, PyObject, PyResult, Python}; +use crate::{ffi, FromPyObject, Py, PyAny, PyErr, PyResult, Python}; use std::borrow::Cow; use std::ffi::OsString; use std::path::{Path, PathBuf}; @@ -25,7 +25,7 @@ impl<'py> IntoPyObject<'py> for &Path { #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { - static PY_PATH: GILOnceCell = GILOnceCell::new(); + static PY_PATH: GILOnceCell> = GILOnceCell::new(); PY_PATH .import(py, "pathlib", "Path")? .call((self.as_os_str(),), None) diff --git a/src/coroutine.rs b/src/coroutine.rs index 671defb1770..cf58b742f81 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, IntoPyObject, IntoPyObjectExt, Py, PyAny, PyErr, PyObject, PyResult, Python, + Bound, IntoPyObject, IntoPyObjectExt, Py, PyAny, PyErr, PyResult, Python, }; pub(crate) mod cancel; @@ -31,7 +31,8 @@ pub struct Coroutine { name: Option>, qualname_prefix: Option<&'static str>, throw_callback: Option, - future: Option> + Send>>>, + #[allow(clippy::type_complexity)] + future: Option>> + Send>>>, waker: Option>, } @@ -71,7 +72,7 @@ impl Coroutine { } } - fn poll(&mut self, py: Python<'_>, throw: Option) -> PyResult { + fn poll(&mut self, py: Python<'_>, throw: Option>) -> PyResult> { // raise if the coroutine has already been run to completion let future_rs = match self.future { Some(ref mut fut) => fut, @@ -145,11 +146,11 @@ impl Coroutine { } } - fn send(&mut self, py: Python<'_>, _value: &Bound<'_, PyAny>) -> PyResult { + fn send(&mut self, py: Python<'_>, _value: &Bound<'_, PyAny>) -> PyResult> { self.poll(py, None) } - fn throw(&mut self, py: Python<'_>, exc: PyObject) -> PyResult { + fn throw(&mut self, py: Python<'_>, exc: Py) -> PyResult> { self.poll(py, Some(exc)) } @@ -163,7 +164,7 @@ impl Coroutine { self_ } - fn __next__(&mut self, py: Python<'_>) -> PyResult { + fn __next__(&mut self, py: Python<'_>) -> PyResult> { self.poll(py, None) } } diff --git a/src/coroutine/cancel.rs b/src/coroutine/cancel.rs index 47f5d69430a..49185fa56d7 100644 --- a/src/coroutine/cancel.rs +++ b/src/coroutine/cancel.rs @@ -1,4 +1,4 @@ -use crate::{Py, PyAny, PyObject}; +use crate::{Py, PyAny}; use std::future::Future; use std::pin::Pin; use std::sync::{Arc, Mutex}; @@ -6,7 +6,7 @@ use std::task::{Context, Poll, Waker}; #[derive(Debug, Default)] struct Inner { - exception: Option, + exception: Option>, waker: Option, } @@ -28,7 +28,7 @@ impl CancelHandle { } /// Poll to retrieve the exception thrown in the associated coroutine. - pub fn poll_cancelled(&mut self, cx: &mut Context<'_>) -> Poll { + pub fn poll_cancelled(&mut self, cx: &mut Context<'_>) -> Poll> { let mut inner = self.0.lock().unwrap(); if let Some(exc) = inner.exception.take() { return Poll::Ready(exc); @@ -43,7 +43,7 @@ impl CancelHandle { } /// Retrieve the exception thrown in the associated coroutine. - pub async fn cancelled(&mut self) -> PyObject { + pub async fn cancelled(&mut self) -> Py { Cancelled(self).await } @@ -57,7 +57,7 @@ impl CancelHandle { struct Cancelled<'a>(&'a mut CancelHandle); impl Future for Cancelled<'_> { - type Output = PyObject; + type Output = Py; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { self.0.poll_cancelled(cx) } diff --git a/src/coroutine/waker.rs b/src/coroutine/waker.rs index 9400d66c3d6..0afb024a9ab 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, Py, PyAny, PyObject, PyResult, Python}; +use crate::{intern, wrap_pyfunction, Bound, Py, PyAny, PyResult, Python}; use pyo3_macros::pyfunction; use std::sync::Arc; use std::task::Wake; @@ -52,13 +52,13 @@ impl Wake for AsyncioWaker { } struct LoopAndFuture { - event_loop: PyObject, - future: PyObject, + event_loop: Py, + future: Py, } impl LoopAndFuture { fn new(py: Python<'_>) -> PyResult { - static GET_RUNNING_LOOP: GILOnceCell = GILOnceCell::new(); + static GET_RUNNING_LOOP: GILOnceCell> = GILOnceCell::new(); let import = || -> PyResult<_> { let module = py.import("asyncio")?; Ok(module.getattr("get_running_loop")?.into()) diff --git a/src/err/err_state.rs b/src/err/err_state.rs index bb1fe1a465f..8616979310f 100644 --- a/src/err/err_state.rs +++ b/src/err/err_state.rs @@ -9,7 +9,7 @@ use crate::{ ffi, ffi_ptr_ext::FfiPtrExt, types::{PyAnyMethods, PyTraceback, PyType}, - Bound, Py, PyAny, PyErrArguments, PyObject, PyTypeInfo, Python, + Bound, Py, PyAny, PyErrArguments, PyTypeInfo, Python, }; pub(crate) struct PyErrState { @@ -263,8 +263,8 @@ impl PyErrStateNormalized { } pub(crate) struct PyErrStateLazyFnOutput { - pub(crate) ptype: PyObject, - pub(crate) pvalue: PyObject, + pub(crate) ptype: Py, + pub(crate) pvalue: Py, } pub(crate) type PyErrStateLazyFn = @@ -368,7 +368,7 @@ fn raise_lazy(py: Python<'_>, lazy: Box) { mod tests { use crate::{ - exceptions::PyValueError, sync::GILOnceCell, PyErr, PyErrArguments, PyObject, Python, + exceptions::PyValueError, sync::GILOnceCell, Py, PyAny, PyErr, PyErrArguments, Python, }; #[test] @@ -379,7 +379,7 @@ mod tests { struct RecursiveArgs; impl PyErrArguments for RecursiveArgs { - fn arguments(self, py: Python<'_>) -> PyObject { + fn arguments(self, py: Python<'_>) -> Py { // .value(py) triggers normalization ERR.get(py) .expect("is set just below") @@ -403,7 +403,7 @@ mod tests { struct GILSwitchArgs; impl PyErrArguments for GILSwitchArgs { - fn arguments(self, py: Python<'_>) -> PyObject { + fn arguments(self, py: Python<'_>) -> Py { // releasing the GIL potentially allows for other threads to deadlock // with the normalization going on here py.detach(|| { diff --git a/src/err/impls.rs b/src/err/impls.rs index d763ba93d80..6de043aabe0 100644 --- a/src/err/impls.rs +++ b/src/err/impls.rs @@ -1,5 +1,5 @@ -use crate::IntoPyObject; -use crate::{err::PyErrArguments, exceptions, PyErr, PyObject, Python}; +use crate::{err::PyErrArguments, exceptions, PyErr, Python}; +use crate::{IntoPyObject, Py, PyAny}; use std::io; /// Convert `PyErr` to `io::Error` @@ -77,7 +77,7 @@ impl From for PyErr { } impl PyErrArguments for io::Error { - fn arguments(self, py: Python<'_>) -> PyObject { + fn arguments(self, py: Python<'_>) -> Py { //FIXME(icxolu) remove unwrap self.to_string() .into_pyobject(py) @@ -94,7 +94,7 @@ impl From> for PyErr { } impl PyErrArguments for io::IntoInnerError { - fn arguments(self, py: Python<'_>) -> PyObject { + fn arguments(self, py: Python<'_>) -> Py { self.into_error().arguments(py) } } @@ -108,7 +108,7 @@ impl From for PyErr { macro_rules! impl_to_pyerr { ($err: ty, $pyexc: ty) => { impl PyErrArguments for $err { - fn arguments(self, py: Python<'_>) -> PyObject { + fn arguments(self, py: Python<'_>) -> $crate::Py<$crate::PyAny> { // FIXME(icxolu) remove unwrap self.to_string() .into_pyobject(py) diff --git a/src/err/mod.rs b/src/err/mod.rs index 9c48e30325e..16265865283 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -10,7 +10,7 @@ use crate::{ exceptions::{self, PyBaseException}, ffi, }; -use crate::{Borrowed, BoundObject, Py, PyAny, PyObject, Python}; +use crate::{Borrowed, BoundObject, Py, PyAny, Python}; use std::borrow::Cow; use std::ffi::CStr; @@ -98,14 +98,14 @@ 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; + fn arguments(self, py: Python<'_>) -> Py; } impl PyErrArguments for T where T: for<'py> IntoPyObject<'py> + Send + Sync, { - fn arguments(self, py: Python<'_>) -> PyObject { + fn arguments(self, py: Python<'_>) -> Py { // FIXME: `arguments` should become fallible match self.into_pyobject(py) { Ok(obj) => obj.into_any().unbind(), @@ -391,7 +391,7 @@ impl PyErr { name: &CStr, doc: Option<&CStr>, base: Option<&Bound<'py, PyType>>, - dict: Option, + dict: Option>, ) -> PyResult> { let base: *mut ffi::PyObject = match base { None => std::ptr::null_mut(), @@ -725,7 +725,7 @@ struct PyDowncastErrorArguments { } impl PyErrArguments for PyDowncastErrorArguments { - fn arguments(self, py: Python<'_>) -> PyObject { + fn arguments(self, py: Python<'_>) -> Py { const FAILED_TO_EXTRACT: Cow<'_, str> = Cow::Borrowed(""); let from = self.from.bind(py).qualname(); let from = match &from { diff --git a/src/impl_/callback.rs b/src/impl_/callback.rs index e8340c829a1..7ce01560aee 100644 --- a/src/impl_/callback.rs +++ b/src/impl_/callback.rs @@ -3,7 +3,7 @@ use crate::err::{PyErr, PyResult}; use crate::exceptions::PyOverflowError; use crate::ffi::{self, Py_hash_t}; -use crate::{BoundObject, IntoPyObject, PyObject, Python}; +use crate::{BoundObject, IntoPyObject, Py, PyAny, Python}; use std::ffi::c_int; /// A type which can be the return type of a python C-API callback @@ -106,12 +106,12 @@ impl IntoPyCallbackOutput<'_, usize> for usize { } } -impl<'py, T> IntoPyCallbackOutput<'py, PyObject> for T +impl<'py, T> IntoPyCallbackOutput<'py, Py> for T where T: IntoPyObject<'py>, { #[inline] - fn convert(self, py: Python<'py>) -> PyResult { + fn convert(self, py: Python<'py>) -> PyResult> { self.into_pyobject(py) .map(BoundObject::into_any) .map(BoundObject::unbind) diff --git a/src/impl_/pyclass/lazy_type_object.rs b/src/impl_/pyclass/lazy_type_object.rs index ac4aa3fd3d9..71de18328e5 100644 --- a/src/impl_/pyclass/lazy_type_object.rs +++ b/src/impl_/pyclass/lazy_type_object.rs @@ -11,12 +11,11 @@ use crate::types::PyTypeMethods; use crate::{ exceptions::PyRuntimeError, ffi, - impl_::pyclass::MaybeRuntimePyMethodDef, - impl_::pymethods::PyMethodDefType, + impl_::{pyclass::MaybeRuntimePyMethodDef, pymethods::PyMethodDefType}, pyclass::{create_type_object, PyClassTypeObject}, sync::GILOnceCell, types::PyType, - Bound, PyClass, PyErr, PyObject, PyResult, Python, + Bound, Py, PyAny, PyClass, PyErr, PyResult, Python, }; use std::sync::Mutex; @@ -235,7 +234,7 @@ impl LazyTypeObjectInner { fn initialize_tp_dict( py: Python<'_>, type_object: *mut ffi::PyObject, - items: Vec<(&'static CStr, PyObject)>, + items: Vec<(&'static CStr, Py)>, ) -> 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 50d02b6a645..829869b0f1e 100644 --- a/src/impl_/pymethods.rs +++ b/src/impl_/pymethods.rs @@ -10,8 +10,8 @@ use crate::pyclass::boolean_struct::False; use crate::types::PyType; use crate::{ ffi, Bound, DowncastError, Py, PyAny, PyClass, PyClassGuard, PyClassGuardMut, - PyClassInitializer, PyErr, PyObject, PyRef, PyRefMut, PyResult, PyTraverseError, PyTypeCheck, - PyVisit, Python, + PyClassInitializer, PyErr, PyRef, PyRefMut, PyResult, PyTraverseError, PyTypeCheck, PyVisit, + Python, }; use std::ffi::CStr; use std::ffi::{c_int, c_void}; @@ -84,7 +84,7 @@ pub enum PyMethodType { PyCFunctionFastWithKeywords(ffi::PyCFunctionFastWithKeywords), } -pub type PyClassAttributeFactory = for<'p> fn(Python<'p>) -> PyResult; +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. diff --git a/src/impl_/wrap.rs b/src/impl_/wrap.rs index 9d14d321ae2..9d3372cfe60 100644 --- a/src/impl_/wrap.rs +++ b/src/impl_/wrap.rs @@ -1,6 +1,8 @@ use std::{convert::Infallible, marker::PhantomData, ops::Deref}; -use crate::{ffi, types::PyNone, Bound, IntoPyObject, IntoPyObjectExt, PyObject, PyResult, Python}; +use crate::{ + ffi, types::PyNone, Bound, IntoPyObject, IntoPyObjectExt, Py, PyAny, PyResult, Python, +}; /// Used to wrap values in `Option` for default arguments. pub trait SomeWrap { @@ -89,7 +91,7 @@ impl<'py, T: IntoPyObject<'py>, E> IntoPyObjectConverter> { } #[inline] - pub fn map_into_pyobject(&self, py: Python<'py>, obj: PyResult) -> PyResult + pub fn map_into_pyobject(&self, py: Python<'py>, obj: PyResult) -> PyResult> where T: IntoPyObject<'py>, { @@ -126,7 +128,7 @@ impl UnknownReturnType { } #[inline] - pub fn map_into_pyobject<'py>(&self, _: Python<'py>, _: PyResult) -> PyResult + pub fn map_into_pyobject<'py>(&self, _: Python<'py>, _: PyResult) -> PyResult> where T: IntoPyObject<'py>, { diff --git a/src/instance.rs b/src/instance.rs index 29bac257fee..844405ba27e 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -1689,7 +1689,7 @@ impl Py { /// # use pyo3::{prelude::*, intern}; /// # /// #[pyfunction] - /// fn version(sys: Py, py: Python<'_>) -> PyResult { + /// fn version(sys: Py, py: Python<'_>) -> PyResult> { /// sys.getattr(py, intern!(py, "version")) /// } /// # @@ -1698,7 +1698,7 @@ impl Py { /// # version(sys, py).unwrap(); /// # }); /// ``` - pub fn getattr<'py, N>(&self, py: Python<'py>, attr_name: N) -> PyResult + pub fn getattr<'py, N>(&self, py: Python<'py>, attr_name: N) -> PyResult> where N: IntoPyObject<'py, Target = PyString>, { @@ -1715,10 +1715,10 @@ impl Py { /// # Example: `intern!`ing the attribute name /// /// ``` - /// # use pyo3::{intern, pyfunction, types::PyModule, IntoPyObjectExt, PyObject, Python, PyResult}; + /// # use pyo3::{intern, pyfunction, types::PyModule, IntoPyObjectExt, Py, PyAny, Python, PyResult}; /// # /// #[pyfunction] - /// fn set_answer(ob: PyObject, py: Python<'_>) -> PyResult<()> { + /// fn set_answer(ob: Py, py: Python<'_>) -> PyResult<()> { /// ob.setattr(py, intern!(py, "answer"), 42) /// } /// # @@ -1743,7 +1743,7 @@ impl Py { py: Python<'py>, args: A, kwargs: Option<&Bound<'py, PyDict>>, - ) -> PyResult + ) -> PyResult> where A: PyCallArgs<'py>, { @@ -1753,7 +1753,7 @@ impl Py { /// Calls the object with only positional arguments. /// /// This is equivalent to the Python expression `self(*args)`. - pub fn call1<'py, A>(&self, py: Python<'py>, args: A) -> PyResult + pub fn call1<'py, A>(&self, py: Python<'py>, args: A) -> PyResult> where A: PyCallArgs<'py>, { @@ -1763,7 +1763,7 @@ impl Py { /// Calls the object without arguments. /// /// This is equivalent to the Python expression `self()`. - pub fn call0(&self, py: Python<'_>) -> PyResult { + pub fn call0(&self, py: Python<'_>) -> PyResult> { self.bind(py).as_any().call0().map(Bound::unbind) } @@ -1779,7 +1779,7 @@ impl Py { name: N, args: A, kwargs: Option<&Bound<'py, PyDict>>, - ) -> PyResult + ) -> PyResult> where N: IntoPyObject<'py, Target = PyString>, A: PyCallArgs<'py>, @@ -1796,7 +1796,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_method1<'py, N, A>(&self, py: Python<'py>, name: N, args: A) -> PyResult + pub fn call_method1<'py, N, A>(&self, py: Python<'py>, name: N, args: A) -> PyResult> where N: IntoPyObject<'py, Target = PyString>, A: PyCallArgs<'py>, @@ -1813,7 +1813,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_method0<'py, N>(&self, py: Python<'py>, name: N) -> PyResult + pub fn call_method0<'py, N>(&self, py: Python<'py>, name: N) -> PyResult> where N: IntoPyObject<'py, Target = PyString>, { @@ -1941,7 +1941,7 @@ impl AsRef> for Py { } } -impl std::convert::From> for PyObject +impl std::convert::From> for Py where T: DerefToPyAny, { @@ -1951,7 +1951,7 @@ where } } -impl std::convert::From> for PyObject +impl std::convert::From> for Py where T: DerefToPyAny, { @@ -2065,9 +2065,10 @@ impl std::fmt::Debug for Py { /// safely sent between threads. /// /// See the documentation for [`Py`](struct.Py.html). +#[deprecated(since = "0.26.0", note = "use `Py` instead")] pub type PyObject = Py; -impl PyObject { +impl Py { /// Deprecated version of [`PyObject::cast_bound`] #[inline] #[deprecated(since = "0.26.0", note = "use `Py::cast_bound_unchecked` instead")] @@ -2163,7 +2164,7 @@ impl PyObject { #[cfg(test)] mod tests { - use super::{Bound, IntoPyObject, Py, PyObject}; + use super::{Bound, IntoPyObject, Py}; use crate::tests::common::generate_unique_module_name; use crate::types::{dict::IntoPyDict, PyAnyMethods, PyCapsule, PyDict, PyString}; use crate::{ffi, Borrowed, PyAny, PyResult, Python}; @@ -2239,7 +2240,7 @@ mod tests { #[test] fn test_call_for_non_existing_method() { Python::attach(|py| { - let obj: PyObject = PyDict::new(py).into(); + let obj: Py = PyDict::new(py).into(); assert!(obj.call_method0(py, "asdf").is_err()); assert!(obj .call_method(py, "nonexistent_method", (1,), None) @@ -2266,7 +2267,7 @@ mod tests { Python::attach(|py| { let dict: Py = PyDict::new(py).unbind(); let cnt = dict.get_refcnt(py); - let p: PyObject = dict.into(); + let p: Py = dict.into(); assert_eq!(p.get_refcnt(py), cnt); }); } @@ -2355,7 +2356,7 @@ a = A() Python::attach(|py| { let instance = py.eval(ffi::c_str!("object()"), None, None).unwrap(); let ptr = instance.as_ptr(); - let instance: PyObject = instance.clone().unbind(); + let instance: Py = instance.clone().unbind(); assert_eq!(instance.as_ptr(), ptr); }) } diff --git a/src/internal/state.rs b/src/internal/state.rs index 8095071af88..1aff268c7f9 100644 --- a/src/internal/state.rs +++ b/src/internal/state.rs @@ -338,17 +338,17 @@ fn decrement_attach_count() { mod tests { use super::*; - use crate::{ffi, types::PyAnyMethods, PyObject, Python}; + use crate::{ffi, types::PyAnyMethods, Py, PyAny, Python}; use std::ptr::NonNull; - fn get_object(py: Python<'_>) -> PyObject { + fn get_object(py: Python<'_>) -> Py { py.eval(ffi::c_str!("object()"), None, None) .unwrap() .unbind() } #[cfg(not(pyo3_disable_reference_pool))] - fn pool_dec_refs_does_not_contain(obj: &PyObject) -> bool { + fn pool_dec_refs_does_not_contain(obj: &Py) -> bool { !get_pool() .pending_decrefs .lock() @@ -359,7 +359,7 @@ mod tests { // With free-threading, 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 { + fn pool_dec_refs_contains(obj: &Py) -> bool { get_pool() .pending_decrefs .lock() @@ -527,7 +527,7 @@ mod tests { // Rebuild obj so that it can be dropped unsafe { - PyObject::from_owned_ptr( + Py::::from_owned_ptr( pool.python(), ffi::PyCapsule_GetPointer(capsule, std::ptr::null()) as _, ) diff --git a/src/lib.rs b/src/lib.rs index 55a17d9f0bd..34b4a6ae156 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -343,7 +343,9 @@ pub use crate::class::*; pub use crate::conversion::{FromPyObject, IntoPyObject, IntoPyObjectExt}; pub use crate::err::{DowncastError, DowncastIntoError, PyErr, PyErrArguments, PyResult, ToPyErr}; -pub use crate::instance::{Borrowed, Bound, BoundObject, Py, PyObject}; +#[allow(deprecated)] +pub use crate::instance::PyObject; +pub use crate::instance::{Borrowed, Bound, BoundObject, Py}; #[cfg(not(any(PyPy, GraalPy)))] #[allow(deprecated)] pub use crate::interpreter_lifecycle::{ diff --git a/src/marker.rs b/src/marker.rs index bf0a2fad3a1..ea75e3757a2 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -125,7 +125,7 @@ use crate::types::{ PyType, }; use crate::version::PythonVersionInfo; -use crate::{ffi, Bound, PyObject, PyTypeInfo}; +use crate::{ffi, Bound, Py, PyTypeInfo}; use std::ffi::CStr; use std::marker::PhantomData; @@ -689,21 +689,21 @@ impl<'py> Python<'py> { /// Gets the Python builtin value `None`. #[allow(non_snake_case)] // the Python keyword starts with uppercase #[inline] - pub fn None(self) -> PyObject { + pub fn None(self) -> Py { 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 { + pub fn Ellipsis(self) -> Py { 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 { + pub fn NotImplemented(self) -> Py { PyNotImplemented::get(self).to_owned().into_any().unbind() } diff --git a/src/prelude.rs b/src/prelude.rs index 258b522c103..31cf4b11b71 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -10,7 +10,9 @@ pub use crate::conversion::{FromPyObject, IntoPyObject}; pub use crate::err::{PyErr, PyResult}; -pub use crate::instance::{Borrowed, Bound, Py, PyObject}; +#[allow(deprecated)] +pub use crate::instance::PyObject; +pub use crate::instance::{Borrowed, Bound, Py}; pub use crate::marker::Python; pub use crate::pycell::{PyRef, PyRefMut}; pub use crate::pyclass_init::PyClassInitializer; diff --git a/src/tests/common.rs b/src/tests/common.rs index 683d68b1bf1..df5af81c247 100644 --- a/src/tests/common.rs +++ b/src/tests/common.rs @@ -123,8 +123,8 @@ with warnings.catch_warnings(record=True) as warning_record: #[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, + pub capture: Option<(PyErr, Py)>, + old_hook: Option>, } #[cfg(all(feature = "macros", Py_3_8, not(Py_GIL_DISABLED)))] diff --git a/src/types/any.rs b/src/types/any.rs index 4c3fc0b53fa..fb3d97b55ce 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -1850,7 +1850,7 @@ class SimpleClass: #[pymethods(crate = "crate")] impl GetattrFail { - fn __getattr__(&self, attr: PyObject) -> PyResult { + fn __getattr__(&self, attr: Py) -> PyResult> { Err(PyValueError::new_err(attr)) } } @@ -2091,7 +2091,7 @@ class SimpleClass: #[pymethods(crate = "crate")] impl DirFail { - fn __dir__(&self) -> PyResult { + fn __dir__(&self) -> PyResult> { Err(PyValueError::new_err("uh-oh!")) } } diff --git a/src/types/bytearray.rs b/src/types/bytearray.rs index 11f9e37137f..c85c20f12f4 100644 --- a/src/types/bytearray.rs +++ b/src/types/bytearray.rs @@ -322,7 +322,7 @@ impl<'py> TryFrom<&Bound<'py, PyAny>> for Bound<'py, PyByteArray> { #[cfg(test)] mod tests { use crate::types::{PyAnyMethods, PyByteArray, PyByteArrayMethods}; - use crate::{exceptions, Bound, PyAny, PyObject, Python}; + use crate::{exceptions, Bound, Py, PyAny, Python}; #[test] fn test_len() { @@ -378,7 +378,7 @@ mod tests { let src = b"Hello Python"; let bytearray = PyByteArray::new(py, src); - let ba: PyObject = bytearray.into(); + let ba: Py = bytearray.into(); let bytearray = PyByteArray::from(ba.bind(py)).unwrap(); assert_eq!(src, unsafe { bytearray.as_bytes() }); diff --git a/src/types/module.rs b/src/types/module.rs index ec6e15a2f92..ba4aa9428f7 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -9,7 +9,7 @@ use crate::types::{ any::PyAnyMethods, list::PyListMethods, PyAny, PyCFunction, PyDict, PyList, PyString, }; use crate::{ - exceptions, ffi, Borrowed, Bound, BoundObject, IntoPyObject, IntoPyObjectExt, PyObject, Python, + exceptions, ffi, Borrowed, Bound, BoundObject, IntoPyObject, IntoPyObjectExt, Py, Python, }; #[cfg(all(not(Py_LIMITED_API), Py_GIL_DISABLED))] use std::ffi::c_int; @@ -287,7 +287,7 @@ pub trait PyModuleMethods<'py>: crate::sealed::Sealed { /// instead. fn add_wrapped(&self, wrapper: &impl Fn(Python<'py>) -> T) -> PyResult<()> where - T: IntoPyCallbackOutput<'py, PyObject>; + T: IntoPyCallbackOutput<'py, Py>; /// Adds a submodule to a module. /// @@ -502,7 +502,7 @@ impl<'py> PyModuleMethods<'py> for Bound<'py, PyModule> { fn add_wrapped(&self, wrapper: &impl Fn(Python<'py>) -> T) -> PyResult<()> where - T: IntoPyCallbackOutput<'py, PyObject>, + T: IntoPyCallbackOutput<'py, Py>, { fn inner(module: &Bound<'_, PyModule>, object: Bound<'_, PyAny>) -> PyResult<()> { let name = object.getattr(__name__(module.py()))?; diff --git a/src/types/sequence.rs b/src/types/sequence.rs index d510f36552b..de6c25a193c 100644 --- a/src/types/sequence.rs +++ b/src/types/sequence.rs @@ -398,10 +398,10 @@ impl PyTypeCheck for PySequence { #[cfg(test)] mod tests { use crate::types::{PyAnyMethods, PyList, PySequence, PySequenceMethods, PyTuple}; - use crate::{ffi, IntoPyObject, PyObject, Python}; + use crate::{ffi, IntoPyObject, Py, PyAny, Python}; use std::ptr; - fn get_object() -> PyObject { + fn get_object() -> Py { // Convenience function for getting a single unique object Python::attach(|py| { let obj = py.eval(ffi::c_str!("object()"), None, None).unwrap(); diff --git a/src/types/string.rs b/src/types/string.rs index c30c2da4bf4..1baa6a18eeb 100644 --- a/src/types/string.rs +++ b/src/types/string.rs @@ -562,7 +562,7 @@ mod tests { use pyo3_ffi::c_str; use super::*; - use crate::{exceptions::PyLookupError, types::PyAnyMethods as _, IntoPyObject, PyObject}; + use crate::{exceptions::PyLookupError, types::PyAnyMethods as _, IntoPyObject}; #[test] fn test_to_cow_utf8() { @@ -606,7 +606,7 @@ mod tests { #[test] fn test_encode_utf8_surrogate() { Python::attach(|py| { - let obj: PyObject = py + let obj: Py = py .eval(ffi::c_str!(r"'\ud800'"), None, None) .unwrap() .into(); diff --git a/tests/test_arithmetics.rs b/tests/test_arithmetics.rs index a73be443a2e..3e0e7712ba4 100644 --- a/tests/test_arithmetics.rs +++ b/tests/test_arithmetics.rs @@ -527,7 +527,7 @@ impl RichComparisons2 { "RC2" } - fn __richcmp__(&self, other: &Bound<'_, PyAny>, op: CompareOp) -> PyResult { + fn __richcmp__(&self, other: &Bound<'_, PyAny>, op: CompareOp) -> PyResult> { match op { CompareOp::Eq => true .into_pyobject(other.py()) @@ -608,7 +608,7 @@ mod return_not_implemented { "RC_Self" } - fn __richcmp__(&self, other: PyRef<'_, Self>, _op: CompareOp) -> PyObject { + fn __richcmp__(&self, other: PyRef<'_, Self>, _op: CompareOp) -> Py { other.py().None() } diff --git a/tests/test_class_basics.rs b/tests/test_class_basics.rs index d085db60351..027f050320e 100644 --- a/tests/test_class_basics.rs +++ b/tests/test_class_basics.rs @@ -207,13 +207,13 @@ struct ClassWithObjectField { // It used to be that PyObject was not supported with (get, set) // - this test is just ensuring it compiles. #[pyo3(get, set)] - value: PyObject, + value: Py, } #[pymethods] impl ClassWithObjectField { #[new] - fn new(value: PyObject) -> ClassWithObjectField { + fn new(value: Py) -> ClassWithObjectField { Self { value } } } @@ -719,14 +719,14 @@ fn test_unsendable_dict_with_weakref() { #[pyclass(generic)] struct ClassWithRuntimeParametrization { #[pyo3(get, set)] - value: PyObject, + value: Py, } #[cfg(Py_3_9)] #[pymethods] impl ClassWithRuntimeParametrization { #[new] - fn new(value: PyObject) -> ClassWithRuntimeParametrization { + fn new(value: Py) -> ClassWithRuntimeParametrization { Self { value } } } diff --git a/tests/test_class_new.rs b/tests/test_class_new.rs index 51db45f4774..ce1d9b83ef1 100644 --- a/tests/test_class_new.rs +++ b/tests/test_class_new.rs @@ -302,7 +302,7 @@ fn test_new_returns_bound() { #[pyo3::pyclass] struct NewClassMethod { #[pyo3(get)] - cls: pyo3::PyObject, + cls: pyo3::Py, } #[pyo3::pymethods] diff --git a/tests/test_gc.rs b/tests/test_gc.rs index 368fd3871bc..8f39c1baca1 100644 --- a/tests/test_gc.rs +++ b/tests/test_gc.rs @@ -151,7 +151,7 @@ fn data_is_dropped() { #[pyclass(subclass)] struct CycleWithClear { - cycle: Option, + cycle: Option>, _guard: DropGuard, } @@ -342,7 +342,7 @@ fn gc_during_borrow() { fn traverse_partial() { #[pyclass] struct PartialTraverse { - member: PyObject, + member: Py, } impl PartialTraverse { @@ -378,7 +378,7 @@ fn traverse_partial() { fn traverse_panic() { #[pyclass] struct PanickyTraverse { - member: PyObject, + member: Py, } impl PanickyTraverse { @@ -753,7 +753,7 @@ fn test_drop_buffer_during_traversal_without_gil() { #[pyclass] struct BufferDropDuringTraversal { inner: Mutex)>>, - cycle: Option, + cycle: Option>, } #[pymethods] diff --git a/tests/test_mapping.rs b/tests/test_mapping.rs index 8e958fc5c2f..bab7c0a418a 100644 --- a/tests/test_mapping.rs +++ b/tests/test_mapping.rs @@ -65,8 +65,8 @@ impl Mapping { &self, py: Python<'_>, key: &str, - default: Option, - ) -> PyResult> { + 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_methods.rs b/tests/test_methods.rs index 5c96aa2afc6..06544f221e9 100644 --- a/tests/test_methods.rs +++ b/tests/test_methods.rs @@ -289,7 +289,7 @@ impl MethSignature { py: Python<'_>, a: i32, kwargs: Option<&Bound<'_, PyDict>>, - ) -> PyResult { + ) -> PyResult> { [ a.into_pyobject(py)?.into_any().into_bound(), kwargs.into_pyobject(py)?.into_any().into_bound(), @@ -304,7 +304,7 @@ impl MethSignature { py: Python<'_>, a: i32, kwargs: Option<&Bound<'_, PyDict>>, - ) -> PyResult { + ) -> PyResult> { [ a.into_pyobject(py)?.into_any().into_bound(), kwargs.into_pyobject(py)?.into_any().into_bound(), @@ -334,7 +334,7 @@ impl MethSignature { py: Python<'_>, args: &Bound<'_, PyTuple>, a: i32, - ) -> PyResult { + ) -> PyResult> { (args, a) .into_pyobject(py) .map(BoundObject::into_any) @@ -357,7 +357,7 @@ impl MethSignature { py: Python<'_>, a: i32, kwargs: Option<&Bound<'_, PyDict>>, - ) -> PyResult { + ) -> PyResult> { [ a.into_pyobject(py)?.into_any().into_bound(), kwargs.into_pyobject(py)?.into_any().into_bound(), @@ -922,9 +922,9 @@ fn test_from_sequence() { #[pyclass] struct r#RawIdents { #[pyo3(get, set)] - r#type: PyObject, - r#subtype: PyObject, - r#subsubtype: PyObject, + r#type: Py, + r#subtype: Py, + r#subsubtype: Py, } #[pymethods] @@ -932,9 +932,9 @@ impl r#RawIdents { #[new] pub fn r#new( r#_py: Python<'_>, - r#type: PyObject, - r#subtype: PyObject, - r#subsubtype: PyObject, + r#type: Py, + r#subtype: Py, + r#subsubtype: Py, ) -> Self { Self { r#type, @@ -944,36 +944,36 @@ impl r#RawIdents { } #[getter(r#subtype)] - pub fn r#get_subtype(&self, py: Python<'_>) -> PyObject { + pub fn r#get_subtype(&self, py: Python<'_>) -> Py { self.r#subtype.clone_ref(py) } #[setter(r#subtype)] - pub fn r#set_subtype(&mut self, r#subtype: PyObject) { + pub fn r#set_subtype(&mut self, r#subtype: Py) { self.r#subtype = r#subtype; } #[getter] - pub fn r#get_subsubtype(&self, py: Python<'_>) -> PyObject { + pub fn r#get_subsubtype(&self, py: Python<'_>) -> Py { self.r#subsubtype.clone_ref(py) } #[setter] - pub fn r#set_subsubtype(&mut self, r#subsubtype: PyObject) { + pub fn r#set_subsubtype(&mut self, r#subsubtype: Py) { self.r#subsubtype = r#subsubtype; } - pub fn r#__call__(&mut self, r#type: PyObject) { + pub fn r#__call__(&mut self, r#type: Py) { self.r#type = r#type; } #[staticmethod] - pub fn r#static_method(r#type: PyObject) -> PyObject { + pub fn r#static_method(r#type: Py) -> Py { r#type } #[classmethod] - pub fn r#class_method(_: &Bound<'_, PyType>, r#type: PyObject) -> PyObject { + pub fn r#class_method(_: &Bound<'_, PyType>, r#type: Py) -> Py { r#type } diff --git a/tests/test_module.rs b/tests/test_module.rs index 9b316134752..2f5766aadaf 100644 --- a/tests/test_module.rs +++ b/tests/test_module.rs @@ -323,7 +323,7 @@ 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>) -> PyResult { +fn ext_vararg_fn(py: Python<'_>, a: i32, args: &Bound<'_, PyTuple>) -> PyResult> { [ a.into_pyobject(py)?.into_any().into_bound(), args.as_any().clone(), @@ -335,7 +335,7 @@ fn ext_vararg_fn(py: Python<'_>, a: i32, args: &Bound<'_, PyTuple>) -> PyResult< #[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>) -> PyResult { + fn int_vararg_fn(py: Python<'_>, a: i32, args: &Bound<'_, PyTuple>) -> PyResult> { ext_vararg_fn(py, a, args) } diff --git a/tests/test_proto_methods.rs b/tests/test_proto_methods.rs index e5e36939c53..e8df772faf1 100644 --- a/tests/test_proto_methods.rs +++ b/tests/test_proto_methods.rs @@ -21,7 +21,7 @@ struct ExampleClass { #[pymethods] impl ExampleClass { - fn __getattr__(&self, py: Python<'_>, attr: &str) -> PyResult { + fn __getattr__(&self, py: Python<'_>, attr: &str) -> PyResult> { if attr == "special_custom_attr" { Ok(self.custom_attr.into_pyobject(py)?.into_any().unbind()) } else { @@ -252,7 +252,7 @@ enum SequenceIndex<'py> { #[pyclass] pub struct Sequence { - values: Vec, + values: Vec>, } #[pymethods] @@ -261,7 +261,7 @@ impl Sequence { self.values.len() } - fn __getitem__(&self, index: SequenceIndex<'_>, py: Python<'_>) -> PyResult { + fn __getitem__(&self, index: SequenceIndex<'_>, py: Python<'_>) -> PyResult> { match index { SequenceIndex::Integer(index) => { let uindex = self.usize_index(index)?; @@ -275,7 +275,7 @@ impl Sequence { } } - fn __setitem__(&mut self, index: isize, value: PyObject) -> PyResult<()> { + fn __setitem__(&mut self, index: isize, value: Py) -> PyResult<()> { let uindex = self.usize_index(index)?; self.values .get_mut(uindex) @@ -293,7 +293,7 @@ impl Sequence { } } - fn append(&mut self, value: PyObject) { + fn append(&mut self, value: Py) { self.values.push(value); } } @@ -634,14 +634,14 @@ fn getattr_and_getattribute() { #[pyclass] #[derive(Debug)] struct OnceFuture { - future: PyObject, + future: Py, polled: bool, } #[pymethods] impl OnceFuture { #[new] - fn new(future: PyObject) -> Self { + fn new(future: Py) -> Self { OnceFuture { future, polled: false, @@ -828,7 +828,7 @@ struct NotHashable; #[pymethods] impl NotHashable { #[classattr] - const __hash__: Option = None; + const __hash__: Option> = None; } #[test] @@ -850,7 +850,7 @@ struct DefaultedContains; #[pymethods] impl DefaultedContains { - fn __iter__(&self, py: Python<'_>) -> PyObject { + fn __iter__(&self, py: Python<'_>) -> Py { PyList::new(py, ["a", "b", "c"]) .unwrap() .as_any() @@ -865,7 +865,7 @@ struct NoContains; #[pymethods] impl NoContains { - fn __iter__(&self, py: Python<'_>) -> PyObject { + fn __iter__(&self, py: Python<'_>) -> Py { PyList::new(py, ["a", "b", "c"]) .unwrap() .as_any() @@ -877,7 +877,7 @@ impl NoContains { // Equivalent to the opt-out const form in NotHashable above, just more verbose, to confirm this // also works. #[classattr] - fn __contains__() -> Option { + fn __contains__() -> Option> { None } } diff --git a/tests/test_pyself.rs b/tests/test_pyself.rs index 4eff50b4770..3f7d3844fce 100644 --- a/tests/test_pyself.rs +++ b/tests/test_pyself.rs @@ -64,7 +64,7 @@ impl Iter { slf } - fn __next__(mut slf: PyRefMut<'_, Self>) -> PyResult> { + fn __next__(mut slf: PyRefMut<'_, Self>) -> PyResult>> { let bytes = slf.keys.bind(slf.py()).as_bytes(); match bytes.get(slf.idx) { Some(&b) => { diff --git a/tests/test_sequence.rs b/tests/test_sequence.rs index 2d0344d6d58..ca1b15ae9a5 100644 --- a/tests/test_sequence.rs +++ b/tests/test_sequence.rs @@ -257,7 +257,7 @@ fn test_inplace_repeat() { #[pyclass] struct AnyObjectList { #[pyo3(get, set)] - items: Vec, + items: Vec>, } #[test] @@ -372,7 +372,7 @@ fn sequence_length() { #[pyclass(generic, sequence)] struct GenericList { #[pyo3(get, set)] - items: Vec, + items: Vec>, } #[cfg(Py_3_10)] @@ -382,7 +382,7 @@ impl GenericList { self.items.len() } - fn __getitem__(&self, idx: isize) -> PyResult { + fn __getitem__(&self, idx: isize) -> PyResult> { match self.items.get(idx as usize) { Some(x) => pyo3::Python::attach(|py| Ok(x.clone_ref(py))), None => Err(PyIndexError::new_err("Index out of bounds")), @@ -402,7 +402,7 @@ fn test_generic_both_subscriptions_types() { GenericList { items: [1, 2, 3] .iter() - .map(|x| -> PyObject { + .map(|x| -> Py { let x: Result, Infallible> = x.into_pyobject(py); x.unwrap().into_any().unbind() }) diff --git a/tests/ui/ambiguous_associated_items.rs b/tests/ui/ambiguous_associated_items.rs index f553ba1f33f..34ebd931ce3 100644 --- a/tests/ui/ambiguous_associated_items.rs +++ b/tests/ui/ambiguous_associated_items.rs @@ -10,16 +10,16 @@ pub enum SimpleItems { #[pyclass] pub enum ComplexItems { - Error(PyObject), - Output(PyObject), - Target(PyObject), + Error(Py), + Output(Py), + Target(Py), } #[derive(IntoPyObject)] enum DeriveItems { - Error(PyObject), - Output(PyObject), - Target(PyObject), + Error(Py), + Output(Py), + Target(Py), } fn main() {} diff --git a/tests/ui/invalid_pyclass_args.rs b/tests/ui/invalid_pyclass_args.rs index 221fc6ca35f..a4782fceb2e 100644 --- a/tests/ui/invalid_pyclass_args.rs +++ b/tests/ui/invalid_pyclass_args.rs @@ -45,7 +45,7 @@ impl EqOptAndManualRichCmp { _py: Python, _other: Bound<'_, PyAny>, _op: pyo3::pyclass::CompareOp, - ) -> PyResult { + ) -> PyResult> { todo!() } } diff --git a/tests/ui/invalid_pyclass_generic.rs b/tests/ui/invalid_pyclass_generic.rs index 45cd5fec9b0..dac8c057d81 100644 --- a/tests/ui/invalid_pyclass_generic.rs +++ b/tests/ui/invalid_pyclass_generic.rs @@ -2,8 +2,7 @@ use pyo3::prelude::*; use pyo3::types::PyType; #[pyclass(generic)] -struct ClassRedefinesClassGetItem { -} +struct ClassRedefinesClassGetItem {} #[pymethods] impl ClassRedefinesClassGetItem { @@ -16,7 +15,7 @@ impl ClassRedefinesClassGetItem { pub fn __class_getitem__( cls: &Bound<'_, PyType>, key: &Bound<'_, PyAny>, - ) -> PyResult { + ) -> PyResult> { pyo3::types::PyGenericAlias::new(cls.py(), cls.as_any(), key) } } diff --git a/tests/ui/invalid_pyclass_generic.stderr b/tests/ui/invalid_pyclass_generic.stderr index 1381c31e1e1..1d6233102e7 100644 --- a/tests/ui/invalid_pyclass_generic.stderr +++ b/tests/ui/invalid_pyclass_generic.stderr @@ -4,7 +4,7 @@ error[E0592]: duplicate definitions with name `__pymethod___class_getitem____` 4 | #[pyclass(generic)] | ^^^^^^^^^^^^^^^^^^^ duplicate definitions for `__pymethod___class_getitem____` ... -8 | #[pymethods] +7 | #[pymethods] | ------------ other definition for `__pymethod___class_getitem____` | = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) @@ -15,11 +15,11 @@ error[E0592]: duplicate definitions with name `__class_getitem__` 4 | #[pyclass(generic)] | ^^^^^^^^^^^^^^^^^^^ duplicate definitions for `__class_getitem__` ... -16 | / pub fn __class_getitem__( -17 | | cls: &Bound<'_, PyType>, -18 | | key: &Bound<'_, PyAny>, -19 | | ) -> PyResult { - | |___________________________- other definition for `__class_getitem__` +15 | / pub fn __class_getitem__( +16 | | cls: &Bound<'_, PyType>, +17 | | key: &Bound<'_, PyAny>, +18 | | ) -> PyResult> { + | |____________________________- other definition for `__class_getitem__` | = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) @@ -35,9 +35,9 @@ note: candidate #1 is defined in an impl for the type `ClassRedefinesClassGetIte 4 | #[pyclass(generic)] | ^^^^^^^^^^^^^^^^^^^ note: candidate #2 is defined in an impl for the type `ClassRedefinesClassGetItem` - --> tests/ui/invalid_pyclass_generic.rs:8:1 + --> tests/ui/invalid_pyclass_generic.rs:7:1 | -8 | #[pymethods] +7 | #[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) @@ -53,13 +53,13 @@ note: candidate #1 is defined in an impl for the type `ClassRedefinesClassGetIte 4 | #[pyclass(generic)] | ^^^^^^^^^^^^^^^^^^^ note: candidate #2 is defined in an impl for the type `ClassRedefinesClassGetItem` - --> tests/ui/invalid_pyclass_generic.rs:16:5 + --> tests/ui/invalid_pyclass_generic.rs:15:5 | -16 | / pub fn __class_getitem__( -17 | | cls: &Bound<'_, PyType>, -18 | | key: &Bound<'_, PyAny>, -19 | | ) -> PyResult { - | |___________________________^ +15 | / pub fn __class_getitem__( +16 | | cls: &Bound<'_, PyType>, +17 | | key: &Bound<'_, PyAny>, +18 | | ) -> PyResult> { + | |____________________________^ = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0034]: multiple applicable items in scope @@ -75,20 +75,20 @@ error[E0034]: multiple applicable items in scope = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0308]: mismatched types - --> tests/ui/invalid_pyclass_generic.rs:20:9 + --> tests/ui/invalid_pyclass_generic.rs:19:9 | -19 | ) -> PyResult { - | ------------------ expected `Result, PyErr>` because of return type -20 | pyo3::types::PyGenericAlias::new(cls.py(), cls.as_any(), key) +18 | ) -> PyResult> { + | ------------------- expected `Result, PyErr>` because of return type +19 | pyo3::types::PyGenericAlias::new(cls.py(), cls.as_any(), key) | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `Result, PyErr>`, found `Result, PyErr>` | - = note: expected enum `Result, PyErr>` + = note: expected enum `Result, PyErr>` found enum `Result, PyErr>` error[E0034]: multiple applicable items in scope - --> tests/ui/invalid_pyclass_generic.rs:16:12 + --> tests/ui/invalid_pyclass_generic.rs:15:12 | -16 | pub fn __class_getitem__( +15 | pub fn __class_getitem__( | ^^^^^^^^^^^^^^^^^ multiple `__pymethod___class_getitem____` found | note: candidate #1 is defined in an impl for the type `ClassRedefinesClassGetItem` @@ -97,16 +97,16 @@ note: candidate #1 is defined in an impl for the type `ClassRedefinesClassGetIte 4 | #[pyclass(generic)] | ^^^^^^^^^^^^^^^^^^^ note: candidate #2 is defined in an impl for the type `ClassRedefinesClassGetItem` - --> tests/ui/invalid_pyclass_generic.rs:8:1 + --> tests/ui/invalid_pyclass_generic.rs:7:1 | -8 | #[pymethods] +7 | #[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_generic.rs:16:12 + --> tests/ui/invalid_pyclass_generic.rs:15:12 | -16 | pub fn __class_getitem__( +15 | pub fn __class_getitem__( | ^^^^^^^^^^^^^^^^^ multiple `__class_getitem__` found | note: candidate #1 is defined in an impl for the type `ClassRedefinesClassGetItem` @@ -115,19 +115,19 @@ note: candidate #1 is defined in an impl for the type `ClassRedefinesClassGetIte 4 | #[pyclass(generic)] | ^^^^^^^^^^^^^^^^^^^ note: candidate #2 is defined in an impl for the type `ClassRedefinesClassGetItem` - --> tests/ui/invalid_pyclass_generic.rs:16:5 + --> tests/ui/invalid_pyclass_generic.rs:15:5 | -16 | / pub fn __class_getitem__( -17 | | cls: &Bound<'_, PyType>, -18 | | key: &Bound<'_, PyAny>, -19 | | ) -> PyResult { - | |___________________________^ +15 | / pub fn __class_getitem__( +16 | | cls: &Bound<'_, PyType>, +17 | | key: &Bound<'_, PyAny>, +18 | | ) -> PyResult> { + | |____________________________^ = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0034]: multiple applicable items in scope - --> tests/ui/invalid_pyclass_generic.rs:19:10 + --> tests/ui/invalid_pyclass_generic.rs:18:10 | -19 | ) -> PyResult { +18 | ) -> PyResult> { | ^^^^^^^^ multiple `wrap` found | = note: candidate #1 is defined in an impl for the type `IntoPyObjectConverter>` From 15045dbca140b275c3fe1ac4d76320510b87431a Mon Sep 17 00:00:00 2001 From: Steven Date: Sat, 16 Aug 2025 18:56:42 -0400 Subject: [PATCH 800/936] Fix typo in free-threading.md (#5330) Fix typo in free-threading.md "not not" -> not --- guide/src/free-threading.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guide/src/free-threading.md b/guide/src/free-threading.md index 11e805c538d..f00b6afdb94 100644 --- a/guide/src/free-threading.md +++ b/guide/src/free-threading.md @@ -164,7 +164,7 @@ simultaneously interacting with the interpreter. 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::attach`] -function. Threads created via the Python [`threading`] module do not not need to +function. Threads created via the Python [`threading`] module do not need to do this, and pyo3 will handle setting up the [`Python<'py>`] token when CPython calls into your extension. From b788b9b5aca3625a077c990738e7e3f7bb026cc1 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sun, 17 Aug 2025 04:12:10 +0100 Subject: [PATCH 801/936] introduce fast path to `LazyTypeObject::get_or_init` (#5324) * introduce fast path to `LazyTypeObject::get_or_init` * fix changelog name * reduce duplication * fixup benchmark * fixup clippy * fixup doc --- guide/src/class.md | 7 ++- newsfragments/5324.changed.md | 1 + pyo3-benches/benches/bench_pyclass.rs | 2 +- pyo3-macros-backend/src/pyclass.rs | 7 ++- src/impl_/pyclass.rs | 2 +- src/impl_/pyclass/lazy_type_object.rs | 38 +++++++++------ tests/test_class_attributes.rs | 66 +++++++++------------------ 7 files changed, 61 insertions(+), 62 deletions(-) create mode 100644 newsfragments/5324.changed.md diff --git a/guide/src/class.md b/guide/src/class.md index 8193b3bdfae..8eb71669d02 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -1385,7 +1385,12 @@ unsafe impl pyo3::type_object::PyTypeInfo for MyClass { #[inline] fn type_object_raw(py: pyo3::Python<'_>) -> *mut pyo3::ffi::PyTypeObject { ::lazy_type_object() - .get_or_init(py) + .get_or_try_init(py) + .unwrap_or_else(|e| pyo3::impl_::pyclass::type_object_init_failed( + py, + e, + ::NAME + )) .as_type_ptr() } } diff --git a/newsfragments/5324.changed.md b/newsfragments/5324.changed.md new file mode 100644 index 00000000000..11aed953100 --- /dev/null +++ b/newsfragments/5324.changed.md @@ -0,0 +1 @@ +Add fast-path to `PyTypeInfo::type_object` for `#[pyclass]` types. diff --git a/pyo3-benches/benches/bench_pyclass.rs b/pyo3-benches/benches/bench_pyclass.rs index 7e026f74bd1..0967a897649 100644 --- a/pyo3-benches/benches/bench_pyclass.rs +++ b/pyo3-benches/benches/bench_pyclass.rs @@ -32,7 +32,7 @@ pub fn first_time_init(b: &mut Bencher<'_>) { // This is using an undocumented internal PyO3 API to measure pyclass performance; please // don't use this in your own code! let ty = LazyTypeObject::::new(); - ty.get_or_init(py); + ty.get_or_try_init(py).unwrap(); }); }); } diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 714d56ed247..3ff700074dc 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -1845,7 +1845,12 @@ fn impl_pytypeinfo(cls: &syn::Ident, attr: &PyClassArgs, ctx: &Ctx) -> TokenStre fn type_object_raw(py: #pyo3_path::Python<'_>) -> *mut #pyo3_path::ffi::PyTypeObject { use #pyo3_path::prelude::PyTypeMethods; <#cls as #pyo3_path::impl_::pyclass::PyClassImpl>::lazy_type_object() - .get_or_init(py) + .get_or_try_init(py) + .unwrap_or_else(|e| #pyo3_path::impl_::pyclass::type_object_init_failed( + py, + e, + ::NAME + )) .as_type_ptr() } } diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index a158d5a1a59..1228c2ea758 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -29,7 +29,7 @@ mod lazy_type_object; mod probes; pub use assertions::*; -pub use lazy_type_object::LazyTypeObject; +pub use lazy_type_object::{type_object_init_failed, LazyTypeObject}; pub use probes::*; /// Gets the offset of the dictionary from the start of the object in bytes. diff --git a/src/impl_/pyclass/lazy_type_object.rs b/src/impl_/pyclass/lazy_type_object.rs index 71de18328e5..c58f4978451 100644 --- a/src/impl_/pyclass/lazy_type_object.rs +++ b/src/impl_/pyclass/lazy_type_object.rs @@ -32,7 +32,7 @@ struct LazyTypeObjectInner { // Threads which have begun initialization of the `tp_dict`. Used for // reentrant initialization detection. initializing_threads: Mutex>, - tp_dict_filled: GILOnceCell<()>, + fully_initialized_type: GILOnceCell>, } impl LazyTypeObject { @@ -43,7 +43,7 @@ impl LazyTypeObject { LazyTypeObjectInner { value: GILOnceCell::new(), initializing_threads: Mutex::new(Vec::new()), - tp_dict_filled: GILOnceCell::new(), + fully_initialized_type: GILOnceCell::new(), }, PhantomData, ) @@ -52,15 +52,18 @@ impl LazyTypeObject { impl LazyTypeObject { /// Gets the type object contained by this `LazyTypeObject`, initializing it if needed. - pub fn get_or_init<'py>(&self, py: Python<'py>) -> &Bound<'py, PyType> { - self.get_or_try_init(py).unwrap_or_else(|err| { - err.print(py); - panic!("failed to create type object for {}", T::NAME) - }) + #[inline] + pub fn get_or_try_init<'py>(&self, py: Python<'py>) -> PyResult<&Bound<'py, PyType>> { + if let Some(type_object) = self.0.fully_initialized_type.get(py) { + // Fast path + return Ok(type_object.bind(py)); + } + + self.try_init(py) } - /// Fallible version of the above. - pub(crate) fn get_or_try_init<'py>(&self, py: Python<'py>) -> PyResult<&Bound<'py, PyType>> { + #[cold] + fn try_init<'py>(&self, py: Python<'py>) -> PyResult<&Bound<'py, PyType>> { self.0 .get_or_try_init(py, create_type_object::, T::NAME, T::items_iter()) } @@ -116,7 +119,7 @@ impl LazyTypeObjectInner { // `tp_dict`, it can still request the type object through `get_or_init`, // but the `tp_dict` may appear empty of course. - if self.tp_dict_filled.get(py).is_some() { + if self.fully_initialized_type.get(py).is_some() { // `tp_dict` is already filled: ok. return Ok(()); } @@ -184,8 +187,8 @@ impl LazyTypeObjectInner { // Now we hold the GIL and we can assume it won't be released until we // return from the function. - let result = self.tp_dict_filled.get_or_try_init(py, move || { - let result = initialize_tp_dict(py, type_object.as_ptr(), items); + let result = self.fully_initialized_type.get_or_try_init(py, move || { + initialize_tp_dict(py, type_object.as_ptr(), items)?; #[cfg(Py_3_14)] if is_immutable_type { // freeze immutable types after __dict__ is initialized @@ -216,13 +219,13 @@ impl LazyTypeObjectInner { self.initializing_threads.lock().unwrap() }; threads.clear(); - result + Ok(type_object.clone().unbind()) }); if let Err(err) = result { return Err(wrap_in_runtime_error( py, - err.clone_ref(py), + err, format!("An error occurred while initializing `{name}.__dict__`"), )); } @@ -249,6 +252,13 @@ fn initialize_tp_dict( // This is necessary for making static `LazyTypeObject`s unsafe impl Sync for LazyTypeObject {} +/// Used in the macro-expanded implementation of `type_object_raw` for `#[pyclass]` types +#[cold] +pub fn type_object_init_failed(py: Python<'_>, err: PyErr, type_name: &str) -> ! { + err.write_unraisable(py, None); + panic!("failed to create type object for `{type_name}`") +} + #[cold] fn wrap_in_runtime_error(py: Python<'_>, err: PyErr, message: String) -> PyErr { let runtime_err = PyRuntimeError::new_err(message); diff --git a/tests/test_class_attributes.rs b/tests/test_class_attributes.rs index 5cad559cf8b..b1a019c0457 100644 --- a/tests/test_class_attributes.rs +++ b/tests/test_class_attributes.rs @@ -152,40 +152,10 @@ fn recursive_class_attributes() { } #[test] +#[cfg(all(Py_3_8, not(Py_GIL_DISABLED)))] // sys.unraisablehook not available until Python 3.8 fn test_fallible_class_attribute() { - use pyo3::{exceptions::PyValueError, types::PyString}; - - struct CaptureStdErr<'py> { - oldstderr: Bound<'py, PyAny>, - string_io: Bound<'py, PyAny>, - } - - impl<'py> CaptureStdErr<'py> { - fn new(py: Python<'py>) -> PyResult { - let sys = py.import("sys")?; - let oldstderr = sys.getattr("stderr")?; - let string_io = py.import("io")?.getattr("StringIO")?.call0()?; - sys.setattr("stderr", &string_io)?; - Ok(Self { - oldstderr, - string_io, - }) - } - - fn reset(self) -> PyResult { - let py = self.string_io.py(); - let payload = self - .string_io - .getattr("getvalue")? - .call0()? - .cast::()? - .to_cow()? - .into_owned(); - let sys = py.import("sys")?; - sys.setattr("stderr", self.oldstderr)?; - Ok(payload) - } - } + use common::UnraisableCapture; + use pyo3::exceptions::PyValueError; #[pyclass] struct BrokenClass; @@ -199,21 +169,29 @@ fn test_fallible_class_attribute() { } Python::attach(|py| { - let stderr = CaptureStdErr::new(py).unwrap(); + let capture = UnraisableCapture::install(py); assert!(std::panic::catch_unwind(|| py.get_type::()).is_err()); - assert_eq!( - stderr.reset().unwrap().trim(), - "\ -ValueError: failed to create class attribute - -The above exception was the direct cause of the following exception: -RuntimeError: An error occurred while initializing `BrokenClass.fails_to_init` + let (err, object) = capture.borrow_mut(py).capture.take().unwrap(); + assert!(object.is_none(py)); -The above exception was the direct cause of the following exception: + assert_eq!( + err.to_string(), + "RuntimeError: An error occurred while initializing class BrokenClass" + ); + let cause = err.cause(py).unwrap(); + assert_eq!( + cause.to_string(), + "RuntimeError: An error occurred while initializing `BrokenClass.fails_to_init`" + ); + let cause = cause.cause(py).unwrap(); + assert_eq!( + cause.to_string(), + "ValueError: failed to create class attribute" + ); + assert!(cause.cause(py).is_none()); -RuntimeError: An error occurred while initializing class BrokenClass" - ) + capture.borrow_mut(py).uninstall(py); }); } From 52554ce0a33321893af17577a3ea0d179ad1b563 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 18 Aug 2025 11:01:31 -0600 Subject: [PATCH 802/936] Add PyMutex wrappers (#4523) * define public pyo3_ffi::PyMutex::new() * add pyo3::types::mutex for PyMutex wrappers * add changelog entry * update rust API for PyMutex bindings following code review * make the test do something more interesting * add Sync impl and make lock accept an immutable reference * respond to review comments * add test for PyMutex blocking behavior * expand docstrings * fix clippy under abi3 * simplify test * use AtomicBool instead of OnceLock * use std::hint::spin_loop() per clippy lint * make PyMutex::new() const * fix docstring formatting * tweaks from review comments * add poisoning to PyMutex * add missing conditional compilation for older pythons in FFI * doc tweaks * add PyMutex_IsLocked ffi wrapper * simplify conditional compilation * Add wrappers for PyMutex_IsLocked and use it in a test * add missing conditional compilation * delete unnecessary pub(crate) * add missing conditional compilation * fix clippy nits * remove unnecessary PhantomPinned field on PyMutex * revert unnecessary conditional compilation change * fix clippy on Python 3.13 * fix guide build * exclude more tests that use threads on wasm * don't expose PyMutex wrappers on wasm32 * try a different approach for non-unwinding targets * fix wasm clippy * improve test coverage * Remove unnecessary pub visibility --- newsfragments/4523.added.md | 2 + pyo3-ffi/src/cpython/lock.rs | 16 +- pyo3-ffi/src/object.rs | 9 +- src/types/mod.rs | 4 + src/types/mutex.rs | 468 +++++++++++++++++++++++++++++++++++ 5 files changed, 490 insertions(+), 9 deletions(-) create mode 100644 newsfragments/4523.added.md create mode 100644 src/types/mutex.rs diff --git a/newsfragments/4523.added.md b/newsfragments/4523.added.md new file mode 100644 index 00000000000..a699bfc4956 --- /dev/null +++ b/newsfragments/4523.added.md @@ -0,0 +1,2 @@ +* Added a Rust wrapper for `PyMutex`, available on Python 3.13 and newer. +* Added bindings for PyMutex_IsLocked, available on Python 3.14 and newer. \ No newline at end of file diff --git a/pyo3-ffi/src/cpython/lock.rs b/pyo3-ffi/src/cpython/lock.rs index 6c80b00d3c1..487173f9afb 100644 --- a/pyo3-ffi/src/cpython/lock.rs +++ b/pyo3-ffi/src/cpython/lock.rs @@ -1,14 +1,26 @@ -use std::marker::PhantomPinned; +#[cfg(Py_3_14)] +use std::os::raw::c_int; use std::sync::atomic::AtomicU8; #[repr(transparent)] #[derive(Debug)] pub struct PyMutex { pub(crate) _bits: AtomicU8, - pub(crate) _pin: PhantomPinned, +} + +// we don't impl Default because PyO3's safe wrappers don't need it +#[allow(clippy::new_without_default)] +impl PyMutex { + pub const fn new() -> PyMutex { + PyMutex { + _bits: AtomicU8::new(0), + } + } } extern "C" { pub fn PyMutex_Lock(m: *mut PyMutex); pub fn PyMutex_Unlock(m: *mut PyMutex); + #[cfg(Py_3_14)] + pub fn PyMutex_IsLocked(m: *mut PyMutex) -> c_int; } diff --git a/pyo3-ffi/src/object.rs b/pyo3-ffi/src/object.rs index 0e3aafe4b65..8e93df176d8 100644 --- a/pyo3-ffi/src/object.rs +++ b/pyo3-ffi/src/object.rs @@ -4,12 +4,10 @@ use crate::refcount; #[cfg(Py_GIL_DISABLED)] use crate::PyMutex; use std::ffi::{c_char, c_int, c_uint, c_ulong, c_void}; -#[cfg(Py_GIL_DISABLED)] -use std::marker::PhantomPinned; use std::mem; use std::ptr; #[cfg(Py_GIL_DISABLED)] -use std::sync::atomic::{AtomicIsize, AtomicU32, AtomicU8}; +use std::sync::atomic::{AtomicIsize, AtomicU32}; #[cfg(Py_LIMITED_API)] opaque_struct!(pub PyTypeObject); @@ -110,10 +108,7 @@ pub const PyObject_HEAD_INIT: PyObject = PyObject { #[cfg(all(Py_GIL_DISABLED, not(Py_3_14)))] _padding: 0, #[cfg(Py_GIL_DISABLED)] - ob_mutex: PyMutex { - _bits: AtomicU8::new(0), - _pin: PhantomPinned, - }, + ob_mutex: PyMutex::new(), #[cfg(Py_GIL_DISABLED)] ob_gc_bits: 0, #[cfg(Py_GIL_DISABLED)] diff --git a/src/types/mod.rs b/src/types/mod.rs index 666262d75f9..3def87297dd 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -32,6 +32,8 @@ pub use self::mapping::{PyMapping, PyMappingMethods}; pub use self::mappingproxy::PyMappingProxy; pub use self::memoryview::PyMemoryView; pub use self::module::{PyModule, PyModuleMethods}; +#[cfg(all(not(Py_LIMITED_API), Py_3_13))] +pub use self::mutex::{PyMutex, PyMutexGuard}; pub use self::none::PyNone; pub use self::notimplemented::PyNotImplemented; pub use self::num::PyInt; @@ -240,6 +242,8 @@ pub(crate) mod mapping; pub(crate) mod mappingproxy; mod memoryview; pub(crate) mod module; +#[cfg(all(not(Py_LIMITED_API), Py_3_13))] +mod mutex; mod none; mod notimplemented; mod num; diff --git a/src/types/mutex.rs b/src/types/mutex.rs new file mode 100644 index 00000000000..d6b49be5038 --- /dev/null +++ b/src/types/mutex.rs @@ -0,0 +1,468 @@ +use std::cell::UnsafeCell; +use std::marker::PhantomData; +use std::ops::{Deref, DerefMut}; +#[cfg(panic = "unwind")] +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{LockResult, PoisonError}; +#[cfg(panic = "unwind")] +use std::thread; + +// See std::sync::poison in the rust standard library. +// This is more-or-less copied from there since it is not public. +// this type detects a panic and poisons the wrapping mutex +struct Flag { + #[cfg(panic = "unwind")] + failed: AtomicBool, +} + +impl Flag { + #[inline] + const fn new() -> Flag { + Flag { + #[cfg(panic = "unwind")] + failed: AtomicBool::new(false), + } + } + + /// Checks the flag for an unguarded borrow, where we only care about existing poison. + #[inline] + fn borrow(&self) -> LockResult<()> { + if self.get() { + Err(PoisonError::new(())) + } else { + Ok(()) + } + } + + /// Checks the flag for a guarded borrow, where we may also set poison when `done`. + #[inline] + fn guard(&self) -> LockResult { + let ret = Guard { + #[cfg(panic = "unwind")] + panicking: thread::panicking(), + }; + if self.get() { + Err(PoisonError::new(ret)) + } else { + Ok(ret) + } + } + + #[inline] + #[cfg(panic = "unwind")] + fn done(&self, guard: &Guard) { + if !guard.panicking && thread::panicking() { + self.failed.store(true, Ordering::Relaxed); + } + } + + #[inline] + #[cfg(not(panic = "unwind"))] + fn done(&self, _guard: &Guard) {} + + #[inline] + #[cfg(panic = "unwind")] + fn get(&self) -> bool { + self.failed.load(Ordering::Relaxed) + } + + #[inline(always)] + #[cfg(not(panic = "unwind"))] + fn get(&self) -> bool { + false + } + + #[inline] + fn clear(&self) { + #[cfg(panic = "unwind")] + self.failed.store(false, Ordering::Relaxed) + } +} + +#[derive(Clone)] +pub(crate) struct Guard { + #[cfg(panic = "unwind")] + panicking: bool, +} + +/// Wrapper for [`PyMutex`](https://docs.python.org/3/c-api/init.html#c.PyMutex), exposing an RAII guard interface. +/// +/// Compared with `std::sync::Mutex` or `parking_lot::Mutex`, this is a very +/// stripped-down locking primitive that only supports blocking lock and unlock +/// operations and does not support `try_lock` or APIs that depend on +/// `try_lock`. For this reason, it is not possible to avoid the possibility of +/// possibly blocking when calling `lock` and extreme care must be taken to avoid +/// introducing a deadlock. +/// +/// This type is most useful when arbitrary Python code might execute while the +/// lock is held. On the GIL-enabled build, PyMutex will release the GIL if the +/// thread is blocked on acquiring the lock. On the free-threaded build, threads +/// blocked on acquiring a PyMutex will not prevent the garbage collector from +/// running. +/// +/// ## Poisoning +/// +/// Like `std::sync::Mutex`, `PyMutex` implements poisoning. A mutex +/// is considered poisoned whenever a thread panics while holding the mutex. Once +/// a mutex is poisoned, all other threads are unable to access the data by +/// default as it is likely to be tainted (some invariant is not being held). +/// +/// This means that the `lock` method returns a `Result` which indicated whether +/// the mutex has been poisoned or not. Must usage will simple `unwrap()` these +/// results, propagating panics among threads to ensure a possible invalid +/// invariant is not being observed. +/// +/// A poisoned mutex, however, does not prevent all access to the underlying +/// data. The `PoisonError` type has an `into_inner` method which will return +/// the guard that would have otherwise been returned on a successful lock. This +/// allows access to the data, despite the lock being poisoned. +pub struct PyMutex { + mutex: UnsafeCell, + poison: Flag, + data: UnsafeCell, +} + +/// RAII guard to handle releasing a PyMutex lock. +/// +/// The lock is released when `PyMutexGuard` is dropped. +pub struct PyMutexGuard<'a, T: ?Sized> { + inner: &'a PyMutex, + poison: Guard, + // this is equivalent to impl !Send, which we can't do + // because negative trait bounds aren't supported yet + _phantom: PhantomData<*const ()>, +} + +/// `T` must be `Sync` for a [`PyMutexGuard`] to be `Sync` +/// because it is possible to get a `&T` from `&MutexGuard` (via `Deref`). +unsafe impl Sync for PyMutexGuard<'_, T> {} + +/// `T` must be `Send` for a [`PyMutex`] to be `Send` because it is possible to acquire +/// the owned `T` from the `PyMutex` via [`into_inner`]. +/// +/// [`into_inner`]: PyMutex::into_inner +unsafe impl Send for PyMutex {} + +/// `T` must be `Send` for [`PyMutex`] to be `Sync`. +/// This ensures that the protected data can be accessed safely from multiple threads +/// without causing data races or other unsafe behavior. +/// +/// [`PyMutex`] provides mutable access to `T` to one thread at a time. However, it's essential +/// for `T` to be `Send` because it's not safe for non-`Send` structures to be accessed in +/// this manner. For instance, consider [`Rc`], a non-atomic reference counted smart pointer, +/// which is not `Send`. With `Rc`, we can have multiple copies pointing to the same heap +/// allocation with a non-atomic reference count. If we were to use `Mutex>`, it would +/// only protect one instance of `Rc` from shared access, leaving other copies vulnerable +/// to potential data races. +/// +/// Also note that it is not necessary for `T` to be `Sync` as `&T` is only made available +/// to one thread at a time if `T` is not `Sync`. +/// +/// [`Rc`]: std::rc::Rc +unsafe impl Sync for PyMutex {} + +impl PyMutex { + /// Acquire the mutex, blocking the current thread until it is able to do so. + pub fn lock(&self) -> LockResult> { + unsafe { crate::ffi::PyMutex_Lock(UnsafeCell::raw_get(&self.mutex)) }; + PyMutexGuard::new(self) + } + + /// Create a new mutex in an unlocked state ready for use. + pub const fn new(value: T) -> Self { + Self { + mutex: UnsafeCell::new(crate::ffi::PyMutex::new()), + data: UnsafeCell::new(value), + poison: Flag::new(), + } + } + + /// Check if the mutex is locked. + /// + /// Note that this is only useful for debugging or test purposes and should + /// not be used to make concurrency control decisions, as the lock state may + /// change immediately after the check. + #[cfg(Py_3_14)] + pub fn is_locked(&self) -> bool { + let ret = unsafe { crate::ffi::PyMutex_IsLocked(UnsafeCell::raw_get(&self.mutex)) }; + ret != 0 + } + + /// Consumes this mutex, returning the underlying data. + /// + /// # Errors + /// + /// If another user of this mutex panicked while holding the mutex, then + /// this call will return an error containing the underlying data + /// instead. + pub fn into_inner(self) -> LockResult + where + T: Sized, + { + let data = self.data.into_inner(); + map_result(self.poison.borrow(), |()| data) + } + + /// Clear the poisoned state from a mutex. + /// + /// If the mutex is poisoned, it will remain poisoned until this function is called. This + /// allows recovering from a poisoned state and marking that it has recovered. For example, if + /// the value is overwritten by a known-good value, then the mutex can be marked as + /// un-poisoned. Or possibly, the value could be inspected to determine if it is in a + /// consistent state, and if so the poison is removed. + pub fn clear_poison(&self) { + self.poison.clear(); + } +} + +#[cfg_attr(not(panic = "unwind"), allow(clippy::unnecessary_wraps))] +fn map_result(result: LockResult, f: F) -> LockResult +where + F: FnOnce(T) -> U, +{ + match result { + Ok(t) => Ok(f(t)), + #[cfg(panic = "unwind")] + Err(e) => Err(PoisonError::new(f(e.into_inner()))), + #[cfg(not(panic = "unwind"))] + Err(_) => { + unreachable!(); + } + } +} + +impl<'mutex, T: ?Sized> PyMutexGuard<'mutex, T> { + fn new(lock: &'mutex PyMutex) -> LockResult> { + map_result(lock.poison.guard(), |guard| PyMutexGuard { + inner: lock, + poison: guard, + _phantom: PhantomData, + }) + } +} + +impl<'a, T: ?Sized> Drop for PyMutexGuard<'a, T> { + fn drop(&mut self) { + unsafe { + self.inner.poison.done(&self.poison); + crate::ffi::PyMutex_Unlock(UnsafeCell::raw_get(&self.inner.mutex)) + }; + } +} + +impl<'a, T> Deref for PyMutexGuard<'a, T> { + type Target = T; + + fn deref(&self) -> &T { + // safety: cannot be null pointer because PyMutex::new always + // creates a valid PyMutex pointer + unsafe { &*self.inner.data.get() } + } +} + +impl<'a, T> DerefMut for PyMutexGuard<'a, T> { + fn deref_mut(&mut self) -> &mut T { + // safety: cannot be null pointer because PyMutex::new always + // creates a valid PyMutex pointer + unsafe { &mut *self.inner.data.get() } + } +} + +#[cfg(test)] +mod tests { + #[cfg(not(target_arch = "wasm32"))] + use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, Barrier, + }; + + use super::*; + #[cfg(not(target_arch = "wasm32"))] + use crate::types::{PyAnyMethods, PyDict, PyDictMethods, PyNone}; + #[cfg(not(target_arch = "wasm32"))] + use crate::Py; + #[cfg(not(target_arch = "wasm32"))] + use crate::Python; + + #[cfg(not(target_arch = "wasm32"))] + #[test] + fn test_pymutex() { + let mutex = Python::attach(|py| -> PyMutex> { + let d = PyDict::new(py); + PyMutex::new(d.unbind()) + }); + #[cfg_attr(not(Py_3_14), allow(unused_variables))] + let mutex = Python::attach(|py| { + let mutex = py.detach(|| -> PyMutex> { + std::thread::spawn(|| { + let dict_guard = mutex.lock().unwrap(); + Python::attach(|py| { + let dict = dict_guard.bind(py); + dict.set_item(PyNone::get(py), PyNone::get(py)).unwrap(); + }); + #[cfg(Py_3_14)] + assert!(mutex.is_locked()); + drop(dict_guard); + #[cfg(Py_3_14)] + assert!(!mutex.is_locked()); + mutex + }) + .join() + .unwrap() + }); + + let dict_guard = mutex.lock().unwrap(); + #[cfg(Py_3_14)] + assert!(mutex.is_locked()); + let d = dict_guard.bind(py); + + assert!(d + .get_item(PyNone::get(py)) + .unwrap() + .unwrap() + .eq(PyNone::get(py)) + .unwrap()); + #[cfg(Py_3_14)] + assert!(mutex.is_locked()); + drop(dict_guard); + #[cfg(Py_3_14)] + assert!(!mutex.is_locked()); + mutex + }); + #[cfg(Py_3_14)] + assert!(!mutex.is_locked()); + } + + #[cfg(not(target_arch = "wasm32"))] + #[test] + fn test_pymutex_blocks() { + let mutex = PyMutex::new(()); + let first_thread_locked_once = AtomicBool::new(false); + let second_thread_locked_once = AtomicBool::new(false); + let finished = AtomicBool::new(false); + let barrier = Barrier::new(2); + + std::thread::scope(|s| { + s.spawn(|| { + let guard = mutex.lock(); + first_thread_locked_once.store(true, Ordering::SeqCst); + while !finished.load(Ordering::SeqCst) { + if second_thread_locked_once.load(Ordering::SeqCst) { + // Wait a little to guard against the unlikely event that + // the other thread isn't blocked on acquiring the mutex yet. + // If PyMutex had a try_lock implementation this would be + // unnecessary + std::thread::sleep(std::time::Duration::from_millis(10)); + // block (and hold the mutex) until the receiver actually receives something + barrier.wait(); + finished.store(true, Ordering::SeqCst); + } + } + drop(guard); + }); + + s.spawn(|| { + while !first_thread_locked_once.load(Ordering::SeqCst) { + std::hint::spin_loop(); + } + second_thread_locked_once.store(true, Ordering::SeqCst); + let guard = mutex.lock(); + assert!(finished.load(Ordering::SeqCst)); + drop(guard); + }); + + barrier.wait(); + }); + } + + #[cfg(not(target_arch = "wasm32"))] + #[test] + fn test_recover_poison() { + let mutex = Python::attach(|py| -> PyMutex> { + let d = PyDict::new(py); + d.set_item("hello", "world").unwrap(); + PyMutex::new(d.unbind()) + }); + + let lock = Arc::new(mutex); + let lock2 = Arc::clone(&lock); + + let _ = thread::spawn(move || { + let _guard = lock2.lock().unwrap(); + + // poison the mutex + panic!(); + }) + .join(); + + // by now the lock is poisoned, use into_inner to recover the value despite that + let guard = match lock.lock() { + Ok(_) => { + unreachable!(); + } + Err(poisoned) => poisoned.into_inner(), + }; + + Python::attach(|py| { + assert!( + (*guard) + .bind(py) + .get_item("hello") + .unwrap() + .unwrap() + .extract::<&str>() + .unwrap() + == "world" + ); + }); + + // now test recovering via PyMutex::into_inner + let mutex = PyMutex::new(0); + assert_eq!(mutex.into_inner().unwrap(), 0); + + let mutex = PyMutex::new(0); + let _ = std::thread::scope(|s| { + s.spawn(|| { + let _guard = mutex.lock().unwrap(); + + // poison the mutex + panic!(); + }) + .join() + }); + + match mutex.into_inner() { + Ok(_) => { + unreachable!() + } + Err(e) => { + assert!(e.into_inner() == 0) + } + } + + // now test recovering via PyMutex::clear_poison + let mutex = PyMutex::new(0); + let _ = std::thread::scope(|s| { + s.spawn(|| { + let _guard = mutex.lock().unwrap(); + + // poison the mutex + panic!(); + }) + .join() + }); + mutex.clear_poison(); + assert!(*mutex.lock().unwrap() == 0); + } + + #[test] + fn test_send_not_send() { + use crate::impl_::pyclass::{value_of, IsSend, IsSync}; + + assert!(!value_of!(IsSend, PyMutexGuard<'_, i32>)); + assert!(value_of!(IsSync, PyMutexGuard<'_, i32>)); + + assert!(value_of!(IsSend, PyMutex)); + assert!(value_of!(IsSync, PyMutex)); + } +} From 6e9a7fe8ec752c07eb1478df888247e592ddc5cd Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Wed, 20 Aug 2025 06:30:35 +0200 Subject: [PATCH 803/936] unpin nightly (#5335) --- .github/workflows/ci.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8aecae55b64..c2d86de775e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -415,11 +415,9 @@ jobs: - uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'CI-save-pr-cache') }} - - uses: dtolnay/rust-toolchain@master + - uses: dtolnay/rust-toolchain@nightly with: components: rust-src - # FIXME: unpin once https://github.com/rust-lang/rust/issues/145437 is resolved - toolchain: nightly-2025-08-14 - 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 @@ -439,11 +437,9 @@ jobs: - uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'CI-save-pr-cache') }} - - uses: dtolnay/rust-toolchain@master + - uses: dtolnay/rust-toolchain@nightly with: components: rust-src - # FIXME: unpin once https://github.com/rust-lang/rust/issues/145437 is resolved - toolchain: nightly-2025-08-14 - run: cargo rustdoc --lib --no-default-features --features full,jiff-02 -Zunstable-options --config "build.rustdocflags=[\"--cfg\", \"docsrs\"]" emscripten: From 12a1792c4364466a0fda880252dffdc82655c1a5 Mon Sep 17 00:00:00 2001 From: Thomas Tanon Date: Wed, 20 Aug 2025 10:32:51 +0200 Subject: [PATCH 804/936] Support Goblin 0.10 alongside 0.9 (#5337) --- newsfragments/5337.changed.md | 1 + pyo3-introspection/Cargo.toml | 2 +- pyo3-introspection/src/introspection.rs | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 newsfragments/5337.changed.md diff --git a/newsfragments/5337.changed.md b/newsfragments/5337.changed.md new file mode 100644 index 00000000000..d773e1602f4 --- /dev/null +++ b/newsfragments/5337.changed.md @@ -0,0 +1 @@ +Introspection: add compatibility with Goblin 0.10 \ No newline at end of file diff --git a/pyo3-introspection/Cargo.toml b/pyo3-introspection/Cargo.toml index ccdc080c48e..74d9edf24ae 100644 --- a/pyo3-introspection/Cargo.toml +++ b/pyo3-introspection/Cargo.toml @@ -10,7 +10,7 @@ edition = "2021" [dependencies] anyhow = "1" -goblin = "0.9.0" +goblin = ">=0.9, <0.11" serde = { version = "1", features = ["derive"] } serde_json = "1" unicode-ident = "1" diff --git a/pyo3-introspection/src/introspection.rs b/pyo3-introspection/src/introspection.rs index eedbd625e5c..c089292c1cb 100644 --- a/pyo3-introspection/src/introspection.rs +++ b/pyo3-introspection/src/introspection.rs @@ -328,7 +328,7 @@ fn find_introspection_chunks_in_pe(pe: &PE<'_>, library_content: &[u8]) -> Resul .iter() .find(|section| section.name().unwrap_or_default() == ".rdata") .context("No .rdata section found")?; - let rdata_shift = pe.image_base + let rdata_shift = usize::try_from(pe.image_base).context("image_base overflow")? + usize::try_from(rdata_data_section.virtual_address) .context(".rdata virtual_address overflow")? - usize::try_from(rdata_data_section.pointer_to_raw_data) From e93393c84213bf5b02a9624eaa2b0cb2de12c1ec Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Thu, 21 Aug 2025 06:16:37 +0100 Subject: [PATCH 805/936] panic on attach if interpreter is shutting down (#5317) * panic on attach if interpreter is shutting down * fixup for older versions * fix clippy --- newsfragments/5317.added.md | 1 + newsfragments/5317.changed.md | 1 + newsfragments/5317.fixed.md | 1 + newsfragments/5317.removed.md | 1 + pyo3-ffi/src/cpython/pylifecycle.rs | 34 +++++++++-------------------- pyo3-ffi/src/pylifecycle.rs | 8 ++++++- src/internal/state.rs | 24 ++++++++++++++++++-- src/marker.rs | 23 +++++-------------- 8 files changed, 49 insertions(+), 44 deletions(-) create mode 100644 newsfragments/5317.added.md create mode 100644 newsfragments/5317.changed.md create mode 100644 newsfragments/5317.fixed.md create mode 100644 newsfragments/5317.removed.md diff --git a/newsfragments/5317.added.md b/newsfragments/5317.added.md new file mode 100644 index 00000000000..b6a571de59c --- /dev/null +++ b/newsfragments/5317.added.md @@ -0,0 +1 @@ +Add FFI definitions `Py_Version` and `Py_IsFinalizing`. diff --git a/newsfragments/5317.changed.md b/newsfragments/5317.changed.md new file mode 100644 index 00000000000..7d80ec4cfdd --- /dev/null +++ b/newsfragments/5317.changed.md @@ -0,0 +1 @@ +`Python::attach` will now panic if the Python interpreter is in the process of shutting down. diff --git a/newsfragments/5317.fixed.md b/newsfragments/5317.fixed.md new file mode 100644 index 00000000000..0d0a752e338 --- /dev/null +++ b/newsfragments/5317.fixed.md @@ -0,0 +1 @@ +Fix FFI definition `Py_Exit` (never returns, was `()` return value, now `!`). diff --git a/newsfragments/5317.removed.md b/newsfragments/5317.removed.md new file mode 100644 index 00000000000..5269ccda918 --- /dev/null +++ b/newsfragments/5317.removed.md @@ -0,0 +1 @@ +Remove private FFI definitions `_Py_IsCoreInitialized` and `_Py_InitializeMain` diff --git a/pyo3-ffi/src/cpython/pylifecycle.rs b/pyo3-ffi/src/cpython/pylifecycle.rs index 1609c6ab7e4..975dbce1915 100644 --- a/pyo3-ffi/src/cpython/pylifecycle.rs +++ b/pyo3-ffi/src/cpython/pylifecycle.rs @@ -2,9 +2,10 @@ use crate::{PyConfig, PyPreConfig, PyStatus, Py_ssize_t}; use libc::wchar_t; use std::ffi::{c_char, c_int}; -// "private" functions in cpython/pylifecycle.h accepted in PEP 587 extern "C" { - // skipped _Py_SetStandardStreamEncoding; + + // skipped Py_FrozenMain + pub fn Py_PreInitialize(src_config: *const PyPreConfig) -> PyStatus; pub fn Py_PreInitializeFromBytesArgs( src_config: *const PyPreConfig, @@ -16,34 +17,14 @@ extern "C" { argc: Py_ssize_t, argv: *mut *mut wchar_t, ) -> PyStatus; - pub fn _Py_IsCoreInitialized() -> c_int; pub fn Py_InitializeFromConfig(config: *const PyConfig) -> PyStatus; - pub fn _Py_InitializeMain() -> PyStatus; pub fn Py_RunMain() -> c_int; pub fn Py_ExitStatusException(status: PyStatus) -> !; - // skipped _Py_RestoreSignals - // skipped Py_FdIsInteractive - // skipped _Py_FdIsInteractive - - // skipped _Py_SetProgramFullPath - - // skipped _Py_gitidentifier - // skipped _Py_getversion - - // skipped _Py_IsFinalizing - - // skipped _PyOS_URandom - // skipped _PyOS_URandomNonblock - - // skipped _Py_CoerceLegacyLocale - // skipped _Py_LegacyLocaleDetected - // skipped _Py_SetLocaleFromEnv - } #[cfg(Py_3_12)] @@ -76,6 +57,11 @@ pub const _PyInterpreterConfig_INIT: PyInterpreterConfig = PyInterpreterConfig { gil: PyInterpreterConfig_OWN_GIL, }; +// https://github.com/python/cpython/blob/902de283a8303177eb95bf5bc252d2421fcbd758/Include/cpython/pylifecycle.h#L63-L65 +#[cfg(Py_3_12)] +const _PyInterpreterConfig_LEGACY_CHECK_MULTI_INTERP_EXTENSIONS: c_int = + if cfg!(Py_GIL_DISABLED) { 1 } else { 0 }; + #[cfg(Py_3_12)] pub const _PyInterpreterConfig_LEGACY_INIT: PyInterpreterConfig = PyInterpreterConfig { use_main_obmalloc: 1, @@ -83,7 +69,7 @@ pub const _PyInterpreterConfig_LEGACY_INIT: PyInterpreterConfig = PyInterpreterC allow_exec: 1, allow_threads: 1, allow_daemon_threads: 1, - check_multi_interp_extensions: 0, + check_multi_interp_extensions: _PyInterpreterConfig_LEGACY_CHECK_MULTI_INTERP_EXTENSIONS, gil: PyInterpreterConfig_SHARED_GIL, }; @@ -96,4 +82,4 @@ extern "C" { } // skipped atexit_datacallbackfunc -// skipped _Py_AtExit +// skipped PyUnstable_AtExit diff --git a/pyo3-ffi/src/pylifecycle.rs b/pyo3-ffi/src/pylifecycle.rs index 53b82a55b91..e822da09f98 100644 --- a/pyo3-ffi/src/pylifecycle.rs +++ b/pyo3-ffi/src/pylifecycle.rs @@ -18,7 +18,7 @@ extern "C" { #[cfg_attr(PyPy, link_name = "PyPy_AtExit")] pub fn Py_AtExit(func: Option) -> c_int; - pub fn Py_Exit(arg1: c_int); + pub fn Py_Exit(arg1: c_int) -> !; 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; @@ -89,4 +89,10 @@ type PyOS_sighandler_t = unsafe extern "C" fn(arg1: c_int); extern "C" { pub fn PyOS_getsig(arg1: c_int) -> PyOS_sighandler_t; pub fn PyOS_setsig(arg1: c_int, arg2: PyOS_sighandler_t) -> PyOS_sighandler_t; + + #[cfg(Py_3_11)] + pub static Py_Version: std::ffi::c_ulong; + + #[cfg(Py_3_13)] + pub fn Py_IsFinalizing() -> c_int; } diff --git a/src/internal/state.rs b/src/internal/state.rs index 1aff268c7f9..fa52e7e4986 100644 --- a/src/internal/state.rs +++ b/src/internal/state.rs @@ -57,6 +57,14 @@ impl AttachGuard { crate::interpreter_lifecycle::ensure_initialized(); + // Calling `PyGILState_Ensure` while finalizing may crash CPython in unpredictable + // ways, we'll make a best effort attempt here to avoid that. (There's a time of + // check to time-of-use issue, but it's better than nothing.) + assert!( + !is_finalizing(), + "Cannot attach to the Python interpreter while it is finalizing." + ); + // SAFETY: We have ensured the Python interpreter is initialized. unsafe { Self::acquire_unchecked() } } @@ -75,8 +83,8 @@ impl AttachGuard { _ => {} } - // SAFETY: This API is always sound to call - if unsafe { ffi::Py_IsInitialized() } == 0 { + // SAFETY: These APIs are always sound to call + if unsafe { ffi::Py_IsInitialized() } == 0 || is_finalizing() { // If the interpreter is not initialized, we cannot attach. return None; } @@ -130,6 +138,18 @@ impl AttachGuard { } } +fn is_finalizing() -> bool { + // SAFTETY: always safe to call this + #[cfg(Py_3_13)] + unsafe { + ffi::Py_IsFinalizing() != 0 + } + + // can't reliably check this before 3.13 + #[cfg(not(Py_3_13))] + false +} + /// The Drop implementation for `AttachGuard` will decrement the attach count (and potentially detach). impl Drop for AttachGuard { fn drop(&mut self) { diff --git a/src/marker.rs b/src/marker.rs index ea75e3757a2..8f8b48d983a 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -392,6 +392,7 @@ impl Python<'_> { /// /// - If the [`auto-initialize`] feature is not enabled and the Python interpreter is not /// initialized. + /// - If the Python interpreter is in the process of [shutting down]. /// /// # Examples /// @@ -409,6 +410,7 @@ impl Python<'_> { /// ``` /// /// [`auto-initialize`]: https://pyo3.rs/main/features.html#auto-initialize + /// [shutting down]: https://docs.python.org/3/glossary.html#term-interpreter-shutdown #[inline] #[track_caller] pub fn attach(f: F) -> R @@ -422,6 +424,7 @@ impl Python<'_> { /// Variant of [`Python::attach`] which will do no work if the interpreter is in a /// state where it cannot be attached to: /// - in the middle of GC traversal + /// - in the process of shutting down /// - not initialized #[inline] #[track_caller] @@ -464,27 +467,13 @@ impl Python<'_> { /// Like [`Python::attach`] except Python interpreter state checking is skipped. /// - /// Normally when the GIL is acquired, we check that the Python interpreter is an - /// appropriate state (e.g. it is fully initialized). This function skips those - /// checks. + /// Normally when the GIL is acquired, PyO3 checks that the Python interpreter is + /// in an appropriate state (e.g. it is fully initialized). This function skips + /// those checks. /// /// # Safety /// /// If [`Python::attach`] would succeed, it is safe to call this function. - /// - /// In most cases, you should use [`Python::attach`]. - /// - /// A justified scenario for calling this function is during multi-phase interpreter - /// initialization when [`Python::attach`] would fail before - // this link is only valid on 3.8+not pypy and up. - #[cfg_attr( - all(Py_3_8, not(PyPy)), - doc = "[`_Py_InitializeMain`](crate::ffi::_Py_InitializeMain)" - )] - #[cfg_attr(any(not(Py_3_8), PyPy), doc = "`_Py_InitializeMain`")] - /// is called because the interpreter is only partially initialized. - /// - /// Behavior in other scenarios is not documented. #[inline] #[track_caller] pub unsafe fn with_gil_unchecked(f: F) -> R From 56efcddd3b7c6b8edb418bdec9c87ce440ad5c47 Mon Sep 17 00:00:00 2001 From: Thomas Tanon Date: Thu, 21 Aug 2025 20:41:30 +0200 Subject: [PATCH 806/936] Basic introspection of #[derive(FromPyObject)] (#5339) Struct fields are not supported yet --- newsfragments/5339.added.md | 1 + pyo3-macros-backend/src/derive_attributes.rs | 2 +- pyo3-macros-backend/src/frompyobject.rs | 116 +++++++++++++++++-- pyo3-macros-backend/src/introspection.rs | 8 +- pyo3-macros-backend/src/pyclass.rs | 9 ++ pytests/src/pyclasses.rs | 24 +++- pytests/stubs/pyclasses.pyi | 5 + src/conversion.rs | 9 ++ src/impl_/pyclass.rs | 3 + 9 files changed, 162 insertions(+), 15 deletions(-) create mode 100644 newsfragments/5339.added.md diff --git a/newsfragments/5339.added.md b/newsfragments/5339.added.md new file mode 100644 index 00000000000..a12a1da6e3a --- /dev/null +++ b/newsfragments/5339.added.md @@ -0,0 +1 @@ +Basic introspection of `#[derive(FromPyObject)]` (no struct fields support yet) \ No newline at end of file diff --git a/pyo3-macros-backend/src/derive_attributes.rs b/pyo3-macros-backend/src/derive_attributes.rs index 63328a107b4..6ec78e17eb0 100644 --- a/pyo3-macros-backend/src/derive_attributes.rs +++ b/pyo3-macros-backend/src/derive_attributes.rs @@ -44,7 +44,7 @@ impl Parse for ContainerAttribute { } } -#[derive(Default)] +#[derive(Default, Clone)] pub struct ContainerAttributes { /// Treat the Container as a Wrapper, operate directly on its field pub transparent: Option, diff --git a/pyo3-macros-backend/src/frompyobject.rs b/pyo3-macros-backend/src/frompyobject.rs index 2c288709f93..b674f4e530e 100644 --- a/pyo3-macros-backend/src/frompyobject.rs +++ b/pyo3-macros-backend/src/frompyobject.rs @@ -1,5 +1,9 @@ use crate::attributes::{DefaultAttribute, FromPyWithAttribute, RenamingRule}; use crate::derive_attributes::{ContainerAttributes, FieldAttributes, FieldGetter}; +#[cfg(feature = "experimental-inspect")] +use crate::introspection::ConcatenationBuilder; +#[cfg(feature = "experimental-inspect")] +use crate::utils::TypeExt; use crate::utils::{self, Ctx}; use proc_macro2::TokenStream; use quote::{format_ident, quote, quote_spanned, ToTokens}; @@ -96,6 +100,16 @@ impl<'a> Enum<'a> { ) ) } + + #[cfg(feature = "experimental-inspect")] + fn write_input_type(&self, builder: &mut ConcatenationBuilder, ctx: &Ctx) { + for (i, var) in self.variants.iter().enumerate() { + if i > 0 { + builder.push_str(" | "); + } + var.write_input_type(builder, ctx); + } + } } struct NamedStructField<'a> { @@ -103,10 +117,12 @@ struct NamedStructField<'a> { getter: Option, from_py_with: Option, default: Option, + ty: &'a syn::Type, } struct TupleStructField { from_py_with: Option, + ty: syn::Type, } /// Container Style @@ -120,7 +136,8 @@ enum ContainerType<'a> { /// 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::Ident, Option), + #[cfg_attr(not(feature = "experimental-inspect"), allow(unused))] + StructNewtype(&'a syn::Ident, Option, &'a syn::Type), /// Tuple struct, e.g. `struct Foo(String)`. /// /// Variant contains a list of conversion methods for each of the fields that are directly @@ -129,7 +146,8 @@ enum ContainerType<'a> { /// Tuple newtype, e.g. `#[transparent] struct Foo(String)` /// /// The wrapped field is directly extracted from the object. - TupleNewtype(Option), + #[cfg_attr(not(feature = "experimental-inspect"), allow(unused))] + TupleNewtype(Option, Box), } /// Data container @@ -168,6 +186,7 @@ impl<'a> Container<'a> { ); Ok(TupleStructField { from_py_with: attrs.from_py_with, + ty: field.ty.clone(), }) }) .collect::>>()?; @@ -176,7 +195,7 @@ impl<'a> Container<'a> { // Always treat a 1-length tuple struct as "transparent", even without the // explicit annotation. let field = tuple_fields.pop().unwrap(); - ContainerType::TupleNewtype(field.from_py_with) + ContainerType::TupleNewtype(field.from_py_with, Box::new(field.ty)) } else if options.transparent.is_some() { bail_spanned!( fields.span() => "transparent structs and variants can only have 1 field" @@ -216,6 +235,7 @@ impl<'a> Container<'a> { getter: attrs.getter, from_py_with: attrs.from_py_with, default: attrs.default, + ty: &field.ty, }) }) .collect::>>()?; @@ -237,7 +257,7 @@ impl<'a> Container<'a> { field.getter.is_none(), field.ident.span() => "`transparent` structs may not have a `getter` for the inner field" ); - ContainerType::StructNewtype(field.ident, field.from_py_with) + ContainerType::StructNewtype(field.ident, field.from_py_with, field.ty) } else { ContainerType::Struct(struct_fields) } @@ -274,10 +294,10 @@ impl<'a> Container<'a> { /// Build derivation body for a struct. fn build(&self, ctx: &Ctx) -> TokenStream { match &self.ty { - ContainerType::StructNewtype(ident, from_py_with) => { + ContainerType::StructNewtype(ident, from_py_with, _) => { self.build_newtype_struct(Some(ident), from_py_with, ctx) } - ContainerType::TupleNewtype(from_py_with) => { + ContainerType::TupleNewtype(from_py_with, _) => { self.build_newtype_struct(None, from_py_with, ctx) } ContainerType::Tuple(tups) => self.build_tuple_struct(tups, ctx), @@ -438,6 +458,51 @@ impl<'a> Container<'a> { quote!(::std::result::Result::Ok(#self_ty{#fields})) } + + #[cfg(feature = "experimental-inspect")] + fn write_input_type(&self, builder: &mut ConcatenationBuilder, ctx: &Ctx) { + match &self.ty { + ContainerType::StructNewtype(_, from_py_with, ty) => { + Self::write_field_input_type(from_py_with, ty, builder, ctx); + } + ContainerType::TupleNewtype(from_py_with, ty) => { + Self::write_field_input_type(from_py_with, ty, builder, ctx); + } + ContainerType::Tuple(tups) => { + builder.push_str("tuple["); + for (i, TupleStructField { from_py_with, ty }) in tups.iter().enumerate() { + if i > 0 { + builder.push_str(", "); + } + Self::write_field_input_type(from_py_with, ty, builder, ctx); + } + builder.push_str("]"); + } + ContainerType::Struct(_) => { + // TODO: implement using a Protocol? + builder.push_str("_typeshed.Incomplete") + } + } + } + + #[cfg(feature = "experimental-inspect")] + fn write_field_input_type( + from_py_with: &Option, + ty: &syn::Type, + builder: &mut ConcatenationBuilder, + ctx: &Ctx, + ) { + if from_py_with.is_some() { + // We don't know what from_py_with is doing + builder.push_str("_typeshed.Incomplete") + } else { + let ty = ty.clone().elide_lifetimes(); + let pyo3_crate_path = &ctx.pyo3_path; + builder.push_tokens( + quote! { <#ty as #pyo3_crate_path::FromPyObject<'_>>::INPUT_TYPE.as_bytes() }, + ) + } + } } fn verify_and_get_lifetime(generics: &syn::Generics) -> Result> { @@ -487,7 +552,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, options)?; + let en = Enum::new(en, &tokens.ident, options.clone())?; en.build(ctx) } syn::Data::Struct(st) => { @@ -495,7 +560,7 @@ pub fn build_derive_from_pyobject(tokens: &DeriveInput) -> Result { bail_spanned!(lit_str.span() => "`annotation` is unsupported for structs"); } let ident = &tokens.ident; - let st = Container::new(&st.fields, parse_quote!(#ident), options)?; + let st = Container::new(&st.fields, parse_quote!(#ident), options.clone())?; st.build(ctx) } syn::Data::Union(_) => bail_spanned!( @@ -503,6 +568,40 @@ pub fn build_derive_from_pyobject(tokens: &DeriveInput) -> Result { ), }; + #[cfg(feature = "experimental-inspect")] + let input_type = { + let mut builder = ConcatenationBuilder::default(); + if tokens + .generics + .params + .iter() + .all(|p| matches!(p, syn::GenericParam::Lifetime(_))) + { + match &tokens.data { + syn::Data::Enum(en) => { + Enum::new(en, &tokens.ident, options)?.write_input_type(&mut builder, ctx) + } + syn::Data::Struct(st) => { + let ident = &tokens.ident; + Container::new(&st.fields, parse_quote!(#ident), options.clone())? + .write_input_type(&mut builder, ctx) + } + syn::Data::Union(_) => { + // Not supported at this point + builder.push_str("_typeshed.Incomplete") + } + } + } else { + // We don't know how to deal with generic parameters + // Blocked by https://github.com/rust-lang/rust/issues/76560 + builder.push_str("_typeshed.Incomplete") + }; + let input_type = builder.into_token_stream(&ctx.pyo3_path); + quote! { const INPUT_TYPE: &'static str = unsafe { ::std::str::from_utf8_unchecked(#input_type) }; } + }; + #[cfg(not(feature = "experimental-inspect"))] + let input_type = quote! {}; + let ident = &tokens.ident; Ok(quote!( #[automatically_derived] @@ -510,6 +609,7 @@ pub fn build_derive_from_pyobject(tokens: &DeriveInput) -> Result { fn extract_bound(obj: &#pyo3_path::Bound<#lt_param, #pyo3_path::PyAny>) -> #pyo3_path::PyResult { #derives } + #input_type } )) } diff --git a/pyo3-macros-backend/src/introspection.rs b/pyo3-macros-backend/src/introspection.rs index 868bfe18fb5..4b40d9c6438 100644 --- a/pyo3-macros-backend/src/introspection.rs +++ b/pyo3-macros-backend/src/introspection.rs @@ -464,13 +464,13 @@ impl<'a> From> for AttributedIntrospectionNode<'a> { } #[derive(Default)] -struct ConcatenationBuilder { +pub struct ConcatenationBuilder { elements: Vec, current_string: String, } impl ConcatenationBuilder { - fn push_tokens(&mut self, token_stream: TokenStream) { + pub fn push_tokens(&mut self, token_stream: TokenStream) { if !self.current_string.is_empty() { self.elements.push(ConcatenationBuilderElement::String(take( &mut self.current_string, @@ -480,7 +480,7 @@ impl ConcatenationBuilder { .push(ConcatenationBuilderElement::TokenStream(token_stream)); } - fn push_str(&mut self, value: &str) { + pub fn push_str(&mut self, value: &str) { self.current_string.push_str(value); } @@ -502,7 +502,7 @@ impl ConcatenationBuilder { self.current_string.push('"'); } - fn into_token_stream(self, pyo3_crate_path: &PyO3CratePath) -> TokenStream { + pub fn into_token_stream(self, pyo3_crate_path: &PyO3CratePath) -> TokenStream { let mut elements = self.elements; if !self.current_string.is_empty() { elements.push(ConcatenationBuilderElement::String(self.current_string)); diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 3ff700074dc..b4feb2f7fd5 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -2373,6 +2373,13 @@ impl<'a> PyClassImplsBuilder<'a> { } }; + let type_name = if cfg!(feature = "experimental-inspect") { + let full_name = get_class_python_module_and_name(cls, self.attr); + quote! { const TYPE_NAME: &'static str = #full_name; } + } else { + quote! {} + }; + Ok(quote! { #assertions @@ -2393,6 +2400,8 @@ impl<'a> PyClassImplsBuilder<'a> { type WeakRef = #weakref; type BaseNativeType = #base_nativetype; + #type_name + fn items_iter() -> #pyo3_path::impl_::pyclass::PyClassItemsIter { use #pyo3_path::impl_::pyclass::*; let collector = PyClassImplCollector::::new(); diff --git a/pytests/src/pyclasses.rs b/pytests/src/pyclasses.rs index d9ec2547478..bd5146a2086 100644 --- a/pytests/src/pyclasses.rs +++ b/pytests/src/pyclasses.rs @@ -5,6 +5,7 @@ use pyo3::prelude::*; use pyo3::types::PyType; #[pyclass] +#[derive(Clone, Default)] struct EmptyClass {} #[pymethods] @@ -104,6 +105,7 @@ impl ClassWithDict { } #[pyclass] +#[derive(Clone)] struct ClassWithDecorators { attr: usize, } @@ -142,6 +144,24 @@ impl ClassWithDecorators { } } +#[derive(FromPyObject, IntoPyObject)] +enum AClass { + NewType(EmptyClass), + Tuple(EmptyClass, EmptyClass), + Struct { + f: EmptyClass, + #[pyo3(item(42))] + g: EmptyClass, + #[pyo3(default)] + h: EmptyClass, + }, +} + +#[pyfunction] +fn map_a_class(cls: AClass) -> AClass { + cls +} + #[pymodule(gil_used = false)] pub mod pyclasses { #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] @@ -149,7 +169,7 @@ pub mod pyclasses { use super::ClassWithDict; #[pymodule_export] use super::{ - AssertingBaseClass, ClassWithDecorators, ClassWithoutConstructor, EmptyClass, PyClassIter, - PyClassThreadIter, + map_a_class, AssertingBaseClass, ClassWithDecorators, ClassWithoutConstructor, EmptyClass, + PyClassIter, PyClassThreadIter, }; } diff --git a/pytests/stubs/pyclasses.pyi b/pytests/stubs/pyclasses.pyi index 27c825d7d04..7d66ce4cd40 100644 --- a/pytests/stubs/pyclasses.pyi +++ b/pytests/stubs/pyclasses.pyi @@ -1,3 +1,4 @@ +import _typeshed import typing class AssertingBaseClass: @@ -34,3 +35,7 @@ class PyClassIter: class PyClassThreadIter: def __new__(cls, /) -> None: ... def __next__(self, /) -> int: ... + +def map_a_class( + cls: EmptyClass | tuple[EmptyClass, EmptyClass] | _typeshed.Incomplete, +) -> typing.Any: ... diff --git a/src/conversion.rs b/src/conversion.rs index dc851e9f8d2..e42d30a089d 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -407,6 +407,9 @@ impl FromPyObject<'_> for T where T: PyClass + Clone, { + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = ::TYPE_NAME; + fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { let bound = obj.cast::()?; Ok(bound.try_borrow()?.clone()) @@ -417,6 +420,9 @@ impl<'py, T> FromPyObject<'py> for PyRef<'py, T> where T: PyClass, { + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = ::TYPE_NAME; + fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { obj.cast::()?.try_borrow().map_err(Into::into) } @@ -426,6 +432,9 @@ impl<'py, T> FromPyObject<'py> for PyRefMut<'py, T> where T: PyClass, { + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = ::TYPE_NAME; + fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { obj.cast::()?.try_borrow_mut().map_err(Into::into) } diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index 1228c2ea758..22e7ac93b24 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -219,6 +219,9 @@ pub trait PyClassImpl: Sized + 'static { /// from the PyClassDocGenerator` type. const DOC: &'static CStr; + #[cfg(feature = "experimental-inspect")] + const TYPE_NAME: &'static str; + fn items_iter() -> PyClassItemsIter; #[inline] From 18480c716b15e9a8e1b07fb77b84697ef58b9ec1 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Thu, 21 Aug 2025 20:09:35 +0100 Subject: [PATCH 807/936] add `PyOnceLock` type (#5223) * add `PyOnceLock` type --------- Co-authored-by: Nathan Goldbaum * Apply suggestions from code review Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> * Update src/impl_/exceptions.rs Co-authored-by: Nathan Goldbaum --------- Co-authored-by: Nathan Goldbaum Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> --- Cargo.toml | 2 + guide/src/faq.md | 14 +- guide/src/free-threading.md | 55 +++--- guide/src/migration.md | 41 +++- newsfragments/5171.packaging.md | 1 - newsfragments/5223.added.md | 1 + newsfragments/5223.changed.md | 1 + pyo3-macros-backend/src/pyclass.rs | 8 +- src/conversions/bigdecimal.rs | 6 +- src/conversions/chrono.rs | 4 +- src/conversions/num_rational.rs | 4 +- src/conversions/rust_decimal.rs | 4 +- src/conversions/std/ipaddr.rs | 6 +- src/conversions/std/path.rs | 4 +- src/conversions/std/time.rs | 4 +- src/conversions/uuid.rs | 4 +- src/coroutine/waker.rs | 10 +- src/err/err_state.rs | 6 +- src/exceptions.rs | 6 +- src/impl_/exceptions.rs | 6 +- src/impl_/pyclass.rs | 4 +- src/impl_/pyclass/lazy_type_object.rs | 7 +- src/impl_/pymodule.rs | 6 +- src/internal/get_slot.rs | 4 +- src/pyclass/create_type_object.rs | 4 +- src/sync.rs | 29 ++- src/sync/once_lock.rs | 270 ++++++++++++++++++++++++++ src/types/code.rs | 4 +- src/types/datetime.rs | 8 +- src/types/mapping.rs | 4 +- src/types/module.rs | 2 +- src/types/sequence.rs | 4 +- tests/test_class_new.rs | 4 +- 33 files changed, 438 insertions(+), 99 deletions(-) create mode 100644 newsfragments/5223.added.md create mode 100644 newsfragments/5223.changed.md create mode 100644 src/sync/once_lock.rs diff --git a/Cargo.toml b/Cargo.toml index 3758235defa..ca912d91444 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ rust-version.workspace = true [dependencies] libc = "0.2.62" memoffset = "0.9" +once_cell = "1.21" # ffi bindings to the python interpreter, split into a separate crate so they can be used independently pyo3-ffi = { path = "pyo3-ffi", version = "=0.25.1" } @@ -129,6 +130,7 @@ auto-initialize = [] # Enables `Clone`ing references to Python objects `Py` which panics if the GIL is not held. py-clone = [] +# Adds `OnceExt` and `MutexExt` implementations to the `parking_lot` types parking_lot = ["dep:parking_lot", "lock_api"] arc_lock = ["lock_api", "lock_api/arc_lock", "parking_lot?/arc_lock"] diff --git a/guide/src/faq.md b/guide/src/faq.md index 8efe0b61bad..e00bec71f76 100644 --- a/guide/src/faq.md +++ b/guide/src/faq.md @@ -4,18 +4,18 @@ Sorry that you're having trouble using PyO3. If you can't find the answer to you ## I'm experiencing deadlocks using PyO3 with `std::sync::OnceLock`, `std::sync::LazyLock`, `lazy_static`, and `once_cell`! -`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: +`OnceLock`, `LazyLock`, and their thirdparty predecessors use blocking to ensure only one thread ever initializes them. Because the Python interpreter can introduce additional locks (the Python GIL and GC can both require all other threads to pause) this can lead to deadlocks in the following way: -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 `OnceLock` value. +1. A thread (thread A) which is attached to the Python interpreter starts initialization of a `OnceLock` value. +2. The initialization code calls some Python API which temporarily detaches from the interpreter e.g. `Python::import`. +3. Another thread (thread B) attaches to the Python interpreter 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. +5. On non-free-threaded Python, thread A is now also blocked, because it waits to re-attach to the interpreter (by taking the GIL which thread B still holds). 6. Deadlock. -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. +PyO3 provides a struct [`PyOnceLock`] which implements a single-initialization API based on these types that avoids deadlocks. You can also make use of the [`OnceExt`] and [`OnceLockExt`] extension traits that enable using the standard library types for this purpose by providing new methods for these types that avoid the risk of deadlocking with the Python interpreter. This means they can be used in place of other choices when you are experiencing the deadlock described above. See the documentation for [`PyOnceLock`] and [`OnceExt`] for further details and an example how to use them. -[`GILOnceCell`]: {{#PYO3_DOCS_URL}}/pyo3/sync/struct.GILOnceCell.html +[`PyOnceLock`]: {{#PYO3_DOCS_URL}}/pyo3/sync/struct.PyOnceLock.html [`OnceExt`]: {{#PYO3_DOCS_URL}}/pyo3/sync/trait.OnceExt.html [`OnceLockExt`]: {{#PYO3_DOCS_URL}}/pyo3/sync/trait.OnceLockExt.html diff --git a/guide/src/free-threading.md b/guide/src/free-threading.md index f00b6afdb94..8cff3b03384 100644 --- a/guide/src/free-threading.md +++ b/guide/src/free-threading.md @@ -285,31 +285,39 @@ 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 -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 -race to initialize the cell. While the cell will only ever be initialized -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 +To initialize data exactly once, use the [`PyOnceLock`] type, which is a close equivalent +to [`std::sync::OnceLock`][`OnceLock`] that also helps avoid deadlocks by detaching from +the Python interpreter when threads are blocking waiting for another thread to +complete intialization. If already using [`OnceLock`] and it is impractical +to replace with a [`PyOnceLock`], there is the [`OnceLockExt`] extension trait +which adds [`OnceLockExt::get_or_init_py_attached`] to detach from the interpreter +when blocking in the same fashion as [`PyOnceLock`]. Here is an example using +[`PyOnceLock`] to single-initialize a runtime cache holding a `Py`: + +```rust +# use pyo3::prelude::*; +use pyo3::sync::PyOnceLock; +use pyo3::types::PyDict; + +let cache: PyOnceLock> = PyOnceLock::new(); + +Python::attach(|py| { + // guaranteed to be called once and only once + cache.get_or_init(py, || PyDict::new(py).unbind()) +}); +``` + +In cases where a function must run exactly once, you can bring +the [`OnceExt`] trait 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`. +where the thread is attached to the Python interpreter. These functions are analogous to +[`Once::call_once`], [`Once::call_once_force`] except they accept a [`Python<'py>`] +token in addition to an `FnOnce`. All of these functions detach from the +interpreter before blocking and re-attach before executing the function, +avoiding deadlocks that are possible without using the PyO3 +extension traits. Here the same example as above built using a [`Once`] instead of a +[`PyOnceLock`]: ```rust # use pyo3::prelude::*; @@ -411,4 +419,5 @@ interpreter. [`Python::detach`]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.detach [`Python::attach`]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.attach [`Python<'py>`]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html +[`PyOnceLock`]: {{#PYO3_DOCS_URL}}/pyo3/sync/struct.PyOnceLock.html [`threading`]: https://docs.python.org/3/library/threading.html diff --git a/guide/src/migration.md b/guide/src/migration.md index 4a48dbfabef..acb7a5c7c7f 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -21,7 +21,46 @@ For this reason we chose to rename these to more modern terminology introduced i Click to expand The type alias `PyObject` (aka `Py`) is often confused with the identically named FFI definition `pyo3::ffi::PyObject`. For this reason we are deprecating its usage. To migrate simply replace its usage by the target type `Py`. +
+ +### Replacement of `GILOnceCell` with `PyOnceLock` +
+Click to expand + +Similar to the above renaming of `Python::with_gil` and related APIs, the `GILOnceCell` type was designed for a Python interpreter which was limited by the GIL. Aside from its name, it allowed for the "once" initialization to race because the racing was mediated by the GIL and was extremely unlikely to manifest in practice. + +With the introduction of free-threaded Python the racy initialization behavior is more likely to be problematic and so a new type `PyOnceLock` has been introduced which performs true single-initialization correctly while attached to the Python interpreter. It exposes the same API as `GILOnceCell`, so should be a drop-in replacement with the notable exception that if the racy initialization of `GILOnceCell` was inadvertently relied on (e.g. due to circular references) then the stronger once-ever guarantee of `PyOnceLock` may lead to deadlocking which requires refactoring. + +Before: +```rust +# #![allow(deprecated)] +# use pyo3::prelude::*; +# use pyo3::sync::GILOnceCell; +# use pyo3::types::PyType; +# fn main() -> PyResult<()> { +# Python::attach(|py| { +static DECIMAL_TYPE: GILOnceCell> = GILOnceCell::new(); +DECIMAL_TYPE.import(py, "decimal", "Decimal")?; +# Ok(()) +# }) +# } +``` + +After: + +```rust +# use pyo3::prelude::*; +# use pyo3::sync::PyOnceLock; +# use pyo3::types::PyType; +# fn main() -> PyResult<()> { +# Python::attach(|py| { +static DECIMAL_TYPE: PyOnceLock> = PyOnceLock::new(); +DECIMAL_TYPE.import(py, "decimal", "Decimal")?; +# Ok(()) +# }) +# } +```
### Deprecation of `GILProtected` @@ -64,7 +103,7 @@ Python::attach(|py| { # }) # } ``` - +
### `PyMemoryError` now maps to `io::ErrorKind::OutOfMemory` when converted to `io::Error`
diff --git a/newsfragments/5171.packaging.md b/newsfragments/5171.packaging.md index cb502462c1a..7d54315d9a7 100644 --- a/newsfragments/5171.packaging.md +++ b/newsfragments/5171.packaging.md @@ -1,2 +1 @@ Update MSRV to 1.74. -Drop `once_cell` dependency. diff --git a/newsfragments/5223.added.md b/newsfragments/5223.added.md new file mode 100644 index 00000000000..c8847b310d7 --- /dev/null +++ b/newsfragments/5223.added.md @@ -0,0 +1 @@ +Add `PyOnceLock` type for thread-safe single-initialization. diff --git a/newsfragments/5223.changed.md b/newsfragments/5223.changed.md new file mode 100644 index 00000000000..1c1ff1b2bf1 --- /dev/null +++ b/newsfragments/5223.changed.md @@ -0,0 +1 @@ +Deprecate `GILOnceCell` type in favour of `PyOnceLock`. diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index b4feb2f7fd5..9d999fa86d9 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -2468,12 +2468,8 @@ impl<'a> PyClassImplsBuilder<'a> { impl #pyo3_path::impl_::pyclass::PyClassWithFreeList for #cls { #[inline] 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)) - }) + static FREELIST: #pyo3_path::sync::PyOnceLock<::std::sync::Mutex<#pyo3_path::impl_::freelist::PyObjectFreeList>> = #pyo3_path::sync::PyOnceLock::new(); + &FREELIST.get_or_init(py, || ::std::sync::Mutex::new(#pyo3_path::impl_::freelist::PyObjectFreeList::with_capacity(#freelist))) } } } diff --git a/src/conversions/bigdecimal.rs b/src/conversions/bigdecimal.rs index 809cd167329..af976431894 100644 --- a/src/conversions/bigdecimal.rs +++ b/src/conversions/bigdecimal.rs @@ -54,7 +54,7 @@ use std::str::FromStr; use crate::types::PyTuple; use crate::{ exceptions::PyValueError, - sync::GILOnceCell, + sync::PyOnceLock, types::{PyAnyMethods, PyStringMethods, PyType}, Bound, FromPyObject, IntoPyObject, Py, PyAny, PyErr, PyResult, Python, }; @@ -62,12 +62,12 @@ use bigdecimal::BigDecimal; use num_bigint::Sign; fn get_decimal_cls(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> { - static DECIMAL_CLS: GILOnceCell> = GILOnceCell::new(); + static DECIMAL_CLS: PyOnceLock> = PyOnceLock::new(); DECIMAL_CLS.import(py, "decimal", "Decimal") } fn get_invalid_operation_error_cls(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> { - static INVALID_OPERATION_CLS: GILOnceCell> = GILOnceCell::new(); + static INVALID_OPERATION_CLS: PyOnceLock> = PyOnceLock::new(); INVALID_OPERATION_CLS.import(py, "decimal", "InvalidOperation") } diff --git a/src/conversions/chrono.rs b/src/conversions/chrono.rs index 634c1a1d212..5ea8904fca2 100644 --- a/src/conversions/chrono.rs +++ b/src/conversions/chrono.rs @@ -52,7 +52,7 @@ use crate::types::{PyDateAccess, PyDeltaAccess, PyTimeAccess}; #[cfg(feature = "chrono-local")] use crate::{ exceptions::PyRuntimeError, - sync::GILOnceCell, + sync::PyOnceLock, types::{PyString, PyStringMethods}, Py, }; @@ -449,7 +449,7 @@ impl<'py> IntoPyObject<'py> for Local { type Error = PyErr; fn into_pyobject(self, py: Python<'py>) -> Result { - static LOCAL_TZ: GILOnceCell> = GILOnceCell::new(); + static LOCAL_TZ: PyOnceLock> = PyOnceLock::new(); let tz = LOCAL_TZ .get_or_try_init(py, || { let iana_name = iana_time_zone::get_timezone().map_err(|e| { diff --git a/src/conversions/num_rational.rs b/src/conversions/num_rational.rs index 8371c48f838..25aac24aa7a 100644 --- a/src/conversions/num_rational.rs +++ b/src/conversions/num_rational.rs @@ -45,7 +45,7 @@ use crate::conversion::IntoPyObject; use crate::ffi; -use crate::sync::GILOnceCell; +use crate::sync::PyOnceLock; use crate::types::any::PyAnyMethods; use crate::types::PyType; use crate::{Bound, FromPyObject, Py, PyAny, PyErr, PyResult, Python}; @@ -54,7 +54,7 @@ use crate::{Bound, FromPyObject, Py, PyAny, PyErr, PyResult, Python}; use num_bigint::BigInt; use num_rational::Ratio; -static FRACTION_CLS: GILOnceCell> = GILOnceCell::new(); +static FRACTION_CLS: PyOnceLock> = PyOnceLock::new(); fn get_fraction_cls(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> { FRACTION_CLS.import(py, "fractions", "Fraction") diff --git a/src/conversions/rust_decimal.rs b/src/conversions/rust_decimal.rs index 597f469d21a..b533f97c5a5 100644 --- a/src/conversions/rust_decimal.rs +++ b/src/conversions/rust_decimal.rs @@ -51,7 +51,7 @@ use crate::conversion::IntoPyObject; use crate::exceptions::PyValueError; -use crate::sync::GILOnceCell; +use crate::sync::PyOnceLock; use crate::types::any::PyAnyMethods; use crate::types::string::PyStringMethods; use crate::types::PyType; @@ -74,7 +74,7 @@ impl FromPyObject<'_> for Decimal { } } -static DECIMAL_CLS: GILOnceCell> = GILOnceCell::new(); +static DECIMAL_CLS: PyOnceLock> = PyOnceLock::new(); fn get_decimal_cls(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> { DECIMAL_CLS.import(py, "decimal", "Decimal") diff --git a/src/conversions/std/ipaddr.rs b/src/conversions/std/ipaddr.rs index 699013c53e2..7e6453ed654 100644 --- a/src/conversions/std/ipaddr.rs +++ b/src/conversions/std/ipaddr.rs @@ -3,7 +3,7 @@ use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use crate::conversion::IntoPyObject; use crate::exceptions::PyValueError; use crate::instance::Bound; -use crate::sync::GILOnceCell; +use crate::sync::PyOnceLock; use crate::types::any::PyAnyMethods; use crate::types::string::PyStringMethods; use crate::types::PyType; @@ -35,7 +35,7 @@ impl<'py> IntoPyObject<'py> for Ipv4Addr { type Error = PyErr; fn into_pyobject(self, py: Python<'py>) -> Result { - static IPV4_ADDRESS: GILOnceCell> = GILOnceCell::new(); + static IPV4_ADDRESS: PyOnceLock> = PyOnceLock::new(); IPV4_ADDRESS .import(py, "ipaddress", "IPv4Address")? .call1((u32::from_be_bytes(self.octets()),)) @@ -59,7 +59,7 @@ impl<'py> IntoPyObject<'py> for Ipv6Addr { type Error = PyErr; fn into_pyobject(self, py: Python<'py>) -> Result { - static IPV6_ADDRESS: GILOnceCell> = GILOnceCell::new(); + static IPV6_ADDRESS: PyOnceLock> = PyOnceLock::new(); IPV6_ADDRESS .import(py, "ipaddress", "IPv6Address")? .call1((u128::from_be_bytes(self.octets()),)) diff --git a/src/conversions/std/path.rs b/src/conversions/std/path.rs index ef51367e03c..17cad8f2694 100644 --- a/src/conversions/std/path.rs +++ b/src/conversions/std/path.rs @@ -1,7 +1,7 @@ use crate::conversion::IntoPyObject; use crate::ffi_ptr_ext::FfiPtrExt; use crate::instance::Bound; -use crate::sync::GILOnceCell; +use crate::sync::PyOnceLock; use crate::types::any::PyAnyMethods; use crate::{ffi, FromPyObject, Py, PyAny, PyErr, PyResult, Python}; use std::borrow::Cow; @@ -25,7 +25,7 @@ impl<'py> IntoPyObject<'py> for &Path { #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { - static PY_PATH: GILOnceCell> = GILOnceCell::new(); + static PY_PATH: PyOnceLock> = PyOnceLock::new(); PY_PATH .import(py, "pathlib", "Path")? .call((self.as_os_str(),), None) diff --git a/src/conversions/std/time.rs b/src/conversions/std/time.rs index 62ee9693dc3..d2e2ed7c237 100644 --- a/src/conversions/std/time.rs +++ b/src/conversions/std/time.rs @@ -2,7 +2,7 @@ use crate::conversion::IntoPyObject; use crate::exceptions::{PyOverflowError, PyValueError}; #[cfg(Py_LIMITED_API)] use crate::intern; -use crate::sync::GILOnceCell; +use crate::sync::PyOnceLock; use crate::types::any::PyAnyMethods; #[cfg(not(Py_LIMITED_API))] use crate::types::PyDeltaAccess; @@ -125,7 +125,7 @@ impl<'py> IntoPyObject<'py> for &SystemTime { } fn unix_epoch_py(py: Python<'_>) -> PyResult> { - static UNIX_EPOCH: GILOnceCell> = GILOnceCell::new(); + static UNIX_EPOCH: PyOnceLock> = PyOnceLock::new(); Ok(UNIX_EPOCH .get_or_try_init(py, || { let utc = PyTzInfo::utc(py)?; diff --git a/src/conversions/uuid.rs b/src/conversions/uuid.rs index 2227617ba48..37d9aa768eb 100644 --- a/src/conversions/uuid.rs +++ b/src/conversions/uuid.rs @@ -68,13 +68,13 @@ use uuid::Uuid; use crate::conversion::IntoPyObject; use crate::exceptions::PyTypeError; use crate::instance::Bound; -use crate::sync::GILOnceCell; +use crate::sync::PyOnceLock; use crate::types::any::PyAnyMethods; use crate::types::PyType; use crate::{intern, FromPyObject, Py, PyAny, PyErr, PyResult, Python}; fn get_uuid_cls(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> { - static UUID_CLS: GILOnceCell> = GILOnceCell::new(); + static UUID_CLS: PyOnceLock> = PyOnceLock::new(); UUID_CLS.import(py, "uuid", "UUID") } diff --git a/src/coroutine/waker.rs b/src/coroutine/waker.rs index 0afb024a9ab..f425545346d 100644 --- a/src/coroutine/waker.rs +++ b/src/coroutine/waker.rs @@ -1,4 +1,4 @@ -use crate::sync::GILOnceCell; +use crate::sync::PyOnceLock; use crate::types::any::PyAnyMethods; use crate::types::PyCFunction; use crate::{intern, wrap_pyfunction, Bound, Py, PyAny, PyResult, Python}; @@ -14,11 +14,11 @@ use std::task::Wake; /// /// [1]: AsyncioWaker::initialize_future /// [2]: AsyncioWaker::wake -pub struct AsyncioWaker(GILOnceCell>); +pub struct AsyncioWaker(PyOnceLock>); impl AsyncioWaker { pub(super) fn new() -> Self { - Self(GILOnceCell::new()) + Self(PyOnceLock::new()) } pub(super) fn reset(&mut self) { @@ -58,7 +58,7 @@ struct LoopAndFuture { impl LoopAndFuture { fn new(py: Python<'_>) -> PyResult { - static GET_RUNNING_LOOP: GILOnceCell> = GILOnceCell::new(); + static GET_RUNNING_LOOP: PyOnceLock> = PyOnceLock::new(); let import = || -> PyResult<_> { let module = py.import("asyncio")?; Ok(module.getattr("get_running_loop")?.into()) @@ -69,7 +69,7 @@ impl LoopAndFuture { } fn set_result(&self, py: Python<'_>) -> PyResult<()> { - static RELEASE_WAITER: GILOnceCell> = GILOnceCell::new(); + static RELEASE_WAITER: PyOnceLock> = PyOnceLock::new(); let release_waiter = RELEASE_WAITER.get_or_try_init(py, || { wrap_pyfunction!(release_waiter, py).map(Bound::unbind) })?; diff --git a/src/err/err_state.rs b/src/err/err_state.rs index 8616979310f..d2285a7a1ca 100644 --- a/src/err/err_state.rs +++ b/src/err/err_state.rs @@ -368,13 +368,13 @@ fn raise_lazy(py: Python<'_>, lazy: Box) { mod tests { use crate::{ - exceptions::PyValueError, sync::GILOnceCell, Py, PyAny, PyErr, PyErrArguments, Python, + exceptions::PyValueError, sync::PyOnceLock, Py, PyAny, PyErr, PyErrArguments, Python, }; #[test] #[should_panic(expected = "Re-entrant normalization of PyErrState detected")] fn test_reentrant_normalization() { - static ERR: GILOnceCell = GILOnceCell::new(); + static ERR: PyOnceLock = PyOnceLock::new(); struct RecursiveArgs; @@ -398,7 +398,7 @@ mod tests { #[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(); + static ERR: PyOnceLock = PyOnceLock::new(); struct GILSwitchArgs; diff --git a/src/exceptions.rs b/src/exceptions.rs index d3b9f994dd5..fca1cd63766 100644 --- a/src/exceptions.rs +++ b/src/exceptions.rs @@ -252,9 +252,9 @@ macro_rules! create_exception_type_object { impl $name { fn type_object_raw(py: $crate::Python<'_>) -> *mut $crate::ffi::PyTypeObject { - use $crate::sync::GILOnceCell; - static TYPE_OBJECT: GILOnceCell<$crate::Py<$crate::types::PyType>> = - GILOnceCell::new(); + use $crate::sync::PyOnceLock; + static TYPE_OBJECT: PyOnceLock<$crate::Py<$crate::types::PyType>> = + PyOnceLock::new(); TYPE_OBJECT .get_or_init(py, || diff --git a/src/impl_/exceptions.rs b/src/impl_/exceptions.rs index 15b6f53bbe2..546f7e2f318 100644 --- a/src/impl_/exceptions.rs +++ b/src/impl_/exceptions.rs @@ -1,7 +1,7 @@ -use crate::{sync::GILOnceCell, types::PyType, Bound, Py, Python}; +use crate::{sync::PyOnceLock, types::PyType, Bound, Py, Python}; pub struct ImportedExceptionTypeObject { - imported_value: GILOnceCell>, + imported_value: PyOnceLock>, module: &'static str, name: &'static str, } @@ -9,7 +9,7 @@ pub struct ImportedExceptionTypeObject { impl ImportedExceptionTypeObject { pub const fn new(module: &'static str, name: &'static str) -> Self { Self { - imported_value: GILOnceCell::new(), + imported_value: PyOnceLock::new(), module, name, } diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index 22e7ac93b24..4150ccd92ea 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -997,8 +997,8 @@ unsafe fn bpo_35810_workaround(py: Python<'_>, ty: *mut ffi::PyTypeObject) { { // Must check version at runtime for abi3 wheels - they could run against a higher version // than the build config suggests. - use crate::sync::GILOnceCell; - static IS_PYTHON_3_8: GILOnceCell = GILOnceCell::new(); + use crate::sync::PyOnceLock; + static IS_PYTHON_3_8: PyOnceLock = PyOnceLock::new(); if *IS_PYTHON_3_8.get_or_init(py, || py.version_info() >= (3, 8)) { // No fix needed - the wheel is running on a sufficiently new interpreter. diff --git a/src/impl_/pyclass/lazy_type_object.rs b/src/impl_/pyclass/lazy_type_object.rs index c58f4978451..408d00ad6d7 100644 --- a/src/impl_/pyclass/lazy_type_object.rs +++ b/src/impl_/pyclass/lazy_type_object.rs @@ -6,6 +6,8 @@ use std::{ #[cfg(Py_3_14)] use crate::err::error_on_minusone; +#[allow(deprecated)] +use crate::sync::GILOnceCell; #[cfg(Py_3_14)] use crate::types::PyTypeMethods; use crate::{ @@ -13,7 +15,6 @@ use crate::{ ffi, impl_::{pyclass::MaybeRuntimePyMethodDef, pymethods::PyMethodDefType}, pyclass::{create_type_object, PyClassTypeObject}, - sync::GILOnceCell, types::PyType, Bound, Py, PyAny, PyClass, PyErr, PyResult, Python, }; @@ -28,10 +29,12 @@ pub struct LazyTypeObject(LazyTypeObjectInner, PhantomData); // Non-generic inner of LazyTypeObject to keep code size down struct LazyTypeObjectInner { + #[allow(deprecated)] value: GILOnceCell, // Threads which have begun initialization of the `tp_dict`. Used for // reentrant initialization detection. initializing_threads: Mutex>, + #[allow(deprecated)] fully_initialized_type: GILOnceCell>, } @@ -41,8 +44,10 @@ impl LazyTypeObject { pub const fn new() -> Self { LazyTypeObject( LazyTypeObjectInner { + #[allow(deprecated)] value: GILOnceCell::new(), initializing_threads: Mutex::new(Vec::new()), + #[allow(deprecated)] fully_initialized_type: GILOnceCell::new(), }, PhantomData, diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index a9736672bc7..027ce2982ee 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -25,7 +25,7 @@ use crate::PyErr; use crate::{ ffi, impl_::pymethods::PyMethodDef, - sync::GILOnceCell, + sync::PyOnceLock, types::{PyCFunction, PyModule, PyModuleMethods}, Bound, Py, PyClass, PyResult, PyTypeInfo, Python, }; @@ -43,7 +43,7 @@ pub struct ModuleDef { ))] interpreter: AtomicI64, /// Initialized module object, cached to avoid reinitialization. - module: GILOnceCell>, + module: PyOnceLock>, /// Whether or not the module supports running without the GIL gil_used: AtomicBool, } @@ -89,7 +89,7 @@ impl ModuleDef { not(all(windows, Py_LIMITED_API, not(Py_3_10))) ))] interpreter: AtomicI64::new(-1), - module: GILOnceCell::new(), + module: PyOnceLock::new(), gil_used: AtomicBool::new(true), } } diff --git a/src/internal/get_slot.rs b/src/internal/get_slot.rs index f938fc8a6ac..9d1633bed6d 100644 --- a/src/internal/get_slot.rs +++ b/src/internal/get_slot.rs @@ -134,9 +134,9 @@ impl_slots! { #[cfg(all(Py_LIMITED_API, not(Py_3_10)))] fn is_runtime_3_10(py: crate::Python<'_>) -> bool { - use crate::sync::GILOnceCell; + use crate::sync::PyOnceLock; - static IS_RUNTIME_3_10: GILOnceCell = GILOnceCell::new(); + static IS_RUNTIME_3_10: PyOnceLock = PyOnceLock::new(); *IS_RUNTIME_3_10.get_or_init(py, || py.version_info() >= (3, 10)) } diff --git a/src/pyclass/create_type_object.rs b/src/pyclass/create_type_object.rs index 1a6dd032925..b9bc1a7df96 100644 --- a/src/pyclass/create_type_object.rs +++ b/src/pyclass/create_type_object.rs @@ -540,8 +540,8 @@ fn bpo_45315_workaround(py: Python<'_>, class_name: CString) { { // Must check version at runtime for abi3 wheels - they could run against a higher version // than the build config suggests. - use crate::sync::GILOnceCell; - static IS_PYTHON_3_11: GILOnceCell = GILOnceCell::new(); + use crate::sync::PyOnceLock; + static IS_PYTHON_3_11: PyOnceLock = PyOnceLock::new(); if *IS_PYTHON_3_11.get_or_init(py, || py.version_info() >= (3, 11)) { // No fix needed - the wheel is running on a sufficiently new interpreter. diff --git a/src/sync.rs b/src/sync.rs index 54cafe3c1e4..c1e3ff177e2 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -17,9 +17,13 @@ use std::{ sync::{Once, OnceState}, }; +pub(crate) mod once_lock; + #[cfg(not(Py_GIL_DISABLED))] use crate::PyVisit; +pub use self::once_lock::PyOnceLock; + /// Value with concurrent access protected by the GIL. /// /// This is a synchronization primitive based on Python's global interpreter lock (GIL). @@ -113,6 +117,7 @@ unsafe impl Sync for GILProtected where T: Send {} /// between threads: /// /// ``` +/// #![allow(deprecated)] /// use pyo3::sync::GILOnceCell; /// use pyo3::prelude::*; /// use pyo3::types::PyList; @@ -126,6 +131,10 @@ unsafe impl Sync for GILProtected where T: Send {} /// } /// # Python::attach(|py| assert_eq!(get_shared_list(py).len(), 0)); /// ``` +#[deprecated( + since = "0.26.0", + note = "Prefer `pyo3::sync::PyOnceLock`, which avoids the possibility of racing during initialization." +)] pub struct GILOnceCell { once: Once, data: UnsafeCell>, @@ -135,6 +144,7 @@ pub struct GILOnceCell { /// `PhantomData` to make sure dropck understands we're dropping T in our Drop impl. /// /// ```compile_error,E0597 + /// #![allow(deprecated)] /// use pyo3::Python; /// use pyo3::sync::GILOnceCell; /// @@ -153,6 +163,7 @@ pub struct GILOnceCell { _marker: PhantomData, } +#[allow(deprecated)] impl Default for GILOnceCell { fn default() -> Self { Self::new() @@ -162,9 +173,12 @@ impl Default for GILOnceCell { // T: Send is needed for Sync because the thread which drops the GILOnceCell can be different // 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). +#[allow(deprecated)] unsafe impl Sync for GILOnceCell {} +#[allow(deprecated)] unsafe impl Send for GILOnceCell {} +#[allow(deprecated)] impl GILOnceCell { /// Create a `GILOnceCell` which does not yet contain a value. pub const fn new() -> Self { @@ -298,6 +312,7 @@ impl GILOnceCell { } } +#[allow(deprecated)] impl GILOnceCell> { /// Creates a new cell that contains a new Python reference to the same contained object. /// @@ -315,6 +330,7 @@ impl GILOnceCell> { } } +#[allow(deprecated)] impl GILOnceCell> where T: PyTypeCheck, @@ -327,6 +343,7 @@ where /// /// `GILOnceCell` can be used to avoid importing a class multiple times: /// ``` + /// #![allow(deprecated)] /// # use pyo3::prelude::*; /// # use pyo3::sync::GILOnceCell; /// # use pyo3::types::{PyDict, PyType}; @@ -364,6 +381,7 @@ where } } +#[allow(deprecated)] impl Drop for GILOnceCell { fn drop(&mut self) { if self.once.is_completed() { @@ -420,12 +438,12 @@ macro_rules! intern { /// Implementation detail for `intern!` macro. #[doc(hidden)] -pub struct Interned(&'static str, GILOnceCell>); +pub struct Interned(&'static str, PyOnceLock>); impl Interned { /// Creates an empty holder for an interned `str`. pub const fn new(value: &'static str) -> Self { - Interned(value, GILOnceCell::new()) + Interned(value, PyOnceLock::new()) } /// Gets or creates the interned `str` value. @@ -536,7 +554,7 @@ mod once_lock_ext_sealed { impl Sealed for std::sync::OnceLock {} } -/// Helper trait for `Once` to help avoid deadlocking when using a `Once` when attached to a +/// Extension trait for [`Once`] to help avoid deadlocking when using a [`Once`] when attached to a /// Python thread. pub trait OnceExt: Sealed { ///The state of `Once` @@ -816,9 +834,6 @@ where // into the C API. let ts_guard = unsafe { SuspendAttach::new() }; - // 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 || { @@ -875,6 +890,7 @@ mod tests { } #[test] + #[allow(deprecated)] fn test_once_cell() { Python::attach(|py| { let mut cell = GILOnceCell::new(); @@ -900,6 +916,7 @@ mod tests { } #[test] + #[allow(deprecated)] fn test_once_cell_drop() { #[derive(Debug)] struct RecordDrop<'a>(&'a mut bool); diff --git a/src/sync/once_lock.rs b/src/sync/once_lock.rs new file mode 100644 index 00000000000..fd608215049 --- /dev/null +++ b/src/sync/once_lock.rs @@ -0,0 +1,270 @@ +use crate::{ + internal::state::SuspendAttach, types::any::PyAnyMethods, Bound, Py, PyResult, PyTypeCheck, + Python, +}; + +/// An equivalent to [`std::sync::OnceLock`] for initializing objects while attached to +/// the Python interpreter. +/// +/// Unlike `OnceLock`, this type will not deadlock with the interpreter. +/// Before blocking calls the cell will detach from the runtime and then +/// re-attach once the cell is unblocked. +/// +/// # Re-entrant initialization +/// +/// Like `OnceLock`, it is an error to re-entrantly initialize a `PyOnceLock`. The exact +/// behavior in this case is not guaranteed, it may either deadlock or panic. +/// +/// # Examples +/// +/// The following example shows how to use `PyOnceLock` to share a reference to a Python list +/// between threads: +/// +/// ``` +/// use pyo3::sync::PyOnceLock; +/// use pyo3::prelude::*; +/// use pyo3::types::PyList; +/// +/// static LIST_CELL: PyOnceLock> = PyOnceLock::new(); +/// +/// pub fn get_shared_list(py: Python<'_>) -> &Bound<'_, PyList> { +/// LIST_CELL +/// .get_or_init(py, || PyList::empty(py).unbind()) +/// .bind(py) +/// } +/// # Python::attach(|py| assert_eq!(get_shared_list(py).len(), 0)); +/// ``` +#[derive(Default)] +pub struct PyOnceLock { + inner: once_cell::sync::OnceCell, +} + +impl PyOnceLock { + /// Create a `PyOnceLock` which does not yet contain a value. + pub const fn new() -> Self { + Self { + inner: once_cell::sync::OnceCell::new(), + } + } + + /// 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> { + self.inner.get() + } + + /// Get a reference to the contained value, initializing it if needed using the provided + /// closure. + /// + /// See the type-level documentation for detail on re-entrancy and concurrent initialization. + #[inline] + pub fn get_or_init(&self, py: Python<'_>, f: F) -> &T + where + F: FnOnce() -> T, + { + self.inner + .get() + .unwrap_or_else(|| init_once_cell_py_attached(&self.inner, py, f)) + } + + /// Like `get_or_init`, but accepts a fallible initialization function. If it fails, the cell + /// is left uninitialized. + /// + /// See the type-level documentation for detail on re-entrancy and concurrent initialization. + pub fn get_or_try_init(&self, py: Python<'_>, f: F) -> Result<&T, E> + where + F: FnOnce() -> Result, + { + self.inner + .get() + .map_or_else(|| try_init_once_cell_py_attached(&self.inner, py, f), Ok) + } + + /// 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.inner.get_mut() + } + + /// Set the value in the cell. + /// + /// 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> { + self.inner.set(value) + } + + /// 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.inner.take() + } + + /// Consumes the cell, returning the wrapped value. + /// + /// Returns None if the cell has not yet been written. + pub fn into_inner(self) -> Option { + self.inner.into_inner() + } +} + +impl PyOnceLock> { + /// Creates a new cell that contains a new Python reference to the same contained object. + /// + /// Returns an uninitialized cell if `self` has not yet been initialized. + pub fn clone_ref(&self, py: Python<'_>) -> Self { + let cloned = PyOnceLock::new(); + if let Some(value) = self.get(py) { + let _ = cloned.set(py, value.clone_ref(py)); + } + cloned + } +} + +impl PyOnceLock> +where + T: PyTypeCheck, +{ + /// This is a shorthand method for `get_or_init` which imports the type from Python on init. + /// + /// # Example: Using `PyOnceLock` to store a class in a static variable. + /// + /// `PyOnceLock` can be used to avoid importing a class multiple times: + /// ``` + /// # use pyo3::prelude::*; + /// # use pyo3::sync::PyOnceLock; + /// # 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: PyOnceLock> = PyOnceLock::new(); + /// ORDERED_DICT + /// .import(py, "collections", "OrderedDict")? + /// .call1((dict,)) + /// } + /// + /// # Python::attach(|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, T>> { + self.get_or_try_init(py, || { + let type_object = py + .import(module_name)? + .getattr(attr_name)? + .downcast_into()?; + Ok(type_object.unbind()) + }) + .map(|ty| ty.bind(py)) + } +} + +#[cold] +fn init_once_cell_py_attached<'a, F, T>( + cell: &'a once_cell::sync::OnceCell, + _py: Python<'_>, + f: F, +) -> &'a T +where + F: FnOnce() -> T, +{ + // 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 { SuspendAttach::new() }; + + // By having detached here, we guarantee that `.get_or_init` cannot deadlock with + // the Python interpreter + cell.get_or_init(move || { + drop(ts_guard); + f() + }) +} + +#[cold] +fn try_init_once_cell_py_attached<'a, F, T, E>( + cell: &'a once_cell::sync::OnceCell, + _py: Python<'_>, + f: F, +) -> Result<&'a T, E> +where + F: FnOnce() -> Result, +{ + // 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 { SuspendAttach::new() }; + + // By having detached here, we guarantee that `.get_or_init` cannot deadlock with + // the Python interpreter + cell.get_or_try_init(move || { + drop(ts_guard); + f() + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_once_cell() { + Python::attach(|py| { + let mut cell = PyOnceLock::new(); + + assert!(cell.get(py).is_none()); + + assert_eq!(cell.get_or_try_init(py, || Err(5)), Err(5)); + assert!(cell.get(py).is_none()); + + assert_eq!(cell.get_or_try_init(py, || Ok::<_, ()>(2)), Ok(&2)); + assert_eq!(cell.get(py), Some(&2)); + + assert_eq!(cell.get_or_try_init(py, || Err(5)), Ok(&2)); + + assert_eq!(cell.take(), Some(2)); + assert_eq!(cell.into_inner(), None); + + let cell_py = PyOnceLock::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)); + }) + } + + #[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::attach(|py| { + let mut dropped = false; + let cell = PyOnceLock::new(); + cell.set(py, RecordDrop(&mut dropped)).unwrap(); + let drop_container = cell.get(py).unwrap(); + + assert!(!*drop_container.0); + drop(cell); + assert!(dropped); + }); + } +} diff --git a/src/types/code.rs b/src/types/code.rs index 450e586abc9..dbd12ee6075 100644 --- a/src/types/code.rs +++ b/src/types/code.rs @@ -30,8 +30,8 @@ impl crate::PyTypeCheck for PyCode { fn type_check(object: &Bound<'_, PyAny>) -> bool { let py = object.py(); - static TYPE: crate::sync::GILOnceCell> = - crate::sync::GILOnceCell::new(); + static TYPE: crate::sync::PyOnceLock> = + crate::sync::PyOnceLock::new(); TYPE.import(py, "types", "CodeType") .and_then(|ty| object.is_instance(ty)) diff --git a/src/types/datetime.rs b/src/types/datetime.rs index d519d8a69d0..adca98e9878 100644 --- a/src/types/datetime.rs +++ b/src/types/datetime.rs @@ -21,7 +21,7 @@ use crate::ffi::{PyDateTime_DATE_GET_TZINFO, PyDateTime_TIME_GET_TZINFO, Py_IsNo use crate::types::{any::PyAnyMethods, PyString, PyType}; #[cfg(not(Py_LIMITED_API))] use crate::{ffi_ptr_ext::FfiPtrExt, py_result_ext::PyResultExt, types::PyTuple, BoundObject}; -use crate::{sync::GILOnceCell, Py}; +use crate::{sync::PyOnceLock, Py}; #[cfg(Py_LIMITED_API)] use crate::{types::IntoPyDict, PyTypeCheck}; use crate::{Borrowed, Bound, IntoPyObject, PyAny, PyErr, Python}; @@ -63,7 +63,7 @@ impl DatetimeTypes { } fn try_get(py: Python<'_>) -> PyResult<&Self> { - static TYPES: GILOnceCell = GILOnceCell::new(); + static TYPES: PyOnceLock = PyOnceLock::new(); TYPES.get_or_try_init(py, || { let datetime = py.import("datetime")?; Ok::<_, PyErr>(Self { @@ -801,7 +801,7 @@ impl PyTzInfo { #[cfg(Py_LIMITED_API)] { - static UTC: GILOnceCell> = GILOnceCell::new(); + static UTC: PyOnceLock> = PyOnceLock::new(); UTC.get_or_try_init(py, || { Ok(DatetimeTypes::get(py) .timezone @@ -819,7 +819,7 @@ impl PyTzInfo { where T: IntoPyObject<'py, Target = PyString>, { - static ZONE_INFO: GILOnceCell> = GILOnceCell::new(); + static ZONE_INFO: PyOnceLock> = PyOnceLock::new(); let zoneinfo = ZONE_INFO.import(py, "zoneinfo", "ZoneInfo"); diff --git a/src/types/mapping.rs b/src/types/mapping.rs index f0d039d4028..8887f19db9e 100644 --- a/src/types/mapping.rs +++ b/src/types/mapping.rs @@ -3,7 +3,7 @@ use crate::err::PyResult; use crate::ffi_ptr_ext::FfiPtrExt; use crate::instance::Bound; use crate::py_result_ext::PyResultExt; -use crate::sync::GILOnceCell; +use crate::sync::PyOnceLock; use crate::type_object::PyTypeInfo; use crate::types::any::PyAnyMethods; use crate::types::{PyAny, PyDict, PyList, PyType}; @@ -161,7 +161,7 @@ impl<'py> PyMappingMethods<'py> for Bound<'py, PyMapping> { } fn get_mapping_abc(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> { - static MAPPING_ABC: GILOnceCell> = GILOnceCell::new(); + static MAPPING_ABC: PyOnceLock> = PyOnceLock::new(); MAPPING_ABC.import(py, "collections.abc", "Mapping") } diff --git a/src/types/module.rs b/src/types/module.rs index ba4aa9428f7..72ba2d341d9 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -80,7 +80,7 @@ impl PyModule { /// ``` /// /// If you want to import a class, you can store a reference to it with - /// [`GILOnceCell::import`][crate::sync::GILOnceCell#method.import]. + /// [`PyOnceLock::import`][crate::sync::PyOnceLock::import]. pub fn import<'py, N>(py: Python<'py>, name: N) -> PyResult> where N: IntoPyObject<'py, Target = PyString>, diff --git a/src/types/sequence.rs b/src/types/sequence.rs index de6c25a193c..59849fdd7b2 100644 --- a/src/types/sequence.rs +++ b/src/types/sequence.rs @@ -6,7 +6,7 @@ use crate::inspect::types::TypeInfo; use crate::instance::Bound; use crate::internal_tricks::get_ssize_index; use crate::py_result_ext::PyResultExt; -use crate::sync::GILOnceCell; +use crate::sync::PyOnceLock; use crate::type_object::PyTypeInfo; use crate::types::{any::PyAnyMethods, PyAny, PyList, PyString, PyTuple, PyType}; use crate::{ @@ -370,7 +370,7 @@ where } fn get_sequence_abc(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> { - static SEQUENCE_ABC: GILOnceCell> = GILOnceCell::new(); + static SEQUENCE_ABC: PyOnceLock> = PyOnceLock::new(); SEQUENCE_ABC.import(py, "collections.abc", "Sequence") } diff --git a/tests/test_class_new.rs b/tests/test_class_new.rs index ce1d9b83ef1..7f56ebabc23 100644 --- a/tests/test_class_new.rs +++ b/tests/test_class_new.rs @@ -2,7 +2,7 @@ use pyo3::exceptions::PyValueError; use pyo3::prelude::*; -use pyo3::sync::GILOnceCell; +use pyo3::sync::PyOnceLock; use pyo3::types::IntoPyDict; #[pyclass] @@ -216,7 +216,7 @@ struct NewExisting { impl NewExisting { #[new] fn new(py: pyo3::Python<'_>, val: usize) -> pyo3::Py { - static PRE_BUILT: GILOnceCell<[pyo3::Py; 2]> = GILOnceCell::new(); + static PRE_BUILT: PyOnceLock<[pyo3::Py; 2]> = PyOnceLock::new(); let existing = PRE_BUILT.get_or_init(py, || { [ pyo3::Py::new(py, NewExisting { num: 0 }).unwrap(), From c5aacbd42c33256af8de57f5ee05c31a17599873 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Thu, 21 Aug 2025 20:18:07 +0100 Subject: [PATCH 808/936] ci: run 3.14 on PR for the moment (#5309) * ci: run 3.14 on PR for the moment * use `-dev` suffixes --- .github/workflows/build.yml | 1 - .github/workflows/ci.yml | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2dda8fb57be..68d62abd035 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -133,7 +133,6 @@ jobs: - name: Test python examples and tests shell: bash run: nox -s test-py - continue-on-error: ${{ endsWith(inputs.python-version, '-dev') }} env: CARGO_TARGET_DIR: ${{ github.workspace }}/target diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c2d86de775e..9071c024749 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -196,6 +196,24 @@ jobs: python-architecture: "x64", rust-target: "x86_64-unknown-linux-gnu", } + # As we approach 3.14 release date, let's also have testing for 3.14 and 3.14t + # on linux + - rust: stable + python-version: "3.14-dev" + platform: + { + os: "ubuntu-latest", + python-architecture: "x64", + rust-target: "x86_64-unknown-linux-gnu", + } + - rust: stable + python-version: "3.14t-dev" + platform: + { + os: "ubuntu-latest", + python-architecture: "x64", + rust-target: "x86_64-unknown-linux-gnu", + } build-full: if: ${{ contains(github.event.pull_request.labels.*.name, 'CI-build-full') || github.event_name != 'pull_request' }} From 417a5c561dc6417353d1f324275dc8fed654c32c Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Thu, 21 Aug 2025 21:40:21 +0200 Subject: [PATCH 809/936] rename `Python::with_gil_unchecked` as `Python::attach_unchecked` (#5340) --- newsfragments/5340.changed.md | 1 + src/marker.rs | 20 +++++++++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 newsfragments/5340.changed.md diff --git a/newsfragments/5340.changed.md b/newsfragments/5340.changed.md new file mode 100644 index 00000000000..ba669356a06 --- /dev/null +++ b/newsfragments/5340.changed.md @@ -0,0 +1 @@ +rename `Python::with_gil_unchecked` to `Python::attach_unchecked` \ No newline at end of file diff --git a/src/marker.rs b/src/marker.rs index 8f8b48d983a..a9a7e378964 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -465,10 +465,24 @@ impl Python<'_> { crate::interpreter_lifecycle::initialize(); } + /// See [Python::attach_unchecked] + /// # Safety + /// + /// If [`Python::attach`] would succeed, it is safe to call this function. + #[inline] + #[track_caller] + #[deprecated(note = "use `Python::attach_unchecked` instead", since = "0.26.0")] + pub unsafe fn with_gil_unchecked(f: F) -> R + where + F: for<'py> FnOnce(Python<'py>) -> R, + { + unsafe { Self::attach_unchecked(f) } + } + /// Like [`Python::attach`] except Python interpreter state checking is skipped. /// - /// Normally when the GIL is acquired, PyO3 checks that the Python interpreter is - /// in an appropriate state (e.g. it is fully initialized). This function skips + /// Normally when attaching to the Python interpreter, PyO3 checks that it is in + /// an appropriate state (e.g. it is fully initialized). This function skips /// those checks. /// /// # Safety @@ -476,7 +490,7 @@ impl Python<'_> { /// If [`Python::attach`] would succeed, it is safe to call this function. #[inline] #[track_caller] - pub unsafe fn with_gil_unchecked(f: F) -> R + pub unsafe fn attach_unchecked(f: F) -> R where F: for<'py> FnOnce(Python<'py>) -> R, { From b97b2906d136c1759a7e933bf7c5e85b3b5efdf2 Mon Sep 17 00:00:00 2001 From: Thomas Tanon Date: Fri, 22 Aug 2025 11:10:18 +0200 Subject: [PATCH 810/936] Introspection: forward annotations in some blanket impls (#5351) --- newsfragments/5351.added.md | 1 + src/conversion.rs | 3 +++ src/conversions/std/cell.rs | 9 +++++++++ src/instance.rs | 6 ++++++ src/pycell.rs | 12 ++++++++++++ src/pyclass/guard.rs | 3 +++ 6 files changed, 34 insertions(+) create mode 100644 newsfragments/5351.added.md diff --git a/newsfragments/5351.added.md b/newsfragments/5351.added.md new file mode 100644 index 00000000000..9cefb8c1d19 --- /dev/null +++ b/newsfragments/5351.added.md @@ -0,0 +1 @@ +Introspection: copy annotations in some generic impls \ No newline at end of file diff --git a/src/conversion.rs b/src/conversion.rs index e42d30a089d..9d54124e29a 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -187,6 +187,9 @@ where type Output = <&'a T as IntoPyObject<'py>>::Output; type Error = <&'a T as IntoPyObject<'py>>::Error; + #[cfg(feature = "experimental-inspect")] + const OUTPUT_TYPE: &'static str = <&'a T as IntoPyObject<'py>>::OUTPUT_TYPE; + #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { (*self).into_pyobject(py) diff --git a/src/conversions/std/cell.rs b/src/conversions/std/cell.rs index 983a0350228..17f29d37141 100644 --- a/src/conversions/std/cell.rs +++ b/src/conversions/std/cell.rs @@ -10,6 +10,9 @@ impl<'py, T: Copy + IntoPyObject<'py>> IntoPyObject<'py> for Cell { type Output = T::Output; type Error = T::Error; + #[cfg(feature = "experimental-inspect")] + const OUTPUT_TYPE: &'static str = T::OUTPUT_TYPE; + #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { self.get().into_pyobject(py) @@ -21,6 +24,9 @@ impl<'py, T: Copy + IntoPyObject<'py>> IntoPyObject<'py> for &Cell { type Output = T::Output; type Error = T::Error; + #[cfg(feature = "experimental-inspect")] + const OUTPUT_TYPE: &'static str = T::OUTPUT_TYPE; + #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { self.get().into_pyobject(py) @@ -28,6 +34,9 @@ impl<'py, T: Copy + IntoPyObject<'py>> IntoPyObject<'py> for &Cell { } impl<'py, T: FromPyObject<'py>> FromPyObject<'py> for Cell { + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = T::INPUT_TYPE; + fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { ob.extract().map(Cell::new) } diff --git a/src/instance.rs b/src/instance.rs index 844405ba27e..3bbbb4729b5 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -2028,6 +2028,9 @@ impl FromPyObject<'_> for Py where T: PyTypeCheck, { + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = T::PYTHON_TYPE; + /// Extracts `Self` from the source `PyObject`. fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { ob.extract::>().map(Bound::unbind) @@ -2038,6 +2041,9 @@ impl<'py, T> FromPyObject<'py> for Bound<'py, T> where T: PyTypeCheck, { + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = T::PYTHON_TYPE; + /// Extracts `Self` from the source `PyObject`. fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { ob.cast().cloned().map_err(Into::into) diff --git a/src/pycell.rs b/src/pycell.rs index de21c39b203..fed6b69013f 100644 --- a/src/pycell.rs +++ b/src/pycell.rs @@ -476,6 +476,9 @@ impl<'py, T: PyClass> IntoPyObject<'py> for PyRef<'py, T> { type Output = Bound<'py, T>; type Error = Infallible; + #[cfg(feature = "experimental-inspect")] + const OUTPUT_TYPE: &'static str = T::PYTHON_TYPE; + fn into_pyobject(self, _py: Python<'py>) -> Result { Ok(self.inner.clone()) } @@ -486,6 +489,9 @@ impl<'a, 'py, T: PyClass> IntoPyObject<'py> for &'a PyRef<'py, T> { type Output = Borrowed<'a, 'py, T>; type Error = Infallible; + #[cfg(feature = "experimental-inspect")] + const OUTPUT_TYPE: &'static str = T::PYTHON_TYPE; + fn into_pyobject(self, _py: Python<'py>) -> Result { Ok(self.inner.as_borrowed()) } @@ -647,6 +653,9 @@ impl<'py, T: PyClass> IntoPyObject<'py> for PyRefMut<'py, T> { type Output = Bound<'py, T>; type Error = Infallible; + #[cfg(feature = "experimental-inspect")] + const OUTPUT_TYPE: &'static str = T::PYTHON_TYPE; + fn into_pyobject(self, _py: Python<'py>) -> Result { Ok(self.inner.clone()) } @@ -657,6 +666,9 @@ impl<'a, 'py, T: PyClass> IntoPyObject<'py> for &'a PyRefMut<'py type Output = Borrowed<'a, 'py, T>; type Error = Infallible; + #[cfg(feature = "experimental-inspect")] + const OUTPUT_TYPE: &'static str = T::PYTHON_TYPE; + fn into_pyobject(self, _py: Python<'py>) -> Result { Ok(self.inner.as_borrowed()) } diff --git a/src/pyclass/guard.rs b/src/pyclass/guard.rs index 9ee045c1ab6..2436be3d1c4 100644 --- a/src/pyclass/guard.rs +++ b/src/pyclass/guard.rs @@ -299,6 +299,9 @@ impl<'a, 'py, T: PyClass> IntoPyObject<'py> for &PyClassGuard<'a, T> { type Output = Borrowed<'a, 'py, T>; type Error = Infallible; + #[cfg(feature = "experimental-inspect")] + const OUTPUT_TYPE: &'static str = T::PYTHON_TYPE; + #[inline] fn into_pyobject(self, py: crate::Python<'py>) -> Result { // SAFETY: `ptr` is guaranteed to be valid for 'a and points to an From 488eeda11f8952d71e6365c7be407fffe0df8e74 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 22 Aug 2025 15:19:25 +0100 Subject: [PATCH 811/936] ci: configure trusted publishing (#5353) --- .github/workflows/release.yaml | 26 -------------------------- .github/workflows/release.yml | 23 +++++++++++++++++++++++ 2 files changed, 23 insertions(+), 26 deletions(-) delete mode 100644 .github/workflows/release.yaml create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml deleted file mode 100644 index dd4cf1fd196..00000000000 --- a/.github/workflows/release.yaml +++ /dev/null @@ -1,26 +0,0 @@ -name: Release Rust Crate - -on: - push: - tags: - - "v*" - -jobs: - release: - runs-on: ubuntu-latest - environment: release - steps: - - name: Checkout repository - uses: actions/checkout@v5 - - - 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 - - - name: Publish to crates.io - run: nox -s publish - env: - CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000000..9874c528109 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,23 @@ +name: Release Rust Crate + +on: + push: + tags: + - "v*" + +jobs: + release: + runs-on: ubuntu-latest + environment: release + steps: + - uses: actions/checkout@v5 + + - uses: astral-sh/setup-uv@v6 + + - uses: rust-lang/crates-io-auth-action@v1 + id: auth + + - name: Publish to crates.io + run: nox -s publish + env: + CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} From 39bf3b8617e83b949dd913e752b4623d78edef99 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 22 Aug 2025 15:44:29 +0100 Subject: [PATCH 812/936] add `Python::try_attach` (#5342) * add `Python::try_attach` * newsfragment * build `attach` in terms of `try_attach` * gate `Finalizing` error on 3.13 * fix msrv --- newsfragments/5342.added.md | 1 + pytests/src/misc.rs | 2 +- src/internal/state.rs | 155 ++++++++++++++++++++---------------- src/marker.rs | 25 ++++-- 4 files changed, 106 insertions(+), 77 deletions(-) create mode 100644 newsfragments/5342.added.md diff --git a/newsfragments/5342.added.md b/newsfragments/5342.added.md new file mode 100644 index 00000000000..8972d8d6fda --- /dev/null +++ b/newsfragments/5342.added.md @@ -0,0 +1 @@ +Add `Python::try_attach`. diff --git a/pytests/src/misc.rs b/pytests/src/misc.rs index 5ac6dcd5540..a356d9bea3a 100644 --- a/pytests/src/misc.rs +++ b/pytests/src/misc.rs @@ -24,7 +24,7 @@ fn hammer_gil_in_thread() -> LockHolder { // now the interpreter has shut down, so hammer the GIL. In buggy // versions of PyO3 this will cause a crash. loop { - Python::attach(|_py| ()); + Python::try_attach(|_py| ()); } }); LockHolder { sender } diff --git a/src/internal/state.rs b/src/internal/state.rs index fa52e7e4986..6aafb1fcb30 100644 --- a/src/internal/state.rs +++ b/src/internal/state.rs @@ -7,6 +7,7 @@ use crate::{ffi, Python}; use std::cell::Cell; #[cfg(not(pyo3_disable_reference_pool))] use std::sync::OnceLock; +#[cfg_attr(pyo3_disable_reference_pool, allow(unused_imports))] use std::{mem, ptr::NonNull, sync}; std::thread_local! { @@ -43,79 +44,103 @@ pub(crate) enum AttachGuard { Ensured { gstate: ffi::PyGILState_STATE }, } +/// Possible error when calling `try_attach()` +pub(crate) enum AttachError { + /// Forbidden during GC traversal. + ForbiddenDuringTraverse, + /// The interpreter is not initialized. + NotInitialized, + #[cfg(Py_3_13)] + /// The interpreter is finalizing. + Finalizing, +} + impl AttachGuard { /// PyO3 internal API for attaching to the Python interpreter. The public API is Python::attach. /// /// If the thread was already attached via PyO3, this returns /// `AttachGuard::Assumed`. Otherwise, the thread will attach now and /// `AttachGuard::Ensured` will be returned. - pub(crate) fn acquire() -> Self { - if thread_is_attached() { - // SAFETY: We just checked that the thread is already attached. - return unsafe { Self::assume() }; + pub(crate) fn attach() -> Self { + match Self::try_attach() { + Ok(guard) => guard, + Err(AttachError::ForbiddenDuringTraverse) => { + panic!("{}", ForbidAttaching::FORBIDDEN_DURING_TRAVERSE) + } + Err(AttachError::NotInitialized) => { + // try to initialize the interpreter and try again + crate::interpreter_lifecycle::ensure_initialized(); + unsafe { Self::do_attach_unchecked() } + } + #[cfg(Py_3_13)] + Err(AttachError::Finalizing) => { + panic!("Cannot attach to the Python interpreter while it is finalizing."); + } } - - crate::interpreter_lifecycle::ensure_initialized(); - - // Calling `PyGILState_Ensure` while finalizing may crash CPython in unpredictable - // ways, we'll make a best effort attempt here to avoid that. (There's a time of - // check to time-of-use issue, but it's better than nothing.) - assert!( - !is_finalizing(), - "Cannot attach to the Python interpreter while it is finalizing." - ); - - // SAFETY: We have ensured the Python interpreter is initialized. - unsafe { Self::acquire_unchecked() } } - /// Variant of the above which will will return `None` if the interpreter cannot be attached to. - #[cfg(any(not(Py_LIMITED_API), Py_3_11, test))] // see Python::try_attach - pub(crate) fn try_acquire() -> Option { + /// Variant of the above which will will return gracefully if the interpreter cannot be attached to. + pub(crate) fn try_attach() -> Result { match ATTACH_COUNT.try_with(|c| c.get()) { Ok(i) if i > 0 => { // SAFETY: We just checked that the thread is already attached. - return Some(unsafe { Self::assume() }); + return Ok(unsafe { Self::assume() }); } // Cannot attach during GC traversal. - Ok(ATTACH_FORBIDDEN_DURING_TRAVERSE) => return None, + Ok(ATTACH_FORBIDDEN_DURING_TRAVERSE) => { + return Err(AttachError::ForbiddenDuringTraverse) + } // other cases handled below _ => {} } - // SAFETY: These APIs are always sound to call - if unsafe { ffi::Py_IsInitialized() } == 0 || is_finalizing() { + // SAFETY: always safe to call this + if unsafe { ffi::Py_IsInitialized() } == 0 { + return Err(AttachError::NotInitialized); + } + + // Calling `PyGILState_Ensure` while finalizing may crash CPython in unpredictable + // ways, we'll make a best effort attempt here to avoid that. (There's a time of + // check to time-of-use issue, but it's better than nothing.) + // + // SAFETY: always safe to call this + #[cfg(Py_3_13)] + if unsafe { ffi::Py_IsFinalizing() } != 0 { // If the interpreter is not initialized, we cannot attach. - return None; + return Err(AttachError::Finalizing); } - // SAFETY: We have ensured the Python interpreter is initialized. - Some(unsafe { Self::acquire_unchecked() }) + // SAFETY: We have done everything reasonable to ensure we're in a safe state to + // attach to the Python interpreter. + Ok(unsafe { Self::do_attach_unchecked() }) } /// Acquires the `AttachGuard` without performing any state checking. /// /// This can be called in "unsafe" contexts where the normal interpreter state - /// checking performed by `AttachGuard::acquire` may fail. This includes calling + /// checking performed by `AttachGuard::try_attach` may fail. This includes calling /// as part of multi-phase interpreter initialization. /// /// # Safety /// /// The caller must ensure that the Python interpreter is sufficiently initialized /// for a thread to be able to attach to it. - pub(crate) unsafe fn acquire_unchecked() -> Self { + pub(crate) unsafe fn attach_unchecked() -> Self { if thread_is_attached() { return unsafe { Self::assume() }; } + unsafe { Self::do_attach_unchecked() } + } + + /// Attach to the interpreter, without a fast-path to check if the thread is already attached. + #[cold] + unsafe fn do_attach_unchecked() -> Self { // SAFETY: interpreter is sufficiently initialized to attach a thread. let gstate = unsafe { ffi::PyGILState_Ensure() }; increment_attach_count(); - - #[cfg(not(pyo3_disable_reference_pool))] - if let Some(pool) = POOL.get() { - pool.update_counts(unsafe { Python::assume_gil_acquired() }); - } + // SAFETY: just attached to the interpreter + drop_deferred_references(unsafe { Python::assume_gil_acquired() }); AttachGuard::Ensured { gstate } } @@ -123,33 +148,19 @@ impl AttachGuard { /// to the interpreter. pub(crate) unsafe fn assume() -> Self { increment_attach_count(); - let guard = AttachGuard::Assumed; - #[cfg(not(pyo3_disable_reference_pool))] - if let Some(pool) = POOL.get() { - pool.update_counts(guard.python()); - } - guard + // SAFETY: invariant of calling this function + drop_deferred_references(unsafe { Python::assume_gil_acquired() }); + AttachGuard::Assumed } /// Gets the Python token associated with this [`AttachGuard`]. #[inline] - pub fn python(&self) -> Python<'_> { + pub(crate) fn python(&self) -> Python<'_> { + // SAFETY: this guard guarantees the thread is attached unsafe { Python::assume_gil_acquired() } } } -fn is_finalizing() -> bool { - // SAFTETY: always safe to call this - #[cfg(Py_3_13)] - unsafe { - ffi::Py_IsFinalizing() != 0 - } - - // can't reliably check this before 3.13 - #[cfg(not(Py_3_13))] - false -} - /// The Drop implementation for `AttachGuard` will decrement the attach count (and potentially detach). impl Drop for AttachGuard { fn drop(&mut self) { @@ -164,7 +175,7 @@ impl Drop for AttachGuard { } } -// Vector of PyObject +#[cfg(not(pyo3_disable_reference_pool))] type PyObjVec = Vec>; #[cfg(not(pyo3_disable_reference_pool))] @@ -185,7 +196,7 @@ impl ReferencePool { self.pending_decrefs.lock().unwrap().push(obj); } - fn update_counts(&self, _py: Python<'_>) { + fn drop_deferred_references(&self, _py: Python<'_>) { let mut pending_decrefs = self.pending_decrefs.lock().unwrap(); if pending_decrefs.is_empty() { return; @@ -214,6 +225,15 @@ fn get_pool() -> &'static ReferencePool { POOL.get_or_init(ReferencePool::new) } +#[cfg_attr(pyo3_disable_reference_pool, inline(always))] +#[cfg_attr(pyo3_disable_reference_pool, allow(unused_variables))] +fn drop_deferred_references(py: Python<'_>) { + #[cfg(not(pyo3_disable_reference_pool))] + if let Some(pool) = POOL.get() { + pool.drop_deferred_references(py); + } +} + /// A guard which can be used to temporarily detach from the interpreter and restore on `Drop`. pub(crate) struct SuspendAttach { count: isize, @@ -238,7 +258,7 @@ impl Drop for SuspendAttach { // Update counts of `Py` that were dropped while not attached. #[cfg(not(pyo3_disable_reference_pool))] if let Some(pool) = POOL.get() { - pool.update_counts(Python::assume_gil_acquired()); + pool.drop_deferred_references(Python::assume_gil_acquired()); } } } @@ -250,6 +270,8 @@ pub(crate) struct ForbidAttaching { } impl ForbidAttaching { + const FORBIDDEN_DURING_TRAVERSE: &'static str = "Attaching a thread to the interpreter is prohibited while a __traverse__ implementation is running."; + /// Lock access to the interpreter while an implementation of `__traverse__` is running pub fn during_traverse() -> Self { Self::new(ATTACH_FORBIDDEN_DURING_TRAVERSE) @@ -264,9 +286,7 @@ impl ForbidAttaching { #[cold] fn bail(current: isize) { match current { - ATTACH_FORBIDDEN_DURING_TRAVERSE => panic!( - "Attaching a thread to the interpreter is prohibited while a __traverse__ implementation is running." - ), + ATTACH_FORBIDDEN_DURING_TRAVERSE => panic!("{}", Self::FORBIDDEN_DURING_TRAVERSE), _ => panic!("Attaching a thread to the interpreter is currently prohibited."), } } @@ -359,7 +379,6 @@ mod tests { use super::*; use crate::{ffi, types::PyAnyMethods, Py, PyAny, Python}; - use std::ptr::NonNull; fn get_object(py: Python<'_>) -> Py { py.eval(ffi::c_str!("object()"), None, None) @@ -531,8 +550,8 @@ mod tests { #[test] #[cfg(not(pyo3_disable_reference_pool))] - fn test_update_counts_does_not_deadlock() { - // update_counts can run arbitrary Python code during Py_DECREF. + fn test_drop_deferred_references_does_not_deadlock() { + // drop_deferred_references can run arbitrary Python code during Py_DECREF. // if the locking is implemented incorrectly, it will deadlock. use crate::ffi; @@ -541,8 +560,8 @@ mod tests { 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. + // This line will implicitly call drop_deferred_references + // -> and so cause deadlock if drop_deferred_references is not handling recursion correctly. let pool = unsafe { AttachGuard::assume() }; // Rebuild obj so that it can be dropped @@ -562,22 +581,22 @@ mod tests { get_pool().register_decref(NonNull::new(capsule).unwrap()); // Updating the counts will call decref on the capsule, which calls capsule_drop - get_pool().update_counts(py); + get_pool().drop_deferred_references(py); }) } #[test] #[cfg(not(pyo3_disable_reference_pool))] - fn test_attach_guard_update_counts() { + fn test_attach_guard_drop_deferred_references() { Python::attach(|py| { let obj = get_object(py); - // For AttachGuard::acquire + // For AttachGuard::attach get_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 = AttachGuard::acquire(); + let _guard = AttachGuard::attach(); assert!(pool_dec_refs_does_not_contain(&obj)); // For AttachGuard::assume diff --git a/src/marker.rs b/src/marker.rs index a9a7e378964..bf0b3c05b2b 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -393,6 +393,10 @@ impl Python<'_> { /// - If the [`auto-initialize`] feature is not enabled and the Python interpreter is not /// initialized. /// - If the Python interpreter is in the process of [shutting down]. + /// - If the middle of GC traversal. + /// + /// To avoid possible initialization or panics if calling in a context where the Python + /// interpreter might be unavailable, consider using [`Python::try_attach`]. /// /// # Examples /// @@ -417,24 +421,29 @@ impl Python<'_> { where F: for<'py> FnOnce(Python<'py>) -> R, { - let guard = AttachGuard::acquire(); + let guard = AttachGuard::attach(); f(guard.python()) } - /// Variant of [`Python::attach`] which will do no work if the interpreter is in a - /// state where it cannot be attached to: + /// Variant of [`Python::attach`] which will return without attaching to the Python + /// interpreter if the interpreter is in a state where it cannot be attached to: /// - in the middle of GC traversal /// - in the process of shutting down /// - not initialized + /// + /// Note that due to the nature of the underlying Python APIs used to implement this, + /// the behavior is currently provided on a best-effort basis; it is expected that a + /// future CPython version will introduce APIs which guarantee this behaviour. This + /// function is still recommended for use in the meanwhile as it provides the best + /// possible behaviour and should transparently change to an optimal implementation + /// once such APIs are available. #[inline] #[track_caller] - #[cfg(any(not(Py_LIMITED_API), Py_3_11, test))] // only used in buffer.rs for now, allow in test cfg for simplicity - // TODO: make this API public? - pub(crate) fn try_attach(f: F) -> Option + pub fn try_attach(f: F) -> Option where F: for<'py> FnOnce(Python<'py>) -> R, { - let guard = AttachGuard::try_acquire()?; + let guard = AttachGuard::try_attach().ok()?; Some(f(guard.python())) } @@ -494,7 +503,7 @@ impl Python<'_> { where F: for<'py> FnOnce(Python<'py>) -> R, { - let guard = unsafe { AttachGuard::acquire_unchecked() }; + let guard = unsafe { AttachGuard::attach_unchecked() }; f(guard.python()) } From 62f7114e72528e48b31427f03dc7a4a199270c56 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Fri, 22 Aug 2025 21:03:48 +0200 Subject: [PATCH 813/936] rename `Python::assume_gil_acquired` to `Python::assume_attached` (#5354) --- newsfragments/5354.changed.md | 1 + src/coroutine.rs | 4 ++-- src/impl_/pyclass.rs | 12 +++++------- src/impl_/pymethods.rs | 2 +- src/internal/get_slot.rs | 2 +- src/internal/state.rs | 8 ++++---- src/marker.rs | 23 ++++++++++++++++------- src/types/datetime.rs | 2 +- 8 files changed, 31 insertions(+), 23 deletions(-) create mode 100644 newsfragments/5354.changed.md diff --git a/newsfragments/5354.changed.md b/newsfragments/5354.changed.md new file mode 100644 index 00000000000..165f65c0567 --- /dev/null +++ b/newsfragments/5354.changed.md @@ -0,0 +1 @@ +rename `Python::assume_gil_acquired` to `Python::assume_attached` \ No newline at end of file diff --git a/src/coroutine.rs b/src/coroutine.rs index cf58b742f81..fe352c5ef01 100644 --- a/src/coroutine.rs +++ b/src/coroutine.rs @@ -60,8 +60,8 @@ 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_py_any(unsafe { Python::assume_gil_acquired() }) + // SAFETY: attached when future is polled (see `Coroutine::poll`) + obj.into_py_any(unsafe { Python::assume_attached() }) }; Self { name: name.map(Bound::unbind), diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index 4150ccd92ea..d960b5477c4 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -926,12 +926,12 @@ pub trait PyClassWithFreeList: PyClass { /// /// # Safety /// - `subtype` must be a valid pointer to the type object of T or a subclass. -/// - The GIL must be held. +/// - The calling thread must be attached to the interpreter pub unsafe extern "C" fn alloc_with_freelist( subtype: *mut ffi::PyTypeObject, nitems: ffi::Py_ssize_t, ) -> *mut ffi::PyObject { - let py = unsafe { Python::assume_gil_acquired() }; + let py = unsafe { Python::assume_attached() }; #[cfg(not(Py_3_8))] unsafe { @@ -958,17 +958,15 @@ pub unsafe extern "C" fn alloc_with_freelist( /// /// # Safety /// - `obj` must be a valid pointer to an instance of T (not a subclass). -/// - The GIL must be held. +/// - The calling thread must be attached to the interpreter pub unsafe extern "C" fn free_with_freelist(obj: *mut c_void) { let obj = obj as *mut ffi::PyObject; unsafe { debug_assert_eq!( - T::type_object_raw(Python::assume_gil_acquired()), + T::type_object_raw(Python::assume_attached()), ffi::Py_TYPE(obj) ); - let mut free_list = T::get_free_list(Python::assume_gil_acquired()) - .lock() - .unwrap(); + let mut free_list = T::get_free_list(Python::assume_attached()).lock().unwrap(); if let Some(obj) = free_list.insert(obj) { drop(free_list); let ty = ffi::Py_TYPE(obj); diff --git a/src/impl_/pymethods.rs b/src/impl_/pymethods.rs index 829869b0f1e..cbc7b626d03 100644 --- a/src/impl_/pymethods.rs +++ b/src/impl_/pymethods.rs @@ -749,7 +749,7 @@ mod tests { ) -> *mut ffi::PyObject { assert_eq!(nargs, 0); assert!(kwargs.is_null()); - unsafe { Python::assume_gil_acquired().None().into_ptr() } + unsafe { Python::assume_attached().None().into_ptr() } } let f = PyCFunction::internal_new( diff --git a/src/internal/get_slot.rs b/src/internal/get_slot.rs index 9d1633bed6d..3eeb16b645d 100644 --- a/src/internal/get_slot.rs +++ b/src/internal/get_slot.rs @@ -56,7 +56,7 @@ where 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()), + is_runtime_3_10(crate::Python::assume_attached()), ) } } diff --git a/src/internal/state.rs b/src/internal/state.rs index 6aafb1fcb30..4105b31792f 100644 --- a/src/internal/state.rs +++ b/src/internal/state.rs @@ -140,7 +140,7 @@ impl AttachGuard { let gstate = unsafe { ffi::PyGILState_Ensure() }; increment_attach_count(); // SAFETY: just attached to the interpreter - drop_deferred_references(unsafe { Python::assume_gil_acquired() }); + drop_deferred_references(unsafe { Python::assume_attached() }); AttachGuard::Ensured { gstate } } @@ -149,7 +149,7 @@ impl AttachGuard { pub(crate) unsafe fn assume() -> Self { increment_attach_count(); // SAFETY: invariant of calling this function - drop_deferred_references(unsafe { Python::assume_gil_acquired() }); + drop_deferred_references(unsafe { Python::assume_attached() }); AttachGuard::Assumed } @@ -157,7 +157,7 @@ impl AttachGuard { #[inline] pub(crate) fn python(&self) -> Python<'_> { // SAFETY: this guard guarantees the thread is attached - unsafe { Python::assume_gil_acquired() } + unsafe { Python::assume_attached() } } } @@ -258,7 +258,7 @@ impl Drop for SuspendAttach { // Update counts of `Py` that were dropped while not attached. #[cfg(not(pyo3_disable_reference_pool))] if let Some(pool) = POOL.get() { - pool.drop_deferred_references(Python::assume_gil_acquired()); + pool.drop_deferred_references(Python::assume_attached()); } } } diff --git a/src/marker.rs b/src/marker.rs index bf0b3c05b2b..52262d4119a 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -808,24 +808,33 @@ impl<'py> Python<'py> { } impl<'unbound> Python<'unbound> { + /// Deprecated version of [`Python::assume_attached`] + /// + /// # Safety + /// See [`Python::assume_attached`] + #[inline] + #[deprecated(since = "0.26.0", note = "use `Python::assume_attached` instead")] + pub unsafe fn assume_gil_acquired() -> Python<'unbound> { + unsafe { Self::assume_attached() } + } /// Unsafely creates a Python token with an unbounded lifetime. /// - /// Many of PyO3 APIs use `Python<'_>` as proof that the GIL is held, but this function can be - /// used to call them unsafely. + /// Many of PyO3 APIs use [`Python<'_>`] as proof that the calling thread is attached to the + /// interpreter, but this function can be used to call them unsafely. /// /// # Safety /// /// - This token and any borrowed Python references derived from it can only be safely used - /// whilst the currently executing thread is actually holding the GIL. + /// whilst the currently executing thread is actually attached to the interpreter. /// - 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 + /// holding a [`Python<'py>`] token means the thread is attached and stays attached for the + /// lifetime `'py`. If you let it or borrowed Python references escape to safe code you are /// responsible for bounding the lifetime `'unbound` appropriately. For more on unbounded /// lifetimes, see the [nomicon]. /// /// [nomicon]: https://doc.rust-lang.org/nomicon/unbounded-lifetimes.html #[inline] - pub unsafe fn assume_gil_acquired() -> Python<'unbound> { + pub unsafe fn assume_attached() -> Python<'unbound> { Python(PhantomData, PhantomData) } } @@ -906,7 +915,7 @@ mod tests { fn test_detach_panics_safely() { Python::attach(|py| { let result = std::panic::catch_unwind(|| unsafe { - let py = Python::assume_gil_acquired(); + let py = Python::assume_attached(); py.detach(|| { panic!("There was a panic!"); }); diff --git a/src/types/datetime.rs b/src/types/datetime.rs index adca98e9878..601440dc11e 100644 --- a/src/types/datetime.rs +++ b/src/types/datetime.rs @@ -100,7 +100,7 @@ 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(unsafe { Python::assume_gil_acquired() }); + let _ = ensure_datetime_api(unsafe { Python::assume_attached() }); unsafe { crate::ffi::$name($arg) } } )* From 2fa818b91de3aa3488defa644ccba55e69325ad9 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sat, 23 Aug 2025 06:26:20 +0100 Subject: [PATCH 814/936] avoid races in tests asserting warnings (#5357) --- src/conversions/chrono.rs | 9 +-- src/conversions/num_bigint.rs | 2 +- src/conversions/num_complex.rs | 2 +- src/err/mod.rs | 3 +- src/instance.rs | 2 +- src/lib.rs | 3 +- src/test_utils.rs | 6 ++ src/tests/mod.rs | 7 -- src/types/any.rs | 2 +- src/types/typeobject.rs | 2 +- tests/test_arithmetics.rs | 3 +- tests/test_buffer.rs | 3 +- tests/test_buffer_protocol.rs | 5 +- tests/test_bytes.rs | 3 +- tests/test_class_attributes.rs | 5 +- tests/test_class_basics.rs | 5 +- tests/test_class_comparisons.rs | 3 +- tests/test_class_conversion.rs | 3 +- tests/test_class_formatting.rs | 3 +- tests/test_coroutine.rs | 3 +- tests/test_declarative_module.rs | 3 +- tests/test_default_impls.rs | 3 +- tests/test_enum.rs | 3 +- tests/test_exceptions.rs | 5 +- tests/test_frompy_intopy_roundtrip.rs | 3 +- tests/test_frompyobject.rs | 3 +- tests/test_gc.rs | 3 +- tests/test_getter_setter.rs | 3 +- tests/test_inheritance.rs | 3 +- tests/test_intopyobject.rs | 3 +- tests/test_macro_docs.rs | 3 +- tests/test_macros.rs | 3 +- tests/test_mapping.rs | 3 +- tests/test_methods.rs | 6 +- tests/test_module.rs | 5 +- tests/test_multiple_pymethods.rs | 3 +- tests/test_proto_methods.rs | 3 +- tests/test_pyfunction.rs | 6 +- tests/test_pyself.rs | 3 +- tests/test_sequence.rs | 3 +- tests/test_static_slots.rs | 3 +- tests/test_string.rs | 3 +- tests/test_text_signature.rs | 3 +- .../common.rs => tests/test_utils/mod.rs | 77 ++++++++++--------- tests/test_variable_arguments.rs | 3 +- tests/test_various.rs | 3 +- 46 files changed, 97 insertions(+), 139 deletions(-) create mode 100644 src/test_utils.rs rename src/tests/common.rs => tests/test_utils/mod.rs (75%) diff --git a/src/conversions/chrono.rs b/src/conversions/chrono.rs index 5ea8904fca2..bd97d3409fc 100644 --- a/src/conversions/chrono.rs +++ b/src/conversions/chrono.rs @@ -593,7 +593,7 @@ fn py_time_to_naive_time(py_time: &Bound<'_, PyAny>) -> PyResult { #[cfg(test)] mod tests { use super::*; - use crate::{types::PyTuple, BoundObject}; + use crate::{test_utils::assert_warnings, types::PyTuple, BoundObject}; use std::{cmp::Ordering, panic}; #[test] @@ -875,7 +875,6 @@ 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), @@ -915,7 +914,6 @@ 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), @@ -1101,7 +1099,6 @@ 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), @@ -1163,10 +1160,10 @@ mod tests { .unwrap() } - #[cfg(not(any(target_arch = "wasm32", Py_GIL_DISABLED)))] + #[cfg(not(any(target_arch = "wasm32")))] mod proptests { use super::*; - use crate::tests::common::CatchWarnings; + use crate::test_utils::CatchWarnings; use crate::types::IntoPyDict; use proptest::prelude::*; use std::ffi::CString; diff --git a/src/conversions/num_bigint.rs b/src/conversions/num_bigint.rs index f9b4f11fa69..0f8924bbf12 100644 --- a/src/conversions/num_bigint.rs +++ b/src/conversions/num_bigint.rs @@ -317,7 +317,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::test_utils::generate_unique_module_name; use crate::types::{PyAnyMethods as _, PyDict, PyModule}; use indoc::indoc; use pyo3_ffi::c_str; diff --git a/src/conversions/num_complex.rs b/src/conversions/num_complex.rs index edcc0ef2d54..cdb7ad3fc42 100644 --- a/src/conversions/num_complex.rs +++ b/src/conversions/num_complex.rs @@ -196,7 +196,7 @@ complex_conversion!(f64); #[cfg(test)] mod tests { use super::*; - use crate::tests::common::generate_unique_module_name; + use crate::test_utils::generate_unique_module_name; use crate::types::PyAnyMethods as _; use crate::types::{complex::PyComplexMethods, PyModule}; use crate::IntoPyObject; diff --git a/src/err/mod.rs b/src/err/mod.rs index 16265865283..fe8d3755d4a 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -853,6 +853,7 @@ mod tests { use super::PyErrState; use crate::exceptions::{self, PyTypeError, PyValueError}; use crate::impl_::pyclass::{value_of, IsSend, IsSync}; + use crate::test_utils::assert_warnings; use crate::{ffi, PyErr, PyTypeInfo, Python}; #[test] @@ -1052,7 +1053,6 @@ 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() }, @@ -1072,7 +1072,6 @@ 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/instance.rs b/src/instance.rs index 3bbbb4729b5..f9347f2b2ca 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -2171,7 +2171,7 @@ impl Py { #[cfg(test)] mod tests { use super::{Bound, IntoPyObject, Py}; - use crate::tests::common::generate_unique_module_name; + use crate::test_utils::generate_unique_module_name; use crate::types::{dict::IntoPyDict, PyAnyMethods, PyCapsule, PyDict, PyString}; use crate::{ffi, Borrowed, PyAny, PyResult, Python}; use pyo3_ffi::c_str; diff --git a/src/lib.rs b/src/lib.rs index 34b4a6ae156..bdf0dd7cd5c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -410,7 +410,8 @@ pub use inventory; // Re-exported for `#[pyclass]` and `#[pymethods]` with `mult /// Tests and helpers which reside inside PyO3's main library. Declared first so that macros /// are available in unit tests. #[cfg(test)] -#[macro_use] +mod test_utils; +#[cfg(test)] mod tests; // Macro dependencies, also contains macros exported for use across the codebase and diff --git a/src/test_utils.rs b/src/test_utils.rs new file mode 100644 index 00000000000..32b74997bcb --- /dev/null +++ b/src/test_utils.rs @@ -0,0 +1,6 @@ +// Brings in `test_utils` from the `tests` directory +// +// to make that file function (lots of references to `pyo3` within it) need +// re-bind `crate` as pyo3 +use crate as pyo3; +include!("../tests/test_utils/mod.rs"); diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 136236d7db0..3b72764301c 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -1,10 +1,3 @@ -#[macro_use] -pub(crate) mod common { - #[cfg(not(Py_GIL_DISABLED))] - use crate as pyo3; - include!("./common.rs"); -} - /// Test macro hygiene - this is in the crate since we won't have /// `pyo3` available in the crate root. #[cfg(all(test, feature = "macros"))] diff --git a/src/types/any.rs b/src/types/any.rs index fb3d97b55ce..665fb607480 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -1624,7 +1624,7 @@ mod tests { use crate::{ basic::CompareOp, ffi, - tests::common::generate_unique_module_name, + test_utils::generate_unique_module_name, types::{IntoPyDict, PyAny, PyAnyMethods, PyBool, PyInt, PyList, PyModule, PyTypeMethods}, Bound, BoundObject, IntoPyObject, PyTypeInfo, Python, }; diff --git a/src/types/typeobject.rs b/src/types/typeobject.rs index 58c94d46cf4..953a78bc6aa 100644 --- a/src/types/typeobject.rs +++ b/src/types/typeobject.rs @@ -244,7 +244,7 @@ impl<'py> PyTypeMethods<'py> for Bound<'py, PyType> { #[cfg(test)] mod tests { - use crate::tests::common::generate_unique_module_name; + use crate::test_utils::generate_unique_module_name; use crate::types::{PyAnyMethods, PyBool, PyInt, PyModule, PyTuple, PyType, PyTypeMethods}; use crate::PyAny; use crate::Python; diff --git a/tests/test_arithmetics.rs b/tests/test_arithmetics.rs index 3e0e7712ba4..bf3f99c3462 100644 --- a/tests/test_arithmetics.rs +++ b/tests/test_arithmetics.rs @@ -4,8 +4,7 @@ use pyo3::class::basic::CompareOp; use pyo3::py_run; use pyo3::{prelude::*, BoundObject}; -#[path = "../src/tests/common.rs"] -mod common; +mod test_utils; #[pyclass] struct UnaryArithmetic { diff --git a/tests/test_buffer.rs b/tests/test_buffer.rs index 4d975f36479..7ffa3e58864 100644 --- a/tests/test_buffer.rs +++ b/tests/test_buffer.rs @@ -9,8 +9,7 @@ use std::{ }; #[macro_use] -#[path = "../src/tests/common.rs"] -mod common; +mod test_utils; enum TestGetBufferError { NullShape, diff --git a/tests/test_buffer_protocol.rs b/tests/test_buffer_protocol.rs index d5f50d02e5d..5a2baa33d05 100644 --- a/tests/test_buffer_protocol.rs +++ b/tests/test_buffer_protocol.rs @@ -13,8 +13,7 @@ use std::ptr; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; -#[path = "../src/tests/common.rs"] -mod common; +mod test_utils; #[pyclass] struct TestBufferClass { @@ -98,8 +97,8 @@ fn test_buffer_referenced() { #[test] #[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; + use test_utils::UnraisableCapture; #[pyclass] struct ReleaseBufferError {} diff --git a/tests/test_bytes.rs b/tests/test_bytes.rs index 7df25c2fe58..0caaf2a37ff 100644 --- a/tests/test_bytes.rs +++ b/tests/test_bytes.rs @@ -3,8 +3,7 @@ use pyo3::prelude::*; use pyo3::types::PyBytes; -#[path = "../src/tests/common.rs"] -mod common; +mod test_utils; #[pyfunction] fn bytes_pybytes_conversion(bytes: &[u8]) -> &[u8] { diff --git a/tests/test_class_attributes.rs b/tests/test_class_attributes.rs index b1a019c0457..e8d104d9d10 100644 --- a/tests/test_class_attributes.rs +++ b/tests/test_class_attributes.rs @@ -3,8 +3,7 @@ use pyo3::prelude::*; use pyo3::py_run; -#[path = "../src/tests/common.rs"] -mod common; +mod test_utils; #[pyclass] struct Foo { @@ -154,8 +153,8 @@ fn recursive_class_attributes() { #[test] #[cfg(all(Py_3_8, not(Py_GIL_DISABLED)))] // sys.unraisablehook not available until Python 3.8 fn test_fallible_class_attribute() { - use common::UnraisableCapture; use pyo3::exceptions::PyValueError; + use test_utils::UnraisableCapture; #[pyclass] struct BrokenClass; diff --git a/tests/test_class_basics.rs b/tests/test_class_basics.rs index 027f050320e..1dda580c2a4 100644 --- a/tests/test_class_basics.rs +++ b/tests/test_class_basics.rs @@ -4,8 +4,7 @@ use pyo3::prelude::*; use pyo3::types::PyType; use pyo3::{py_run, PyClass}; -#[path = "../src/tests/common.rs"] -mod common; +mod test_utils; #[pyclass] struct EmptyClass {} @@ -619,12 +618,12 @@ fn access_frozen_class_without_gil() { #[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; use std::sync::{ atomic::{AtomicBool, Ordering}, Arc, }; use std::thread::spawn; + use test_utils::UnraisableCapture; #[pyclass(unsendable)] struct Unsendable { diff --git a/tests/test_class_comparisons.rs b/tests/test_class_comparisons.rs index 2f716ca431a..e8509d1e266 100644 --- a/tests/test_class_comparisons.rs +++ b/tests/test_class_comparisons.rs @@ -2,8 +2,7 @@ use pyo3::prelude::*; -#[path = "../src/tests/common.rs"] -mod common; +mod test_utils; #[pyclass(eq)] #[derive(Debug, Clone, PartialEq)] diff --git a/tests/test_class_conversion.rs b/tests/test_class_conversion.rs index c451a37ac35..2ff0f56a9b3 100644 --- a/tests/test_class_conversion.rs +++ b/tests/test_class_conversion.rs @@ -3,8 +3,7 @@ use pyo3::prelude::*; #[macro_use] -#[path = "../src/tests/common.rs"] -mod common; +mod test_utils; #[pyclass] #[derive(Clone, Debug, PartialEq)] diff --git a/tests/test_class_formatting.rs b/tests/test_class_formatting.rs index 976e6da4e5e..1b4add063d4 100644 --- a/tests/test_class_formatting.rs +++ b/tests/test_class_formatting.rs @@ -4,8 +4,7 @@ use pyo3::prelude::*; use pyo3::py_run; use std::fmt::{Display, Formatter}; -#[path = "../src/tests/common.rs"] -mod common; +mod test_utils; #[pyclass(eq, str)] #[derive(Debug, PartialEq)] diff --git a/tests/test_coroutine.rs b/tests/test_coroutine.rs index 868caf7e69b..24e007ab5a3 100644 --- a/tests/test_coroutine.rs +++ b/tests/test_coroutine.rs @@ -14,8 +14,7 @@ use pyo3::{ #[cfg(target_has_atomic = "64")] use std::sync::atomic::{AtomicBool, Ordering}; -#[path = "../src/tests/common.rs"] -mod common; +mod test_utils; fn handle_windows(test: &str) -> String { let set_event_loop_policy = r#" diff --git a/tests/test_declarative_module.rs b/tests/test_declarative_module.rs index f3a01001952..57800cdce2e 100644 --- a/tests/test_declarative_module.rs +++ b/tests/test_declarative_module.rs @@ -7,8 +7,7 @@ use pyo3::exceptions::PyException; use pyo3::prelude::*; use pyo3::sync::OnceLockExt; -#[path = "../src/tests/common.rs"] -mod common; +mod test_utils; mod some_module { use pyo3::create_exception; diff --git a/tests/test_default_impls.rs b/tests/test_default_impls.rs index e5735f5dd2a..19dc0773a80 100644 --- a/tests/test_default_impls.rs +++ b/tests/test_default_impls.rs @@ -2,8 +2,7 @@ use pyo3::prelude::*; -#[path = "../src/tests/common.rs"] -mod common; +mod test_utils; // Test default generated __repr__. #[pyclass(eq, eq_int)] diff --git a/tests/test_enum.rs b/tests/test_enum.rs index f1a2ab3eff6..cdc9935a6df 100644 --- a/tests/test_enum.rs +++ b/tests/test_enum.rs @@ -4,8 +4,7 @@ use pyo3::prelude::*; use pyo3::py_run; use pyo3::types::PyString; -#[path = "../src/tests/common.rs"] -mod common; +mod test_utils; #[pyclass(eq, eq_int)] #[derive(Debug, PartialEq, Eq, Clone)] diff --git a/tests/test_exceptions.rs b/tests/test_exceptions.rs index 081041ed12d..97b5466d205 100644 --- a/tests/test_exceptions.rs +++ b/tests/test_exceptions.rs @@ -7,8 +7,7 @@ use std::fmt; #[cfg(not(target_os = "windows"))] use std::fs::File; -#[path = "../src/tests/common.rs"] -mod common; +mod test_utils; #[pyfunction] #[cfg(not(target_os = "windows"))] @@ -101,9 +100,9 @@ fn test_exception_nosegfault() { #[test] #[cfg(all(Py_3_8, not(Py_GIL_DISABLED)))] fn test_write_unraisable() { - use common::UnraisableCapture; use pyo3::{exceptions::PyRuntimeError, ffi, types::PyNotImplemented}; use std::ptr; + use test_utils::UnraisableCapture; Python::attach(|py| { let capture = UnraisableCapture::install(py); diff --git a/tests/test_frompy_intopy_roundtrip.rs b/tests/test_frompy_intopy_roundtrip.rs index e8e41c14ef2..c4b120c5292 100644 --- a/tests/test_frompy_intopy_roundtrip.rs +++ b/tests/test_frompy_intopy_roundtrip.rs @@ -6,8 +6,7 @@ use std::collections::HashMap; use std::hash::Hash; #[macro_use] -#[path = "../src/tests/common.rs"] -mod common; +mod test_utils; #[derive(Debug, Clone, IntoPyObject, IntoPyObjectRef, FromPyObject)] pub struct A<'py> { diff --git a/tests/test_frompyobject.rs b/tests/test_frompyobject.rs index 35cc813a603..9a72e57284c 100644 --- a/tests/test_frompyobject.rs +++ b/tests/test_frompyobject.rs @@ -5,8 +5,7 @@ use pyo3::prelude::*; use pyo3::types::{IntoPyDict, PyDict, PyList, PyString, PyTuple}; #[macro_use] -#[path = "../src/tests/common.rs"] -mod common; +mod test_utils; /// Helper function that concatenates the error message from /// each error in the traceback into a single string that can diff --git a/tests/test_gc.rs b/tests/test_gc.rs index 8f39c1baca1..f97868fec9f 100644 --- a/tests/test_gc.rs +++ b/tests/test_gc.rs @@ -13,8 +13,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Once; use std::sync::{Arc, Mutex}; -#[path = "../src/tests/common.rs"] -mod common; +mod test_utils; #[pyclass(freelist = 2)] struct ClassWithFreelist {} diff --git a/tests/test_getter_setter.rs b/tests/test_getter_setter.rs index 4d15627ea51..582cfd89c5f 100644 --- a/tests/test_getter_setter.rs +++ b/tests/test_getter_setter.rs @@ -7,8 +7,7 @@ use pyo3::py_run; use pyo3::types::PyString; use pyo3::types::{IntoPyDict, PyList}; -#[path = "../src/tests/common.rs"] -mod common; +mod test_utils; #[pyclass] struct ClassWithProperties { diff --git a/tests/test_inheritance.rs b/tests/test_inheritance.rs index 0500c0c2f83..99c26ae1041 100644 --- a/tests/test_inheritance.rs +++ b/tests/test_inheritance.rs @@ -6,8 +6,7 @@ use pyo3::py_run; use pyo3::ffi; use pyo3::types::IntoPyDict; -#[path = "../src/tests/common.rs"] -mod common; +mod test_utils; #[pyclass(subclass)] struct BaseClass { diff --git a/tests/test_intopyobject.rs b/tests/test_intopyobject.rs index 58795d7c7f6..72ecc0f234d 100644 --- a/tests/test_intopyobject.rs +++ b/tests/test_intopyobject.rs @@ -6,8 +6,7 @@ use std::collections::HashMap; use std::hash::Hash; #[macro_use] -#[path = "../src/tests/common.rs"] -mod common; +mod test_utils; #[derive(Debug, IntoPyObject)] pub struct A<'py> { diff --git a/tests/test_macro_docs.rs b/tests/test_macro_docs.rs index fec11e51904..e27277a9326 100644 --- a/tests/test_macro_docs.rs +++ b/tests/test_macro_docs.rs @@ -4,8 +4,7 @@ use pyo3::prelude::*; use pyo3::types::IntoPyDict; #[macro_use] -#[path = "../src/tests/common.rs"] -mod common; +mod test_utils; #[pyclass] /// The MacroDocs class. diff --git a/tests/test_macros.rs b/tests/test_macros.rs index 72b5b45ef7f..e7b1997145a 100644 --- a/tests/test_macros.rs +++ b/tests/test_macros.rs @@ -5,8 +5,7 @@ use pyo3::prelude::*; #[macro_use] -#[path = "../src/tests/common.rs"] -mod common; +mod test_utils; macro_rules! make_struct_using_macro { // Ensure that one doesn't need to fall back on the escape type: tt diff --git a/tests/test_mapping.rs b/tests/test_mapping.rs index bab7c0a418a..df52372d143 100644 --- a/tests/test_mapping.rs +++ b/tests/test_mapping.rs @@ -10,8 +10,7 @@ use pyo3::types::PyList; use pyo3::types::PyMapping; use pyo3::types::PySequence; -#[path = "../src/tests/common.rs"] -mod common; +mod test_utils; #[pyclass(mapping)] struct Mapping { diff --git a/tests/test_methods.rs b/tests/test_methods.rs index 06544f221e9..7fbc1a138b6 100644 --- a/tests/test_methods.rs +++ b/tests/test_methods.rs @@ -2,7 +2,6 @@ #[cfg(not(Py_LIMITED_API))] use pyo3::exceptions::PyWarning; -#[cfg(not(Py_GIL_DISABLED))] use pyo3::exceptions::{PyFutureWarning, PyUserWarning}; use pyo3::prelude::*; use pyo3::py_run; @@ -11,8 +10,7 @@ use pyo3::types::{IntoPyDict, PyDict, PyList, PySet, PyString, PyTuple, PyType}; use pyo3::BoundObject; use pyo3_macros::pyclass; -#[path = "../src/tests/common.rs"] -mod common; +mod test_utils; #[pyclass] struct InstanceMethod { @@ -1223,7 +1221,6 @@ impl UserDefinedWarning { } #[test] -#[cfg(not(Py_GIL_DISABLED))] // FIXME: enable once `warnings` is thread-safe fn test_pymethods_warn() { // We do not test #[classattr] nor __traverse__ // because it doesn't make sense to implement deprecated methods for them. @@ -1424,7 +1421,6 @@ fn test_pymethods_warn() { } #[test] -#[cfg(not(Py_GIL_DISABLED))] // FIXME: enable once `warnings` is thread-safe fn test_py_methods_multiple_warn() { #[pyclass] struct MultipleWarnContainer {} diff --git a/tests/test_module.rs b/tests/test_module.rs index 2f5766aadaf..5e6244420e0 100644 --- a/tests/test_module.rs +++ b/tests/test_module.rs @@ -8,8 +8,7 @@ use pyo3::types::{IntoPyDict, PyDict, PyTuple}; use pyo3::BoundObject; use pyo3_ffi::c_str; -#[path = "../src/tests/common.rs"] -mod common; +mod test_utils; #[pyclass] struct AnonClass {} @@ -168,7 +167,7 @@ fn test_module_from_code_bound() { py, c_str!("def add(a,b):\n\treturn a+b"), c_str!("adder_mod.py"), - &common::generate_unique_module_name("adder_mod"), + &test_utils::generate_unique_module_name("adder_mod"), ) .expect("Module code should be loaded"); diff --git a/tests/test_multiple_pymethods.rs b/tests/test_multiple_pymethods.rs index ccbda42b865..1a788425113 100644 --- a/tests/test_multiple_pymethods.rs +++ b/tests/test_multiple_pymethods.rs @@ -4,8 +4,7 @@ use pyo3::prelude::*; use pyo3::types::PyType; #[macro_use] -#[path = "../src/tests/common.rs"] -mod common; +mod test_utils; #[pyclass] struct PyClassWithMultiplePyMethods {} diff --git a/tests/test_proto_methods.rs b/tests/test_proto_methods.rs index e8df772faf1..94a8ddd368a 100644 --- a/tests/test_proto_methods.rs +++ b/tests/test_proto_methods.rs @@ -6,8 +6,7 @@ use pyo3::{prelude::*, py_run}; use std::iter; use std::sync::Mutex; -#[path = "../src/tests/common.rs"] -mod common; +mod test_utils; #[pyclass] struct EmptyClass; diff --git a/tests/test_pyfunction.rs b/tests/test_pyfunction.rs index 54447b92940..07bd9f5482c 100644 --- a/tests/test_pyfunction.rs +++ b/tests/test_pyfunction.rs @@ -7,7 +7,6 @@ use std::collections::HashMap; use pyo3::buffer::PyBuffer; #[cfg(not(Py_LIMITED_API))] use pyo3::exceptions::PyWarning; -#[cfg(not(Py_GIL_DISABLED))] use pyo3::exceptions::{PyFutureWarning, PyUserWarning}; use pyo3::ffi::c_str; use pyo3::prelude::*; @@ -18,8 +17,7 @@ use pyo3::types::PyFunction; use pyo3::types::{self, PyCFunction}; use pyo3_macros::pyclass; -#[path = "../src/tests/common.rs"] -mod common; +mod test_utils; #[pyfunction(name = "struct")] fn struct_function() {} @@ -669,7 +667,6 @@ impl UserDefinedWarning { } #[test] -#[cfg(not(Py_GIL_DISABLED))] // FIXME: enable once `warnings` is thread-safe fn test_pyfunction_warn() { #[pyfunction] #[pyo3(warn(message = "this function raises warning"))] @@ -724,7 +721,6 @@ fn test_pyfunction_warn() { } #[test] -#[cfg(not(Py_GIL_DISABLED))] // FIXME: enable once `warnings` is thread-safe fn test_pyfunction_multiple_warnings() { #[pyfunction] #[pyo3(warn(message = "this function raises warning"))] diff --git a/tests/test_pyself.rs b/tests/test_pyself.rs index 3f7d3844fce..b9b9b0ea4d2 100644 --- a/tests/test_pyself.rs +++ b/tests/test_pyself.rs @@ -5,8 +5,7 @@ use pyo3::prelude::*; use pyo3::types::{PyBytes, PyString}; use std::collections::HashMap; -#[path = "../src/tests/common.rs"] -mod common; +mod test_utils; /// Assumes it's a file reader or so. /// Inspired by https://github.com/jothan/cordoba, thanks. diff --git a/tests/test_sequence.rs b/tests/test_sequence.rs index ca1b15ae9a5..970f143503f 100644 --- a/tests/test_sequence.rs +++ b/tests/test_sequence.rs @@ -6,8 +6,7 @@ use pyo3::{ffi, prelude::*}; use pyo3::py_run; -#[path = "../src/tests/common.rs"] -mod common; +mod test_utils; #[pyclass] struct ByteSequence { diff --git a/tests/test_static_slots.rs b/tests/test_static_slots.rs index b11ecd08fb9..c00a8815270 100644 --- a/tests/test_static_slots.rs +++ b/tests/test_static_slots.rs @@ -6,8 +6,7 @@ use pyo3::types::IntoPyDict; use pyo3::py_run; -#[path = "../src/tests/common.rs"] -mod common; +mod test_utils; #[pyclass] struct Count5(); diff --git a/tests/test_string.rs b/tests/test_string.rs index 6ac6615d8a2..1648e067760 100644 --- a/tests/test_string.rs +++ b/tests/test_string.rs @@ -2,8 +2,7 @@ use pyo3::prelude::*; -#[path = "../src/tests/common.rs"] -mod common; +mod test_utils; #[pyfunction] fn take_str(_s: &str) {} diff --git a/tests/test_text_signature.rs b/tests/test_text_signature.rs index d1decc9bf3b..88057b05b6a 100644 --- a/tests/test_text_signature.rs +++ b/tests/test_text_signature.rs @@ -4,8 +4,7 @@ use pyo3::prelude::*; use pyo3::types::{PyDict, PyTuple}; use pyo3::{types::PyType, wrap_pymodule}; -#[path = "../src/tests/common.rs"] -mod common; +mod test_utils; #[test] fn class_without_docs_or_signature() { diff --git a/src/tests/common.rs b/tests/test_utils/mod.rs similarity index 75% rename from src/tests/common.rs rename to tests/test_utils/mod.rs index df5af81c247..523bc1ede6f 100644 --- a/src/tests/common.rs +++ b/tests/test_utils/mod.rs @@ -1,21 +1,28 @@ +// Common macros and helpers for tests +// +// This file is used in two different ways, which makes it a bit of a pain to build: +// - as a module `include!`-ed from `src/test_utils.rs` +// - as a module `mod test_utils` in various integration tests +// // the inner mod enables the #![allow(dead_code)] to -// be applied - `test_utils.rs` uses `include!` to pull in this file +// be applied - `src/test_utils.rs` uses `include!` to pull in this file -/// Common macros and helpers for tests -#[allow(dead_code)] // many tests do not use the complete set of functionality offered here +#[allow(dead_code, unused_macros)] // many tests do not use the complete set of functionality offered here #[allow(missing_docs)] // only used in tests #[macro_use] mod inner { - #[allow(unused_imports)] // pulls in `use crate as pyo3` in `test_utils.rs` + #[allow(unused_imports)] + // pulls in `use crate as pyo3` in `src/test_utils.rs`, no function in integration tests use super::*; - #[cfg(not(Py_GIL_DISABLED))] use pyo3::prelude::*; - #[cfg(not(Py_GIL_DISABLED))] + use pyo3::sync::MutexExt; use pyo3::types::{IntoPyDict, PyList}; + use std::sync::{Mutex, PoisonError}; + use uuid::Uuid; #[macro_export] @@ -76,31 +83,22 @@ mod inner { py_expect_warning!($py, *d, $code, [$(($warning_msg, $warning_category)),+]) }}; ($py:expr, *$dict:expr, $code:expr, [$(($warning_msg:literal, $warning_category:path)),+] $(,)?) => {{ - let code_lines: Vec<&str> = $code.lines().collect(); - let indented_code: String = code_lines.iter() - .map(|line| format!(" {}", line)) // add 4 spaces indentation - .collect::>() - .join("\n"); - - let wrapped_code = format!(r#" -import warnings -with warnings.catch_warnings(record=True) as warning_record: -{} -"#, indented_code); - - $py.run(&std::ffi::CString::new(wrapped_code).unwrap(), None, Some(&$dict.as_borrowed())).expect("Failed to run warning testing code"); - let expected_warnings = [$(($warning_msg, <$warning_category as pyo3::PyTypeInfo>::type_object($py))),+]; - let warning_record: Bound<'_, pyo3::types::PyList> = $dict.get_item("warning_record").expect("Failed to capture warnings").expect("Failed to downcast to PyList").extract().unwrap(); - - assert_eq!(warning_record.len(), expected_warnings.len(), "Expecting {} warnings but got {}", expected_warnings.len(), warning_record.len()); - - for ((index, warning), (msg, category)) in warning_record.iter().enumerate().zip(expected_warnings.iter()) { - let actual_msg = warning.getattr("message").unwrap().str().unwrap().to_string_lossy().to_string(); - let actual_category = warning.getattr("category").unwrap(); - - assert_eq!(actual_msg, msg.to_string(), "Warning message mismatch at index {}, expecting `{}` but got `{}`", index, msg, actual_msg); - assert!(actual_category.is(category), "Warning category mismatch at index {}, expecting {:?} but got {:?}", index, category, actual_category); - } + $crate::test_utils::CatchWarnings::enter($py, |warning_record| { + $py.run(&std::ffi::CString::new($code).unwrap(), None, Some(&$dict.as_borrowed())).expect("Failed to run warning testing code"); + let expected_warnings = [$(($warning_msg, <$warning_category as pyo3::PyTypeInfo>::type_object($py))),+]; + + assert_eq!(warning_record.len(), expected_warnings.len(), "Expecting {} warnings but got {}", expected_warnings.len(), warning_record.len()); + + for ((index, warning), (msg, category)) in warning_record.iter().enumerate().zip(expected_warnings.iter()) { + let actual_msg = warning.getattr("message").unwrap().str().unwrap().to_string_lossy().to_string(); + let actual_category = warning.getattr("category").unwrap(); + + assert_eq!(actual_msg, msg.to_string(), "Warning message mismatch at index {}, expecting `{}` but got `{}`", index, msg, actual_msg); + assert!(actual_category.is(category), "Warning category mismatch at index {}, expecting {:?} but got {:?}", index, category, actual_category); + } + + Ok(()) + }).expect("failed to test warnings"); }}; } @@ -166,17 +164,23 @@ with warnings.catch_warnings(record=True) as warning_record: } } - #[cfg(not(Py_GIL_DISABLED))] pub struct CatchWarnings<'py> { catch_warnings: Bound<'py, PyAny>, } - #[cfg(not(Py_GIL_DISABLED))] + /// catch_warnings is not thread-safe, so only one thread can be using this struct at + /// a time. + static CATCH_WARNINGS_MUTEX: Mutex<()> = Mutex::new(()); + impl<'py> CatchWarnings<'py> { pub fn enter( py: Python<'py>, f: impl FnOnce(&Bound<'py, PyList>) -> PyResult, ) -> PyResult { + // NB this is best-effort, other tests could always call the warnings API directly. + let _mutex_guard = CATCH_WARNINGS_MUTEX + .lock_py_attached(py) + .unwrap_or_else(PoisonError::into_inner); let warnings = py.import("warnings")?; let kwargs = [("record", true)].into_py_dict(py)?; let catch_warnings = warnings @@ -188,7 +192,6 @@ with warnings.catch_warnings(record=True) as warning_record: } } - #[cfg(not(Py_GIL_DISABLED))] impl Drop for CatchWarnings<'_> { fn drop(&mut self) { let py = self.catch_warnings.py(); @@ -198,11 +201,9 @@ with warnings.catch_warnings(record=True) as warning_record: } } - #[cfg(not(Py_GIL_DISABLED))] - #[macro_export] macro_rules! assert_warnings { ($py:expr, $body:expr, [$(($category:ty, $message:literal)),+] $(,)? ) => {{ - $crate::tests::common::CatchWarnings::enter($py, |w| { + $crate::test_utils::CatchWarnings::enter($py, |w| { use $crate::types::{PyListMethods, PyStringMethods}; $body; let expected_warnings = [$((<$category as $crate::type_object::PyTypeInfo>::type_object($py), $message)),+]; @@ -222,6 +223,8 @@ with warnings.catch_warnings(record=True) as warning_record: }}; } + pub(crate) use assert_warnings; + 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() diff --git a/tests/test_variable_arguments.rs b/tests/test_variable_arguments.rs index 2c7851914aa..d186e8def3b 100644 --- a/tests/test_variable_arguments.rs +++ b/tests/test_variable_arguments.rs @@ -3,8 +3,7 @@ use pyo3::prelude::*; use pyo3::types::{PyDict, PyTuple}; -#[path = "../src/tests/common.rs"] -mod common; +mod test_utils; #[pyclass] struct MyClass {} diff --git a/tests/test_various.rs b/tests/test_various.rs index c0ed072f18e..cf2ae82b5ff 100644 --- a/tests/test_various.rs +++ b/tests/test_various.rs @@ -6,8 +6,7 @@ use pyo3::types::PyTuple; use std::fmt; -#[path = "../src/tests/common.rs"] -mod common; +mod test_utils; #[pyclass] struct MutRefArg { From 50b019338c4b2268f5b16f43961df7ba25187afc Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sun, 24 Aug 2025 20:12:00 +0100 Subject: [PATCH 815/936] fix warning leaking out of test in `test_methods` (#5362) --- tests/test_methods.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_methods.rs b/tests/test_methods.rs index 7fbc1a138b6..64fe36592e0 100644 --- a/tests/test_methods.rs +++ b/tests/test_methods.rs @@ -10,6 +10,8 @@ use pyo3::types::{IntoPyDict, PyDict, PyList, PySet, PyString, PyTuple, PyType}; use pyo3::BoundObject; use pyo3_macros::pyclass; +use crate::test_utils::CatchWarnings; + mod test_utils; #[pyclass] @@ -1288,7 +1290,7 @@ fn test_pymethods_warn() { Python::attach(|py| { let typeobj = py.get_type::(); - let obj = typeobj.call0().unwrap(); + let obj = CatchWarnings::enter(py, |_| typeobj.call0()).unwrap(); // FnType::Fn py_expect_warning!( From a0c7d2cc7e97033691f122b436be3b4eb3a8e0b0 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sun, 24 Aug 2025 21:22:44 +0200 Subject: [PATCH 816/936] fixup `Py::cast_bound` (#5361) --- src/instance.rs | 87 ++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 68 insertions(+), 19 deletions(-) diff --git a/src/instance.rs b/src/instance.rs index f9347f2b2ca..5f81d5dc936 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -2075,9 +2075,56 @@ impl std::fmt::Debug for Py { pub type PyObject = Py; impl Py { - /// Deprecated version of [`PyObject::cast_bound`] + /// Downcast this `Py` to a concrete Python type or pyclass. + /// + /// Note that you can often avoid casting yourself by just specifying the desired type in + /// function or method signatures. However, manual casting is sometimes necessary. + /// + /// For extracting a Rust-only type, see [`Py::extract`]. + /// + /// # Example: Downcasting to a specific Python object + /// + /// ```rust + /// use pyo3::prelude::*; + /// use pyo3::types::{PyDict, PyList}; + /// + /// Python::attach(|py| { + /// let any = PyDict::new(py).into_any().unbind(); + /// + /// assert!(any.downcast_bound::(py).is_ok()); + /// assert!(any.downcast_bound::(py).is_err()); + /// }); + /// ``` + /// + /// # Example: Getting a reference to a pyclass + /// + /// This is useful if you want to mutate a `Py` that might actually be a pyclass. + /// + /// ```rust + /// # fn main() -> Result<(), pyo3::PyErr> { + /// use pyo3::prelude::*; + /// + /// #[pyclass] + /// struct Class { + /// i: i32, + /// } + /// + /// Python::attach(|py| { + /// let class = Py::new(py, Class { i: 0 })?.into_any(); + /// + /// let class_bound = class.downcast_bound::(py)?; + /// + /// class_bound.borrow_mut().i += 1; + /// + /// // Alternatively you can get a `PyRefMut` directly + /// let class_ref: PyRefMut<'_, Class> = class.extract(py)?; + /// assert_eq!(class_ref.i, 1); + /// Ok(()) + /// }) + /// # } + /// ``` + // FIXME(icxolu) deprecate in favor of `Py::cast_bound` #[inline] - #[deprecated(since = "0.26.0", note = "use `Py::cast_bound_unchecked` instead")] pub fn downcast_bound<'py, T>( &self, py: Python<'py>, @@ -2088,7 +2135,20 @@ impl Py { self.cast_bound(py) } - /// Cast this `Py` to a concrete Python type or pyclass. + /// Casts the `Py` to a concrete Python object type without checking validity. + /// + /// # Safety + /// + /// Callers must ensure that the type is valid or risk type confusion. + // FIXME(icxolu) deprecate in favor of `Py::cast_bound_unchecked` + #[inline] + pub unsafe fn downcast_bound_unchecked<'py, T>(&self, py: Python<'py>) -> &Bound<'py, T> { + unsafe { self.cast_bound_unchecked(py) } + } +} + +impl Py { + /// Cast this `Py` to a concrete Python type or pyclass. /// /// Note that you can often avoid casting yourself by just specifying the desired type in /// function or method signatures. However, manual casting is sometimes necessary. @@ -2136,34 +2196,23 @@ impl Py { /// }) /// # } /// ``` - pub fn cast_bound<'py, T>( + pub fn cast_bound<'py, U>( &self, py: Python<'py>, - ) -> Result<&Bound<'py, T>, DowncastError<'_, 'py>> + ) -> Result<&Bound<'py, U>, DowncastError<'_, 'py>> where - T: PyTypeCheck, + U: PyTypeCheck, { self.bind(py).cast() } - /// Casts the PyObject to a concrete Python object type without checking validity. - /// - /// # Safety - /// - /// Callers must ensure that the type is valid or risk type confusion. - #[inline] - #[deprecated(since = "0.26.0", note = "use `Py::cast_bound_unchecked` instead")] - pub unsafe fn downcast_bound_unchecked<'py, T>(&self, py: Python<'py>) -> &Bound<'py, T> { - unsafe { self.cast_bound_unchecked(py) } - } - - /// Casts the PyObject to a concrete Python object type without checking validity. + /// Casts the `Py` to a concrete Python object type without checking validity. /// /// # Safety /// /// Callers must ensure that the type is valid or risk type confusion. #[inline] - pub unsafe fn cast_bound_unchecked<'py, T>(&self, py: Python<'py>) -> &Bound<'py, T> { + pub unsafe fn cast_bound_unchecked<'py, U>(&self, py: Python<'py>) -> &Bound<'py, U> { unsafe { self.bind(py).cast_unchecked() } } } From 9aa53f4d7760a1036ab81597c463e6aeb710848f Mon Sep 17 00:00:00 2001 From: Thomas Tanon Date: Sun, 24 Aug 2025 21:22:54 +0200 Subject: [PATCH 817/936] Generate introspection for #[pyclass(eq, eq_int, ord, hash, str)] (#5338) * Generate introspection for #[pyclass(eq, ord, hash, str)] * Group arguments * Update pyo3-macros-backend/src/pyclass.rs Co-authored-by: David Hewitt --------- Co-authored-by: David Hewitt --- newsfragments/5338.added.md | 1 + pyo3-macros-backend/src/introspection.rs | 2 +- pyo3-macros-backend/src/pyclass.rs | 170 ++++++++++++++++++++--- pytests/src/comparisons.rs | 35 ++++- pytests/src/enums.rs | 23 +-- pytests/src/lib.rs | 4 +- pytests/stubs/comparisons.pyi | 13 ++ pytests/stubs/enums.pyi | 14 ++ pytests/tests/test_comparisons.py | 7 +- src/conversions/std/string.rs | 24 ++++ 10 files changed, 243 insertions(+), 50 deletions(-) create mode 100644 newsfragments/5338.added.md create mode 100644 pytests/stubs/enums.pyi diff --git a/newsfragments/5338.added.md b/newsfragments/5338.added.md new file mode 100644 index 00000000000..be50ef2489f --- /dev/null +++ b/newsfragments/5338.added.md @@ -0,0 +1 @@ +Introspection: support #[pyclass(eq, eq_int, ord, hash, str)] \ No newline at end of file diff --git a/pyo3-macros-backend/src/introspection.rs b/pyo3-macros-backend/src/introspection.rs index 4b40d9c6438..ace5163c5be 100644 --- a/pyo3-macros-backend/src/introspection.rs +++ b/pyo3-macros-backend/src/introspection.rs @@ -547,7 +547,7 @@ pub fn introspection_id_const() -> TokenStream { } } -fn unique_element_id() -> u64 { +pub fn unique_element_id() -> u64 { let mut hasher = DefaultHasher::new(); format!("{:?}", Span::call_site()).hash(&mut hasher); // Distinguishes between call sites GLOBAL_COUNTER_FOR_UNIQUE_NAMES diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 9d999fa86d9..53a1693db07 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -15,10 +15,15 @@ use crate::attributes::{ }; use crate::combine_errors::CombineErrors; #[cfg(feature = "experimental-inspect")] -use crate::introspection::{class_introspection_code, introspection_id_const}; +use crate::introspection::{ + class_introspection_code, function_introspection_code, introspection_id_const, + unique_element_id, +}; use crate::konst::{ConstAttributes, ConstSpec}; use crate::method::{FnArg, FnSpec, PyArg, RegularArg}; use crate::pyfunction::ConstructorAttribute; +#[cfg(feature = "experimental-inspect")] +use crate::pyfunction::FunctionSignature; use crate::pyimpl::{gen_py_const, get_cfg_attributes, PyClassMethodsType}; use crate::pymethod::{ impl_py_class_attribute, impl_py_getter_def, impl_py_setter_def, MethodAndMethodDef, @@ -859,7 +864,20 @@ fn implement_py_formatting( fmt_impl } }; - let fmt_slot = generate_protocol_slot(ty, &mut fmt_impl, &__STR__, "__str__", ctx).unwrap(); + let fmt_slot = generate_protocol_slot( + ty, + &mut fmt_impl, + &__STR__, + "__str__", + #[cfg(feature = "experimental-inspect")] + FunctionIntrospectionData { + names: &["__str__"], + arguments: Vec::new(), + returns: parse_quote! { ::std::string::String }, + }, + ctx, + ) + .unwrap(); (fmt_impl, fmt_slot) } @@ -935,8 +953,7 @@ fn impl_simple_enum( } } }; - let repr_slot = - generate_default_protocol_slot(&ty, &mut repr_impl, &__REPR__, ctx).unwrap(); + let repr_slot = generate_default_protocol_slot(&ty, &mut repr_impl, &__REPR__, ctx)?; (repr_impl, repr_slot) }; @@ -958,12 +975,18 @@ fn impl_simple_enum( } } }; - let int_slot = generate_default_protocol_slot(&ty, &mut int_impl, &__INT__, ctx).unwrap(); + let int_slot = generate_default_protocol_slot(&ty, &mut int_impl, &__INT__, ctx)?; (int_impl, int_slot) }; - let (default_richcmp, default_richcmp_slot) = - pyclass_richcmp_simple_enum(&args.options, &ty, repr_type, ctx)?; + let (default_richcmp, default_richcmp_slot) = pyclass_richcmp_simple_enum( + &args.options, + &ty, + repr_type, + #[cfg(feature = "experimental-inspect")] + &get_class_python_name(cls, args).to_string(), + ctx, + )?; let (default_hash, default_hash_slot) = pyclass_hash(&args.options, &ty, ctx)?; let mut default_slots = vec![default_repr_slot, default_int_slot]; @@ -1070,12 +1093,18 @@ fn impl_complex_enum( } } }); - + let output_type = if cfg!(feature = "experimental-inspect") { + let full_name = get_class_python_module_and_name(cls, &args); + quote! { const OUTPUT_TYPE: &'static str = #full_name; } + } else { + quote! {} + }; quote! { impl<'py> #pyo3_path::conversion::IntoPyObject<'py> for #cls { type Target = Self; type Output = #pyo3_path::Bound<'py, >::Target>; type Error = #pyo3_path::PyErr; + #output_type fn into_pyobject(self, py: #pyo3_path::Python<'py>) -> ::std::result::Result< ::Output, @@ -1462,20 +1491,59 @@ fn gen_complex_enum_variant_class_ident(enum_: &syn::Ident, variant: &syn::Ident format_ident!("{}_{}", enum_, variant) } +#[cfg(feature = "experimental-inspect")] +struct FunctionIntrospectionData<'a> { + names: &'a [&'a str], + arguments: Vec>, + returns: syn::Type, +} + fn generate_protocol_slot( cls: &syn::Type, method: &mut syn::ImplItemFn, slot: &SlotDef, name: &str, + #[cfg(feature = "experimental-inspect")] introspection_data: FunctionIntrospectionData<'_>, ctx: &Ctx, ) -> syn::Result { let spec = FnSpec::parse( &mut method.sig, &mut Vec::new(), PyFunctionOptions::default(), - ) - .unwrap(); - slot.generate_type_slot(&syn::parse_quote!(#cls), &spec, name, ctx) + )?; + #[cfg_attr(not(feature = "experimental-inspect"), allow(unused_mut))] + let mut def = slot.generate_type_slot(&syn::parse_quote!(#cls), &spec, name, ctx)?; + #[cfg(feature = "experimental-inspect")] + { + // We generate introspection data + let associated_method = def.associated_method; + let signature = FunctionSignature::from_arguments(introspection_data.arguments); + let returns = introspection_data.returns; + let introspection = introspection_data + .names + .iter() + .map(|name| { + function_introspection_code( + &ctx.pyo3_path, + None, + name, + &signature, + Some("self"), + parse_quote!(-> #returns), + [], + Some(cls), + ) + }) + .collect::>(); + let const_name = format_ident!("_{}", unique_element_id()); // We need an explicit name here + def.associated_method = quote! { + #associated_method + const #const_name: () = { + #(#introspection)* + }; + }; + } + Ok(def) } fn generate_default_protocol_slot( @@ -1488,8 +1556,7 @@ fn generate_default_protocol_slot( &mut method.sig, &mut Vec::new(), PyFunctionOptions::default(), - ) - .unwrap(); + )?; let name = spec.name.to_string(); slot.generate_type_slot( &syn::parse_quote!(#cls), @@ -1913,10 +1980,10 @@ fn pyclass_richcmp_simple_enum( options: &PyClassPyO3Options, cls: &syn::Type, repr_type: &syn::Ident, + #[cfg(feature = "experimental-inspect")] class_name: &str, ctx: &Ctx, ) -> Result<(Option, Option)> { 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."); } @@ -1967,9 +2034,36 @@ fn pyclass_richcmp_simple_enum( } }; let richcmp_slot = if options.eq.is_some() { - generate_protocol_slot(cls, &mut richcmp_impl, &__RICHCMP__, "__richcmp__", ctx).unwrap() + generate_protocol_slot( + cls, + &mut richcmp_impl, + &__RICHCMP__, + "__richcmp__", + #[cfg(feature = "experimental-inspect")] + FunctionIntrospectionData { + names: &["__eq__", "__ne__"], + arguments: vec![FnArg::Regular(RegularArg { + name: Cow::Owned(format_ident!("other")), + // we need to set a type, let's pick something small, it is overridden by annotation anyway + ty: &parse_quote!(!), + from_py_with: None, + default_value: None, + option_wrapped_type: None, + annotation: Some(match (options.eq.is_some(), options.eq_int.is_some()) { + (true, true) => { + format!("{class_name} | int") + } + (true, false) => class_name.into(), + (false, true) => "int".into(), + (false, false) => unreachable!(), + }), + })], + returns: parse_quote! { ::std::primitive::bool }, + }, + ctx, + )? } else { - generate_default_protocol_slot(cls, &mut richcmp_impl, &__RICHCMP__, ctx).unwrap() + generate_default_protocol_slot(cls, &mut richcmp_impl, &__RICHCMP__, ctx)? }; Ok((Some(richcmp_impl), Some(richcmp_slot))) } @@ -2004,9 +2098,30 @@ fn pyclass_richcmp( } } }; - let richcmp_slot = - generate_protocol_slot(cls, &mut richcmp_impl, &__RICHCMP__, "__richcmp__", ctx) - .unwrap(); + let richcmp_slot = generate_protocol_slot( + cls, + &mut richcmp_impl, + &__RICHCMP__, + "__richcmp__", + #[cfg(feature = "experimental-inspect")] + FunctionIntrospectionData { + names: if options.ord.is_some() { + &["__eq__", "__ne__", "__lt__", "__le__", "__gt__", "__ge__"] + } else { + &["__eq__", "__ne__"] + }, + arguments: vec![FnArg::Regular(RegularArg { + name: Cow::Owned(format_ident!("other")), + ty: &parse_quote!(&#cls), + from_py_with: None, + default_value: None, + option_wrapped_type: None, + annotation: None, + })], + returns: parse_quote! { ::std::primitive::bool }, + }, + ctx, + )?; Ok((Some(richcmp_impl), Some(richcmp_slot))) } else { Ok((None, None)) @@ -2033,8 +2148,19 @@ fn pyclass_hash( ::std::hash::Hasher::finish(&s) } }; - let hash_slot = - generate_protocol_slot(cls, &mut hash_impl, &__HASH__, "__hash__", ctx).unwrap(); + let hash_slot = generate_protocol_slot( + cls, + &mut hash_impl, + &__HASH__, + "__hash__", + #[cfg(feature = "experimental-inspect")] + FunctionIntrospectionData { + names: &["__hash__"], + arguments: Vec::new(), + returns: parse_quote! { ::std::primitive::u64 }, + }, + ctx, + )?; Ok((Some(hash_impl), Some(hash_slot))) } None => Ok((None, None)), @@ -2462,7 +2588,7 @@ impl<'a> PyClassImplsBuilder<'a> { let cls = self.cls; let Ctx { pyo3_path, .. } = ctx; - self.attr.options.freelist.as_ref().map_or(quote!{}, |freelist| { + self.attr.options.freelist.as_ref().map_or(quote! {}, |freelist| { let freelist = &freelist.value; quote! { impl #pyo3_path::impl_::pyclass::PyClassWithFreeList for #cls { diff --git a/pytests/src/comparisons.rs b/pytests/src/comparisons.rs index 9269ec96621..df28b423c3c 100644 --- a/pytests/src/comparisons.rs +++ b/pytests/src/comparisons.rs @@ -1,7 +1,8 @@ use pyo3::basic::CompareOp; use pyo3::prelude::*; +use std::fmt; -#[pyclass] +#[pyclass(frozen)] struct Eq(i64); #[pymethods] @@ -20,7 +21,7 @@ impl Eq { } } -#[pyclass] +#[pyclass(frozen)] struct EqDefaultNe(i64); #[pymethods] @@ -35,7 +36,7 @@ impl EqDefaultNe { } } -#[pyclass(eq)] +#[pyclass(eq, frozen)] #[derive(PartialEq, Eq)] struct EqDerived(i64); @@ -47,7 +48,7 @@ impl EqDerived { } } -#[pyclass] +#[pyclass(frozen)] struct Ordered(i64); #[pymethods] @@ -82,7 +83,7 @@ impl Ordered { } } -#[pyclass] +#[pyclass(frozen)] struct OrderedRichCmp(i64); #[pymethods] @@ -97,7 +98,25 @@ impl OrderedRichCmp { } } -#[pyclass] +#[pyclass(eq, ord, hash, str, frozen)] +#[derive(PartialEq, Eq, Ord, PartialOrd, Hash)] +struct OrderedDerived(i64); + +impl fmt::Display for OrderedDerived { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +#[pymethods] +impl OrderedDerived { + #[new] + fn new(value: i64) -> Self { + Self(value) + } +} + +#[pyclass(frozen)] struct OrderedDefaultNe(i64); #[pymethods] @@ -131,5 +150,7 @@ impl OrderedDefaultNe { #[pymodule(gil_used = false)] pub mod comparisons { #[pymodule_export] - use super::{Eq, EqDefaultNe, EqDerived, Ordered, OrderedDefaultNe, OrderedRichCmp}; + use super::{ + Eq, EqDefaultNe, EqDerived, Ordered, OrderedDefaultNe, OrderedDerived, OrderedRichCmp, + }; } diff --git a/pytests/src/enums.rs b/pytests/src/enums.rs index 8652321700a..02431fdd1e8 100644 --- a/pytests/src/enums.rs +++ b/pytests/src/enums.rs @@ -1,21 +1,12 @@ -use pyo3::{ - pyclass, pyfunction, pymodule, - types::{PyModule, PyModuleMethods}, - wrap_pyfunction, Bound, PyResult, -}; +use pyo3::{pyclass, pyfunction, pymodule}; #[pymodule(gil_used = false)] -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!(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(()) +pub mod enums { + #[pymodule_export] + use super::{ + do_complex_stuff, do_mixed_complex_stuff, do_simple_stuff, do_tuple_stuff, ComplexEnum, + MixedComplexEnum, SimpleEnum, SimpleTupleEnum, TupleEnum, + }; } #[pyclass(eq, eq_int)] diff --git a/pytests/src/lib.rs b/pytests/src/lib.rs index 0480847094e..76c07937eed 100644 --- a/pytests/src/lib.rs +++ b/pytests/src/lib.rs @@ -24,7 +24,8 @@ mod pyo3_pytests { #[pymodule_export] use { - comparisons::comparisons, consts::consts, pyclasses::pyclasses, pyfunctions::pyfunctions, + comparisons::comparisons, consts::consts, enums::enums, pyclasses::pyclasses, + pyfunctions::pyfunctions, }; // Inserting to sys.modules allows importing submodules nicely from Python @@ -37,7 +38,6 @@ mod pyo3_pytests { #[cfg(not(Py_LIMITED_API))] m.add_wrapped(wrap_pymodule!(datetime::datetime))?; m.add_wrapped(wrap_pymodule!(dict_iter::dict_iter))?; - m.add_wrapped(wrap_pymodule!(enums::enums))?; m.add_wrapped(wrap_pymodule!(misc::misc))?; m.add_wrapped(wrap_pymodule!(objstore::objstore))?; m.add_wrapped(wrap_pymodule!(othermod::othermod))?; diff --git a/pytests/stubs/comparisons.pyi b/pytests/stubs/comparisons.pyi index 7d4f9d6d31e..f5e44130057 100644 --- a/pytests/stubs/comparisons.pyi +++ b/pytests/stubs/comparisons.pyi @@ -8,6 +8,8 @@ class EqDefaultNe: def __new__(cls, /, value: int) -> None: ... class EqDerived: + def __eq__(self, /, other: EqDerived) -> bool: ... + def __ne__(self, /, other: EqDerived) -> bool: ... def __new__(cls, /, value: int) -> None: ... class Ordered: @@ -27,6 +29,17 @@ class OrderedDefaultNe: def __lt__(self, /, other: OrderedDefaultNe) -> bool: ... def __new__(cls, /, value: int) -> None: ... +class OrderedDerived: + def __eq__(self, /, other: OrderedDerived) -> bool: ... + def __ge__(self, /, other: OrderedDerived) -> bool: ... + def __gt__(self, /, other: OrderedDerived) -> bool: ... + def __hash__(self, /) -> int: ... + def __le__(self, /, other: OrderedDerived) -> bool: ... + def __lt__(self, /, other: OrderedDerived) -> bool: ... + def __ne__(self, /, other: OrderedDerived) -> bool: ... + def __new__(cls, /, value: int) -> None: ... + def __str__(self, /) -> str: ... + class OrderedRichCmp: def __eq__(self, /, other: OrderedRichCmp) -> bool: ... def __ge__(self, /, other: OrderedRichCmp) -> bool: ... diff --git a/pytests/stubs/enums.pyi b/pytests/stubs/enums.pyi new file mode 100644 index 00000000000..95eaabac451 --- /dev/null +++ b/pytests/stubs/enums.pyi @@ -0,0 +1,14 @@ +class ComplexEnum: ... +class MixedComplexEnum: ... + +class SimpleEnum: + def __eq__(self, /, other: SimpleEnum | int) -> bool: ... + def __ne__(self, /, other: SimpleEnum | int) -> bool: ... + +class SimpleTupleEnum: ... +class TupleEnum: ... + +def do_complex_stuff(thing: ComplexEnum) -> ComplexEnum: ... +def do_mixed_complex_stuff(thing: MixedComplexEnum) -> MixedComplexEnum: ... +def do_simple_stuff(thing: SimpleEnum) -> SimpleEnum: ... +def do_tuple_stuff(thing: TupleEnum) -> TupleEnum: ... diff --git a/pytests/tests/test_comparisons.py b/pytests/tests/test_comparisons.py index 7b836a60fda..2c889be4189 100644 --- a/pytests/tests/test_comparisons.py +++ b/pytests/tests/test_comparisons.py @@ -8,6 +8,7 @@ EqDerived, Ordered, OrderedDefaultNe, + OrderedDerived, OrderedRichCmp, ) from typing_extensions import Self @@ -134,9 +135,11 @@ def __ge__(self, other: Self) -> bool: @pytest.mark.parametrize( - "ty", (Ordered, OrderedRichCmp, PyOrdered), ids=("rust", "rust-richcmp", "python") + "ty", + (Ordered, OrderedDerived, OrderedRichCmp, PyOrdered), + ids=("rust", "rust-derived", "rust-richcmp", "python"), ) -def test_ordered(ty: Type[Union[Ordered, OrderedRichCmp, PyOrdered]]): +def test_ordered(ty: Type[Union[Ordered, OrderedDerived, OrderedRichCmp, PyOrdered]]): a = ty(0) b = ty(0) c = ty(1) diff --git a/src/conversions/std/string.rs b/src/conversions/std/string.rs index 402028d39be..e4c804facd2 100644 --- a/src/conversions/std/string.rs +++ b/src/conversions/std/string.rs @@ -14,6 +14,9 @@ impl<'py> IntoPyObject<'py> for &str { type Output = Bound<'py, Self::Target>; type Error = Infallible; + #[cfg(feature = "experimental-inspect")] + const OUTPUT_TYPE: &'static str = String::OUTPUT_TYPE; + #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { Ok(PyString::new(py, self)) @@ -30,6 +33,9 @@ impl<'py> IntoPyObject<'py> for &&str { type Output = Bound<'py, Self::Target>; type Error = Infallible; + #[cfg(feature = "experimental-inspect")] + const OUTPUT_TYPE: &'static str = String::OUTPUT_TYPE; + #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { (*self).into_pyobject(py) @@ -46,6 +52,9 @@ impl<'py> IntoPyObject<'py> for Cow<'_, str> { type Output = Bound<'py, Self::Target>; type Error = Infallible; + #[cfg(feature = "experimental-inspect")] + const OUTPUT_TYPE: &'static str = String::OUTPUT_TYPE; + #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { (*self).into_pyobject(py) @@ -62,6 +71,9 @@ impl<'py> IntoPyObject<'py> for &Cow<'_, str> { type Output = Bound<'py, Self::Target>; type Error = Infallible; + #[cfg(feature = "experimental-inspect")] + const OUTPUT_TYPE: &'static str = String::OUTPUT_TYPE; + #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { (&**self).into_pyobject(py) @@ -78,6 +90,9 @@ impl<'py> IntoPyObject<'py> for char { type Output = Bound<'py, Self::Target>; type Error = Infallible; + #[cfg(feature = "experimental-inspect")] + const OUTPUT_TYPE: &'static str = String::OUTPUT_TYPE; + fn into_pyobject(self, py: Python<'py>) -> Result { let mut bytes = [0u8; 4]; Ok(PyString::new(py, self.encode_utf8(&mut bytes))) @@ -94,6 +109,9 @@ impl<'py> IntoPyObject<'py> for &char { type Output = Bound<'py, Self::Target>; type Error = Infallible; + #[cfg(feature = "experimental-inspect")] + const OUTPUT_TYPE: &'static str = String::OUTPUT_TYPE; + #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { (*self).into_pyobject(py) @@ -110,6 +128,9 @@ impl<'py> IntoPyObject<'py> for String { type Output = Bound<'py, Self::Target>; type Error = Infallible; + #[cfg(feature = "experimental-inspect")] + const OUTPUT_TYPE: &'static str = "str"; + fn into_pyobject(self, py: Python<'py>) -> Result { Ok(PyString::new(py, &self)) } @@ -125,6 +146,9 @@ impl<'py> IntoPyObject<'py> for &String { type Output = Bound<'py, Self::Target>; type Error = Infallible; + #[cfg(feature = "experimental-inspect")] + const OUTPUT_TYPE: &'static str = String::OUTPUT_TYPE; + #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { Ok(PyString::new(py, self)) From 9cc0bb63b889f30d9aefa5b4642f8914b8932180 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Mon, 25 Aug 2025 18:13:12 +0100 Subject: [PATCH 818/936] fix "no constructor defined" messages on PyPy (#5329) * fix "cannot create '%s' instances" message to match CPython * don't use unlimited C API directly * review feedback * fixup * fixup xfail on PyPy --- pytests/tests/test_pyclasses.py | 4 +-- src/pyclass/create_type_object.rs | 41 ++++++++++--------------------- 2 files changed, 15 insertions(+), 30 deletions(-) diff --git a/pytests/tests/test_pyclasses.py b/pytests/tests/test_pyclasses.py index b89877af9c9..f30c1241736 100644 --- a/pytests/tests/test_pyclasses.py +++ b/pytests/tests/test_pyclasses.py @@ -96,8 +96,8 @@ def __new__(cls): @pytest.mark.xfail( - platform.python_implementation() == "PyPy" and sys.version_info < (3, 11), - reason="broken on older PyPy due to https://github.com/pypy/pypy/issues/5319 on supported PyPy", + platform.python_implementation() == "PyPy" and sys.version_info[:2] == (3, 11), + reason="broken on PyPy 3.11 due to https://github.com/pypy/pypy/issues/5319, waiting for next release", ) @pytest.mark.parametrize( "cls, exc_message", diff --git a/src/pyclass/create_type_object.rs b/src/pyclass/create_type_object.rs index b9bc1a7df96..599acd5c898 100644 --- a/src/pyclass/create_type_object.rs +++ b/src/pyclass/create_type_object.rs @@ -437,14 +437,13 @@ impl PyTypeBuilder { unsafe { self.push_slot(ffi::Py_tp_base, self.tp_base) } if !self.has_new { - // PyPy doesn't respect the flag - // https://github.com/pypy/pypy/issues/5318 - #[cfg(not(all(Py_3_10, not(PyPy))))] + // Flag introduced in 3.10, only worked in PyPy on 3.11 + #[cfg(not(any(all(Py_3_10, not(PyPy)), all(Py_3_11, PyPy))))] { // Safety: This is the correct slot type for Py_tp_new unsafe { self.push_slot(ffi::Py_tp_new, no_constructor_defined as *mut c_void) } } - #[cfg(all(Py_3_10, not(PyPy)))] + #[cfg(any(all(Py_3_10, not(PyPy)), all(Py_3_11, PyPy)))] { self.class_flags |= ffi::Py_TPFLAGS_DISALLOW_INSTANTIATION; } @@ -558,7 +557,7 @@ fn bpo_45315_workaround(py: Python<'_>, class_name: CString) { } /// Default new implementation -#[cfg(not(all(Py_3_10, not(PyPy))))] +#[cfg(not(any(all(Py_3_10, not(PyPy)), all(Py_3_11, PyPy))))] unsafe extern "C" fn no_constructor_defined( subtype: *mut ffi::PyTypeObject, _args: *mut ffi::PyObject, @@ -567,29 +566,15 @@ unsafe extern "C" fn no_constructor_defined( unsafe { trampoline(|py| { let tpobj = PyType::from_borrowed_type_ptr(py, subtype); - #[cfg(not(PyPy))] - { - // unlike `fully_qualified_name`, this always include the module - let module = tpobj - .module() - .map_or_else(|_| "".into(), |s| s.to_string()); - let qualname = tpobj.qualname(); - let qualname = qualname.map_or_else(|_| "".into(), |s| s.to_string()); - Err(crate::exceptions::PyTypeError::new_err(format!( - "cannot create '{module}.{qualname}' instances" - ))) - } - #[cfg(PyPy)] - { - // https://github.com/pypy/pypy/issues/5319 - // .qualname() seems wrong on PyPy, includes the module already - let full_name = tpobj - .qualname() - .map_or_else(|_| "".into(), |s| s.to_string()); - Err(crate::exceptions::PyTypeError::new_err(format!( - "cannot create '{full_name}' instances" - ))) - } + // unlike `fully_qualified_name`, this always include the module + let module = tpobj + .module() + .map_or_else(|_| "".into(), |s| s.to_string()); + let qualname = tpobj.qualname(); + let qualname = qualname.map_or_else(|_| "".into(), |s| s.to_string()); + Err(crate::exceptions::PyTypeError::new_err(format!( + "cannot create '{module}.{qualname}' instances" + ))) }) } } From acaa2be20a8da7dd5fcdff00bd9c2e1ebadb0788 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 25 Aug 2025 15:13:15 -0600 Subject: [PATCH 819/936] Update free-threading docs for 0.26 (#5355) * Update free-threading docs for Aug 2025 * fix grammar and links --- guide/src/free-threading.md | 55 ++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 32 deletions(-) diff --git a/guide/src/free-threading.md b/guide/src/free-threading.md index 8cff3b03384..920f2b69a35 100644 --- a/guide/src/free-threading.md +++ b/guide/src/free-threading.md @@ -8,12 +8,14 @@ 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. +new](https://docs.python.org/3/whatsnew/3.13.html#whatsnew313-free-threaded-cpython) +entry in the 3.13 release notes, the [free-threading HOWTO +guide](https://docs.python.org/3/howto/free-threading-extensions.html#freethreading-extensions-howto) +in the CPython docs, the [extension porting +guide](https://py-free-threading.github.io/porting-extensions/) in the +community-maintained Python free-threading guide, 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 @@ -122,7 +124,7 @@ free-threaded build. The free-threaded interpreter does not have a GIL. Many existing extensions providing mutable data structures relied on the GIL provided locking for operations on Python objects, to make interior mutability thread-safe. -Historically PyO3s API was designed around the same strong assumptions, but is +Historically, PyO3's API was designed around the same strong assumptions, but is transitioning towards more general APIs applicable for both builds. Working with PyO3 under the free-threaded interpreter requires some additional @@ -133,24 +135,14 @@ same way as in GIL-enabled builds using the `Python` token, but unlike in GIL-enabled builds it does not provide exclusive access anymore. Additionally it is also still neccessary to detach (via [`Python::detach`]) from the interpreter for possibly long running oprations that don't interact with the interpreter, -even though other Python threads are still able to run. Both operations are -explained in more details 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 -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 [`GILOnceCell`] is historical. - -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 +even though other Python threads are still able to run. + +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 analogous to releasing and acquiring the GIL in the GIL-enabled build. @@ -179,7 +171,7 @@ situations: 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. +* During a call to `os.fork()`, to ensure a process-wide consistent state. This is a non-exhaustive list and there may be other situations in future Python versions that can trigger global synchronization events. @@ -260,11 +252,11 @@ Traceback (most recent call last) 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 -the GIL. +We may allow user-selectable semantics for mutable pyclass definitions in a +future version of PyO3, 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 the GIL. ### Cannot build extensions using the limited API @@ -403,7 +395,6 @@ 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 From 1775bafe6eb00c66e099eb49b898e952ce73e453 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 25 Aug 2025 15:14:19 -0600 Subject: [PATCH 820/936] Use links to latest CPython stable release in docs (#5366) * Use links to latest CPython stable release in docs * fix link to CPython github --- Architecture.md | 2 +- guide/src/function.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Architecture.md b/Architecture.md index db7c8b4bb17..2f66598c7e3 100644 --- a/Architecture.md +++ b/Architecture.md @@ -37,7 +37,7 @@ 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/3.13/Include). +We aim to provide straight-forward Rust wrappers resembling the file structure of [`cpython/Include`](https://github.com/python/cpython/tree/main/Include). 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. diff --git a/guide/src/function.md b/guide/src/function.md index 1ecd436df63..b129480559c 100644 --- a/guide/src/function.md +++ b/guide/src/function.md @@ -95,9 +95,9 @@ The `#[pyo3]` attribute can be used to modify properties of the generated Python ``` - `#[pyo3(warn(message = "...", category = ...))]` - This option is used to display a warning when the function is used in Python. It is equivalent to [`warnings.warn(message, category)`](https://docs.python.org/3.13/library/warnings.html#warnings.warn). - The `message` parameter is a string that will be displayed when the function is called, and the `category` parameter is optional and has to be a subclass of [`Warning`](https://docs.python.org/3.12/library/exceptions.html#Warning). - When the `category` parameter is not provided, the warning will be defaulted to [`UserWarning`](https://docs.python.org/3.12/library/exceptions.html#UserWarning). + This option is used to display a warning when the function is used in Python. It is equivalent to [`warnings.warn(message, category)`](https://docs.python.org/3/library/warnings.html#warnings.warn). + The `message` parameter is a string that will be displayed when the function is called, and the `category` parameter is optional and has to be a subclass of [`Warning`](https://docs.python.org/3/library/exceptions.html#Warning). + When the `category` parameter is not provided, the warning will be defaulted to [`UserWarning`](https://docs.python.org/3/library/exceptions.html#UserWarning). > Note: when used with `#[pymethods]`, this attribute does not work with `#[classattr]` nor `__traverse__` magic method. From ed030a79c18f2127090f2b5390b6c068f28289df Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Thu, 28 Aug 2025 14:38:52 -0400 Subject: [PATCH 821/936] fixes #5202 -- treat the `PYO3_BUILD_EXTENSION_MODULE` env var the same as teh extension-module feature (#5343) --- newsfragments/5343.added.md | 1 + pyo3-build-config/src/impl_.rs | 5 +++++ pytests/Cargo.toml | 2 +- pytests/pyproject.toml | 2 +- 4 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 newsfragments/5343.added.md diff --git a/newsfragments/5343.added.md b/newsfragments/5343.added.md new file mode 100644 index 00000000000..358d3c8fbad --- /dev/null +++ b/newsfragments/5343.added.md @@ -0,0 +1 @@ +When building pyo3, setting the `PYO3_BUILD_EXTENSION_MODULE` causes the same effect as the `extension-module` feature. Eventually we expect maturin and setuptools-rust to set this environment variable automatically. Users with their own build systems will need to do the same. diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index aae49026a41..f0d37416b1e 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -862,9 +862,14 @@ pub fn get_abi3_version() -> Option { /// Checks if the `extension-module` feature is enabled for the PyO3 crate. /// +/// This can be triggered either by: +/// - The `extension-module` Cargo feature +/// - Setting the `PYO3_BUILD_EXTENSION_MODULE` environment variable +/// /// Must be called from a PyO3 crate build script. pub fn is_extension_module() -> bool { cargo_env_var("CARGO_FEATURE_EXTENSION_MODULE").is_some() + || env_var("PYO3_BUILD_EXTENSION_MODULE").is_some() } /// Checks if we need to link to `libpython` for the current build target. diff --git a/pytests/Cargo.toml b/pytests/Cargo.toml index b744c6e16cc..0b8dfe5d5ba 100644 --- a/pytests/Cargo.toml +++ b/pytests/Cargo.toml @@ -8,7 +8,7 @@ publish = false rust-version = "1.74" [dependencies] -pyo3 = { path = "../", features = ["extension-module", "experimental-inspect"] } +pyo3 = { path = "../", features = ["experimental-inspect"] } [build-dependencies] pyo3-build-config = { path = "../pyo3-build-config" } diff --git a/pytests/pyproject.toml b/pytests/pyproject.toml index 5f78a573124..12a5fdf1e91 100644 --- a/pytests/pyproject.toml +++ b/pytests/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["maturin>=1,<2"] +requires = ["maturin>=1.9.4,<2"] build-backend = "maturin" [tool.pytest.ini_options] From f4d533fb9a5ad1f5aa84c1f1e6abdd4b97ed6025 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Thu, 28 Aug 2025 12:40:05 -0600 Subject: [PATCH 822/936] Clarity and grammar fixes in free-threading guide (#5369) --- guide/src/free-threading.md | 24 ++++++++---------------- 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/guide/src/free-threading.md b/guide/src/free-threading.md index 920f2b69a35..317e413a1aa 100644 --- a/guide/src/free-threading.md +++ b/guide/src/free-threading.md @@ -122,20 +122,10 @@ free-threaded build. ## Special considerations for the free-threaded build The free-threaded interpreter does not have a GIL. Many existing extensions -providing mutable data structures relied on the GIL provided locking for -operations on Python objects, to make interior mutability thread-safe. -Historically, PyO3's API was designed around the same strong assumptions, but is -transitioning towards more general APIs applicable for both builds. - -Working with PyO3 under the free-threaded interpreter requires some additional -care and mental overhead compared with a GIL-enabled interpreter. Most notable -it is still neccessary to be attached (via [`Python::attach`]) to the Python -interpreter to perform any operation on Python objects. PyO3 models this the -same way as in GIL-enabled builds using the `Python` token, but unlike in -GIL-enabled builds it does not provide exclusive access anymore. Additionally it -is also still neccessary to detach (via [`Python::detach`]) from the interpreter -for possibly long running oprations that don't interact with the interpreter, -even though other Python threads are still able to run. +providing mutable data structures relied on the GIL to lock Python objects and +make interior mutability thread-safe. Historically, PyO3's API was designed +around the same strong assumptions, but is transitioning towards more general +APIs applicable for both builds. 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 @@ -153,14 +143,16 @@ 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. -You still need to obtain a `'py` lifetime is to interact with Python +### Attaching to the runtime + +You still need to obtain a `'py` lifetime 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::attach`] function. Threads created via the Python [`threading`] module do not need to 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 +### Detaching to avoid hangs and deadlocks The free-threaded build triggers global synchronization events in the following situations: From a43cf79bbfbe9edd4545dc915c40fdcea7b5b10b Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 29 Aug 2025 10:24:53 +0100 Subject: [PATCH 823/936] release: 0.26.0 (#5367) --- CHANGELOG.md | 81 ++++++++++++++++++- 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/4364.added.md | 1 - newsfragments/4523.added.md | 2 - newsfragments/4568.changed.md | 1 - newsfragments/4742.fixed.md | 1 - newsfragments/4777.changed.md | 1 - newsfragments/5017.added.md | 1 - newsfragments/5017.changed.md | 1 - newsfragments/5089.added.md | 1 - newsfragments/5144.changed.md | 1 - newsfragments/5146.changed.md | 1 - newsfragments/5152.packaging.md | 1 - newsfragments/5154.added.md | 3 - newsfragments/5154.removed.md | 1 - newsfragments/5159.fixed.md | 1 - newsfragments/5171.packaging.md | 1 - newsfragments/5192.packaging.md | 1 - newsfragments/5196.removed.md | 1 - newsfragments/5198.fixed.md | 1 - newsfragments/5201.changed.md | 1 - newsfragments/5206.changed.md | 1 - newsfragments/5207.added.md | 1 - newsfragments/5208.added.md | 1 - newsfragments/5209.changed.md | 1 - newsfragments/5210.changed.md | 1 - newsfragments/5217.added.md | 1 - newsfragments/5221.changed.md | 1 - newsfragments/5223.added.md | 1 - newsfragments/5223.changed.md | 1 - newsfragments/5233.added.md | 1 - newsfragments/5239.fixed.md | 1 - newsfragments/5241.added.md | 1 - newsfragments/5242.fixed.md | 1 - newsfragments/5247.changed.md | 1 - newsfragments/5251.fixed.md | 1 - newsfragments/5252.added.md | 1 - newsfragments/5255.added.md | 1 - newsfragments/5256.added.md | 1 - newsfragments/5258.added.md | 1 - newsfragments/5272.added.md | 2 - newsfragments/5273.fixed.md | 1 - newsfragments/5281.fixed.md | 1 - newsfragments/5285.changed.md | 1 - newsfragments/5286.changed.md | 1 - newsfragments/5289.added.md | 1 - newsfragments/5300.added.md | 1 - newsfragments/5317.added.md | 1 - newsfragments/5317.changed.md | 1 - newsfragments/5317.fixed.md | 1 - newsfragments/5317.removed.md | 1 - newsfragments/5318.fixed.md | 1 - newsfragments/5320.added.md | 1 - newsfragments/5324.changed.md | 1 - newsfragments/5325.changed.md | 1 - newsfragments/5337.changed.md | 1 - newsfragments/5338.added.md | 1 - newsfragments/5339.added.md | 1 - newsfragments/5340.changed.md | 1 - newsfragments/5342.added.md | 1 - newsfragments/5343.added.md | 1 - newsfragments/5351.added.md | 1 - newsfragments/5354.changed.md | 1 - pyo3-build-config/Cargo.toml | 2 +- pyo3-ffi/Cargo.toml | 4 +- pyo3-ffi/README.md | 4 +- pyo3-introspection/Cargo.toml | 2 +- pyo3-macros-backend/Cargo.toml | 6 +- pyo3-macros/Cargo.toml | 4 +- pyproject.toml | 2 +- tests/ui/reject_generics.stderr | 4 +- 77 files changed, 104 insertions(+), 92 deletions(-) delete mode 100644 newsfragments/4364.added.md delete mode 100644 newsfragments/4523.added.md delete mode 100644 newsfragments/4568.changed.md delete mode 100644 newsfragments/4742.fixed.md delete mode 100644 newsfragments/4777.changed.md delete mode 100644 newsfragments/5017.added.md delete mode 100644 newsfragments/5017.changed.md delete mode 100644 newsfragments/5089.added.md delete mode 100644 newsfragments/5144.changed.md delete mode 100644 newsfragments/5146.changed.md delete mode 100644 newsfragments/5152.packaging.md delete mode 100644 newsfragments/5154.added.md delete mode 100644 newsfragments/5154.removed.md delete mode 100644 newsfragments/5159.fixed.md delete mode 100644 newsfragments/5171.packaging.md delete mode 100644 newsfragments/5192.packaging.md delete mode 100644 newsfragments/5196.removed.md delete mode 100644 newsfragments/5198.fixed.md delete mode 100644 newsfragments/5201.changed.md delete mode 100644 newsfragments/5206.changed.md delete mode 100644 newsfragments/5207.added.md delete mode 100644 newsfragments/5208.added.md delete mode 100644 newsfragments/5209.changed.md delete mode 100644 newsfragments/5210.changed.md delete mode 100644 newsfragments/5217.added.md delete mode 100644 newsfragments/5221.changed.md delete mode 100644 newsfragments/5223.added.md delete mode 100644 newsfragments/5223.changed.md delete mode 100644 newsfragments/5233.added.md delete mode 100644 newsfragments/5239.fixed.md delete mode 100644 newsfragments/5241.added.md delete mode 100644 newsfragments/5242.fixed.md delete mode 100644 newsfragments/5247.changed.md delete mode 100644 newsfragments/5251.fixed.md delete mode 100644 newsfragments/5252.added.md delete mode 100644 newsfragments/5255.added.md delete mode 100644 newsfragments/5256.added.md delete mode 100644 newsfragments/5258.added.md delete mode 100644 newsfragments/5272.added.md delete mode 100644 newsfragments/5273.fixed.md delete mode 100644 newsfragments/5281.fixed.md delete mode 100644 newsfragments/5285.changed.md delete mode 100644 newsfragments/5286.changed.md delete mode 100644 newsfragments/5289.added.md delete mode 100644 newsfragments/5300.added.md delete mode 100644 newsfragments/5317.added.md delete mode 100644 newsfragments/5317.changed.md delete mode 100644 newsfragments/5317.fixed.md delete mode 100644 newsfragments/5317.removed.md delete mode 100644 newsfragments/5318.fixed.md delete mode 100644 newsfragments/5320.added.md delete mode 100644 newsfragments/5324.changed.md delete mode 100644 newsfragments/5325.changed.md delete mode 100644 newsfragments/5337.changed.md delete mode 100644 newsfragments/5338.added.md delete mode 100644 newsfragments/5339.added.md delete mode 100644 newsfragments/5340.changed.md delete mode 100644 newsfragments/5342.added.md delete mode 100644 newsfragments/5343.added.md delete mode 100644 newsfragments/5351.added.md delete mode 100644 newsfragments/5354.changed.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d7bc1ac680..d42b92916fb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,82 @@ To see unreleased changes, please see the [CHANGELOG on the main branch guide](h +## [0.26.0] - 2025-08-29 + +### Packaging + +- Bump hashbrown dependency to 0.15. [#5152](https://github.com/PyO3/pyo3/pull/5152) +- Update MSRV to 1.74. [#5171](https://github.com/PyO3/pyo3/pull/5171) +- Set the same maximum supported version for alternative interpreters as for CPython. [#5192](https://github.com/PyO3/pyo3/pull/5192) +- Add optional `bytes` dependency to add conversions for `bytes::Bytes`. [#5252](https://github.com/PyO3/pyo3/pull/5252) +- Publish new crate `pyo3-introspection` to pair with the `experimental-inspect` feature. [#5300](https://github.com/PyO3/pyo3/pull/5300) +- The `PYO3_BUILD_EXTENSION_MODULE` now causes the same effect as the `extension-module` feature. Eventually we expect maturin and setuptools-rust to set this environment variable automatically. Users with their own build systems will need to do the same. [#5343](https://github.com/PyO3/pyo3/pull/5343) + +### Added + +- Add `#[pyo3(warn(message = "...", category = ...))]` attribute for automatic warnings generation for `#[pyfunction]` and `#[pymethods]`. [#4364](https://github.com/PyO3/pyo3/pull/4364) +- Add `PyMutex`, available on Python 3.13 and newer. [#4523](https://github.com/PyO3/pyo3/pull/4523) +- Add FFI definition `PyMutex_IsLocked`, available on Python 3.14 and newer. [#4523](https://github.com/PyO3/pyo3/pull/4523) +- Add `PyString::from_encoded_object`. [#5017](https://github.com/PyO3/pyo3/pull/5017) +- `experimental-inspect`: add basic input type annotations. [#5089](https://github.com/PyO3/pyo3/pull/5089) +- Add FFI function definitions for `PyFrameObject` from CPython 3.13. [#5154](https://github.com/PyO3/pyo3/pull/5154) +- `experimental-inspect`: tag modules created using `#[pymodule]` or `#[pymodule_init]` functions as incomplete. [#5207](https://github.com/PyO3/pyo3/pull/5207) +- `experimental-inspect`: add basic return type support. [#5208](https://github.com/PyO3/pyo3/pull/5208) +- Add `PyCode::compile` and `PyCodeMethods::run` to create and execute code objects. [#5217](https://github.com/PyO3/pyo3/pull/5217) +- Add `PyOnceLock` type for thread-safe single-initialization. [#5223](https://github.com/PyO3/pyo3/pull/5223) +- Add `PyClassGuard(Mut)` pyclass holders. In the future they will replace `PyRef(Mut)`. [#5233](https://github.com/PyO3/pyo3/pull/5233) +- `experimental-inspect`: allow annotations in `#[pyo3(signature)]` signature attribute. [#5241](https://github.com/PyO3/pyo3/pull/5241) +- Implement `MutexExt` for parking_lot's/lock_api `ReentrantMutex`. [#5258](https://github.com/PyO3/pyo3/pull/5258) +- `experimental-inspect`: support class associated constants. [#5272](https://github.com/PyO3/pyo3/pull/5272) +- Add `Bound::cast` family of functions superseding the `PyAnyMethods::downcast` family. [#5289](https://github.com/PyO3/pyo3/pull/5289) +- Add FFI definitions `Py_Version` and `Py_IsFinalizing`. [#5317](https://github.com/PyO3/pyo3/pull/5317) +- `experimental-inspect`: add output type annotation for `#[pyclass]`. [#5320](https://github.com/PyO3/pyo3/pull/5320) +- `experimental-inspect`: support `#[pyclass(eq, eq_int, ord, hash, str)]`. [#5338](https://github.com/PyO3/pyo3/pull/5338) +- `experimental-inspect`: add basic support for `#[derive(FromPyObject)]` (no struct fields support yet). [#5339](https://github.com/PyO3/pyo3/pull/5339) +- Add `Python::try_attach`. [#5342](https://github.com/PyO3/pyo3/pull/5342) + +### Changed + +- Use `Py_TPFLAGS_DISALLOW_INSTANTIATION` instead of a `__new__` which always fails for a `#[pyclass]` without a `#[new]` on Python 3.10 and up. [#4568](https://github.com/PyO3/pyo3/pull/4568) +- `PyModule::from_code` now defaults `file_name` to `` if empty. [#4777](https://github.com/PyO3/pyo3/pull/4777) +- Deprecate `PyString::from_object` in favour of `PyString::from_encoded_object`. [#5017](https://github.com/PyO3/pyo3/pull/5017) +- When building with `abi3` for a Python version newer than pyo3 supports, automatically fall back to an abi3 build for the latest supported version. [#5144](https://github.com/PyO3/pyo3/pull/5144) +- Change `is_instance_of` trait bound from `PyTypeInfo` to `PyTypeCheck`. [#5146](https://github.com/PyO3/pyo3/pull/5146) +- Many PyO3 proc macros now report multiple errors instead of only the first one. [#5159](https://github.com/PyO3/pyo3/pull/5159) +- Change `MutexExt` return type to be an associated type. [#5201](https://github.com/PyO3/pyo3/pull/5201) +- Use `PyCallArgs` for `Py::call` and friends so they're equivalent to their `Bound` counterpart. [#5206](https://github.com/PyO3/pyo3/pull/5206) +- Rename `Python::with_gil` to `Python::attach`. [#5209](https://github.com/PyO3/pyo3/pull/5209) +- Rename `Python::allow_threads` to `Python::detach` [#5221](https://github.com/PyO3/pyo3/pull/5221) +- Deprecate `GILOnceCell` type in favour of `PyOnceLock`. [#5223](https://github.com/PyO3/pyo3/pull/5223) +- Rename `pyo3::prepare_freethreaded_python` to `Python::initialize`. [#5247](https://github.com/PyO3/pyo3/pull/5247) +- Convert `PyMemoryError` into/from `io::ErrorKind::OutOfMemory`. [#5256](https://github.com/PyO3/pyo3/pull/5256) +- Deprecate `GILProtected`. [#5285](https://github.com/PyO3/pyo3/pull/5285) +- Move `#[pyclass]` docstring formatting from import time to compile time. [#5286](https://github.com/PyO3/pyo3/pull/5286) +- `Python::attach` will now panic if the Python interpreter is in the process of shutting down. [#5317](https://github.com/PyO3/pyo3/pull/5317) +- Add fast-path to `PyTypeInfo::type_object` for `#[pyclass]` types. [#5324](https://github.com/PyO3/pyo3/pull/5324) +- Deprecate `PyObject` type alias for `Py`. [#5325](https://github.com/PyO3/pyo3/pull/5325) +- Rename `Python::with_gil_unchecked` to `Python::attach_unchecked`. [#5340](https://github.com/PyO3/pyo3/pull/5340) +- Rename `Python::assume_gil_acquired` to `Python::assume_attached`. [#5354](https://github.com/PyO3/pyo3/pull/5354) + +### Removed + +- Remove FFI definition of internals of `PyFrameObject`. [#5154](https://github.com/PyO3/pyo3/pull/5154) +- Remove `Eq` and `PartialEq` implementations on `PyGetSetDef` FFI definition. [#5196](https://github.com/PyO3/pyo3/pull/5196) +- Remove private FFI definitions `_Py_IsCoreInitialized` and `_Py_InitializeMain`. [#5317](https://github.com/PyO3/pyo3/pull/5317) + +### Fixed + +- Use critical section in `PyByteArray::to_vec` on freethreaded build to replicate GIL-enabled "soundness". [#4742](https://github.com/PyO3/pyo3/pull/4742) +- Fix precision loss when converting `bigdecimal` into Python. [#5198](https://github.com/PyO3/pyo3/pull/5198) +- Don't treat win7 target as a cross-compilation. [#5210](https://github.com/PyO3/pyo3/pull/5210) +- WASM targets no longer require exception handling support for Python < 3.14. [#5239](https://github.com/PyO3/pyo3/pull/5239) +- Fix segfault when dropping `PyBuffer` after the Python interpreter has been finalized. [#5242](https://github.com/PyO3/pyo3/pull/5242) +- `experimental-inspect`: better automated imports generation. [#5251](https://github.com/PyO3/pyo3/pull/5251) +- `experimental-inspect`: fix introspection of `__richcmp__`, `__concat__`, `__repeat__`, `__inplace_concat__` and `__inplace_repeat__`. [#5273](https://github.com/PyO3/pyo3/pull/5273) +- fixed a leaked borrow, when converting a mutable sub class into a frozen base class using `PyRef::into_super` [#5281](https://github.com/PyO3/pyo3/pull/5281) +- Fix FFI definition `Py_Exit` (never returns, was `()` return value, now `!`). [#5317](https://github.com/PyO3/pyo3/pull/5317) +- `experimental-inspect`: fix handling of module members gated behind `#[cfg(...)]` attributes. [#5318](https://github.com/PyO3/pyo3/pull/5318) + ## [0.25.1] - 2025-06-12 ### Packaging @@ -2220,8 +2296,9 @@ Yanked - Initial release -[Unreleased]: https://github.com/pyo3/pyo3/compare/v0.25.1...HEAD -[0.25.0]: https://github.com/pyo3/pyo3/compare/v0.25.0...v0.25.1 +[Unreleased]: https://github.com/pyo3/pyo3/compare/v0.26.0...HEAD +[0.26.0]: https://github.com/pyo3/pyo3/compare/v0.25.1...v0.26.0 +[0.25.1]: https://github.com/pyo3/pyo3/compare/v0.25.0...v0.25.1 [0.25.0]: https://github.com/pyo3/pyo3/compare/v0.24.2...v0.25.0 [0.24.2]: https://github.com/pyo3/pyo3/compare/v0.24.1...v0.24.2 [0.24.1]: https://github.com/pyo3/pyo3/compare/v0.24.0...v0.24.1 diff --git a/Cargo.toml b/Cargo.toml index ca912d91444..030174f62e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3" -version = "0.26.0-dev" +version = "0.26.0" description = "Bindings to Python interpreter" authors = ["PyO3 Project and Contributors "] readme = "README.md" @@ -30,10 +30,10 @@ memoffset = "0.9" once_cell = "1.21" # ffi bindings to the python interpreter, split into a separate crate so they can be used independently -pyo3-ffi = { path = "pyo3-ffi", version = "=0.25.1" } +pyo3-ffi = { path = "pyo3-ffi", version = "=0.26.0" } # support crates for macros feature -pyo3-macros = { path = "pyo3-macros", version = "=0.25.1", optional = true } +pyo3-macros = { path = "pyo3-macros", version = "=0.26.0", optional = true } indoc = { version = "2.0.1", optional = true } unindent = { version = "0.2.1", optional = true } @@ -85,7 +85,7 @@ uuid = { version = "1.10.0", features = ["v4"] } parking_lot = { version = "0.12.3", features = ["arc_lock"] } [build-dependencies] -pyo3-build-config = { path = "pyo3-build-config", version = "=0.25.1", features = ["resolve-config"] } +pyo3-build-config = { path = "pyo3-build-config", version = "=0.26.0", features = ["resolve-config"] } [features] default = ["macros"] diff --git a/README.md b/README.md index 72af4eee9ff..7508d851dbc 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ name = "string_sum" crate-type = ["cdylib"] [dependencies] -pyo3 = { version = "0.25.1", features = ["extension-module"] } +pyo3 = { version = "0.26.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.25.1" +version = "0.26.0" features = ["auto-initialize"] ``` diff --git a/examples/decorator/.template/pre-script.rhai b/examples/decorator/.template/pre-script.rhai index 110a6e82130..3abc923be1f 100644 --- a/examples/decorator/.template/pre-script.rhai +++ b/examples/decorator/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.25.1"); +variable::set("PYO3_VERSION", "0.26.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 110a6e82130..3abc923be1f 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.25.1"); +variable::set("PYO3_VERSION", "0.26.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 d21dae34ad8..27204393c47 100644 --- a/examples/plugin/.template/pre-script.rhai +++ b/examples/plugin/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.25.1"); +variable::set("PYO3_VERSION", "0.26.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 b27db6f2700..9f25f008fe6 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.25.1"); +variable::set("PYO3_VERSION", "0.26.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 110a6e82130..3abc923be1f 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.25.1"); +variable::set("PYO3_VERSION", "0.26.0"); file::rename(".template/Cargo.toml", "Cargo.toml"); file::rename(".template/pyproject.toml", "pyproject.toml"); file::delete(".template"); diff --git a/newsfragments/4364.added.md b/newsfragments/4364.added.md deleted file mode 100644 index 503cb7ae2c5..00000000000 --- a/newsfragments/4364.added.md +++ /dev/null @@ -1 +0,0 @@ -Added `#[pyo3(warn(message = "...", category = ...))]` attribute for automatic warnings generation for `#[pyfunction]` and `#[pymethods]`. \ No newline at end of file diff --git a/newsfragments/4523.added.md b/newsfragments/4523.added.md deleted file mode 100644 index a699bfc4956..00000000000 --- a/newsfragments/4523.added.md +++ /dev/null @@ -1,2 +0,0 @@ -* Added a Rust wrapper for `PyMutex`, available on Python 3.13 and newer. -* Added bindings for PyMutex_IsLocked, available on Python 3.14 and newer. \ No newline at end of file diff --git a/newsfragments/4568.changed.md b/newsfragments/4568.changed.md deleted file mode 100644 index bbc68355952..00000000000 --- a/newsfragments/4568.changed.md +++ /dev/null @@ -1 +0,0 @@ -Use `Py_TPFLAGS_DISALLOW_INSTANTIATION` instead of a `__new__` which always fails for a `#[pyclass]` without a `#[new]` on Python 3.10 and up. diff --git a/newsfragments/4742.fixed.md b/newsfragments/4742.fixed.md deleted file mode 100644 index 7bfe38b0c38..00000000000 --- a/newsfragments/4742.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Use critical section in `PyByteArray::to_vec` on freethreaded build to replicate GIL-enabled "soundness". diff --git a/newsfragments/4777.changed.md b/newsfragments/4777.changed.md deleted file mode 100644 index b4e2163a3b8..00000000000 --- a/newsfragments/4777.changed.md +++ /dev/null @@ -1 +0,0 @@ -`PyModule::from_code` now defaults `file_name` to `` if empty. Ref #4769 \ No newline at end of file diff --git a/newsfragments/5017.added.md b/newsfragments/5017.added.md deleted file mode 100644 index bf27988735e..00000000000 --- a/newsfragments/5017.added.md +++ /dev/null @@ -1 +0,0 @@ -Add `PyString::from_encoded_object`. diff --git a/newsfragments/5017.changed.md b/newsfragments/5017.changed.md deleted file mode 100644 index 9d5f477cff7..00000000000 --- a/newsfragments/5017.changed.md +++ /dev/null @@ -1 +0,0 @@ -Deprecate `PyString::from_object` in favour of `PyString::from_encoded_object`. diff --git a/newsfragments/5089.added.md b/newsfragments/5089.added.md deleted file mode 100644 index 1ce0d3cb893..00000000000 --- a/newsfragments/5089.added.md +++ /dev/null @@ -1 +0,0 @@ -Introspection: Adds very basic input type annotations (not all types are covered yet) \ No newline at end of file diff --git a/newsfragments/5144.changed.md b/newsfragments/5144.changed.md deleted file mode 100644 index 3812e70197a..00000000000 --- a/newsfragments/5144.changed.md +++ /dev/null @@ -1 +0,0 @@ -When building with `abi3` for a Python version newer than pyo3 supports, we now automatically fall back to an abi3 build for the latest version. diff --git a/newsfragments/5146.changed.md b/newsfragments/5146.changed.md deleted file mode 100644 index 72958216001..00000000000 --- a/newsfragments/5146.changed.md +++ /dev/null @@ -1 +0,0 @@ -Allow `is_instance_of` for types that only implement `PyTypeCheck` diff --git a/newsfragments/5152.packaging.md b/newsfragments/5152.packaging.md deleted file mode 100644 index 464c3081a33..00000000000 --- a/newsfragments/5152.packaging.md +++ /dev/null @@ -1 +0,0 @@ -Bump hashbrown to 0.15.0 \ No newline at end of file diff --git a/newsfragments/5154.added.md b/newsfragments/5154.added.md deleted file mode 100644 index 97147e17117..00000000000 --- a/newsfragments/5154.added.md +++ /dev/null @@ -1,3 +0,0 @@ -* added missing ffi functions for `PyFrameObject` but without - including unstable API from python 3.13 -* moved ffi functions to correct files to mach CPython as of 3.13 \ No newline at end of file diff --git a/newsfragments/5154.removed.md b/newsfragments/5154.removed.md deleted file mode 100644 index a625a742c0c..00000000000 --- a/newsfragments/5154.removed.md +++ /dev/null @@ -1 +0,0 @@ -* Removed access to internals of PyFrameObject \ No newline at end of file diff --git a/newsfragments/5159.fixed.md b/newsfragments/5159.fixed.md deleted file mode 100644 index 089de2c17d6..00000000000 --- a/newsfragments/5159.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Changed macro implementations to show multiple errors to users instead of only the first one. \ No newline at end of file diff --git a/newsfragments/5171.packaging.md b/newsfragments/5171.packaging.md deleted file mode 100644 index 7d54315d9a7..00000000000 --- a/newsfragments/5171.packaging.md +++ /dev/null @@ -1 +0,0 @@ -Update MSRV to 1.74. diff --git a/newsfragments/5192.packaging.md b/newsfragments/5192.packaging.md deleted file mode 100644 index 12fb9a3808c..00000000000 --- a/newsfragments/5192.packaging.md +++ /dev/null @@ -1 +0,0 @@ -Set the same maximum supported version for alternative interpreters as for CPython diff --git a/newsfragments/5196.removed.md b/newsfragments/5196.removed.md deleted file mode 100644 index 6784fb05709..00000000000 --- a/newsfragments/5196.removed.md +++ /dev/null @@ -1 +0,0 @@ -Remove `Eq` and `PartialEq` implementations on `PyGetSetDef` \ No newline at end of file diff --git a/newsfragments/5198.fixed.md b/newsfragments/5198.fixed.md deleted file mode 100644 index fb07972894b..00000000000 --- a/newsfragments/5198.fixed.md +++ /dev/null @@ -1 +0,0 @@ -fix precision loss when converting `bigdecimal` into python diff --git a/newsfragments/5201.changed.md b/newsfragments/5201.changed.md deleted file mode 100644 index a61a8ca992b..00000000000 --- a/newsfragments/5201.changed.md +++ /dev/null @@ -1 +0,0 @@ -Move `MutexExt` return type into associated type diff --git a/newsfragments/5206.changed.md b/newsfragments/5206.changed.md deleted file mode 100644 index dba13327867..00000000000 --- a/newsfragments/5206.changed.md +++ /dev/null @@ -1 +0,0 @@ -use `PyCallArgs` for `Py::call` and friends so they're equivalent to their `Bound` counterpart \ No newline at end of file diff --git a/newsfragments/5207.added.md b/newsfragments/5207.added.md deleted file mode 100644 index 23691f14bb5..00000000000 --- a/newsfragments/5207.added.md +++ /dev/null @@ -1 +0,0 @@ -Type stubs: tag modules created using `#[pymodule]` or `#[pymodule_init]` functions as incomplete \ No newline at end of file diff --git a/newsfragments/5208.added.md b/newsfragments/5208.added.md deleted file mode 100644 index 13f44a20a19..00000000000 --- a/newsfragments/5208.added.md +++ /dev/null @@ -1 +0,0 @@ -Introspection and sub generation: add basic return type support \ No newline at end of file diff --git a/newsfragments/5209.changed.md b/newsfragments/5209.changed.md deleted file mode 100644 index 17e097b2bc7..00000000000 --- a/newsfragments/5209.changed.md +++ /dev/null @@ -1 +0,0 @@ -rename `Python::with_gil` to `Python::attach` \ No newline at end of file diff --git a/newsfragments/5210.changed.md b/newsfragments/5210.changed.md deleted file mode 100644 index 166065fb197..00000000000 --- a/newsfragments/5210.changed.md +++ /dev/null @@ -1 +0,0 @@ -Don't necessarily treat win7 target as a cross-compilation diff --git a/newsfragments/5217.added.md b/newsfragments/5217.added.md deleted file mode 100644 index 2762b422a1b..00000000000 --- a/newsfragments/5217.added.md +++ /dev/null @@ -1 +0,0 @@ -add `PyCode::compile` and `PyCodeMethods::run` to create and execute code objects \ No newline at end of file diff --git a/newsfragments/5221.changed.md b/newsfragments/5221.changed.md deleted file mode 100644 index afc86a3dd12..00000000000 --- a/newsfragments/5221.changed.md +++ /dev/null @@ -1 +0,0 @@ -rename `Python::allow_threads` to `Python::detach` \ No newline at end of file diff --git a/newsfragments/5223.added.md b/newsfragments/5223.added.md deleted file mode 100644 index c8847b310d7..00000000000 --- a/newsfragments/5223.added.md +++ /dev/null @@ -1 +0,0 @@ -Add `PyOnceLock` type for thread-safe single-initialization. diff --git a/newsfragments/5223.changed.md b/newsfragments/5223.changed.md deleted file mode 100644 index 1c1ff1b2bf1..00000000000 --- a/newsfragments/5223.changed.md +++ /dev/null @@ -1 +0,0 @@ -Deprecate `GILOnceCell` type in favour of `PyOnceLock`. diff --git a/newsfragments/5233.added.md b/newsfragments/5233.added.md deleted file mode 100644 index 38ac4051890..00000000000 --- a/newsfragments/5233.added.md +++ /dev/null @@ -1 +0,0 @@ -added new `PyClassGuard(Mut)` pyclass holders. In the future they will replace `PyRef(Mut)` \ No newline at end of file diff --git a/newsfragments/5239.fixed.md b/newsfragments/5239.fixed.md deleted file mode 100644 index 5361be01b9f..00000000000 --- a/newsfragments/5239.fixed.md +++ /dev/null @@ -1 +0,0 @@ -WASM targets no longer require exception handling support for Python < 3.14. diff --git a/newsfragments/5241.added.md b/newsfragments/5241.added.md deleted file mode 100644 index ed70f89c963..00000000000 --- a/newsfragments/5241.added.md +++ /dev/null @@ -1 +0,0 @@ -Introspection: Allow to set annotations in PyO3 signature attribute \ No newline at end of file diff --git a/newsfragments/5242.fixed.md b/newsfragments/5242.fixed.md deleted file mode 100644 index 04210360e0c..00000000000 --- a/newsfragments/5242.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fix segfault when dropping `PyBuffer` after the Python interpreter has been finalized. diff --git a/newsfragments/5247.changed.md b/newsfragments/5247.changed.md deleted file mode 100644 index 0ab8eae0662..00000000000 --- a/newsfragments/5247.changed.md +++ /dev/null @@ -1 +0,0 @@ -rename `pyo3::prepare_freethreaded_python` to `Python::initialize` \ No newline at end of file diff --git a/newsfragments/5251.fixed.md b/newsfragments/5251.fixed.md deleted file mode 100644 index ba58d25440d..00000000000 --- a/newsfragments/5251.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Introspection: better automated imports generation \ No newline at end of file diff --git a/newsfragments/5252.added.md b/newsfragments/5252.added.md deleted file mode 100644 index 3d9362e6c05..00000000000 --- a/newsfragments/5252.added.md +++ /dev/null @@ -1 +0,0 @@ -Add bytes to/from python conversions. diff --git a/newsfragments/5255.added.md b/newsfragments/5255.added.md deleted file mode 100644 index d4dec2f63cf..00000000000 --- a/newsfragments/5255.added.md +++ /dev/null @@ -1 +0,0 @@ -Guide page on type stubs and introspection \ No newline at end of file diff --git a/newsfragments/5256.added.md b/newsfragments/5256.added.md deleted file mode 100644 index 189701405d1..00000000000 --- a/newsfragments/5256.added.md +++ /dev/null @@ -1 +0,0 @@ -Allow converting `PyMemoryError` into/from `io::ErrorKind::OutOfMemory` diff --git a/newsfragments/5258.added.md b/newsfragments/5258.added.md deleted file mode 100644 index 773a74ef134..00000000000 --- a/newsfragments/5258.added.md +++ /dev/null @@ -1 +0,0 @@ -Impl `MutexExt` for parking_lot's/lock_api `ReentrantMutex` diff --git a/newsfragments/5272.added.md b/newsfragments/5272.added.md deleted file mode 100644 index 5b43bad7dc1..00000000000 --- a/newsfragments/5272.added.md +++ /dev/null @@ -1,2 +0,0 @@ -Introspection: support of class associated constants -Introspection: consolidate constants representation into a more generic attribute concept \ No newline at end of file diff --git a/newsfragments/5273.fixed.md b/newsfragments/5273.fixed.md deleted file mode 100644 index bbe7f10ed06..00000000000 --- a/newsfragments/5273.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Introspection: Fixes introspection of `__richcmp__`, `__concat__`, `__repeat__`, `__inplace_concat__` and `__inplace_repeat__` \ No newline at end of file diff --git a/newsfragments/5281.fixed.md b/newsfragments/5281.fixed.md deleted file mode 100644 index aa6ae24cfd2..00000000000 --- a/newsfragments/5281.fixed.md +++ /dev/null @@ -1 +0,0 @@ -fixed a leaked borrow, when converting a mutable sub class into a frozen base class using `PyRef::into_super` \ No newline at end of file diff --git a/newsfragments/5285.changed.md b/newsfragments/5285.changed.md deleted file mode 100644 index 9d1523a72a7..00000000000 --- a/newsfragments/5285.changed.md +++ /dev/null @@ -1 +0,0 @@ -Deprecate `GILProtected`. diff --git a/newsfragments/5286.changed.md b/newsfragments/5286.changed.md deleted file mode 100644 index 1bb754dd0c1..00000000000 --- a/newsfragments/5286.changed.md +++ /dev/null @@ -1 +0,0 @@ -Move `#[pyclass]` docstring formatting from import time to compile time. diff --git a/newsfragments/5289.added.md b/newsfragments/5289.added.md deleted file mode 100644 index 5cdee26e1c9..00000000000 --- a/newsfragments/5289.added.md +++ /dev/null @@ -1 +0,0 @@ -added `Bound::cast` family of functions superseding the `PyAnyMethods::downcast` family \ No newline at end of file diff --git a/newsfragments/5300.added.md b/newsfragments/5300.added.md deleted file mode 100644 index ebcff684f17..00000000000 --- a/newsfragments/5300.added.md +++ /dev/null @@ -1 +0,0 @@ -Publish `pyo3-introspection` to crates.io \ No newline at end of file diff --git a/newsfragments/5317.added.md b/newsfragments/5317.added.md deleted file mode 100644 index b6a571de59c..00000000000 --- a/newsfragments/5317.added.md +++ /dev/null @@ -1 +0,0 @@ -Add FFI definitions `Py_Version` and `Py_IsFinalizing`. diff --git a/newsfragments/5317.changed.md b/newsfragments/5317.changed.md deleted file mode 100644 index 7d80ec4cfdd..00000000000 --- a/newsfragments/5317.changed.md +++ /dev/null @@ -1 +0,0 @@ -`Python::attach` will now panic if the Python interpreter is in the process of shutting down. diff --git a/newsfragments/5317.fixed.md b/newsfragments/5317.fixed.md deleted file mode 100644 index 0d0a752e338..00000000000 --- a/newsfragments/5317.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fix FFI definition `Py_Exit` (never returns, was `()` return value, now `!`). diff --git a/newsfragments/5317.removed.md b/newsfragments/5317.removed.md deleted file mode 100644 index 5269ccda918..00000000000 --- a/newsfragments/5317.removed.md +++ /dev/null @@ -1 +0,0 @@ -Remove private FFI definitions `_Py_IsCoreInitialized` and `_Py_InitializeMain` diff --git a/newsfragments/5318.fixed.md b/newsfragments/5318.fixed.md deleted file mode 100644 index 7d2a8770f3c..00000000000 --- a/newsfragments/5318.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Introspection: properly include module members gated behind `#[cfg(...)]` attributes \ No newline at end of file diff --git a/newsfragments/5320.added.md b/newsfragments/5320.added.md deleted file mode 100644 index bbea32386e3..00000000000 --- a/newsfragments/5320.added.md +++ /dev/null @@ -1 +0,0 @@ -Introspection: Properly generate output type annotation for #[pyclass] \ No newline at end of file diff --git a/newsfragments/5324.changed.md b/newsfragments/5324.changed.md deleted file mode 100644 index 11aed953100..00000000000 --- a/newsfragments/5324.changed.md +++ /dev/null @@ -1 +0,0 @@ -Add fast-path to `PyTypeInfo::type_object` for `#[pyclass]` types. diff --git a/newsfragments/5325.changed.md b/newsfragments/5325.changed.md deleted file mode 100644 index aba0a81cc50..00000000000 --- a/newsfragments/5325.changed.md +++ /dev/null @@ -1 +0,0 @@ -deprecated `PyObject` type alias for `Py` \ No newline at end of file diff --git a/newsfragments/5337.changed.md b/newsfragments/5337.changed.md deleted file mode 100644 index d773e1602f4..00000000000 --- a/newsfragments/5337.changed.md +++ /dev/null @@ -1 +0,0 @@ -Introspection: add compatibility with Goblin 0.10 \ No newline at end of file diff --git a/newsfragments/5338.added.md b/newsfragments/5338.added.md deleted file mode 100644 index be50ef2489f..00000000000 --- a/newsfragments/5338.added.md +++ /dev/null @@ -1 +0,0 @@ -Introspection: support #[pyclass(eq, eq_int, ord, hash, str)] \ No newline at end of file diff --git a/newsfragments/5339.added.md b/newsfragments/5339.added.md deleted file mode 100644 index a12a1da6e3a..00000000000 --- a/newsfragments/5339.added.md +++ /dev/null @@ -1 +0,0 @@ -Basic introspection of `#[derive(FromPyObject)]` (no struct fields support yet) \ No newline at end of file diff --git a/newsfragments/5340.changed.md b/newsfragments/5340.changed.md deleted file mode 100644 index ba669356a06..00000000000 --- a/newsfragments/5340.changed.md +++ /dev/null @@ -1 +0,0 @@ -rename `Python::with_gil_unchecked` to `Python::attach_unchecked` \ No newline at end of file diff --git a/newsfragments/5342.added.md b/newsfragments/5342.added.md deleted file mode 100644 index 8972d8d6fda..00000000000 --- a/newsfragments/5342.added.md +++ /dev/null @@ -1 +0,0 @@ -Add `Python::try_attach`. diff --git a/newsfragments/5343.added.md b/newsfragments/5343.added.md deleted file mode 100644 index 358d3c8fbad..00000000000 --- a/newsfragments/5343.added.md +++ /dev/null @@ -1 +0,0 @@ -When building pyo3, setting the `PYO3_BUILD_EXTENSION_MODULE` causes the same effect as the `extension-module` feature. Eventually we expect maturin and setuptools-rust to set this environment variable automatically. Users with their own build systems will need to do the same. diff --git a/newsfragments/5351.added.md b/newsfragments/5351.added.md deleted file mode 100644 index 9cefb8c1d19..00000000000 --- a/newsfragments/5351.added.md +++ /dev/null @@ -1 +0,0 @@ -Introspection: copy annotations in some generic impls \ No newline at end of file diff --git a/newsfragments/5354.changed.md b/newsfragments/5354.changed.md deleted file mode 100644 index 165f65c0567..00000000000 --- a/newsfragments/5354.changed.md +++ /dev/null @@ -1 +0,0 @@ -rename `Python::assume_gil_acquired` to `Python::assume_attached` \ No newline at end of file diff --git a/pyo3-build-config/Cargo.toml b/pyo3-build-config/Cargo.toml index 8315dae6747..719a42e7974 100644 --- a/pyo3-build-config/Cargo.toml +++ b/pyo3-build-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-build-config" -version = "0.25.1" +version = "0.26.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 3ce95cacf3d..78197c7c263 100644 --- a/pyo3-ffi/Cargo.toml +++ b/pyo3-ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-ffi" -version = "0.25.1" +version = "0.26.0" description = "Python-API bindings for the PyO3 ecosystem" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -44,7 +44,7 @@ generate-import-lib = ["pyo3-build-config/python3-dll-a"] paste = "1" [build-dependencies] -pyo3-build-config = { path = "../pyo3-build-config", version = "=0.25.1", features = ["resolve-config"] } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.26.0", features = ["resolve-config"] } [lints] workspace = true diff --git a/pyo3-ffi/README.md b/pyo3-ffi/README.md index 30623cc50f1..0f6bc8d9533 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.25.1" +version = "0.26.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.25.1" +pyo3_build_config = "0.26.0" ``` If you need to use conditional compilation based on Python version or how diff --git a/pyo3-introspection/Cargo.toml b/pyo3-introspection/Cargo.toml index 74d9edf24ae..0aa76535cc6 100644 --- a/pyo3-introspection/Cargo.toml +++ b/pyo3-introspection/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-introspection" -version = "0.25.1" +version = "0.26.0" description = "Introspect dynamic libraries built with PyO3 to get metadata about the exported Python types" authors = ["PyO3 Project and Contributors "] homepage = "https://github.com/pyo3/pyo3" diff --git a/pyo3-macros-backend/Cargo.toml b/pyo3-macros-backend/Cargo.toml index a85caac82f7..02a2c39f238 100644 --- a/pyo3-macros-backend/Cargo.toml +++ b/pyo3-macros-backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-macros-backend" -version = "0.25.1" +version = "0.26.0" description = "Code generation for PyO3 package" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -17,7 +17,7 @@ rust-version.workspace = true [dependencies] heck = "0.5" proc-macro2 = { version = "1.0.60", default-features = false } -pyo3-build-config = { path = "../pyo3-build-config", version = "=0.25.1", features = ["resolve-config"] } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.26.0", features = ["resolve-config"] } quote = { version = "1", default-features = false } [dependencies.syn] @@ -27,7 +27,7 @@ default-features = false features = ["derive", "parsing", "printing", "clone-impls", "full", "extra-traits", "visit-mut"] [build-dependencies] -pyo3-build-config = { path = "../pyo3-build-config", version = "=0.25.1" } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.26.0" } [lints] workspace = true diff --git a/pyo3-macros/Cargo.toml b/pyo3-macros/Cargo.toml index 0e1d6d725c3..8b7f19634b0 100644 --- a/pyo3-macros/Cargo.toml +++ b/pyo3-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-macros" -version = "0.25.1" +version = "0.26.0" description = "Proc macros for PyO3 package" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -23,7 +23,7 @@ experimental-inspect = ["pyo3-macros-backend/experimental-inspect"] 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.25.1" } +pyo3-macros-backend = { path = "../pyo3-macros-backend", version = "=0.26.0" } [lints] workspace = true diff --git a/pyproject.toml b/pyproject.toml index 48316a920a5..5dc682ea044 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ [tool.towncrier] filename = "CHANGELOG.md" -version = "0.25.1" +version = "0.26.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 02db8433838..5c759ac8a20 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.25.1/class.html#no-generic-parameters +error: #[pyclass] cannot have generic parameters. For an explanation, see https://pyo3.rs/v0.26.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.25.1/class.html#no-lifetime-parameters +error: #[pyclass] cannot have lifetime parameters. For an explanation, see https://pyo3.rs/v0.26.0/class.html#no-lifetime-parameters --> tests/ui/reject_generics.rs:9:27 | 9 | struct ClassWithLifetimes<'a> { From 4502c2e786b53e16ded3b8afbe46202f6707e46c Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 29 Aug 2025 13:43:50 +0100 Subject: [PATCH 824/936] ci: fix release job token permissions (#5374) --- .github/workflows/release.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9874c528109..6b7ae43811a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,6 +7,9 @@ on: jobs: release: + permissions: + id-token: write + runs-on: ubuntu-latest environment: release steps: From 21858b936358d979def9fa68d8d7bc7262bdf290 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 29 Aug 2025 17:52:28 +0100 Subject: [PATCH 825/936] ci: properly invoke nox via uv (#5377) --- .github/workflows/release.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6b7ae43811a..0d199eb7b58 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,6 +4,10 @@ on: push: tags: - "v*" + workflow_dispatch: + inputs: + version: + description: The version to build jobs: release: @@ -13,7 +17,11 @@ jobs: runs-on: ubuntu-latest environment: release steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + # The tag to build or the tag received by the tag event + ref: ${{ github.event.inputs.version || github.ref }} + persist-credentials: false - uses: astral-sh/setup-uv@v6 @@ -21,6 +29,6 @@ jobs: id: auth - name: Publish to crates.io - run: nox -s publish + run: uvx nox -s publish env: CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} From 3ae1919ec65a6720fd93370311787da0a4b4cbbd Mon Sep 17 00:00:00 2001 From: Kyle Altendorf Date: Fri, 29 Aug 2025 12:04:08 -0400 Subject: [PATCH 826/936] ci: long-lines format, maybe avoid odd rendering in PR template (#5376) * semantic newlines, maybe avoid odd rendering * Simplify CI instructions in pull request template * Update .github/pull_request_template.md Co-authored-by: David Hewitt --------- Co-authored-by: David Hewitt --- .github/pull_request_template.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index d2006f366f3..2156c1d7322 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -9,6 +9,4 @@ 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, 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. +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. From aed320b32006ff85d894cf7bd220c2a4d11bc57f Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 29 Aug 2025 21:53:55 +0100 Subject: [PATCH 827/936] ci: tidy up some testing on 3.14t (#5378) --- pytests/pyproject.toml | 5 ++--- pytests/tests/test_pyclasses.py | 9 ++++----- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/pytests/pyproject.toml b/pytests/pyproject.toml index 12a5fdf1e91..ff4f84832f3 100644 --- a/pytests/pyproject.toml +++ b/pytests/pyproject.toml @@ -21,9 +21,8 @@ classifiers = [ [project.optional-dependencies] dev = [ "hypothesis>=3.55", - "pytest-asyncio>=0.21", + "pytest-asyncio>=1.0.0", "pytest-benchmark>=3.4", - # pinned < 8.1 because https://github.com/CodSpeedHQ/pytest-codspeed/issues/27 - "pytest>=7,<8.1", + "pytest>=7", "typing_extensions>=4.0.0" ] diff --git a/pytests/tests/test_pyclasses.py b/pytests/tests/test_pyclasses.py index f30c1241736..04430736733 100644 --- a/pytests/tests/test_pyclasses.py +++ b/pytests/tests/test_pyclasses.py @@ -64,16 +64,15 @@ def test_parallel_iter(): 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] + # should never reach 100 iterations, should error out as soon + # as the borrow error occurs + for _ in tpe.map(lambda _: next(i), range(100)): + pass class AssertingSubClass(pyclasses.AssertingBaseClass): From c6dba9a3f2a340d0ad96107c20d8b80ed63327f2 Mon Sep 17 00:00:00 2001 From: Joel Natividad <1980690+jqnatividad@users.noreply.github.com> Date: Sun, 31 Aug 2025 07:29:52 -0400 Subject: [PATCH 828/936] docs: Fix typos (#5380) * docs: fix typos in markdown files * docs: fix typos in comments * chore: fix typos in "code" * chore: fix changelog typos even for old entries * chore: fix one more changelog typo --- CHANGELOG.md | 20 ++++++++++---------- Contributing.md | 4 ++-- guide/src/class.md | 2 +- guide/src/free-threading.md | 2 +- guide/src/migration.md | 4 ++-- pyo3-build-config/src/impl_.rs | 2 +- pyo3-build-config/src/lib.rs | 6 +++--- pyo3-ffi/src/memoryobject.rs | 2 +- pyo3-macros-backend/src/attributes.rs | 2 +- pyo3-macros-backend/src/method.rs | 2 +- pyo3-macros-backend/src/pymethod.rs | 2 +- src/buffer.rs | 2 +- src/call.rs | 2 +- src/conversions/std/num.rs | 2 +- src/err/mod.rs | 2 +- src/impl_/pyclass.rs | 2 +- src/lib.rs | 2 +- src/pyclass/guard.rs | 6 +++--- src/types/capsule.rs | 2 +- src/types/set.rs | 2 +- src/types/weakref/anyref.rs | 26 +++++++++++++------------- src/types/weakref/reference.rs | 2 +- tests/test_macros.rs | 2 +- 23 files changed, 50 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d42b92916fb..764441bd458 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -190,7 +190,7 @@ To see unreleased changes, please see the [CHANGELOG on the main branch guide](h - 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 `PyString::from_object` causing of bounds reads with `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 @@ -523,7 +523,7 @@ Re-release of 0.23.0 with fixes to docs.rs build. - 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) +- Add `as_super` methods to `PyRef` and `PyRefMut` for accessing 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) @@ -542,7 +542,7 @@ Re-release of 0.23.0 with fixes to docs.rs build. - 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) +- Deprecate implicit integer comparison 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) @@ -734,7 +734,7 @@ Prerelease of PyO3 0.21. See [the GitHub diff](https://github.com/pyo3/pyo3/comp ### Changed - Change `PySet::discard` to return `PyResult` (previously returned nothing). [#3281](https://github.com/PyO3/pyo3/pull/3281) -- Optimize implmentation of `IntoPy` for Rust tuples to Python tuples. [#3321](https://github.com/PyO3/pyo3/pull/3321) +- Optimize implementation of `IntoPy` for Rust tuples to Python tuples. [#3321](https://github.com/PyO3/pyo3/pull/3321) - Change `PyDict::get_item` to no longer suppress arbitrary exceptions (the return type is now `PyResult>` instead of `Option<&PyAny>`), and deprecate `PyDict::get_item_with_error`. [#3330](https://github.com/PyO3/pyo3/pull/3330) - Deprecate FFI definitions which are deprecated in Python 3.12. [#3336](https://github.com/PyO3/pyo3/pull/3336) - `AsPyPointer` is now an `unsafe trait`. [#3358](https://github.com/PyO3/pyo3/pull/3358) @@ -841,7 +841,7 @@ Prerelease of PyO3 0.21. See [the GitHub diff](https://github.com/pyo3/pyo3/comp - Add `#[pyo3(from_item_all)]` when deriving `FromPyObject` to specify `get_item` as getter for all fields. [#3120](https://github.com/PyO3/pyo3/pull/3120) - Add `pyo3::exceptions::PyBaseExceptionGroup` for Python 3.11, and corresponding FFI definition `PyExc_BaseExceptionGroup`. [#3141](https://github.com/PyO3/pyo3/pull/3141) - Accept `#[new]` with `#[classmethod]` to create a constructor which receives a (subtype's) class/`PyType` as its first argument. [#3157](https://github.com/PyO3/pyo3/pull/3157) -- Add `PyClass::get` and `Py::get` for GIL-indepedent access to classes with `#[pyclass(frozen)]`. [#3158](https://github.com/PyO3/pyo3/pull/3158) +- Add `PyClass::get` and `Py::get` for GIL-independent access to classes with `#[pyclass(frozen)]`. [#3158](https://github.com/PyO3/pyo3/pull/3158) - Add `PyAny::is_exact_instance` and `PyAny::is_exact_instance_of`. [#3161](https://github.com/PyO3/pyo3/pull/3161) ### Changed @@ -1799,8 +1799,8 @@ Prerelease of PyO3 0.21. See [the GitHub diff](https://github.com/pyo3/pyo3/comp - Change return type of `PyTuple::slice` and `PyTuple::split_from` from `Py` to `&PyTuple`. [#970](https://github.com/PyO3/pyo3/pull/970) - Change return type of `PyTuple::as_slice` to `&[&PyAny]`. [#971](https://github.com/PyO3/pyo3/pull/971) - Rename `PyTypeInfo::type_object` to `type_object_raw`, and add `Python` argument. [#975](https://github.com/PyO3/pyo3/pull/975) -- Update `num-complex` optional dependendency from `0.2` to `0.3`. [#977](https://github.com/PyO3/pyo3/pull/977) -- Update `num-bigint` optional dependendency from `0.2` to `0.3`. [#978](https://github.com/PyO3/pyo3/pull/978) +- Update `num-complex` optional dependency from `0.2` to `0.3`. [#977](https://github.com/PyO3/pyo3/pull/977) +- Update `num-bigint` optional dependency from `0.2` to `0.3`. [#978](https://github.com/PyO3/pyo3/pull/978) - `#[pyproto]` is re-implemented without specialization. [#961](https://github.com/PyO3/pyo3/pull/961) - `PyClassAlloc::alloc` is renamed to `PyClassAlloc::new`. [#990](https://github.com/PyO3/pyo3/pull/990) - `#[pyproto]` methods can now have return value `T` or `PyResult` (previously only `PyResult` was supported). [#996](https://github.com/PyO3/pyo3/pull/996) @@ -2133,7 +2133,7 @@ Yanked - `IntoPyDictPointer` was replace by `IntoPyDict` which doesn't convert `PyDict` itself anymore and returns a `PyDict` instead of `*mut PyObject`. - `PyTuple::new` now takes an `IntoIterator` instead of a slice - Updated to syn 0.15 -- Splitted `PyTypeObject` into `PyTypeObject` without the create method and `PyTypeCreate` with requires `PyObjectAlloc + PyTypeInfo + Sized`. +- Split `PyTypeObject` into `PyTypeObject` without the create method and `PyTypeCreate` with requires `PyObjectAlloc + PyTypeInfo + Sized`. - Ran `cargo edition --fix` which prefixed path with `crate::` for rust 2018 - Renamed `async` to `pyasync` as async will be a keyword in the 2018 edition. - Starting to use `NonNull<*mut PyObject>` for Py and PyObject by ijl [#260](https://github.com/PyO3/pyo3/pull/260) @@ -2206,7 +2206,7 @@ Yanked - `proc_macro` has been stabilized on nightly ([rust-lang/rust#52081](https://github.com/rust-lang/rust/pull/52081)). This means that we can remove the `proc_macro` feature, but now we need the `use_extern_macros` from the 2018 edition instead. - All proc macro are now prefixed with `py` and live in the prelude. This means you can use `#[pyclass]`, `#[pymethods]`, `#[pyproto]`, `#[pyfunction]` and `#[pymodinit]` directly, at least after a `use pyo3::prelude::*`. They were also moved into a module called `proc_macro`. You shouldn't use `#[pyo3::proc_macro::pyclass]` or other longer paths in attributes because `proc_macro_path_invoc` isn't going to be stabilized soon. - Renamed the `base` option in the `pyclass` macro to `extends`. -- `#[pymodinit]` uses the function name as module name, unless the name is overrriden with `#[pymodinit(name)]` +- `#[pymodinit]` uses the function name as module name, unless the name is overridden with `#[pymodinit(name)]` - The guide is now properly versioned. ## [0.2.7] - 2018-05-18 @@ -2280,7 +2280,7 @@ Yanked - Added inheritance support #15 - Added weakref support #56 - Added subclass support #64 -- Added `self.__dict__` supoort #68 +- Added `self.__dict__` support #68 - Added `pyo3::prelude` module #70 - Better `Iterator` support for PyTuple, PyList, PyDict #75 - Introduce IntoPyDictPointer similar to IntoPyTuple #69 diff --git a/Contributing.md b/Contributing.md index 40fcb1af030..63d51a6c697 100644 --- a/Contributing.md +++ b/Contributing.md @@ -183,8 +183,8 @@ PyO3 supports all officially supported Python versions, as well as the latest Py If you plan to add support for a pre-release version of CPython, here's a (non-exhaustive) checklist: - - [ ] Wait until the last alpha release (usually alpha7), since ABI is not guranteed until the first beta release - - [ ] Add prelease_ver-dev (e.g. `3.14-dev`) to `‎.github/workflows/ci.yml`, and bump version in `noxfile.py`, `pyo3-ffi/Cargo.toml` under `max-version` within `[package.metadata.cpython]`, and `max` within `pyo3-ffi/build.rs` + - [ ] Wait until the last alpha release (usually alpha7), since ABI is not guaranteed until the first beta release + - [ ] Add prerelease_ver-dev (e.g. `3.14-dev`) to `‎.github/workflows/ci.yml`, and bump version in `noxfile.py`, `pyo3-ffi/Cargo.toml` under `max-version` within `[package.metadata.cpython]`, and `max` within `pyo3-ffi/build.rs` - [ ] Add a new abi3-prerelease feature for the version (e.g. `abi3-py314`) - In `pyo3-build-config/Cargo.toml`, set abi3-most_current_stable to ["abi3-prerelease"] and abi3-prerelease to ["abi3"] - In `pyo3-ffi/Cargo.toml`, set abi3-most_current_stable to ["abi3-prerelease", "pyo3-build-config/abi3-most_current_stable"] and abi3-prerelease to ["abi3", "pyo3-build-config/abi3-prerelease"] diff --git a/guide/src/class.md b/guide/src/class.md index 8eb71669d02..6e90f16007b 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -879,7 +879,7 @@ struct MyClass { } #[pyfunction] -fn dissamble_clone(my_class: MyClass) { +fn disassemble_clone(my_class: MyClass) { let MyClass { mut my_field } = my_class; *my_field += 1; } diff --git a/guide/src/free-threading.md b/guide/src/free-threading.md index 317e413a1aa..96146eb11f1 100644 --- a/guide/src/free-threading.md +++ b/guide/src/free-threading.md @@ -272,7 +272,7 @@ the free-threaded build. To initialize data exactly once, use the [`PyOnceLock`] type, which is a close equivalent to [`std::sync::OnceLock`][`OnceLock`] that also helps avoid deadlocks by detaching from the Python interpreter when threads are blocking waiting for another thread to -complete intialization. If already using [`OnceLock`] and it is impractical +complete initialization. If already using [`OnceLock`] and it is impractical to replace with a [`PyOnceLock`], there is the [`OnceLockExt`] extension trait which adds [`OnceLockExt::get_or_init_py_attached`] to detach from the interpreter when blocking in the same fashion as [`PyOnceLock`]. Here is an example using diff --git a/guide/src/migration.md b/guide/src/migration.md index acb7a5c7c7f..8e7f2b6ecab 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -8,8 +8,8 @@ For a detailed list of all changes, see the [CHANGELOG](changelog.md).
Click to expand -The names for these APIs were created when the global interpreter lock (GIL) was mandatory. With the introduction of free-threading in Python 3.13 this is no longer the case, and the naming does not has no universal meaning anymore. -For this reason we chose to rename these to more modern terminology introduced in free-threading: +The names for these APIs were created when the global interpreter lock (GIL) was mandatory. With the introduction of free-threading in Python 3.13 this is no longer the case, and the naming does not have no universal meaning anymore. +For this reason, we chose to rename these to more modern terminology introduced in free-threading: - `Python::with_gil` is now called `Python::attach`, it attaches a Python thread-state to the current thread. In GIL enabled builds there can only be 1 thread attached to the interpreter, in free-threading there can be more. - `Python::allow_threads` is now called `Python::detach`, it detaches a previously attached thread-state. diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index f0d37416b1e..ec582efba14 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -1091,7 +1091,7 @@ impl CrossCompileEnvVars { /// 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 -/// installation. This variable is only needed if PyO3 cannnot determine the version to target +/// installation. This variable is only needed if PyO3 cannot determine the version to target /// from `abi3-py3*` features, or if there are multiple versions of Python present in /// `PYO3_CROSS_LIB_DIR`. /// diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index 534725ac9cc..9b28d4937c3 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -246,9 +246,9 @@ pub mod pyo3_build_script_impl { // 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()?; - Ok(interperter_config) + let mut interpreter_config = InterpreterConfig::from_reader(Cursor::new(CONFIG_FILE))?; + interpreter_config.generate_import_libs()?; + Ok(interpreter_config) } else if let Some(interpreter_config) = make_cross_compile_config()? { // This is a cross compile and need to write the config file. let path = resolve_cross_compile_config_path() diff --git a/pyo3-ffi/src/memoryobject.rs b/pyo3-ffi/src/memoryobject.rs index e11055f3743..01ceea3bdbe 100644 --- a/pyo3-ffi/src/memoryobject.rs +++ b/pyo3-ffi/src/memoryobject.rs @@ -17,7 +17,7 @@ pub unsafe fn PyMemoryView_Check(op: *mut PyObject) -> c_int { } // skipped non-limited PyMemoryView_GET_BUFFER -// skipped non-limited PyMemeryView_GET_BASE +// skipped non-limited PyMemoryView_GET_BASE extern "C" { #[cfg_attr(PyPy, link_name = "PyPyMemoryView_FromObject")] diff --git a/pyo3-macros-backend/src/attributes.rs b/pyo3-macros-backend/src/attributes.rs index af81e7aea69..0355d5d4bec 100644 --- a/pyo3-macros-backend/src/attributes.rs +++ b/pyo3-macros-backend/src/attributes.rs @@ -274,7 +274,7 @@ impl ToTokens for RenamingRuleLitStr { } } -/// Text signatue can be either a literal string or opt-in/out +/// Text signature can be either a literal string or opt-in/out #[derive(Clone, Debug, PartialEq, Eq)] pub enum TextSignatureAttributeValue { Str(LitStr), diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index 0e8977db101..400538ce14d 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -196,7 +196,7 @@ impl<'a> FnArg<'a> { 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 + // `cancel_handle` or `from_py_with`, duplicates and any // combination of the two are already rejected. return Ok(Self::CancelHandle(CancelHandleArg { name: ident, diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index eede90f882f..a642458ecf2 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -851,7 +851,7 @@ pub fn impl_py_getter_def( let method_def = quote! { #cfg_attrs { - #[allow(unused_imports)] // might not be used if all probes are positve + #[allow(unused_imports)] // might not be used if all probes are positive use #pyo3_path::impl_::pyclass::Probe as _; struct Offset; diff --git a/src/buffer.rs b/src/buffer.rs index 722a2b82852..c3aad73e04d 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -908,7 +908,7 @@ mod tests { assert_eq!(buffer.format().to_str().unwrap(), "f"); assert_eq!(buffer.shape(), [4]); - // array creates a 1D contiguious buffer, so it's both C and F contiguous. This would + // array creates a 1D contiguous buffer, so it's both C and F contiguous. This would // be more interesting if we can come up with a 2D buffer but I think it would need a // third-party lib or a custom class. diff --git a/src/call.rs b/src/call.rs index 229e1002456..5e36d1bd4a9 100644 --- a/src/call.rs +++ b/src/call.rs @@ -27,7 +27,7 @@ pub(crate) mod private { /// 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 +/// has no publicly available methods and cannot be implemented outside of /// `pyo3`. The corresponding public API is available through [`call`] /// ([`call0`], [`call1`] and friends) on [`PyAnyMethods`]. /// diff --git a/src/conversions/std/num.rs b/src/conversions/std/num.rs index 029219b39bc..42a65a27929 100644 --- a/src/conversions/std/num.rs +++ b/src/conversions/std/num.rs @@ -79,7 +79,7 @@ macro_rules! extract_int { // however 3.8 & 3.9 do lossy conversion of floats, hence we only use the // simplest logic for 3.10+ where that was fixed - python/cpython#82180. // `PyLong_AsUnsignedLongLong` does not call `PyNumber_Index`, hence the `force_index_call` argument - // See https://github.com/PyO3/pyo3/pull/3742 for detials + // See https://github.com/PyO3/pyo3/pull/3742 for details 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.cast::() { diff --git a/src/err/mod.rs b/src/err/mod.rs index fe8d3755d4a..245b141a80d 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -498,7 +498,7 @@ impl PyErr { /// /// Calling this method has the benefit that the error goes back into a standardized callback /// in Python which for instance allows unittests to ensure that no unraisable error - /// actually happend by hooking `sys.unraisablehook`. + /// actually happened by hooking `sys.unraisablehook`. /// /// Example: /// ```rust diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index d960b5477c4..8950b2ffc95 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -1290,7 +1290,7 @@ impl< } } -/// Field is not `Py`; try to use `IntoPyObject` for `&T` (prefered over `ToPyObject`) to avoid +/// Field is not `Py`; try to use `IntoPyObject` for `&T` (preferred over `ToPyObject`) to avoid /// potentially expensive clones of containers like `Vec` impl PyClassGetterGenerator diff --git a/src/lib.rs b/src/lib.rs index bdf0dd7cd5c..7b1d957d104 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -315,7 +315,7 @@ //! [`ordered-float`]: ./ordered_float/index.html "Documentation about the `ordered-float` 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." +//! [`rust_decimal`]: ./rust_decimal/index.html "Documentation 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." #![doc = concat!("[calling_rust]: https://pyo3.rs/v", env!("CARGO_PKG_VERSION"), "/python-from-rust.html \"Calling Python from Rust - PyO3 user guide\"")] diff --git a/src/pyclass/guard.rs b/src/pyclass/guard.rs index 2436be3d1c4..1404b4e6721 100644 --- a/src/pyclass/guard.rs +++ b/src/pyclass/guard.rs @@ -271,7 +271,7 @@ impl Deref for PyClassGuard<'_, T> { #[inline] fn deref(&self) -> &T { - // SAFETY: `PyClassObject` constains a valid `T`, by construction no + // SAFETY: `PyClassObject` contains a valid `T`, by construction no // mutable alias is enforced unsafe { &*self.as_class_object().get_ptr().cast_const() } } @@ -626,7 +626,7 @@ impl> Deref for PyClassGuardMut<'_, T> { #[inline] fn deref(&self) -> &T { - // SAFETY: `PyClassObject` constains a valid `T`, by construction no + // SAFETY: `PyClassObject` contains a valid `T`, by construction no // alias is enforced unsafe { &*self.as_class_object().get_ptr().cast_const() } } @@ -634,7 +634,7 @@ impl> Deref for PyClassGuardMut<'_, T> { impl> DerefMut for PyClassGuardMut<'_, T> { #[inline] fn deref_mut(&mut self) -> &mut T { - // SAFETY: `PyClassObject` constains a valid `T`, by construction no + // SAFETY: `PyClassObject` contains a valid `T`, by construction no // alias is enforced unsafe { &mut *self.as_class_object().get_ptr() } } diff --git a/src/types/capsule.rs b/src/types/capsule.rs index 0d8849535ed..0c4287eed0a 100644 --- a/src/types/capsule.rs +++ b/src/types/capsule.rs @@ -427,7 +427,7 @@ mod tests { let result: PyResult<&Foo> = unsafe { PyCapsule::import(py, wrong_name.as_ref()) }; assert!(result.is_err()); - // corret name is okay. + // correct name is okay. let cap: &Foo = unsafe { PyCapsule::import(py, name.as_ref())? }; assert_eq!(cap.val, 123); Ok(()) diff --git a/src/types/set.rs b/src/types/set.rs index cdc920b0c12..6b89be40c85 100644 --- a/src/types/set.rs +++ b/src/types/set.rs @@ -369,7 +369,7 @@ mod tests { fn test_set_add() { Python::attach(|py| { let set = PySet::new(py, [1, 2]).unwrap(); - set.add(1).unwrap(); // Add a dupliated element + set.add(1).unwrap(); // Add a duplicated element assert!(set.contains(1).unwrap()); }); } diff --git a/src/types/weakref/anyref.rs b/src/types/weakref/anyref.rs index 25c627402e9..5b6ebda5717 100644 --- a/src/types/weakref/anyref.rs +++ b/src/types/weakref/anyref.rs @@ -66,7 +66,7 @@ pub trait PyWeakrefMethods<'py>: crate::sealed::Sealed { /// let (name, score) = data.get_data(); /// Ok(format!("Processing '{}': score = {}", name, score)) /// } else { - /// Ok("The supplied data reference is nolonger relavent.".to_owned()) + /// Ok("The supplied data reference is no longer relevant.".to_owned()) /// } /// } /// @@ -84,7 +84,7 @@ pub trait PyWeakrefMethods<'py>: crate::sealed::Sealed { /// /// assert_eq!( /// parse_data(reference.as_borrowed())?, - /// "The supplied data reference is nolonger relavent." + /// "The supplied data reference is no longer relevant." /// ); /// /// Ok(()) @@ -94,7 +94,7 @@ 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 @@ -146,7 +146,7 @@ pub trait PyWeakrefMethods<'py>: crate::sealed::Sealed { /// let (name, score) = data.get_data(); /// format!("Processing '{}': score = {}", name, score) /// } else { - /// "The supplied data reference is nolonger relavent.".to_owned() + /// "The supplied data reference is no longer relevant.".to_owned() /// } /// } /// @@ -164,7 +164,7 @@ pub trait PyWeakrefMethods<'py>: crate::sealed::Sealed { /// /// assert_eq!( /// parse_data(reference.as_borrowed()), - /// "The supplied data reference is nolonger relavent." + /// "The supplied data reference is no longer relevant." /// ); /// /// Ok(()) @@ -174,7 +174,7 @@ 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 @@ -216,7 +216,7 @@ pub trait PyWeakrefMethods<'py>: crate::sealed::Sealed { /// let (name, score) = data.get_data(); /// Ok(format!("Processing '{}': score = {}", name, score)) /// } else { - /// Ok("The supplied data reference is nolonger relavent.".to_owned()) + /// Ok("The supplied data reference is no longer relevant.".to_owned()) /// } /// } /// @@ -234,7 +234,7 @@ pub trait PyWeakrefMethods<'py>: crate::sealed::Sealed { /// /// assert_eq!( /// parse_data(reference.as_borrowed())?, - /// "The supplied data reference is nolonger relavent." + /// "The supplied data reference is no longer relevant." /// ); /// /// Ok(()) @@ -244,7 +244,7 @@ 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 @@ -284,9 +284,9 @@ pub trait PyWeakrefMethods<'py>: crate::sealed::Sealed { /// /// 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__")?)) + /// Ok(format!("The object '{}' referred by this reference still exists.", object.getattr("__class__")?.getattr("__qualname__")?)) /// } else { - /// Ok("The object, which this reference refered to, no longer exists".to_owned()) + /// Ok("The object, which this reference referred to, no longer exists".to_owned()) /// } /// } /// @@ -297,14 +297,14 @@ pub trait PyWeakrefMethods<'py>: crate::sealed::Sealed { /// /// assert_eq!( /// parse_data(reference.as_borrowed())?, - /// "The object 'Foo' refered by this reference still exists." + /// "The object 'Foo' referred by this reference still exists." /// ); /// /// drop(data); /// /// assert_eq!( /// parse_data(reference.as_borrowed())?, - /// "The object, which this reference refered to, no longer exists" + /// "The object, which this reference referred to, no longer exists" /// ); /// /// Ok(()) diff --git a/src/types/weakref/reference.rs b/src/types/weakref/reference.rs index f008b2cd252..bf76fd706c7 100644 --- a/src/types/weakref/reference.rs +++ b/src/types/weakref/reference.rs @@ -28,7 +28,7 @@ pyobject_native_type!( #checkfunction=ffi::PyWeakref_CheckRefExact ); -// When targetting alternative or multiple interpreters, it is better to not use the internal API. +// When targeting 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); diff --git a/tests/test_macros.rs b/tests/test_macros.rs index e7b1997145a..f276890aa0b 100644 --- a/tests/test_macros.rs +++ b/tests/test_macros.rs @@ -30,7 +30,7 @@ macro_rules! set_extends_via_macro { set_extends_via_macro!(MyClass2, MyBaseClass); // -// Check that pyfunctiona nd text_signature can be called with macro arguments. +// Check that pyfunctions and text_signature can be called with macro arguments. // macro_rules! fn_macro { From 564a72f00f67a917966dd03bb0b8dbf138c93db4 Mon Sep 17 00:00:00 2001 From: Thomas Tanon Date: Sun, 31 Aug 2025 13:43:43 +0200 Subject: [PATCH 829/936] Basic introspection of #[derive(IntoPyObject)] (#5365) Struct fields are not supported yet --- newsfragments/5365.added.md | 1 + pyo3-macros-backend/src/intopyobject.rs | 106 +++++++++++++++++++++++- pytests/stubs/pyclasses.pyi | 2 +- 3 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 newsfragments/5365.added.md diff --git a/newsfragments/5365.added.md b/newsfragments/5365.added.md new file mode 100644 index 00000000000..c272ba1bfe9 --- /dev/null +++ b/newsfragments/5365.added.md @@ -0,0 +1 @@ +Basic introspection of `#[derive(IntoPyObject)]` (no struct fields support yet) \ No newline at end of file diff --git a/pyo3-macros-backend/src/intopyobject.rs b/pyo3-macros-backend/src/intopyobject.rs index 2c12567e8b7..811e05569cb 100644 --- a/pyo3-macros-backend/src/intopyobject.rs +++ b/pyo3-macros-backend/src/intopyobject.rs @@ -1,5 +1,9 @@ use crate::attributes::{IntoPyWithAttribute, RenamingRule}; use crate::derive_attributes::{ContainerAttributes, FieldAttributes}; +#[cfg(feature = "experimental-inspect")] +use crate::introspection::ConcatenationBuilder; +#[cfg(feature = "experimental-inspect")] +use crate::utils::TypeExt; use crate::utils::{self, Ctx}; use proc_macro2::{Span, TokenStream}; use quote::{format_ident, quote, quote_spanned, ToTokens}; @@ -356,6 +360,55 @@ impl<'a, const REF: bool> Container<'a, REF> { }, } } + + #[cfg(feature = "experimental-inspect")] + fn write_output_type(&self, builder: &mut ConcatenationBuilder, ctx: &Ctx) { + match &self.ty { + ContainerType::StructNewtype(field) | ContainerType::TupleNewtype(field) => { + Self::write_field_output_type(&None, &field.ty, builder, ctx); + } + ContainerType::Tuple(tups) => { + builder.push_str("tuple["); + for ( + i, + TupleStructField { + into_py_with, + field, + }, + ) in tups.iter().enumerate() + { + if i > 0 { + builder.push_str(", "); + } + Self::write_field_output_type(into_py_with, &field.ty, builder, ctx); + } + builder.push_str("]"); + } + ContainerType::Struct(_) => { + // TODO: implement using a Protocol? + builder.push_str("_typeshed.Incomplete") + } + } + } + + #[cfg(feature = "experimental-inspect")] + fn write_field_output_type( + into_py_with: &Option, + ty: &syn::Type, + builder: &mut ConcatenationBuilder, + ctx: &Ctx, + ) { + if into_py_with.is_some() { + // We don't know what into_py_with is doing + builder.push_str("_typeshed.Incomplete") + } else { + let ty = ty.clone().elide_lifetimes(); + let pyo3_crate_path = &ctx.pyo3_path; + builder.push_tokens( + quote! { <#ty as #pyo3_crate_path::IntoPyObject<'_>>::OUTPUT_TYPE.as_bytes() }, + ) + } + } } /// Describes derivation input of an enum. @@ -431,6 +484,16 @@ impl<'a, const REF: bool> Enum<'a, REF> { }, } } + + #[cfg(feature = "experimental-inspect")] + fn write_output_type(&self, builder: &mut ConcatenationBuilder, ctx: &Ctx) { + for (i, var) in self.variants.iter().enumerate() { + if i > 0 { + builder.push_str(" | "); + } + var.write_output_type(builder, ctx); + } + } } // if there is a `'py` lifetime, we treat it as the `Python<'py>` lifetime @@ -484,7 +547,7 @@ pub fn build_derive_into_pyobject(tokens: &DeriveInput) -> Resu Some(Ident::new("self", Span::call_site())), &st.fields, parse_quote!(#ident), - options, + options.clone(), )?; st.build(ctx) } @@ -522,12 +585,53 @@ pub fn build_derive_into_pyobject(tokens: &DeriveInput) -> Resu } else { quote! { #ident } }; + + #[cfg(feature = "experimental-inspect")] + let output_type = { + let mut builder = ConcatenationBuilder::default(); + if tokens + .generics + .params + .iter() + .all(|p| matches!(p, syn::GenericParam::Lifetime(_))) + { + match &tokens.data { + syn::Data::Enum(en) => { + Enum::::new(en, &tokens.ident)?.write_output_type(&mut builder, ctx) + } + syn::Data::Struct(st) => { + let ident = &tokens.ident; + Container::::new( + Some(Ident::new("self", Span::call_site())), + &st.fields, + parse_quote!(#ident), + options, + )? + .write_output_type(&mut builder, ctx) + } + syn::Data::Union(_) => { + // Not supported at this point + builder.push_str("_typeshed.Incomplete") + } + } + } else { + // We don't know how to deal with generic parameters + // Blocked by https://github.com/rust-lang/rust/issues/76560 + builder.push_str("_typeshed.Incomplete") + }; + let output_type = builder.into_token_stream(&ctx.pyo3_path); + quote! { const OUTPUT_TYPE: &'static str = unsafe { ::std::str::from_utf8_unchecked(#output_type) }; } + }; + #[cfg(not(feature = "experimental-inspect"))] + let output_type = quote! {}; + 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; + #output_type fn into_pyobject(self, py: #pyo3_path::Python<#lt_param>) -> ::std::result::Result< >::Output, diff --git a/pytests/stubs/pyclasses.pyi b/pytests/stubs/pyclasses.pyi index 7d66ce4cd40..4c838a590a2 100644 --- a/pytests/stubs/pyclasses.pyi +++ b/pytests/stubs/pyclasses.pyi @@ -38,4 +38,4 @@ class PyClassThreadIter: def map_a_class( cls: EmptyClass | tuple[EmptyClass, EmptyClass] | _typeshed.Incomplete, -) -> typing.Any: ... +) -> EmptyClass | tuple[EmptyClass, EmptyClass] | _typeshed.Incomplete: ... From 8133202ffb513b7fce45d49de17400dfca42b474 Mon Sep 17 00:00:00 2001 From: Thomas Tanon Date: Sun, 31 Aug 2025 15:16:14 +0200 Subject: [PATCH 830/936] Introspection: support `pyo3(get, set)` (#5370) --- newsfragments/5370.added.md | 1 + pyo3-macros-backend/src/pyclass.rs | 96 ++++++++++++++++++++--------- pyo3-macros-backend/src/pymethod.rs | 60 +++++++++++++----- pytests/src/pyclasses.rs | 8 ++- pytests/stubs/pyclasses.pyi | 10 +++ tests/test_compile_error.rs | 1 + 6 files changed, 131 insertions(+), 45 deletions(-) create mode 100644 newsfragments/5370.added.md diff --git a/newsfragments/5370.added.md b/newsfragments/5370.added.md new file mode 100644 index 00000000000..20b8543bebc --- /dev/null +++ b/newsfragments/5370.added.md @@ -0,0 +1 @@ +Introspection: support `#[pyo3(get, set)]` and `#[pyclass(get_all, set_all)]` \ No newline at end of file diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 53a1693db07..ee225399fe3 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -17,7 +17,6 @@ use crate::combine_errors::CombineErrors; #[cfg(feature = "experimental-inspect")] use crate::introspection::{ class_introspection_code, function_introspection_code, introspection_id_const, - unique_element_id, }; use crate::konst::{ConstAttributes, ConstSpec}; use crate::method::{FnArg, FnSpec, PyArg, RegularArg}; @@ -25,6 +24,8 @@ use crate::pyfunction::ConstructorAttribute; #[cfg(feature = "experimental-inspect")] use crate::pyfunction::FunctionSignature; use crate::pyimpl::{gen_py_const, get_cfg_attributes, PyClassMethodsType}; +#[cfg(feature = "experimental-inspect")] +use crate::pymethod::field_python_name; use crate::pymethod::{ impl_py_class_attribute, impl_py_getter_def, impl_py_setter_def, MethodAndMethodDef, MethodAndSlotDef, PropertyType, SlotDef, __GETITEM__, __HASH__, __INT__, __LEN__, __REPR__, @@ -1516,32 +1517,26 @@ fn generate_protocol_slot( #[cfg(feature = "experimental-inspect")] { // We generate introspection data - let associated_method = def.associated_method; let signature = FunctionSignature::from_arguments(introspection_data.arguments); let returns = introspection_data.returns; - let introspection = introspection_data - .names - .iter() - .map(|name| { - function_introspection_code( - &ctx.pyo3_path, - None, - name, - &signature, - Some("self"), - parse_quote!(-> #returns), - [], - Some(cls), - ) - }) - .collect::>(); - let const_name = format_ident!("_{}", unique_element_id()); // We need an explicit name here - def.associated_method = quote! { - #associated_method - const #const_name: () = { - #(#introspection)* - }; - }; + def.add_introspection( + introspection_data + .names + .iter() + .flat_map(|name| { + function_introspection_code( + &ctx.pyo3_path, + None, + name, + &signature, + Some("self"), + parse_quote!(-> #returns), + [], + Some(cls), + ) + }) + .collect(), + ); } Ok(def) } @@ -1829,7 +1824,7 @@ fn complex_enum_variant_field_getter<'a>( unsafety: None, warnings: vec![], #[cfg(feature = "experimental-inspect")] - output: syn::ReturnType::Type(Token![->](field_span), Box::new(variant_cls_type.clone())), + output: parse_quote!(-> #variant_cls_type), }; let property_type = crate::pymethod::PropertyType::Function { @@ -1862,31 +1857,72 @@ fn descriptors_to_items( } if options.get.is_some() { - let getter = impl_py_getter_def( + let renaming_rule = rename_all.map(|rename_all| rename_all.value.rule); + #[cfg_attr(not(feature = "experimental-inspect"), allow(unused_mut))] + let mut getter = impl_py_getter_def( &ty, PropertyType::Descriptor { field_index, field, python_name: options.name.as_ref(), - renaming_rule: rename_all.map(|rename_all| rename_all.value.rule), + renaming_rule, }, ctx, )?; + #[cfg(feature = "experimental-inspect")] + { + // We generate introspection data + let return_type = &field.ty; + getter.add_introspection(function_introspection_code( + &ctx.pyo3_path, + None, + &field_python_name(field, options.name.as_ref(), renaming_rule)?, + &FunctionSignature::from_arguments(vec![]), + Some("self"), + parse_quote!(-> #return_type), + vec!["property".into()], + Some(&parse_quote!(#cls)), + )); + } items.push(getter); } if let Some(set) = options.set { ensure_spanned!(frozen.is_none(), set.span() => "cannot use `#[pyo3(set)]` on a `frozen` class"); - let setter = impl_py_setter_def( + let renaming_rule = rename_all.map(|rename_all| rename_all.value.rule); + #[cfg_attr(not(feature = "experimental-inspect"), allow(unused_mut))] + let mut setter = impl_py_setter_def( &ty, PropertyType::Descriptor { field_index, field, python_name: options.name.as_ref(), - renaming_rule: rename_all.map(|rename_all| rename_all.value.rule), + renaming_rule, }, ctx, )?; + #[cfg(feature = "experimental-inspect")] + { + // We generate introspection data + let name = field_python_name(field, options.name.as_ref(), renaming_rule)?; + setter.add_introspection(function_introspection_code( + &ctx.pyo3_path, + None, + &name, + &FunctionSignature::from_arguments(vec![FnArg::Regular(RegularArg { + name: Cow::Owned(format_ident!("value")), + ty: &field.ty, + from_py_with: None, + default_value: None, + option_wrapped_type: None, + annotation: None, + })]), + Some("self"), + syn::ReturnType::Default, + vec![format!("{name}.setter")], + Some(&parse_quote!(#cls)), + )); + } items.push(setter); }; } diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index a642458ecf2..b2b5ad20375 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -2,6 +2,8 @@ use std::borrow::Cow; use std::ffi::CString; use crate::attributes::{FromPyWithAttribute, NameAttribute, RenamingRule}; +#[cfg(feature = "experimental-inspect")] +use crate::introspection::unique_element_id; use crate::method::{CallingConvention, ExtractErrorMode, PyArg}; use crate::params::{impl_regular_arg_param, Holders}; use crate::pyfunction::WarningFactory; @@ -14,7 +16,7 @@ use crate::{ use crate::{quotes, utils}; use proc_macro2::{Span, TokenStream}; use quote::{format_ident, quote, quote_spanned, ToTokens}; -use syn::{ext::IdentExt, spanned::Spanned, Ident, Result}; +use syn::{ext::IdentExt, spanned::Spanned, Field, Ident, Result}; /// Generated code for a single pymethod item. pub struct MethodAndMethodDef { @@ -24,6 +26,18 @@ pub struct MethodAndMethodDef { pub method_def: TokenStream, } +#[cfg(feature = "experimental-inspect")] +impl MethodAndMethodDef { + pub fn add_introspection(&mut self, data: TokenStream) { + let const_name = format_ident!("_{}", unique_element_id()); // We need an explicit name here + self.associated_method.extend(quote! { + const #const_name: () = { + #data + }; + }); + } +} + /// Generated code for a single pymethod item which is registered by a slot. pub struct MethodAndSlotDef { /// The implementation of the Python wrapper for the pymethod @@ -32,6 +46,18 @@ pub struct MethodAndSlotDef { pub slot_def: TokenStream, } +#[cfg(feature = "experimental-inspect")] +impl MethodAndSlotDef { + pub fn add_introspection(&mut self, data: TokenStream) { + let const_name = format_ident!("_{}", unique_element_id()); // We need an explicit name here + self.associated_method.extend(quote! { + const #const_name: () = { + #data + }; + }); + } +} + pub enum GeneratedPyMethod { Method(MethodAndMethodDef), Proto(MethodAndSlotDef), @@ -957,19 +983,7 @@ impl PropertyType<'_> { renaming_rule, .. } => { - let name = match (python_name, &field.ident) { - (Some(name), _) => name.value.0.to_string(), - (None, Some(field_name)) => { - let mut name = field_name.unraw().to_string(); - if let Some(rule) = renaming_rule { - name = utils::apply_renaming_rule(*rule, &name); - } - name - } - (None, None) => { - bail_spanned!(field.span() => "`get` and `set` with tuple struct fields require `name`"); - } - }; + let name = field_python_name(field, *python_name, *renaming_rule)?; let name = CString::new(name).unwrap(); Ok(LitCStr::new(name, field.span(), ctx)) } @@ -1672,3 +1686,21 @@ impl ToTokens for TokenGeneratorCtx<'_> { (gen)(ctx).to_tokens(tokens) } } + +pub fn field_python_name( + field: &Field, + name_attr: Option<&NameAttribute>, + renaming_rule: Option, +) -> Result { + if let Some(name_attr) = name_attr { + return Ok(name_attr.value.0.to_string()); + } + let Some(ident) = &field.ident else { + bail_spanned!(field.span() => "`get` and `set` with tuple struct fields require `name`"); + }; + let mut name = ident.unraw().to_string(); + if let Some(rule) = renaming_rule { + name = utils::apply_renaming_rule(rule, &name); + } + Ok(name) +} diff --git a/pytests/src/pyclasses.rs b/pytests/src/pyclasses.rs index bd5146a2086..631ec4fa0a0 100644 --- a/pytests/src/pyclasses.rs +++ b/pytests/src/pyclasses.rs @@ -144,6 +144,12 @@ impl ClassWithDecorators { } } +#[pyclass(get_all, set_all)] +struct PlainObject { + foo: String, + bar: usize, +} + #[derive(FromPyObject, IntoPyObject)] enum AClass { NewType(EmptyClass), @@ -170,6 +176,6 @@ pub mod pyclasses { #[pymodule_export] use super::{ map_a_class, AssertingBaseClass, ClassWithDecorators, ClassWithoutConstructor, EmptyClass, - PyClassIter, PyClassThreadIter, + PlainObject, PyClassIter, PyClassThreadIter, }; } diff --git a/pytests/stubs/pyclasses.pyi b/pytests/stubs/pyclasses.pyi index 4c838a590a2..7f7ee521452 100644 --- a/pytests/stubs/pyclasses.pyi +++ b/pytests/stubs/pyclasses.pyi @@ -28,6 +28,16 @@ class EmptyClass: def __new__(cls, /) -> None: ... def method(self, /) -> None: ... +class PlainObject: + @property + def bar(self, /) -> int: ... + @bar.setter + def bar(self, /, value: int) -> None: ... + @property + def foo(self, /) -> str: ... + @foo.setter + def foo(self, /, value: str) -> None: ... + class PyClassIter: def __new__(cls, /) -> None: ... def __next__(self, /) -> int: ... diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index c3b8570d6d3..6d60d9cb0f5 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -5,6 +5,7 @@ fn test_compile_errors() { let t = trybuild::TestCases::new(); + #[cfg(not(feature = "experimental-inspect"))] t.compile_fail("tests/ui/invalid_property_args.rs"); t.compile_fail("tests/ui/invalid_proto_pymethods.rs"); t.compile_fail("tests/ui/invalid_pyclass_args.rs"); From 69d30d4215d492a233fb026924c899c6d15e1e4f Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sun, 31 Aug 2025 14:16:29 +0100 Subject: [PATCH 831/936] docs: update guide to mention GIL less (#5381) * update guide to mention GIL less * Apply suggestions from code review Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> --------- Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> --- Architecture.md | 2 +- Cargo.toml | 3 +- guide/src/class.md | 28 ++++++++----- guide/src/conversions/tables.md | 6 +-- guide/src/faq.md | 3 +- guide/src/features.md | 4 +- guide/src/free-threading.md | 42 +++++++++---------- guide/src/parallelism.md | 13 +++++- guide/src/performance.md | 13 ++++++ guide/src/python-from-rust.md | 17 +++++--- pyo3-benches/Cargo.toml | 8 ++-- .../benches/{bench_gil.rs => bench_attach.rs} | 8 ++-- src/sync.rs | 13 ++++-- 13 files changed, 101 insertions(+), 59 deletions(-) rename pyo3-benches/benches/{bench_gil.rs => bench_attach.rs} (70%) diff --git a/Architecture.md b/Architecture.md index 2f66598c7e3..2b081a487e4 100644 --- a/Architecture.md +++ b/Architecture.md @@ -99,7 +99,7 @@ Thus, when copying a Rust struct to a Python object, we first allocate `PyClassO move `T` into it. The primary way to interact with Python objects implemented in Rust is through the `Bound<'py, T>` smart pointer. -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. +By having the `'py` lifetime of the `Python<'py>` token, this ties the lifetime of the `Bound<'py, T>` smart pointer to the lifetime for which the thread is attached to the Python interpreter and allows PyO3 to call Python APIs at maximum efficiency. `Bound<'py, T>` requires that `T` implements `PyClass`. This trait is somewhat complex and derives many traits, but the most important one is `PyTypeInfo` diff --git a/Cargo.toml b/Cargo.toml index 030174f62e9..a6e0ed838c2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -127,7 +127,8 @@ generate-import-lib = ["pyo3-ffi/generate-import-lib"] # Changes `Python::attach` to automatically initialize the Python interpreter if needed. auto-initialize = [] -# Enables `Clone`ing references to Python objects `Py` which panics if the GIL is not held. +# Enables `Clone`ing references to Python objects `Py` which panics if the +# thread is not attached to the Python interpreter. py-clone = [] # Adds `OnceExt` and `MutexExt` implementations to the `parking_lot` types diff --git a/guide/src/class.md b/guide/src/class.md index 6e90f16007b..911d1e7f72b 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -203,9 +203,10 @@ mod my_module { 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. +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. +However, the Rust borrow checker cannot reason about `&mut` references once an object's ownership has been passed to the Python interpreter. -The Rust borrow checker cannot reason about `&mut` references once an object's ownership has been passed to the Python interpreter. This means that borrow checking is done at runtime using with a scheme very similar to `std::cell::RefCell`. This is known as [interior mutability](https://doc.rust-lang.org/book/ch15-05-interior-mutability.html). +To solve this, PyO3 does borrow checking at runtime using a scheme very similar to `std::cell::RefCell`. This is known as [interior mutability](https://doc.rust-lang.org/book/ch15-05-interior-mutability.html). Users who are familiar with `RefCell` can use `Py` and `Bound<'py, T>` just like `RefCell`. @@ -685,7 +686,8 @@ impl MyClass { } ``` -Calls to these methods are protected by the GIL, so both `&self` and `&mut self` can be used. +Both `&self` and `&mut self` can be used, due to the use of [runtime borrow checking](#bound-and-interior-mutability). + The return type must be `PyResult` or `T` for some `T` that implements `IntoPyObject`; the latter is allowed if the method cannot raise Python exceptions. @@ -828,7 +830,12 @@ impl MyClass { ## Classes as function arguments -Free functions defined using `#[pyfunction]` interact with classes through the same mechanisms as the self parameters of instance methods, i.e. they can take Python-bound references, Python-bound reference wrappers or Python-independent references: +Class objects can be used as arguments to `#[pyfunction]`s and `#[pymethods]` in the same way as the self parameters of instance methods, i.e. they can be passed as: +- `Py` or `Bound<'py, T>` smart pointers to the class Python object, +- `&T` or `&mut T` references to the Rust data contained in the Python object, or +- `PyRef` and `PyRefMut` reference wrappers. + +Examples of each of these below: ```rust,no_run # #![allow(dead_code)] @@ -838,21 +845,21 @@ struct MyClass { my_field: i32, } -// Take a reference when the underlying `Bound` is irrelevant. +// Take a reference to Rust data when the Python object is irrelevant. #[pyfunction] fn increment_field(my_class: &mut MyClass) { my_class.my_field += 1; } // Take a reference wrapper when borrowing should be automatic, -// but interaction with the underlying `Bound` is desired. +// but access to the Python object is still needed #[pyfunction] -fn print_field(my_class: PyRef<'_, MyClass>) { +fn print_field_and_return_me(my_class: PyRef<'_, MyClass>) -> PyRef<'_, MyClass> { println!("{}", my_class.my_field); + my_class } -// Take a reference to the underlying Bound -// when borrowing needs to be managed manually. +// Take (a reference to) a Python object smart pointer when borrowing needs to be managed manually. #[pyfunction] fn increment_then_print_field(my_class: &Bound<'_, MyClass>) { my_class.borrow_mut().my_field += 1; @@ -860,7 +867,8 @@ fn increment_then_print_field(my_class: &Bound<'_, MyClass>) { println!("{}", my_class.borrow().my_field); } -// Take a GIL-independent reference when you want to store the reference elsewhere. +// When the Python object smart pointer needs to be stored elsewhere prefer `Py` over `Bound<'py, T>` +// to avoid the lifetime restrictions. #[pyfunction] fn print_refcnt(my_class: Py, py: Python<'_>) { println!("{}", my_class.get_refcnt(py)); diff --git a/guide/src/conversions/tables.md b/guide/src/conversions/tables.md index 9c2bd0add1f..498e7028a53 100644 --- a/guide/src/conversions/tables.md +++ b/guide/src/conversions/tables.md @@ -51,9 +51,9 @@ It is also worth remembering the following special types: | What | Description | | ---------------- | ------------------------------------- | -| `Python<'py>` | A GIL token, used to pass to PyO3 constructors to prove ownership of the GIL. | -| `Bound<'py, T>` | A Python object connected to the GIL lifetime. This provides access to most of PyO3's APIs. | -| `Py` | A Python object isolated from the GIL lifetime. This can be sent to other threads. | +| `Python<'py>` | A token used to prove attachment to the Python interpreter. | +| `Bound<'py, T>` | A Python object with a lifetime which binds it to the attachment to the Python interpreter. This provides access to most of PyO3's APIs. | +| `Py` | A Python object not connected to any lifetime of attachment to the Python interpreter. This can be sent to other threads. | | `PyRef` | A `#[pyclass]` borrowed immutably. | | `PyRefMut` | A `#[pyclass]` borrowed mutably. | diff --git a/guide/src/faq.md b/guide/src/faq.md index e00bec71f76..3743a7fad72 100644 --- a/guide/src/faq.md +++ b/guide/src/faq.md @@ -166,7 +166,8 @@ print(f"a: {a}\nb: {b}") a: b: ``` -The downside to this approach is that any Rust code working on the `Outer` struct now has to acquire the GIL to do anything with its field. +The downside to this approach is that any Rust code working on the `Outer` struct potentially has to attach to the Python interpreter to do anything with the `inner` field. (If `Inner` is `#[pyclass(frozen)]` and implements `Sync`, then `Py::get` +may be used to access the `Inner` contents from `Py` without needing to attach to the interpreter.) ## I want to use the `pyo3` crate re-exported from dependency but the proc-macros fail! diff --git a/guide/src/features.md b/guide/src/features.md index f9706b586b5..efb0b90646b 100644 --- a/guide/src/features.md +++ b/guide/src/features.md @@ -67,11 +67,11 @@ This is a first step towards adding first-class support for generating type anno ### `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. +This feature was introduced to ease migration. It was found that delayed reference counting (which PyO3 used historically) could not be made sound and hence `Clone`-ing an instance of `Py` is impossible when not attached to Python interpreter (it will panic). 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 associated 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. +This is a performance-oriented conditional compilation flag, e.g. [set via `$RUSTFLAGS`][set-configuration-options], which disabled the global reference pool and the associated overhead for the crossing the Python-Rust boundary. However, if enabled, `Drop`ping an instance of `Py` when not attached to the Python interpreter will abort the process. ### `macros` diff --git a/guide/src/free-threading.md b/guide/src/free-threading.md index 96146eb11f1..266eca85b4c 100644 --- a/guide/src/free-threading.md +++ b/guide/src/free-threading.md @@ -1,26 +1,26 @@ # 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/whatsnew/3.13.html#whatsnew313-free-threaded-cpython) -entry in the 3.13 release notes, the [free-threading HOWTO -guide](https://docs.python.org/3/howto/free-threading-extensions.html#freethreading-extensions-howto) -in the CPython docs, the [extension porting -guide](https://py-free-threading.github.io/porting-extensions/) in the -community-maintained Python free-threading guide, and [PEP -703](https://peps.python.org/pep-0703/), which provides the technical background +CPython 3.14 declared support for the "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. Since version 0.23, PyO3 +supports building Rust extensions for the free-threaded Python build and +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/whatsnew/3.13.html#whatsnew313-free-threaded-cpython) +entry in the 3.13 release notes (when the "free-threaded" build was first added as an experimental +mode), the +[free-threading HOWTO guide](https://docs.python.org/3/howto/free-threading-extensions.html#freethreading-extensions-howto) +in the CPython docs, the +[extension porting guide](https://py-free-threading.github.io/porting-extensions/) +in the community-maintained Python free-threading guide, 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 +In the GIL-enabled build (the only choice before the "free-threaded" build was introduced), +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. @@ -123,9 +123,7 @@ free-threaded build. The free-threaded interpreter does not have a GIL. Many existing extensions providing mutable data structures relied on the GIL to lock Python objects and -make interior mutability thread-safe. Historically, PyO3's API was designed -around the same strong assumptions, but is transitioning towards more general -APIs applicable for both builds. +make interior mutability thread-safe. 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 diff --git a/guide/src/parallelism.md b/guide/src/parallelism.md index a937b49764f..260865eb63e 100644 --- a/guide/src/parallelism.md +++ b/guide/src/parallelism.md @@ -1,8 +1,15 @@ # Parallelism -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. +Historically, CPython was limited by the [global interpreter lock](https://docs.python.org/3/glossary.html#term-global-interpreter-lock) (GIL), which only allowed a single thread to drive the Python interpreter at a time. This made threading in Python a bad fit for [CPU-bound](https://en.wikipedia.org/wiki/CPU-bound) tasks and often forced developers to accept the overhead of multiprocessing. + +Rust is well-suited to multithreaded code, and libraries like [`rayon`] can help you leverage safe parallelism with minimal effort. The [`Python::detach`] method can be used to allow the Python interpreter to do other work while the Rust work is ongoing. + +To enable full parallelism in your application, consider also using [free-threaded Python](./free-threading.md) which is supported since Python 3.14. + +## Parallelism under the Python GIL + +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`] crate to count words in parallel. -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 # #![allow(dead_code)] use pyo3::prelude::*; @@ -32,6 +39,7 @@ fn search(contents: &str, needle: &str) -> usize { ``` But let's assume you have a long running Rust function which you would like to execute several times in parallel. For the sake of example let's take a sequential version of the word count: + ```rust,no_run # #![allow(dead_code)] # fn count_line(line: &str, needle: &str) -> usize { @@ -175,3 +183,4 @@ collecting the results from the worker threads. You should always call cases where worker threads need to acquire the GIL, to prevent deadlocks. [`Python::detach`]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.detach +[`rayon`]: https://github.com/rayon-rs/rayon diff --git a/guide/src/performance.md b/guide/src/performance.md index 998d1f8b940..85b6c337ffd 100644 --- a/guide/src/performance.md +++ b/guide/src/performance.md @@ -98,6 +98,7 @@ 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 @@ -110,6 +111,18 @@ Rust tuples may make use of [`vectorcall`] where as `Bound<'_, PyTuple>` and `Py [`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 +## Detach from the interpreter for long-running Rust-only work + +When executing Rust code which does not need to interact with the Python interpreter, use [`Python::detach`] to allow the Python interpreter to proceed without waiting for the current thread. + +On the GIL-enabled build, this is crucial for best performance as only a single thread may ever be attached at a time. + +On the free-threaded build, this is still best practice as there are several "stop the world" events (such as garbage collection) where all threads attached to the Python interpreter are forced to wait. + +As a rule of thumb, attaching and detaching from the Python interpreter takes less than a millisecond, so any work which is expected to take multiple milliseconds can likely benefit from detaching from the interpreter. + +[`Python::detach`]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.detach + ## 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 being attached to the interpreter. The necessary synchronization to obtain and apply these reference count updates when PyO3-based code next attaches to the interpreter is somewhat expensive and can become a significant part of the cost of crossing the Python-Rust boundary. diff --git a/guide/src/python-from-rust.md b/guide/src/python-from-rust.md index 7ab39161e70..74d530041d4 100644 --- a/guide/src/python-from-rust.md +++ b/guide/src/python-from-rust.md @@ -12,13 +12,14 @@ The subchapters also cover the following topics: ## The `'py` lifetime -To safely interact with the Python interpreter a Rust thread must have a corresponding Python thread state and hold the [Global Interpreter Lock (GIL)](#the-global-interpreter-lock). PyO3 has a `Python<'py>` token that is used to prove that these conditions -are met. Its lifetime `'py` is a central part of PyO3's API. +To safely interact with the Python interpreter a Rust thread must be [attached] to the Python interpreter. +PyO3 has a `Python<'py>` token that is used to prove that these conditions are met. +Its lifetime `'py` is a central part of PyO3's API. The `Python<'py>` token serves three purposes: * It provides global APIs for the Python interpreter, such as [`py.eval()`][eval] and [`py.import()`][import]. -* It can be passed to functions that require a proof of holding the GIL, such as [`Py::clone_ref`][clone_ref]. +* It can be passed to functions that require a proof of attachment, such as [`Py::clone_ref`][clone_ref]. * Its lifetime `'py` is used to bind many of PyO3's types to the Python interpreter, such as [`Bound<'py, T>`][Bound]. PyO3's types that are bound to the `'py` lifetime, for example `Bound<'py, T>`, all contain a `Python<'py>` token. This means they have full access to the Python interpreter and offer a complete API for interacting with Python objects. @@ -27,9 +28,13 @@ Consult [PyO3's API documentation][obtaining-py] to learn how to acquire one of ### The Global Interpreter Lock -Concurrent programming in Python is aided by the Global Interpreter Lock (GIL), which ensures that only one Python thread can use the Python interpreter and its API at the same time. This allows it to be used to synchronize code. See the [`pyo3::sync`] module for synchronization tools PyO3 offers that are based on the GIL's guarantees. +Prior to the introduction of free-threaded Python (first available in 3.13, fully supported in 3.14), the Python interpreter was made thread-safe by the [global interpreter lock]. +This ensured that only one Python thread can use the Python interpreter and its API at the same time. +Historically, Rust code was able to use the GIL as a synchronization guarantee, but the introduction of free-threaded Python removed this possibility. -Non-Python operations (system calls and native Rust code) can unlock the GIL. See [the section on parallelism](parallelism.md) for how to do that using PyO3's API. +The [`pyo3::sync`] module offers synchronization tools which abstract over both Python builds. + +To enable any parallelism on the GIL-enabled build, and best throughput on the free-threaded build, non-Python operations (system calls and native Rust code) should consider detaching from the Python interpreter to allow other work to proceed. See [the section on parallelism](parallelism.md) for how to do that using PyO3's API. ## Python's memory model @@ -41,6 +46,8 @@ PyO3's API reflects this by providing [smart pointer][smart-pointers] types, `Py Because of the lack of exclusive `&mut` references, PyO3's APIs for Python objects, for example [`PyListMethods::append`], use shared references. This is safe because Python objects have internal mechanisms to prevent data races (as of time of writing, the Python GIL). +[attached]: https://docs.python.org/3.14/glossary.html#term-attached-thread-state +[global interpreter lock]: https://docs.python.org/3/c-api/init.html#thread-state-and-the-global-interpreter-lock [smart-pointers]: https://doc.rust-lang.org/book/ch15-00-smart-pointers.html [obtaining-py]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#obtaining-a-python-token [`pyo3::sync`]: {{#PYO3_DOCS_URL}}/pyo3/sync/index.html diff --git a/pyo3-benches/Cargo.toml b/pyo3-benches/Cargo.toml index 7cf5de3768d..982b21c1c5c 100644 --- a/pyo3-benches/Cargo.toml +++ b/pyo3-benches/Cargo.toml @@ -23,6 +23,10 @@ hashbrown = "0.15" name = "bench_any" harness = false +[[bench]] +name = "bench_attach" +harness = false + [[bench]] name = "bench_call" harness = false @@ -47,10 +51,6 @@ harness = false name = "bench_frompyobject" harness = false -[[bench]] -name = "bench_gil" -harness = false - [[bench]] name = "bench_intopyobject" harness = false diff --git a/pyo3-benches/benches/bench_gil.rs b/pyo3-benches/benches/bench_attach.rs similarity index 70% rename from pyo3-benches/benches/bench_gil.rs rename to pyo3-benches/benches/bench_attach.rs index b8fc8496225..745a7555b89 100644 --- a/pyo3-benches/benches/bench_gil.rs +++ b/pyo3-benches/benches/bench_attach.rs @@ -2,20 +2,20 @@ use codspeed_criterion_compat::{criterion_group, criterion_main, Bencher, Criter use pyo3::prelude::*; -fn bench_clean_acquire_gil(b: &mut Bencher<'_>) { +fn bench_clean_attach(b: &mut Bencher<'_>) { // Acquiring first GIL will also create a "clean" GILPool, so this measures the Python overhead. b.iter(|| Python::attach(|_| {})); } -fn bench_dirty_acquire_gil(b: &mut Bencher<'_>) { +fn bench_dirty_attach(b: &mut Bencher<'_>) { let obj = Python::attach(|py| py.None()); // Drop the returned clone of the object so that the reference pool has work to do. b.iter(|| Python::attach(|py| obj.clone_ref(py))); } fn criterion_benchmark(c: &mut Criterion) { - c.bench_function("clean_acquire_gil", bench_clean_acquire_gil); - c.bench_function("dirty_acquire_gil", bench_dirty_acquire_gil); + c.bench_function("clean_attach", bench_clean_attach); + c.bench_function("dirty_attach", bench_dirty_attach); } criterion_group!(benches, criterion_benchmark); diff --git a/src/sync.rs b/src/sync.rs index c1e3ff177e2..8557d83cd30 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -1,9 +1,14 @@ -//! Synchronization mechanisms based on the Python GIL. +//! Synchronization mechanisms which are aware of the existence of the Python interpreter. //! -//! With the acceptance of [PEP 703] (aka a "freethreaded Python") for Python 3.13, these -//! are likely to undergo significant developments in the future. +//! The Python interpreter has multiple "stop the world" situations which may block threads, such as +//! - The Python global interpreter lock (GIL), on GIL-enabled builds of Python, or +//! - The Python garbage collector (GC), which pauses attached threads during collection. //! -//! [PEP 703]: https://peps.python.org/pep-703/ +//! To avoid deadlocks in these cases, threads should take care to be detached from the Python interpreter +//! before performing operations which might block waiting for other threads attached to the Python +//! interpreter. +//! +//! This module provides synchronization primitives which are able to synchronize under these conditions. use crate::{ internal::state::SuspendAttach, sealed::Sealed, From 6de90210ca32dbad21ccd69d2cdf4cd0e06b221d Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Sun, 31 Aug 2025 11:15:28 -0400 Subject: [PATCH 832/936] fixed formatting in bytes conversion docs (#5382) * fixed formatting in bytes conversion docs * fixed example usage for Bytes --- src/conversions/bytes.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/conversions/bytes.rs b/src/conversions/bytes.rs index 673f9d6b2ba..9c615e84a5e 100644 --- a/src/conversions/bytes.rs +++ b/src/conversions/bytes.rs @@ -21,6 +21,7 @@ //! [dependencies] //! bytes = "1.10" #![doc = concat!("pyo3 = { version = \"", env!("CARGO_PKG_VERSION"), "\", features = [\"bytes\"] }")] +//! ``` //! //! Note that you must use compatible versions of bytes and PyO3. //! @@ -30,6 +31,7 @@ //! //! ```rust,no_run //! use pyo3::prelude::*; +//! use bytes::Bytes; //! //! #[pyfunction] //! fn get_message_bytes() -> Bytes { From d7961fe5fbcc69ff01bc9aae999f28a969c0737e Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Sun, 31 Aug 2025 15:53:42 -0400 Subject: [PATCH 833/936] Use uv with nox (#5383) Right now I see creating venvs taking 30 seconds sometimes, this should dramatically speed that up --- .github/workflows/benches.yml | 2 +- .github/workflows/build.yml | 5 ++++- .github/workflows/changelog.yml | 2 +- .github/workflows/ci.yml | 18 +++++++++--------- .github/workflows/netlify-build.yml | 4 ++-- .netlify/build.sh | 2 +- pytests/noxfile.py | 2 +- 7 files changed, 19 insertions(+), 16 deletions(-) diff --git a/.github/workflows/benches.yml b/.github/workflows/benches.yml index 6cc07900ee3..d10882f301a 100644 --- a/.github/workflows/benches.yml +++ b/.github/workflows/benches.yml @@ -37,7 +37,7 @@ jobs: tool: cargo-codspeed - name: Install nox - run: pip install nox + run: pip install nox[uv] - name: Run the benchmarks uses: CodSpeedHQ/action@v3 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 68d62abd035..1f496892ae2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,6 +23,9 @@ on: type: boolean default: false +env: + NOX_DEFAULT_VENV_BACKEND: uv + jobs: build: continue-on-error: ${{ endsWith(inputs.python-version, '-dev') || contains(fromJSON('["3.7", "3.8"]'), inputs.python-version) || contains(fromJSON('["beta", "nightly"]'), inputs.rust) }} @@ -49,7 +52,7 @@ jobs: check-latest: ${{ startsWith(inputs.python-version, 'pypy') }} - name: Install nox - run: python -m pip install --upgrade pip && pip install nox + run: python -m pip install --upgrade pip && pip install nox[uv] - name: Install Rust toolchain uses: dtolnay/rust-toolchain@master diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index c7cd9a6699a..808f899964b 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -13,5 +13,5 @@ jobs: - uses: actions/setup-python@v5 with: python-version: '3.13' - - run: python -m pip install --upgrade pip && pip install nox + - run: python -m pip install --upgrade pip && pip install nox[uv] - run: nox -s check-changelog diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9071c024749..3891020fd73 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ jobs: - uses: actions/setup-python@v5 with: python-version: "3.13" - - run: python -m pip install --upgrade pip && pip install nox + - run: python -m pip install --upgrade pip && pip install nox[uv] - uses: dtolnay/rust-toolchain@stable with: components: rustfmt @@ -73,7 +73,7 @@ jobs: - uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'CI-save-pr-cache') }} - - run: python -m pip install --upgrade pip && pip install nox + - run: python -m pip install --upgrade pip && pip install nox[uv] # This is a smoke test to confirm that CI will run on MSRV (including dev dependencies) - name: Check with MSRV package versions run: | @@ -414,7 +414,7 @@ jobs: save-if: ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'CI-save-pr-cache') }} - uses: dtolnay/rust-toolchain@stable - uses: taiki-e/install-action@valgrind - - run: python -m pip install --upgrade pip && pip install nox + - run: python -m pip install --upgrade pip && pip install nox[uv] - run: nox -s test-rust -- release skip-full env: CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUNNER: valgrind --leak-check=no --error-exitcode=1 @@ -437,7 +437,7 @@ jobs: with: components: rust-src - uses: taiki-e/install-action@cargo-careful - - run: python -m pip install --upgrade pip && pip install nox + - run: python -m pip install --upgrade pip && pip install nox[uv] - run: nox -s test-rust -- careful skip-full env: RUST_BACKTRACE: 1 @@ -480,7 +480,7 @@ jobs: - uses: actions/setup-node@v4 with: node-version: 18 - - run: python -m pip install --upgrade pip && pip install nox + - run: python -m pip install --upgrade pip && pip install nox[uv] - uses: actions/cache/restore@v4 id: cache with: @@ -528,7 +528,7 @@ jobs: echo PYTHONHOME=$(pwd)/python/install >> $GITHUB_ENV echo PYO3_PYTHON=$(pwd)/python/install/bin/python3 >> $GITHUB_ENV - run: python3 -m sysconfig - - run: python3 -m pip install --upgrade pip && pip install nox + - run: python3 -m pip install --upgrade pip && pip install nox[uv] - run: | PYO3_CONFIG_FILE=$(mktemp) cat > $PYO3_CONFIG_FILE << EOF @@ -559,7 +559,7 @@ jobs: with: save-if: ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'CI-save-pr-cache') }} - uses: dtolnay/rust-toolchain@stable - - run: python3 -m pip install --upgrade pip && pip install nox + - run: python3 -m pip install --upgrade pip && pip install nox[uv] - run: python3 -m nox -s test-version-limits check-feature-powerset: @@ -588,7 +588,7 @@ jobs: - 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 pip install --upgrade pip && pip install nox[uv] - run: python3 -m nox -s check-feature-powerset -- ${{ matrix.rust != 'stable' && 'minimal-versions' || '' }} test-cross-compilation: @@ -732,7 +732,7 @@ jobs: - uses: Swatinem/rust-cache@v2 with: save-if: ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'CI-save-pr-cache') }} - - run: python -m pip install --upgrade pip && pip install nox + - run: python -m pip install --upgrade pip && pip install nox[uv] - run: nox -s test-introspection env: CARGO_BUILD_TARGET: ${{ matrix.platform.rust-target }} diff --git a/.github/workflows/netlify-build.yml b/.github/workflows/netlify-build.yml index 19411af3ed5..3af4a6e1599 100644 --- a/.github/workflows/netlify-build.yml +++ b/.github/workflows/netlify-build.yml @@ -50,7 +50,7 @@ jobs: # This builds the book in target/guide/. - name: Build the guide run: | - python -m pip install --upgrade pip && pip install nox + python -m pip install --upgrade pip && pip install nox[uv] nox -s ${{ github.event_name == 'release' && 'build-guide' || 'check-guide' }} env: PYO3_VERSION_TAG: ${{ steps.prepare_tag.outputs.tag_name }} @@ -75,7 +75,7 @@ jobs: - name: Build the site run: | - python -m pip install --upgrade pip && pip install nox towncrier requests + python -m pip install --upgrade pip && pip install nox[uv] towncrier requests nox -s build-netlify-site -- ${{ (github.ref != 'refs/heads/main' && '--preview') || '' }} # Upload the built site as an artifact for deploy workflow to consume diff --git a/.netlify/build.sh b/.netlify/build.sh index 97b7bd72414..dedacb28fb3 100755 --- a/.netlify/build.sh +++ b/.netlify/build.sh @@ -97,7 +97,7 @@ if [ "${INSTALLED_MDBOOK_TABS_VERSION}" != "mdbook-tabs v${MDBOOK_TABS_VERSION}" cargo install mdbook-tabs@${MDBOOK_TABS_VERSION} --force fi -pip install nox +pip install nox[uv] nox -s build-guide mv target/guide/ netlify_build/main/ diff --git a/pytests/noxfile.py b/pytests/noxfile.py index f5a332c7a3c..2bc9c0366d1 100644 --- a/pytests/noxfile.py +++ b/pytests/noxfile.py @@ -8,7 +8,7 @@ @nox.session def test(session: nox.Session): session.env["MATURIN_PEP517_ARGS"] = "--profile=dev" - session.run_always("python", "-m", "pip", "install", "-v", ".[dev]") + session.install("-v", ".[dev]") def try_install_binary(package: str, constraint: str): try: From 55d379cff8e4157024ffe22215715bd04a5fb1a1 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sun, 31 Aug 2025 21:36:42 +0100 Subject: [PATCH 834/936] ci: remove debug print from noxfile (#5385) --- noxfile.py | 1 - 1 file changed, 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index f1ccdd06035..d2d551b12f7 100644 --- a/noxfile.py +++ b/noxfile.py @@ -109,7 +109,6 @@ def test_rust(session: nox.Session): for feature_set in _get_feature_sets(): flags = extra_flags.copy() - print(feature_set) if feature_set is None or "full" not in feature_set: # doctests require at least the macros feature, which is From 5226450f9ae259a3010d1b7ac6c56509382adab4 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Tue, 2 Sep 2025 21:47:18 +0200 Subject: [PATCH 835/936] add second lifetime to `FromPyObject` (#4390) * replace `FromPyObject` with `FromPyObjectBound` * add migration guide entry * add `FromPyObjectOwned` * add newsfragments * use `FromPyObjectOwned` in derive macro * apply doc suggestions Co-authored-by: David Hewitt * improve `FromPyObject` docs * `FromPyObject` docs take 2 * fix clippy * add `FromPyObject::Error` * review feedback --------- Co-authored-by: David Hewitt --- guide/src/class.md | 2 + guide/src/migration.md | 130 +++++++++++ newsfragments/4390.added.md | 2 + newsfragments/4390.changed.md | 2 + newsfragments/4390.removed.md | 1 + pyo3-benches/benches/bench_dict.rs | 15 +- pyo3-macros-backend/src/frompyobject.rs | 10 +- pyo3-macros-backend/src/pyclass.rs | 3 + src/buffer.rs | 10 +- src/conversion.rs | 276 ++++++++++++++---------- src/conversions/bigdecimal.rs | 8 +- src/conversions/bytes.rs | 13 +- src/conversions/chrono.rs | 71 +++--- src/conversions/chrono_tz.rs | 9 +- src/conversions/either.rs | 14 +- src/conversions/hashbrown.rs | 39 ++-- src/conversions/indexmap.rs | 21 +- src/conversions/jiff.rs | 66 ++++-- src/conversions/num_bigint.rs | 32 +-- src/conversions/num_complex.rs | 10 +- src/conversions/num_rational.rs | 8 +- src/conversions/ordered_float.rs | 13 +- src/conversions/rust_decimal.rs | 8 +- src/conversions/smallvec.rs | 23 +- src/conversions/std/array.rs | 24 ++- src/conversions/std/cell.rs | 11 +- src/conversions/std/ipaddr.rs | 9 +- src/conversions/std/map.rs | 38 ++-- src/conversions/std/num.rs | 52 +++-- src/conversions/std/option.rs | 12 +- src/conversions/std/osstr.rs | 13 +- src/conversions/std/path.rs | 10 +- src/conversions/std/set.rs | 41 ++-- src/conversions/std/slice.rs | 14 +- src/conversions/std/string.rs | 30 +-- src/conversions/std/time.rs | 12 +- src/conversions/time.rs | 50 +++-- src/conversions/uuid.rs | 8 +- src/impl_/extract_argument.rs | 34 +-- src/impl_/frompyobject.rs | 16 +- src/inspect/types.rs | 2 +- src/instance.rs | 34 ++- src/prelude.rs | 2 +- src/pybacked.rs | 19 +- src/pyclass/guard.rs | 15 +- src/types/any.rs | 15 +- src/types/boolobject.rs | 15 +- src/types/float.rs | 18 +- src/types/sequence.rs | 17 +- src/types/tuple.rs | 14 +- tests/ui/invalid_cancel_handle.stderr | 12 +- 51 files changed, 863 insertions(+), 460 deletions(-) create mode 100644 newsfragments/4390.added.md create mode 100644 newsfragments/4390.changed.md create mode 100644 newsfragments/4390.removed.md diff --git a/guide/src/class.md b/guide/src/class.md index 911d1e7f72b..942effed147 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -1410,6 +1410,7 @@ impl pyo3::PyClass for MyClass { impl<'a, 'holder, 'py> pyo3::impl_::extract_argument::PyFunctionArgument<'a, 'holder, 'py, false> for &'holder MyClass { type Holder = ::std::option::Option>; + type Error = pyo3::PyErr; #[cfg(feature = "experimental-inspect")] const INPUT_TYPE: &'static str = "MyClass"; @@ -1422,6 +1423,7 @@ impl<'a, 'holder, 'py> pyo3::impl_::extract_argument::PyFunctionArgument<'a, 'ho impl<'a, 'holder, 'py> pyo3::impl_::extract_argument::PyFunctionArgument<'a, 'holder, 'py, false> for &'holder mut MyClass { type Holder = ::std::option::Option>; + type Error = pyo3::PyErr; #[cfg(feature = "experimental-inspect")] const INPUT_TYPE: &'static str = "MyClass"; diff --git a/guide/src/migration.md b/guide/src/migration.md index 8e7f2b6ecab..9985748d8b5 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -3,6 +3,136 @@ 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.26.* to 0.27 +### `FromPyObject` reworked for flexibility and efficiency +
+Click to expand + +With the removal of the `gil-ref` API in PyO3 0.23 it is now possible to fully split the Python lifetime +`'py` and the input lifetime `'a`. This allows borrowing from the input data without extending the +lifetime of being attached to the interpreter. + +`FromPyObject` now takes an additional lifetime `'a` describing the input lifetime. The argument +type of the `extract` method changed from `&Bound<'py, PyAny>` to `Borrowed<'a, 'py, PyAny>`. This was +done because `&'a Bound<'py, PyAny>` would have an implicit restriction `'py: 'a` due to the reference type. + +This new form was partly implemented already in 0.22 using the internal `FromPyObjectBound` trait and +is now extended to all types. + +Most implementations can just add an elided lifetime to migrate. + +Additionally `FromPyObject` gained an associated type `Error`. This is the error type that can be used +in case of a conversion error. During migration using `PyErr` is a good default, later a custom error +type can be introduced to prevent unneccessary creation of Python exception objects and improved type safety. + +Before: +```rust,ignore +impl<'py> FromPyObject<'py> for IpAddr { + fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { + ... + } +} +``` + +After +```rust,ignore +impl<'py> FromPyObject<'_, 'py> for IpAddr { + type Error = PyErr; + + fn extract(obj: Borrowed<'_, 'py, PyAny>) -> Result { + ... + // since `Borrowed` derefs to `&Bound`, the body often + // needs no changes, or adding an occasional `&` + } +} +``` + +Occasionally, more steps are necessary. For generic types, the bounds need to be adjusted. The +correct bound depends on how the type is used. + +For simple wrapper types usually it's possible to just forward the bound. + +Before: +```rust,ignore +struct MyWrapper(T); + +impl<'py, T> FromPyObject<'py> for MyWrapper +where + T: FromPyObject<'py> +{ + fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { + ob.extract().map(MyWrapper) + } +} +``` + +After: +```rust +# use pyo3::prelude::*; +# #[allow(dead_code)] +# pub struct MyWrapper(T); +impl<'a, 'py, T> FromPyObject<'a, 'py> for MyWrapper +where + T: FromPyObject<'a, 'py> +{ + type Error = T::Error; + + fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result { + obj.extract().map(MyWrapper) + } +} +``` + +Container types that need to create temporary Python references during extraction, for example +extracing from a `PyList`, requires a stronger bound. For these the `FromPyObjectOwned` trait was +introduced. It is automatically implemented for any type that implements `FromPyObject` and does not +borrow from the input. It is intended to be used as a trait bound in these situations. + +Before: +```rust,ignore +struct MyVec(Vec); +impl<'py, T> FromPyObject<'py> for Vec +where + T: FromPyObject<'py>, +{ + fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { + let mut v = MyVec(Vec::new()); + for item in obj.try_iter()? { + v.0.push(item?.extract::()?); + } + Ok(v) + } +} +``` + +After: +```rust +# use pyo3::prelude::*; +# #[allow(dead_code)] +# pub struct MyVec(Vec); +impl<'py, T> FromPyObject<'_, 'py> for MyVec +where + T: FromPyObjectOwned<'py> // 👈 can only extract owned values, because each `item` below + // is a temporary short lived owned reference +{ + type Error = PyErr; + + fn extract(obj: Borrowed<'_, 'py, PyAny>) -> Result { + let mut v = MyVec(Vec::new()); + for item in obj.try_iter()? { + v.0.push(item?.extract::().map_err(Into::into)?); // `map_err` is needed because `?` uses `From`, not `Into` 🙁 + } + Ok(v) + } +} +``` + +This is very similar to `serde`s [`Deserialize`] and [`DeserializeOwned`] traits, see [here](https://serde.rs/lifetimes.html). + +[`Deserialize`]: https://docs.rs/serde/latest/serde/trait.Deserialize.html +[`DeserializeOwned`]: https://docs.rs/serde/latest/serde/de/trait.DeserializeOwned.html +
+ ## from 0.25.* to 0.26 ### Rename of `Python::with_gil`, `Python::allow_threads`, and `pyo3::prepare_freethreaded_python`
diff --git a/newsfragments/4390.added.md b/newsfragments/4390.added.md new file mode 100644 index 00000000000..c234b0b6547 --- /dev/null +++ b/newsfragments/4390.added.md @@ -0,0 +1,2 @@ +added `FromPyObjectOwned` as more convenient trait bound +added `Borrowed::extract`, same as `PyAnyMethods::extract`, but does not restrict the lifetime by deref \ No newline at end of file diff --git a/newsfragments/4390.changed.md b/newsfragments/4390.changed.md new file mode 100644 index 00000000000..690bdaf17e5 --- /dev/null +++ b/newsfragments/4390.changed.md @@ -0,0 +1,2 @@ +added second lifetime to `FromPyObject` +reintroduced `extract` method \ No newline at end of file diff --git a/newsfragments/4390.removed.md b/newsfragments/4390.removed.md new file mode 100644 index 00000000000..a40ee2db268 --- /dev/null +++ b/newsfragments/4390.removed.md @@ -0,0 +1 @@ +removed `FromPyObjectBound` \ No newline at end of file diff --git a/pyo3-benches/benches/bench_dict.rs b/pyo3-benches/benches/bench_dict.rs index 7959a60d7ca..8ea38b7f67e 100644 --- a/pyo3-benches/benches/bench_dict.rs +++ b/pyo3-benches/benches/bench_dict.rs @@ -62,8 +62,9 @@ fn extract_hashmap(b: &mut Bencher<'_>) { let dict = (0..LEN as u64) .map(|i| (i, i * 2)) .into_py_dict(py) - .unwrap(); - b.iter(|| HashMap::::extract_bound(&dict)); + .unwrap() + .into_any(); + b.iter(|| HashMap::::extract(dict.as_borrowed())); }); } @@ -73,8 +74,9 @@ fn extract_btreemap(b: &mut Bencher<'_>) { let dict = (0..LEN as u64) .map(|i| (i, i * 2)) .into_py_dict(py) - .unwrap(); - b.iter(|| BTreeMap::::extract_bound(&dict)); + .unwrap() + .into_any(); + b.iter(|| BTreeMap::::extract(dict.as_borrowed())); }); } @@ -84,8 +86,9 @@ fn extract_hashbrown_map(b: &mut Bencher<'_>) { let dict = (0..LEN as u64) .map(|i| (i, i * 2)) .into_py_dict(py) - .unwrap(); - b.iter(|| hashbrown::HashMap::::extract_bound(&dict)); + .unwrap() + .into_any(); + b.iter(|| hashbrown::HashMap::::extract(dict.as_borrowed())); }); } diff --git a/pyo3-macros-backend/src/frompyobject.rs b/pyo3-macros-backend/src/frompyobject.rs index b674f4e530e..27b83673982 100644 --- a/pyo3-macros-backend/src/frompyobject.rs +++ b/pyo3-macros-backend/src/frompyobject.rs @@ -499,7 +499,7 @@ impl<'a> Container<'a> { let ty = ty.clone().elide_lifetimes(); let pyo3_crate_path = &ctx.pyo3_path; builder.push_tokens( - quote! { <#ty as #pyo3_crate_path::FromPyObject<'_>>::INPUT_TYPE.as_bytes() }, + quote! { <#ty as #pyo3_crate_path::FromPyObject<'_, '_>>::INPUT_TYPE.as_bytes() }, ) } } @@ -543,7 +543,7 @@ pub fn build_derive_from_pyobject(tokens: &DeriveInput) -> Result { let gen_ident = ¶m.ident; where_clause .predicates - .push(parse_quote!(#gen_ident: #pyo3_path::FromPyObject<'py>)) + .push(parse_quote!(#gen_ident: #pyo3_path::conversion::FromPyObjectOwned<#lt_param>)) } let derives = match &tokens.data { @@ -605,8 +605,10 @@ pub fn build_derive_from_pyobject(tokens: &DeriveInput) -> Result { let ident = &tokens.ident; Ok(quote!( #[automatically_derived] - 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 { + impl #impl_generics #pyo3_path::FromPyObject<'_, #lt_param> for #ident #ty_generics #where_clause { + type Error = #pyo3_path::PyErr; + fn extract(obj: #pyo3_path::Borrowed<'_, #lt_param, #pyo3_path::PyAny>) -> ::std::result::Result { + let obj: &#pyo3_path::Bound<'_, _> = &*obj; #derives } #input_type diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index ee225399fe3..91f9117fc9e 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -2327,6 +2327,7 @@ impl<'a> PyClassImplsBuilder<'a> { impl<'a, 'holder, 'py> #pyo3_path::impl_::extract_argument::PyFunctionArgument<'a, 'holder, 'py, false> for &'holder #cls { type Holder = ::std::option::Option<#pyo3_path::PyClassGuard<'a, #cls>>; + type Error = #pyo3_path::PyErr; #input_type @@ -2341,6 +2342,7 @@ impl<'a> PyClassImplsBuilder<'a> { impl<'a, 'holder, 'py> #pyo3_path::impl_::extract_argument::PyFunctionArgument<'a, 'holder, 'py, false> for &'holder #cls { type Holder = ::std::option::Option<#pyo3_path::PyClassGuard<'a, #cls>>; + type Error = #pyo3_path::PyErr; #input_type @@ -2353,6 +2355,7 @@ impl<'a> PyClassImplsBuilder<'a> { impl<'a, 'holder, 'py> #pyo3_path::impl_::extract_argument::PyFunctionArgument<'a, 'holder, 'py, false> for &'holder mut #cls { type Holder = ::std::option::Option<#pyo3_path::PyClassGuardMut<'a, #cls>>; + type Error =#pyo3_path::PyErr; #input_type diff --git a/src/buffer.rs b/src/buffer.rs index c3aad73e04d..bdd0ee214fe 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -18,8 +18,8 @@ // DEALINGS IN THE SOFTWARE. //! `PyBuffer` implementation -use crate::Bound; use crate::{err, exceptions::PyBufferError, ffi, FromPyObject, PyAny, PyResult, Python}; +use crate::{Borrowed, Bound, PyErr}; use std::ffi::{ c_char, c_int, c_long, c_longlong, c_schar, c_short, c_uchar, c_uint, c_ulong, c_ulonglong, c_ushort, c_void, @@ -185,9 +185,11 @@ pub unsafe trait Element: Copy { fn is_compatible_format(format: &CStr) -> bool; } -impl FromPyObject<'_> for PyBuffer { - fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult> { - Self::get(obj) +impl FromPyObject<'_, '_> for PyBuffer { + type Error = PyErr; + + fn extract(obj: Borrowed<'_, '_, PyAny>) -> Result, Self::Error> { + Self::get(&obj) } } diff --git a/src/conversion.rs b/src/conversion.rs index 9d54124e29a..3fdb0c47060 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -247,7 +247,8 @@ impl<'py, T> IntoPyObjectExt<'py> for T where T: IntoPyObject<'py> {} /// Extract a type from a Python object. /// /// -/// Normal usage is through the `extract` methods on [`Bound`] and [`Py`], which forward to this trait. +/// Normal usage is through the `extract` methods on [`Bound`], [`Borrowed`] and [`Py`], which +/// forward to this trait. /// /// # Examples /// @@ -271,97 +272,114 @@ impl<'py, T> IntoPyObjectExt<'py> for T where T: IntoPyObject<'py> {} /// # } /// ``` /// -// /// FIXME: until `FromPyObject` can pick up a second lifetime, the below commentary is no longer -// /// true. Update and restore this documentation at that time. -// /// -// /// Note: depending on the implementation, the lifetime of the extracted result may -// /// depend on the lifetime of the `obj` or the `prepared` variable. -// /// -// /// For example, when extracting `&str` from a Python byte string, the resulting string slice will -// /// point to the existing string data (lifetime: `'py`). -// /// On the other hand, when extracting `&str` from a Python Unicode string, the preparation step -// /// will convert the string to UTF-8, and the resulting string slice will have lifetime `'prepared`. -// /// Since which case applies depends on the runtime type of the Python object, -// /// both the `obj` and `prepared` variables must outlive the resulting string slice. -/// -/// During the migration of PyO3 from the "GIL Refs" API to the `Bound` smart pointer, this trait -/// has two methods `extract` and `extract_bound` which are defaulted to call each other. To avoid -/// infinite recursion, implementors must implement at least one of these methods. The recommendation -/// is to implement `extract_bound` and leave `extract` as the default implementation. -pub trait FromPyObject<'py>: Sized { - /// Provides the type hint information for this type when it appears as an argument. - /// - /// For example, `Vec` would be `collections.abc.Sequence[int]`. - /// The default value is `typing.Any`, which is correct for any type. - #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = "typing.Any"; - - /// Extracts `Self` from the bound smart pointer `obj`. - /// - /// Implementors are encouraged to implement this method and leave `extract` defaulted, as - /// this will be most compatible with PyO3's future API. - fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult; - - /// Extracts the type hint information for this type when it appears as an argument. - /// - /// 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 - /// [`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 - } -} - -mod from_py_object_bound_sealed { - use crate::{pyclass::boolean_struct::False, PyClass, PyClassGuard, PyClassGuardMut}; - - /// Private seal for the `FromPyObjectBound` trait. - /// - /// This prevents downstream types from implementing the trait before - /// PyO3 is ready to declare the trait as public API. - pub trait Sealed {} - - // This generic implementation is why the seal is separate from - // `crate::sealed::Sealed`. - impl<'py, T> Sealed for T where T: super::FromPyObject<'py> {} - impl Sealed for PyClassGuard<'_, T> where T: PyClass {} - impl Sealed for PyClassGuardMut<'_, T> where T: PyClass {} - impl Sealed for &'_ str {} - impl Sealed for std::borrow::Cow<'_, str> {} - impl Sealed for &'_ [u8] {} - impl Sealed for std::borrow::Cow<'_, [u8]> {} -} - -/// Expected form of [`FromPyObject`] to be used in a future PyO3 release. +/// Note: Depending on the Python version and implementation, some [`FromPyObject`] implementations +/// may produce a result that borrows into the Python type. This is described by the input lifetime +/// `'a` of `obj`. +/// +/// Types that must not borrow from the input can use [`FromPyObjectOwned`] as a restriction. This +/// is most often the case for collection types. See its documentation for more details. +/// +/// # How to implement [`FromPyObject`]? +/// ## `#[derive(FromPyObject)]` +/// The simplest way to implement [`FromPyObject`] for a custom type is to make use of our derive +/// macro. +/// ```rust,no_run +/// # #![allow(dead_code)] +/// use pyo3::prelude::*; +/// +/// #[derive(FromPyObject)] +/// struct MyObject { +/// msg: String, +/// list: Vec +/// } +/// # fn main() {} +/// ``` +/// By default this will try to extract each field from the Python object by attribute access, but +/// this can be customized. For more information about the derive macro, its configuration as well +/// as its working principle for other types, take a look at the [guide]. +/// +/// In case the derive macro is not sufficient or can not be used for some other reason, +/// [`FromPyObject`] can be implemented manually. In the following types without lifetime parameters +/// are handled first, because they are a little bit simpler. Types with lifetime parameters are +/// explained below. +/// +/// ## Manual implementation for types without lifetime +/// Types that do not contain lifetime parameters are unable to borrow from the Python object, so +/// the lifetimes of [`FromPyObject`] can be elided: +/// ```rust,no_run +/// # #![allow(dead_code)] +/// use pyo3::prelude::*; +/// +/// struct MyObject { +/// msg: String, +/// list: Vec +/// } /// -/// The difference between this and `FromPyObject` is that this trait takes an -/// additional lifetime `'a`, which is the lifetime of the input `Bound`. +/// impl FromPyObject<'_, '_> for MyObject { +/// type Error = PyErr; /// -/// This allows implementations for `&'a str` and `&'a [u8]`, which could not -/// be expressed by the existing `FromPyObject` trait once the GIL Refs API was -/// removed. +/// fn extract(obj: Borrowed<'_, '_, PyAny>) -> Result { +/// Ok(MyObject { +/// msg: obj.getattr("msg")?.extract()?, +/// list: obj.getattr("list")?.extract()?, +/// }) +/// } +/// } +/// +/// # fn main() {} +/// ``` +/// This is basically what the derive macro above expands to. /// -/// # Usage +/// ## Manual implementation for types with lifetime paramaters +/// For types that contain lifetimes, these lifetimes need to be bound to the corresponding +/// [`FromPyObject`] lifetime. This is roughly how the extraction of a typed [`Bound`] is +/// implemented within PyO3. +/// +/// ```rust,no_run +/// # #![allow(dead_code)] +/// use pyo3::prelude::*; +/// use pyo3::types::PyString; /// -/// Users are prevented from implementing this trait, instead they should implement -/// the normal `FromPyObject` trait. This trait has a blanket implementation -/// for `T: FromPyObject`. +/// struct MyObject<'py>(Bound<'py, PyString>); +/// +/// impl<'py> FromPyObject<'_, 'py> for MyObject<'py> { +/// type Error = PyErr; +/// +/// fn extract(obj: Borrowed<'_, 'py, PyAny>) -> Result { +/// Ok(MyObject(obj.cast()?.to_owned())) +/// } +/// } +/// +/// # fn main() {} +/// ``` /// -/// The only case where this trait may have a use case to be implemented is when the -/// lifetime of the extracted value is tied to the lifetime `'a` of the input `Bound` -/// instead of the GIL lifetime `py`, as is the case for the `&'a str` implementation. +/// # Details +/// [`Cow<'a, str>`] is an example of an output type that may or may not borrow from the input +/// lifetime `'a`. Which variant will be produced depends on the runtime type of the Python object. +/// For a Python byte string, the existing string data can be borrowed for `'a` into a +/// [`Cow::Borrowed`]. For a Python Unicode string, the data may have to be reencoded to UTF-8, and +/// copied into a [`Cow::Owned`]. It does _not_ depend on the Python lifetime `'py`. /// -/// Please contact the PyO3 maintainers if you believe you have a use case for implementing -/// this trait before PyO3 is ready to change the main `FromPyObject` trait to take an -/// additional lifetime. +/// The output type may also depend on the Python lifetime `'py`. This allows the output type to +/// keep interacting with the Python interpreter. See also [`Bound<'py, T>`]. /// -/// Similarly, users should typically not call these trait methods and should instead -/// use this via the `extract` method on `Bound` and `Py`. -pub trait FromPyObjectBound<'a, 'py>: Sized + from_py_object_bound_sealed::Sealed { +/// [`Cow<'a, str>`]: std::borrow::Cow +/// [`Cow::Borrowed`]: std::borrow::Cow::Borrowed +/// [`Cow::Owned`]: std::borrow::Cow::Owned +/// [guide]: https://pyo3.rs/latest/conversions/traits.html#deriving-frompyobject +pub trait FromPyObject<'a, 'py>: Sized { + /// The type returned in the event of a conversion error. + /// + /// For most use cases defaulting to [PyErr] here is perfectly acceptable. Using a custom error + /// type can be used to avoid having to create a Python exception object in the case where that + /// exception never reaches Python. This may lead to slightly better performance under certain + /// conditions. + /// + /// # Note + /// Unfortunately `Try` and thus `?` is based on [`From`], not [`Into`], so implementations may + /// need to use `.map_err(Into::into)` sometimes to convert a generic `Error` into a [`PyErr`]. + type Error: Into; + /// Provides the type hint information for this type when it appears as an argument. /// /// For example, `Vec` would be `collections.abc.Sequence[int]`. @@ -373,7 +391,7 @@ pub trait FromPyObjectBound<'a, 'py>: Sized + from_py_object_bound_sealed::Seale /// /// Users are advised against calling this method directly: instead, use this via /// [`Bound<'_, PyAny>::extract`](crate::types::any::PyAnyMethods::extract) or [`Py::extract`]. - fn from_py_object_bound(ob: Borrowed<'a, 'py, PyAny>) -> PyResult; + fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result; /// Extracts the type hint information for this type when it appears as an argument. /// @@ -389,56 +407,98 @@ pub trait FromPyObjectBound<'a, 'py>: Sized + from_py_object_bound_sealed::Seale } } -impl<'py, T> FromPyObjectBound<'_, 'py> for T -where - T: FromPyObject<'py>, -{ - #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = T::INPUT_TYPE; - - fn from_py_object_bound(ob: Borrowed<'_, 'py, PyAny>) -> PyResult { - Self::extract_bound(&ob) - } - - #[cfg(feature = "experimental-inspect")] - fn type_input() -> TypeInfo { - ::type_input() - } -} +/// A data structure that can be extracted without borrowing any data from the input. +/// +/// This is primarily useful for trait bounds. For example a [`FromPyObject`] implementation of a +/// wrapper type may be able to borrow data from the input, but a [`FromPyObject`] implementation of +/// a collection type may only extract owned data. +/// +/// For example [`PyList`] will not hand out references tied to its own lifetime, but "owned" +/// references independent of it. (Similar to [`Vec>`] where you clone the [`Arc`] out). +/// This makes it impossible to collect borrowed types in a collection, since they would not borrow +/// from the original [`PyList`], but the much shorter lived element reference. See the example +/// below. +/// +/// ```,no_run +/// # use pyo3::prelude::*; +/// # #[allow(dead_code)] +/// pub struct MyWrapper(T); +/// +/// impl<'a, 'py, T> FromPyObject<'a, 'py> for MyWrapper +/// where +/// T: FromPyObject<'a, 'py> +/// { +/// type Error = T::Error; +/// +/// fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result { +/// obj.extract().map(MyWrapper) +/// } +/// } +/// +/// # #[allow(dead_code)] +/// pub struct MyVec(Vec); +/// +/// impl<'py, T> FromPyObject<'_, 'py> for MyVec +/// where +/// T: FromPyObjectOwned<'py> // 👈 can only extract owned values, because each `item` below +/// // is a temporary short lived owned reference +/// { +/// type Error = PyErr; +/// +/// fn extract(obj: Borrowed<'_, 'py, PyAny>) -> Result { +/// let mut v = MyVec(Vec::new()); +/// for item in obj.try_iter()? { +/// v.0.push(item?.extract::().map_err(Into::into)?); +/// } +/// Ok(v) +/// } +/// } +/// ``` +/// +/// [`PyList`]: crate::types::PyList +/// [`Arc`]: std::sync::Arc +pub trait FromPyObjectOwned<'py>: for<'a> FromPyObject<'a, 'py> {} +impl<'py, T> FromPyObjectOwned<'py> for T where T: for<'a> FromPyObject<'a, 'py> {} -impl FromPyObject<'_> for T +impl FromPyObject<'_, '_> for T where T: PyClass + Clone, { + type Error = PyErr; + #[cfg(feature = "experimental-inspect")] const INPUT_TYPE: &'static str = ::TYPE_NAME; - fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { + fn extract(obj: Borrowed<'_, '_, PyAny>) -> Result { let bound = obj.cast::()?; Ok(bound.try_borrow()?.clone()) } } -impl<'py, T> FromPyObject<'py> for PyRef<'py, T> +impl<'py, T> FromPyObject<'_, 'py> for PyRef<'py, T> where T: PyClass, { + type Error = PyErr; + #[cfg(feature = "experimental-inspect")] const INPUT_TYPE: &'static str = ::TYPE_NAME; - fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { + fn extract(obj: Borrowed<'_, 'py, PyAny>) -> Result { obj.cast::()?.try_borrow().map_err(Into::into) } } -impl<'py, T> FromPyObject<'py> for PyRefMut<'py, T> +impl<'py, T> FromPyObject<'_, 'py> for PyRefMut<'py, T> where T: PyClass, { + type Error = PyErr; + #[cfg(feature = "experimental-inspect")] const INPUT_TYPE: &'static str = ::TYPE_NAME; - fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { + fn extract(obj: Borrowed<'_, 'py, PyAny>) -> Result { obj.cast::()?.try_borrow_mut().map_err(Into::into) } } diff --git a/src/conversions/bigdecimal.rs b/src/conversions/bigdecimal.rs index af976431894..991c5567cce 100644 --- a/src/conversions/bigdecimal.rs +++ b/src/conversions/bigdecimal.rs @@ -56,7 +56,7 @@ use crate::{ exceptions::PyValueError, sync::PyOnceLock, types::{PyAnyMethods, PyStringMethods, PyType}, - Bound, FromPyObject, IntoPyObject, Py, PyAny, PyErr, PyResult, Python, + Borrowed, Bound, FromPyObject, IntoPyObject, Py, PyAny, PyErr, PyResult, Python, }; use bigdecimal::BigDecimal; use num_bigint::Sign; @@ -71,8 +71,10 @@ fn get_invalid_operation_error_cls(py: Python<'_>) -> PyResult<&Bound<'_, PyType INVALID_OPERATION_CLS.import(py, "decimal", "InvalidOperation") } -impl FromPyObject<'_> for BigDecimal { - fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { +impl FromPyObject<'_, '_> for BigDecimal { + type Error = PyErr; + + fn extract(obj: Borrowed<'_, '_, PyAny>) -> PyResult { let py_str = &obj.str()?; let rs_str = &py_str.to_cow()?; BigDecimal::from_str(rs_str).map_err(|e| PyValueError::new_err(e.to_string())) diff --git a/src/conversions/bytes.rs b/src/conversions/bytes.rs index 9c615e84a5e..0aba7409578 100644 --- a/src/conversions/bytes.rs +++ b/src/conversions/bytes.rs @@ -67,13 +67,14 @@ use bytes::Bytes; use crate::conversion::IntoPyObject; use crate::instance::Bound; use crate::pybacked::PyBackedBytes; -use crate::types::any::PyAnyMethods; use crate::types::PyBytes; -use crate::{FromPyObject, PyAny, PyErr, PyResult, Python}; +use crate::{Borrowed, DowncastError, FromPyObject, PyAny, PyErr, Python}; -impl FromPyObject<'_> for Bytes { - fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { - Ok(Bytes::from_owner(ob.extract::()?)) +impl<'a, 'py> FromPyObject<'a, 'py> for Bytes { + type Error = DowncastError<'a, 'py>; + + fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result { + Ok(Bytes::from_owner(obj.extract::()?)) } } @@ -100,7 +101,7 @@ impl<'py> IntoPyObject<'py> for &Bytes { #[cfg(test)] mod tests { use super::*; - use crate::types::{PyByteArray, PyByteArrayMethods, PyBytes}; + use crate::types::{PyAnyMethods, PyByteArray, PyByteArrayMethods, PyBytes}; use crate::Python; #[test] diff --git a/src/conversions/chrono.rs b/src/conversions/chrono.rs index bd97d3409fc..d250cfc03d5 100644 --- a/src/conversions/chrono.rs +++ b/src/conversions/chrono.rs @@ -41,7 +41,7 @@ //! } //! ``` -use crate::conversion::IntoPyObject; +use crate::conversion::{FromPyObjectOwned, IntoPyObject}; use crate::exceptions::{PyTypeError, PyUserWarning, PyValueError}; use crate::intern; use crate::types::any::PyAnyMethods; @@ -108,8 +108,10 @@ impl<'py> IntoPyObject<'py> for &Duration { } } -impl FromPyObject<'_> for Duration { - fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { +impl FromPyObject<'_, '_> for Duration { + type Error = PyErr; + + fn extract(ob: Borrowed<'_, '_, PyAny>) -> Result { let delta = ob.cast::()?; // Python size are much lower than rust size so we do not need bound checks. // 0 <= microseconds < 1000000 @@ -162,9 +164,11 @@ impl<'py> IntoPyObject<'py> for &NaiveDate { } } -impl FromPyObject<'_> for NaiveDate { - fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { - let date = ob.cast::()?; +impl FromPyObject<'_, '_> for NaiveDate { + type Error = PyErr; + + fn extract(ob: Borrowed<'_, '_, PyAny>) -> Result { + let date = &*ob.cast::()?; py_date_to_naive_date(date) } } @@ -204,9 +208,11 @@ impl<'py> IntoPyObject<'py> for &NaiveTime { } } -impl FromPyObject<'_> for NaiveTime { - fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { - let time = ob.cast::()?; +impl FromPyObject<'_, '_> for NaiveTime { + type Error = PyErr; + + fn extract(ob: Borrowed<'_, '_, PyAny>) -> Result { + let time = &*ob.cast::()?; py_time_to_naive_time(time) } } @@ -247,9 +253,11 @@ impl<'py> IntoPyObject<'py> for &NaiveDateTime { } } -impl FromPyObject<'_> for NaiveDateTime { - fn extract_bound(dt: &Bound<'_, PyAny>) -> PyResult { - let dt = dt.cast::()?; +impl FromPyObject<'_, '_> for NaiveDateTime { + type Error = PyErr; + + fn extract(dt: Borrowed<'_, '_, PyAny>) -> Result { + let dt = &*dt.cast::()?; // If the user tries to convert a timezone aware datetime into a naive one, // we return a hard error. We could silently remove tzinfo, or assume local timezone @@ -324,13 +332,18 @@ where } } -impl FromPyObject<'py>> FromPyObject<'_> for DateTime { - fn extract_bound(dt: &Bound<'_, PyAny>) -> PyResult> { - let dt = dt.cast::()?; +impl<'py, Tz> FromPyObject<'_, 'py> for DateTime +where + Tz: TimeZone + FromPyObjectOwned<'py>, +{ + type Error = PyErr; + + fn extract(dt: Borrowed<'_, 'py, PyAny>) -> Result { + let dt = &*dt.cast::()?; let tzinfo = dt.get_tzinfo(); let tz = if let Some(tzinfo) = tzinfo { - tzinfo.extract()? + tzinfo.extract().map_err(Into::into)? } else { return Err(PyTypeError::new_err( "expected a datetime with non-None tzinfo", @@ -382,12 +395,14 @@ impl<'py> IntoPyObject<'py> for &FixedOffset { } } -impl FromPyObject<'_> for FixedOffset { +impl FromPyObject<'_, '_> for FixedOffset { + type Error = PyErr; + /// Convert python tzinfo to rust [`FixedOffset`]. /// /// Note that the conversion will result in precision lost in microseconds as chrono offset /// does not supports microseconds. - fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { + fn extract(ob: Borrowed<'_, '_, PyAny>) -> Result { let ob = ob.cast::()?; // Passing Python's None to the `utcoffset` function will only @@ -431,8 +446,10 @@ impl<'py> IntoPyObject<'py> for &Utc { } } -impl FromPyObject<'_> for Utc { - fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { +impl FromPyObject<'_, '_> for Utc { + type Error = PyErr; + + fn extract(ob: Borrowed<'_, '_, PyAny>) -> Result { let py_utc = PyTzInfo::utc(ob.py())?; if ob.eq(py_utc)? { Ok(Utc) @@ -475,8 +492,10 @@ impl<'py> IntoPyObject<'py> for &Local { } #[cfg(feature = "chrono-local")] -impl FromPyObject<'_> for Local { - fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { +impl FromPyObject<'_, '_> for Local { + type Error = PyErr; + + fn extract(ob: Borrowed<'_, '_, PyAny>) -> PyResult { let local_tz = Local.into_pyobject(ob.py())?; if ob.eq(local_tz)? { Ok(Local) @@ -543,7 +562,9 @@ fn warn_truncated_leap_second(obj: &Bound<'_, PyAny>) { } #[cfg(not(Py_LIMITED_API))] -fn py_date_to_naive_date(py_date: &impl PyDateAccess) -> PyResult { +fn py_date_to_naive_date( + py_date: impl std::ops::Deref, +) -> PyResult { NaiveDate::from_ymd_opt( py_date.get_year(), py_date.get_month().into(), @@ -563,7 +584,9 @@ fn py_date_to_naive_date(py_date: &Bound<'_, PyAny>) -> PyResult { } #[cfg(not(Py_LIMITED_API))] -fn py_time_to_naive_time(py_time: &impl PyTimeAccess) -> PyResult { +fn py_time_to_naive_time( + py_time: impl std::ops::Deref, +) -> PyResult { NaiveTime::from_hms_micro_opt( py_time.get_hour().into(), py_time.get_minute().into(), diff --git a/src/conversions/chrono_tz.rs b/src/conversions/chrono_tz.rs index ff2b6b74d54..6926559c084 100644 --- a/src/conversions/chrono_tz.rs +++ b/src/conversions/chrono_tz.rs @@ -38,7 +38,7 @@ use crate::conversion::IntoPyObject; use crate::exceptions::PyValueError; use crate::pybacked::PyBackedStr; use crate::types::{any::PyAnyMethods, PyTzInfo}; -use crate::{intern, Bound, FromPyObject, PyAny, PyErr, PyResult, Python}; +use crate::{intern, Borrowed, Bound, FromPyObject, PyAny, PyErr, Python}; use chrono_tz::Tz; use std::str::FromStr; @@ -63,8 +63,10 @@ impl<'py> IntoPyObject<'py> for &Tz { } } -impl FromPyObject<'_> for Tz { - fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { +impl FromPyObject<'_, '_> for Tz { + type Error = PyErr; + + fn extract(ob: Borrowed<'_, '_, PyAny>) -> Result { Tz::from_str( &ob.getattr(intern!(ob.py(), "key"))? .extract::()?, @@ -78,6 +80,7 @@ mod tests { use super::*; use crate::prelude::PyAnyMethods; use crate::types::PyTzInfo; + use crate::Bound; use crate::Python; use chrono::{DateTime, Utc}; use chrono_tz::Tz; diff --git a/src/conversions/either.rs b/src/conversions/either.rs index d073f3e2706..6a5acebae75 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::{ - exceptions::PyTypeError, types::any::PyAnyMethods, Bound, FromPyObject, IntoPyObject, - IntoPyObjectExt, PyAny, PyErr, PyResult, Python, + exceptions::PyTypeError, Borrowed, Bound, FromPyObject, IntoPyObject, IntoPyObjectExt, PyAny, + PyErr, Python, }; use either::Either; @@ -89,13 +89,15 @@ where } #[cfg_attr(docsrs, doc(cfg(feature = "either")))] -impl<'py, L, R> FromPyObject<'py> for Either +impl<'a, 'py, L, R> FromPyObject<'a, 'py> for Either where - L: FromPyObject<'py>, - R: FromPyObject<'py>, + L: FromPyObject<'a, 'py>, + R: FromPyObject<'a, 'py>, { + type Error = PyErr; + #[inline] - fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { + fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result { if let Ok(l) = obj.extract::() { Ok(Either::Left(l)) } else if let Ok(r) = obj.extract::() { diff --git a/src/conversions/hashbrown.rs b/src/conversions/hashbrown.rs index 26fcc5c627e..85bc350999b 100644 --- a/src/conversions/hashbrown.rs +++ b/src/conversions/hashbrown.rs @@ -17,7 +17,7 @@ //! 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::{ - conversion::IntoPyObject, + conversion::{FromPyObjectOwned, IntoPyObject}, types::{ any::PyAnyMethods, dict::PyDictMethods, @@ -25,7 +25,7 @@ use crate::{ set::{try_new_from_iter, PySetMethods}, PyDict, PyFrozenSet, PySet, }, - Bound, FromPyObject, PyAny, PyErr, PyResult, Python, + Borrowed, Bound, FromPyObject, PyAny, PyErr, PyResult, Python, }; use std::{cmp, hash}; @@ -67,17 +67,22 @@ where } } -impl<'py, K, V, S> FromPyObject<'py> for hashbrown::HashMap +impl<'py, K, V, S> FromPyObject<'_, 'py> for hashbrown::HashMap where - K: FromPyObject<'py> + cmp::Eq + hash::Hash, - V: FromPyObject<'py>, + K: FromPyObjectOwned<'py> + cmp::Eq + hash::Hash, + V: FromPyObjectOwned<'py>, S: hash::BuildHasher + Default, { - fn extract_bound(ob: &Bound<'py, PyAny>) -> Result { + type Error = PyErr; + + fn extract(ob: Borrowed<'_, 'py, PyAny>) -> Result { let dict = ob.cast::()?; let mut ret = hashbrown::HashMap::with_capacity_and_hasher(dict.len(), S::default()); - for (k, v) in dict { - ret.insert(k.extract()?, v.extract()?); + for (k, v) in dict.iter() { + ret.insert( + k.extract().map_err(Into::into)?, + v.extract().map_err(Into::into)?, + ); } Ok(ret) } @@ -111,17 +116,25 @@ where } } -impl<'py, K, S> FromPyObject<'py> for hashbrown::HashSet +impl<'py, K, S> FromPyObject<'_, 'py> for hashbrown::HashSet where - K: FromPyObject<'py> + cmp::Eq + hash::Hash, + K: FromPyObjectOwned<'py> + cmp::Eq + hash::Hash, S: hash::BuildHasher + Default, { - fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { + type Error = PyErr; + + fn extract(ob: Borrowed<'_, 'py, PyAny>) -> PyResult { match ob.cast::() { - Ok(set) => set.iter().map(|any| any.extract()).collect(), + Ok(set) => set + .iter() + .map(|any| any.extract().map_err(Into::into)) + .collect(), Err(err) => { if let Ok(frozen_set) = ob.cast::() { - frozen_set.iter().map(|any| any.extract()).collect() + frozen_set + .iter() + .map(|any| any.extract().map_err(Into::into)) + .collect() } else { Err(PyErr::from(err)) } diff --git a/src/conversions/indexmap.rs b/src/conversions/indexmap.rs index b3e8190d471..89ef8172961 100644 --- a/src/conversions/indexmap.rs +++ b/src/conversions/indexmap.rs @@ -87,9 +87,9 @@ //! # if another hash table was used, the order could be random //! ``` -use crate::conversion::IntoPyObject; +use crate::conversion::{FromPyObjectOwned, IntoPyObject}; use crate::types::*; -use crate::{Bound, FromPyObject, PyErr, Python}; +use crate::{Borrowed, Bound, FromPyObject, PyErr, Python}; use std::{cmp, hash}; impl<'py, K, V, H> IntoPyObject<'py> for indexmap::IndexMap @@ -130,17 +130,22 @@ where } } -impl<'py, K, V, S> FromPyObject<'py> for indexmap::IndexMap +impl<'py, K, V, S> FromPyObject<'_, 'py> for indexmap::IndexMap where - K: FromPyObject<'py> + cmp::Eq + hash::Hash, - V: FromPyObject<'py>, + K: FromPyObjectOwned<'py> + cmp::Eq + hash::Hash, + V: FromPyObjectOwned<'py>, S: hash::BuildHasher + Default, { - fn extract_bound(ob: &Bound<'py, PyAny>) -> Result { + type Error = PyErr; + + fn extract(ob: Borrowed<'_, 'py, PyAny>) -> Result { let dict = ob.cast::()?; let mut ret = indexmap::IndexMap::with_capacity_and_hasher(dict.len(), S::default()); - for (k, v) in dict { - ret.insert(k.extract()?, v.extract()?); + for (k, v) in dict.iter() { + ret.insert( + k.extract().map_err(Into::into)?, + v.extract().map_err(Into::into)?, + ); } Ok(ret) } diff --git a/src/conversions/jiff.rs b/src/conversions/jiff.rs index fd2503646c0..7ad84516f47 100644 --- a/src/conversions/jiff.rs +++ b/src/conversions/jiff.rs @@ -51,7 +51,7 @@ use crate::types::{PyAnyMethods, PyNone}; use crate::types::{PyDate, PyDateTime, PyDelta, PyTime, PyTzInfo, PyTzInfoAccess}; #[cfg(not(Py_LIMITED_API))] use crate::types::{PyDateAccess, PyDeltaAccess, PyTimeAccess}; -use crate::{intern, Bound, FromPyObject, IntoPyObject, PyAny, PyErr, PyResult, Python}; +use crate::{intern, Borrowed, Bound, FromPyObject, IntoPyObject, PyAny, PyErr, PyResult, Python}; use jiff::civil::{Date, DateTime, Time}; use jiff::tz::{Offset, TimeZone}; use jiff::{SignedDuration, Span, Timestamp, Zoned}; @@ -122,8 +122,10 @@ impl<'py> IntoPyObject<'py> for &Timestamp { } } -impl<'py> FromPyObject<'py> for Timestamp { - fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { +impl<'a, 'py> FromPyObject<'a, 'py> for Timestamp { + type Error = >::Error; + + fn extract(ob: Borrowed<'_, 'py, PyAny>) -> Result { let zoned = ob.extract::()?; Ok(zoned.timestamp()) } @@ -154,8 +156,10 @@ impl<'py> IntoPyObject<'py> for &Date { } } -impl<'py> FromPyObject<'py> for Date { - fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { +impl<'py> FromPyObject<'_, 'py> for Date { + type Error = PyErr; + + fn extract(ob: Borrowed<'_, 'py, PyAny>) -> PyResult { let date = ob.cast::()?; #[cfg(not(Py_LIMITED_API))] @@ -206,11 +210,13 @@ impl<'py> IntoPyObject<'py> for &Time { } } -impl<'py> FromPyObject<'py> for Time { - fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { - let ob = ob.cast::()?; +impl<'py> FromPyObject<'_, 'py> for Time { + type Error = PyErr; - pytime_to_time(ob) + fn extract(ob: Borrowed<'_, 'py, PyAny>) -> PyResult { + let ob = ob.cast::()?; + #[allow(clippy::explicit_auto_deref)] + pytime_to_time(&*ob) } } @@ -234,8 +240,10 @@ impl<'py> IntoPyObject<'py> for &DateTime { } } -impl<'py> FromPyObject<'py> for DateTime { - fn extract_bound(dt: &Bound<'py, PyAny>) -> PyResult { +impl<'py> FromPyObject<'_, 'py> for DateTime { + type Error = PyErr; + + fn extract(dt: Borrowed<'_, 'py, PyAny>) -> PyResult { let dt = dt.cast::()?; let has_tzinfo = dt.get_tzinfo().is_some(); @@ -243,7 +251,8 @@ impl<'py> FromPyObject<'py> for DateTime { return Err(PyTypeError::new_err("expected a datetime without tzinfo")); } - Ok(DateTime::from_parts(dt.extract()?, pytime_to_time(dt)?)) + #[allow(clippy::explicit_auto_deref)] + Ok(DateTime::from_parts(dt.extract()?, pytime_to_time(&*dt)?)) } } @@ -283,8 +292,10 @@ impl<'py> IntoPyObject<'py> for &Zoned { } } -impl<'py> FromPyObject<'py> for Zoned { - fn extract_bound(dt: &Bound<'py, PyAny>) -> PyResult { +impl<'py> FromPyObject<'_, 'py> for Zoned { + type Error = PyErr; + + fn extract(dt: Borrowed<'_, 'py, PyAny>) -> PyResult { let dt = dt.cast::()?; let tz = dt @@ -295,7 +306,8 @@ impl<'py> FromPyObject<'py> for Zoned { "expected a datetime with non-None tzinfo", )) })?; - let datetime = DateTime::from_parts(dt.extract()?, pytime_to_time(dt)?); + #[allow(clippy::explicit_auto_deref)] + let datetime = DateTime::from_parts(dt.extract()?, pytime_to_time(&*dt)?); let zoned = tz.into_ambiguous_zoned(datetime); #[cfg(not(Py_LIMITED_API))] @@ -340,8 +352,10 @@ impl<'py> IntoPyObject<'py> for &TimeZone { } } -impl<'py> FromPyObject<'py> for TimeZone { - fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { +impl<'py> FromPyObject<'_, 'py> for TimeZone { + type Error = PyErr; + + fn extract(ob: Borrowed<'_, 'py, PyAny>) -> PyResult { let ob = ob.cast::()?; let attr = intern!(ob.py(), "key"); @@ -377,8 +391,10 @@ impl<'py> IntoPyObject<'py> for Offset { } } -impl<'py> FromPyObject<'py> for Offset { - fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { +impl<'py> FromPyObject<'_, 'py> for Offset { + type Error = PyErr; + + fn extract(ob: Borrowed<'_, 'py, PyAny>) -> PyResult { let py = ob.py(); let ob = ob.cast::()?; @@ -425,8 +441,10 @@ impl<'py> IntoPyObject<'py> for SignedDuration { } } -impl<'py> FromPyObject<'py> for SignedDuration { - fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { +impl<'py> FromPyObject<'_, 'py> for SignedDuration { + type Error = PyErr; + + fn extract(ob: Borrowed<'_, 'py, PyAny>) -> PyResult { let delta = ob.cast::()?; #[cfg(not(Py_LIMITED_API))] @@ -450,8 +468,10 @@ impl<'py> FromPyObject<'py> for SignedDuration { } } -impl<'py> FromPyObject<'py> for Span { - fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { +impl<'py> FromPyObject<'_, 'py> for Span { + type Error = PyErr; + + fn extract(ob: Borrowed<'_, 'py, PyAny>) -> PyResult { let duration = ob.extract::()?; Ok(duration.try_into()?) } diff --git a/src/conversions/num_bigint.rs b/src/conversions/num_bigint.rs index 0f8924bbf12..4384231c801 100644 --- a/src/conversions/num_bigint.rs +++ b/src/conversions/num_bigint.rs @@ -50,7 +50,7 @@ #[cfg(Py_LIMITED_API)] use crate::types::{bytes::PyBytesMethods, PyBytes}; use crate::{ - conversion::IntoPyObject, ffi, instance::Bound, types::PyInt, FromPyObject, Py, PyAny, PyErr, + conversion::IntoPyObject, ffi, types::PyInt, Borrowed, Bound, FromPyObject, Py, PyAny, PyErr, PyResult, Python, }; @@ -123,8 +123,10 @@ 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 { - fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { +impl<'py> FromPyObject<'_, 'py> for BigInt { + type Error = PyErr; + + fn extract(ob: Borrowed<'_, 'py, PyAny>) -> Result { let py = ob.py(); // fast path - checking for subclass of `int` just checks a bit in the type object let num_owned: Py; @@ -132,11 +134,11 @@ impl<'py> FromPyObject<'py> for BigInt { long } else { num_owned = unsafe { Py::from_owned_ptr_or_err(py, ffi::PyNumber_Index(ob.as_ptr()))? }; - num_owned.bind(py) + num_owned.bind_borrowed(py) }; #[cfg(not(Py_LIMITED_API))] { - let mut buffer = int_to_u32_vec::(num)?; + let mut buffer = int_to_u32_vec::(&num)?; let sign = if buffer.last().copied().is_some_and(|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) @@ -160,19 +162,21 @@ impl<'py> FromPyObject<'py> for BigInt { } #[cfg(Py_LIMITED_API)] { - let n_bits = int_n_bits(num)?; + 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)?; + let bytes = int_to_py_bytes(&num, (n_bits + 8) / 8, true)?; Ok(BigInt::from_signed_bytes_le(bytes.as_bytes())) } } } #[cfg_attr(docsrs, doc(cfg(feature = "num-bigint")))] -impl<'py> FromPyObject<'py> for BigUint { - fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { +impl<'py> FromPyObject<'_, 'py> for BigUint { + type Error = PyErr; + + fn extract(ob: Borrowed<'_, 'py, PyAny>) -> Result { let py = ob.py(); // fast path - checking for subclass of `int` just checks a bit in the type object let num_owned: Py; @@ -180,20 +184,20 @@ impl<'py> FromPyObject<'py> for BigUint { long } else { num_owned = unsafe { Py::from_owned_ptr_or_err(py, ffi::PyNumber_Index(ob.as_ptr()))? }; - num_owned.bind(py) + num_owned.bind_borrowed(py) }; #[cfg(not(Py_LIMITED_API))] { - let buffer = int_to_u32_vec::(num)?; + let buffer = int_to_u32_vec::(&num)?; Ok(BigUint::new(buffer)) } #[cfg(Py_LIMITED_API)] { - let n_bits = int_n_bits(num)?; + 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.div_ceil(8), false)?; + let bytes = int_to_py_bytes(&num, n_bits.div_ceil(8), false)?; Ok(BigUint::from_bytes_le(bytes.as_bytes())) } } @@ -307,8 +311,8 @@ fn int_n_bits(long: &Bound<'_, PyInt>) -> PyResult { #[cfg(Py_LIMITED_API)] { - use crate::types::any::PyAnyMethods; // slow path + use crate::types::PyAnyMethods; long.call_method0(crate::intern!(py, "bit_length")) .and_then(|any| any.extract()) } diff --git a/src/conversions/num_complex.rs b/src/conversions/num_complex.rs index cdb7ad3fc42..778680153fe 100644 --- a/src/conversions/num_complex.rs +++ b/src/conversions/num_complex.rs @@ -94,7 +94,7 @@ //! assert result == [complex(1,-1), complex(-2,0)] //! ``` use crate::{ - ffi, ffi_ptr_ext::FfiPtrExt, types::PyComplex, Bound, FromPyObject, PyAny, PyErr, PyResult, + ffi, ffi_ptr_ext::FfiPtrExt, types::PyComplex, Borrowed, Bound, FromPyObject, PyAny, PyErr, Python, }; use num_complex::Complex; @@ -146,8 +146,10 @@ macro_rules! complex_conversion { } #[cfg_attr(docsrs, doc(cfg(feature = "num-complex")))] - impl FromPyObject<'_> for Complex<$float> { - fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult> { + impl FromPyObject<'_, '_> for Complex<$float> { + type Error = PyErr; + + fn extract(obj: Borrowed<'_, '_, PyAny>) -> Result, Self::Error> { #[cfg(not(any(Py_LIMITED_API, PyPy)))] unsafe { let val = ffi::PyComplex_AsCComplex(obj.as_ptr()); @@ -169,7 +171,7 @@ macro_rules! complex_conversion { obj.lookup_special(crate::intern!(obj.py(), "__complex__"))? { complex = method.call0()?; - &complex + complex.as_borrowed() } else { // `obj` might still implement `__float__` or `__index__`, which will be // handled by `PyComplex_{Real,Imag}AsDouble`, including propagating any diff --git a/src/conversions/num_rational.rs b/src/conversions/num_rational.rs index 25aac24aa7a..c64a48323a2 100644 --- a/src/conversions/num_rational.rs +++ b/src/conversions/num_rational.rs @@ -48,7 +48,7 @@ use crate::ffi; use crate::sync::PyOnceLock; use crate::types::any::PyAnyMethods; use crate::types::PyType; -use crate::{Bound, FromPyObject, Py, PyAny, PyErr, PyResult, Python}; +use crate::{Borrowed, Bound, FromPyObject, Py, PyAny, PyErr, PyResult, Python}; #[cfg(feature = "num-bigint")] use num_bigint::BigInt; @@ -62,8 +62,10 @@ fn get_fraction_cls(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> { macro_rules! rational_conversion { ($int: ty) => { - impl<'py> FromPyObject<'py> for Ratio<$int> { - fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { + impl<'py> FromPyObject<'_, 'py> for Ratio<$int> { + type Error = PyErr; + + fn extract(obj: Borrowed<'_, 'py, PyAny>) -> Result { let py = obj.py(); let py_numerator_obj = obj.getattr(crate::intern!(py, "numerator"))?; let py_denominator_obj = obj.getattr(crate::intern!(py, "denominator"))?; diff --git a/src/conversions/ordered_float.rs b/src/conversions/ordered_float.rs index a4da92b5b14..71bcd29bca7 100644 --- a/src/conversions/ordered_float.rs +++ b/src/conversions/ordered_float.rs @@ -53,15 +53,17 @@ use crate::conversion::IntoPyObject; use crate::exceptions::PyValueError; -use crate::types::{any::PyAnyMethods, PyFloat}; -use crate::{Bound, FromPyObject, PyAny, PyResult, Python}; +use crate::types::PyFloat; +use crate::{Borrowed, Bound, FromPyObject, PyAny, Python}; use ordered_float::{NotNan, OrderedFloat}; use std::convert::Infallible; macro_rules! float_conversions { ($wrapper:ident, $float_type:ty, $constructor:expr) => { - impl FromPyObject<'_> for $wrapper<$float_type> { - fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { + impl<'a, 'py> FromPyObject<'a, 'py> for $wrapper<$float_type> { + type Error = <$float_type as FromPyObject<'a, 'py>>::Error; + + fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result { let val: $float_type = obj.extract()?; $constructor(val) } @@ -101,6 +103,7 @@ mod test_ordered_float { use super::*; use crate::ffi::c_str; use crate::py_run; + use crate::types::PyAnyMethods; #[cfg(not(target_arch = "wasm32"))] use proptest::prelude::*; @@ -294,7 +297,7 @@ mod test_ordered_float { Python::attach(|py| { let nan_py = py.eval(c_str!("float('nan')"), None, None).unwrap(); - let nan_rs: PyResult> = nan_py.extract(); + let nan_rs: Result, _> = nan_py.extract(); assert!(nan_rs.is_err()); }) diff --git a/src/conversions/rust_decimal.rs b/src/conversions/rust_decimal.rs index b533f97c5a5..d67a076869b 100644 --- a/src/conversions/rust_decimal.rs +++ b/src/conversions/rust_decimal.rs @@ -55,12 +55,14 @@ use crate::sync::PyOnceLock; use crate::types::any::PyAnyMethods; use crate::types::string::PyStringMethods; use crate::types::PyType; -use crate::{Bound, FromPyObject, Py, PyAny, PyErr, PyResult, Python}; +use crate::{Borrowed, Bound, FromPyObject, Py, PyAny, PyErr, PyResult, Python}; use rust_decimal::Decimal; use std::str::FromStr; -impl FromPyObject<'_> for Decimal { - fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { +impl FromPyObject<'_, '_> for Decimal { + type Error = PyErr; + + fn extract(obj: Borrowed<'_, '_, PyAny>) -> Result { // use the string representation to not be lossy if let Ok(val) = obj.extract() { Ok(Decimal::new(val, 0)) diff --git a/src/conversions/smallvec.rs b/src/conversions/smallvec.rs index e6e7262590f..525a8845754 100644 --- a/src/conversions/smallvec.rs +++ b/src/conversions/smallvec.rs @@ -15,14 +15,15 @@ //! //! 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::conversion::{FromPyObjectOwned, IntoPyObject}; use crate::exceptions::PyTypeError; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; use crate::types::any::PyAnyMethods; use crate::types::{PySequence, PyString}; -use crate::PyErr; -use crate::{err::DowncastError, ffi, Bound, FromPyObject, PyAny, PyResult, Python}; +use crate::{ + err::DowncastError, ffi, Borrowed, Bound, FromPyObject, PyAny, PyErr, PyResult, Python, +}; use smallvec::{Array, SmallVec}; impl<'py, A> IntoPyObject<'py> for SmallVec @@ -70,12 +71,14 @@ where } } -impl<'py, A> FromPyObject<'py> for SmallVec +impl<'py, A> FromPyObject<'_, 'py> for SmallVec where A: Array, - A::Item: FromPyObject<'py>, + A::Item: FromPyObjectOwned<'py>, { - fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { + type Error = PyErr; + + fn extract(obj: Borrowed<'_, 'py, PyAny>) -> Result { if obj.is_instance_of::() { return Err(PyTypeError::new_err("Can't extract `str` to `SmallVec`")); } @@ -88,10 +91,10 @@ where } } -fn extract_sequence<'py, A>(obj: &Bound<'py, PyAny>) -> PyResult> +fn extract_sequence<'py, A>(obj: Borrowed<'_, 'py, PyAny>) -> PyResult> where A: Array, - A::Item: FromPyObject<'py>, + A::Item: FromPyObjectOwned<'py>, { // Types that pass `PySequence_Check` usually implement enough of the sequence protocol // to support this function and if not, we will only fail extraction safely. @@ -99,13 +102,13 @@ where if ffi::PySequence_Check(obj.as_ptr()) != 0 { obj.cast_unchecked::() } else { - return Err(DowncastError::new(obj, "Sequence").into()); + return Err(DowncastError::new_from_borrowed(obj, "Sequence").into()); } }; let mut sv = SmallVec::with_capacity(seq.len().unwrap_or(0)); for item in seq.try_iter()? { - sv.push(item?.extract::()?); + sv.push(item?.extract::().map_err(Into::into)?); } Ok(sv) } diff --git a/src/conversions/std/array.rs b/src/conversions/std/array.rs index a7f2d953284..7da9f13dc8b 100644 --- a/src/conversions/std/array.rs +++ b/src/conversions/std/array.rs @@ -1,9 +1,8 @@ -use crate::conversion::IntoPyObject; -use crate::instance::Bound; +use crate::conversion::{FromPyObjectOwned, IntoPyObject}; use crate::types::any::PyAnyMethods; use crate::types::PySequence; use crate::{err::DowncastError, ffi, FromPyObject, PyAny, PyResult, Python}; -use crate::{exceptions, PyErr}; +use crate::{exceptions, Borrowed, Bound, PyErr}; impl<'py, T, const N: usize> IntoPyObject<'py> for [T; N] where @@ -37,18 +36,20 @@ where } } -impl<'py, T, const N: usize> FromPyObject<'py> for [T; N] +impl<'py, T, const N: usize> FromPyObject<'_, 'py> for [T; N] where - T: FromPyObject<'py>, + T: FromPyObjectOwned<'py>, { - fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { + type Error = PyErr; + + fn extract(obj: Borrowed<'_, 'py, PyAny>) -> Result { create_array_from_obj(obj) } } -fn create_array_from_obj<'py, T, const N: usize>(obj: &Bound<'py, PyAny>) -> PyResult<[T; N]> +fn create_array_from_obj<'py, T, const N: usize>(obj: Borrowed<'_, 'py, PyAny>) -> PyResult<[T; N]> where - T: FromPyObject<'py>, + T: FromPyObjectOwned<'py>, { // Types that pass `PySequence_Check` usually implement enough of the sequence protocol // to support this function and if not, we will only fail extraction safely. @@ -56,14 +57,17 @@ where if ffi::PySequence_Check(obj.as_ptr()) != 0 { obj.cast_unchecked::() } else { - return Err(DowncastError::new(obj, "Sequence").into()); + return Err(DowncastError::new_from_borrowed(obj, "Sequence").into()); } }; let seq_len = seq.len()?; if seq_len != N { return Err(invalid_sequence_length(N, seq_len)); } - array_try_from_fn(|idx| seq.get_item(idx).and_then(|any| any.extract())) + array_try_from_fn(|idx| { + seq.get_item(idx) + .and_then(|any| any.extract().map_err(Into::into)) + }) } // TODO use std::array::try_from_fn, if that stabilises: diff --git a/src/conversions/std/cell.rs b/src/conversions/std/cell.rs index 17f29d37141..108df8031cd 100644 --- a/src/conversions/std/cell.rs +++ b/src/conversions/std/cell.rs @@ -1,9 +1,6 @@ use std::cell::Cell; -use crate::{ - conversion::IntoPyObject, types::any::PyAnyMethods, Bound, FromPyObject, PyAny, PyResult, - Python, -}; +use crate::{conversion::IntoPyObject, Borrowed, FromPyObject, PyAny, Python}; impl<'py, T: Copy + IntoPyObject<'py>> IntoPyObject<'py> for Cell { type Target = T::Target; @@ -33,11 +30,13 @@ impl<'py, T: Copy + IntoPyObject<'py>> IntoPyObject<'py> for &Cell { } } -impl<'py, T: FromPyObject<'py>> FromPyObject<'py> for Cell { +impl<'a, 'py, T: FromPyObject<'a, 'py>> FromPyObject<'a, 'py> for Cell { + type Error = T::Error; + #[cfg(feature = "experimental-inspect")] const INPUT_TYPE: &'static str = T::INPUT_TYPE; - fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { + fn extract(ob: Borrowed<'a, 'py, PyAny>) -> Result { ob.extract().map(Cell::new) } } diff --git a/src/conversions/std/ipaddr.rs b/src/conversions/std/ipaddr.rs index 7e6453ed654..6fb1859d196 100644 --- a/src/conversions/std/ipaddr.rs +++ b/src/conversions/std/ipaddr.rs @@ -2,15 +2,16 @@ use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use crate::conversion::IntoPyObject; use crate::exceptions::PyValueError; -use crate::instance::Bound; use crate::sync::PyOnceLock; use crate::types::any::PyAnyMethods; use crate::types::string::PyStringMethods; use crate::types::PyType; -use crate::{intern, FromPyObject, Py, PyAny, PyErr, PyResult, Python}; +use crate::{intern, Borrowed, Bound, FromPyObject, Py, PyAny, PyErr, Python}; -impl FromPyObject<'_> for IpAddr { - fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { +impl FromPyObject<'_, '_> for IpAddr { + type Error = PyErr; + + fn extract(obj: Borrowed<'_, '_, PyAny>) -> Result { match obj.getattr(intern!(obj.py(), "packed")) { Ok(packed) => { if let Ok(packed) = packed.extract::<[u8; 4]>() { diff --git a/src/conversions/std/map.rs b/src/conversions/std/map.rs index fc96d211dc3..676351abf4a 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, + conversion::{FromPyObjectOwned, IntoPyObject}, instance::Bound, types::{any::PyAnyMethods, dict::PyDictMethods, PyDict}, - FromPyObject, PyAny, PyErr, Python, + Borrowed, FromPyObject, PyAny, PyErr, Python, }; impl<'py, K, V, H> IntoPyObject<'py> for collections::HashMap @@ -107,17 +107,22 @@ where } } -impl<'py, K, V, S> FromPyObject<'py> for collections::HashMap +impl<'py, K, V, S> FromPyObject<'_, 'py> for collections::HashMap where - K: FromPyObject<'py> + cmp::Eq + hash::Hash, - V: FromPyObject<'py>, + K: FromPyObjectOwned<'py> + cmp::Eq + hash::Hash, + V: FromPyObjectOwned<'py>, S: hash::BuildHasher + Default, { - fn extract_bound(ob: &Bound<'py, PyAny>) -> Result { + type Error = PyErr; + + fn extract(ob: Borrowed<'_, 'py, PyAny>) -> Result { let dict = ob.cast::()?; let mut ret = collections::HashMap::with_capacity_and_hasher(dict.len(), S::default()); - for (k, v) in dict { - ret.insert(k.extract()?, v.extract()?); + for (k, v) in dict.iter() { + ret.insert( + k.extract().map_err(Into::into)?, + v.extract().map_err(Into::into)?, + ); } Ok(ret) } @@ -128,16 +133,21 @@ where } } -impl<'py, K, V> FromPyObject<'py> for collections::BTreeMap +impl<'py, K, V> FromPyObject<'_, 'py> for collections::BTreeMap where - K: FromPyObject<'py> + cmp::Ord, - V: FromPyObject<'py>, + K: FromPyObjectOwned<'py> + cmp::Ord, + V: FromPyObjectOwned<'py>, { - fn extract_bound(ob: &Bound<'py, PyAny>) -> Result { + type Error = PyErr; + + fn extract(ob: Borrowed<'_, 'py, PyAny>) -> Result { let dict = ob.cast::()?; let mut ret = collections::BTreeMap::new(); - for (k, v) in dict { - ret.insert(k.extract()?, v.extract()?); + for (k, v) in dict.iter() { + ret.insert( + k.extract().map_err(Into::into)?, + v.extract().map_err(Into::into)?, + ); } Ok(ret) } diff --git a/src/conversions/std/num.rs b/src/conversions/std/num.rs index 42a65a27929..986179ad6d3 100644 --- a/src/conversions/std/num.rs +++ b/src/conversions/std/num.rs @@ -3,9 +3,8 @@ 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::{PyBytes, PyInt}; -use crate::{exceptions, ffi, Bound, FromPyObject, PyAny, PyErr, PyResult, Python}; +use crate::{exceptions, ffi, Borrowed, Bound, FromPyObject, PyAny, PyErr, PyResult, Python}; use std::convert::Infallible; use std::ffi::c_long; use std::num::{ @@ -51,11 +50,13 @@ macro_rules! int_fits_larger_int { } } - impl FromPyObject<'_> for $rust_type { + impl FromPyObject<'_, '_> for $rust_type { + type Error = PyErr; + #[cfg(feature = "experimental-inspect")] const INPUT_TYPE: &'static str = <$larger_type>::INPUT_TYPE; - fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { + fn extract(obj: Borrowed<'_, '_, PyAny>) -> Result { let val: $larger_type = obj.extract()?; <$rust_type>::try_from(val) .map_err(|e| exceptions::PyOverflowError::new_err(e.to_string())) @@ -129,17 +130,14 @@ 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 { + impl FromPyObject<'_, '_> for $rust_type { + type Error = PyErr; + #[cfg(feature = "experimental-inspect")] const INPUT_TYPE: &'static str = "int"; - fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult<$rust_type> { + fn extract(obj: Borrowed<'_, '_, PyAny>) -> Result<$rust_type, Self::Error> { extract_int!(obj, !0, $pylong_as_ll_or_ull, $force_index_call) } @@ -194,11 +192,13 @@ macro_rules! int_fits_c_long { } } - impl<'py> FromPyObject<'py> for $rust_type { + impl<'py> FromPyObject<'_, 'py> for $rust_type { + type Error = PyErr; + #[cfg(feature = "experimental-inspect")] const INPUT_TYPE: &'static str = "int"; - fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { + fn extract(obj: Borrowed<'_, 'py, PyAny>) -> Result { let val: c_long = extract_int!(obj, -1, ffi::PyLong_AsLong)?; <$rust_type>::try_from(val) .map_err(|e| exceptions::PyOverflowError::new_err(e.to_string())) @@ -277,11 +277,13 @@ impl<'py> IntoPyObject<'py> for &'_ u8 { } } -impl FromPyObject<'_> for u8 { +impl<'py> FromPyObject<'_, 'py> for u8 { + type Error = PyErr; + #[cfg(feature = "experimental-inspect")] const INPUT_TYPE: &'static str = "int"; - fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { + fn extract(obj: Borrowed<'_, 'py, PyAny>) -> Result { 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())) } @@ -408,11 +410,13 @@ mod fast_128bit_int_conversion { } } - impl FromPyObject<'_> for $rust_type { + impl FromPyObject<'_, '_> for $rust_type { + type Error = PyErr; + #[cfg(feature = "experimental-inspect")] const INPUT_TYPE: &'static str = "int"; - fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult<$rust_type> { + fn extract(ob: Borrowed<'_, '_, PyAny>) -> Result<$rust_type, Self::Error> { 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>()]; @@ -474,6 +478,7 @@ mod fast_128bit_int_conversion { #[cfg(any(Py_LIMITED_API, GraalPy))] mod slow_128bit_int_conversion { use super::*; + use crate::types::any::PyAnyMethods as _; const SHIFT: usize = 64; // for 128bit Integers @@ -526,11 +531,13 @@ mod slow_128bit_int_conversion { } } - impl FromPyObject<'_> for $rust_type { + impl FromPyObject<'_, '_> for $rust_type { + type Error = PyErr; + #[cfg(feature = "experimental-inspect")] const INPUT_TYPE: &'static str = "int"; - fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult<$rust_type> { + fn extract(ob: Borrowed<'_, '_, PyAny>) -> Result<$rust_type, Self::Error> { let py = ob.py(); unsafe { let lower = err_if_invalid_value( @@ -614,11 +621,13 @@ macro_rules! nonzero_int_impl { } } - impl FromPyObject<'_> for $nonzero_type { + impl FromPyObject<'_, '_> for $nonzero_type { + type Error = PyErr; + #[cfg(feature = "experimental-inspect")] const INPUT_TYPE: &'static str = <$primitive_type>::INPUT_TYPE; - fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { + fn extract(obj: Borrowed<'_, '_, PyAny>) -> Result { let val: $primitive_type = obj.extract()?; <$nonzero_type>::try_from(val) .map_err(|_| exceptions::PyValueError::new_err("invalid zero value")) @@ -648,6 +657,7 @@ nonzero_int_impl!(NonZeroUsize, usize); #[cfg(test)] mod test_128bit_integers { use super::*; + use crate::types::PyAnyMethods; #[cfg(not(target_arch = "wasm32"))] use crate::types::PyDict; diff --git a/src/conversions/std/option.rs b/src/conversions/std/option.rs index ae0ec441c61..8cec88b4e6b 100644 --- a/src/conversions/std/option.rs +++ b/src/conversions/std/option.rs @@ -1,7 +1,7 @@ use crate::{ - conversion::IntoPyObject, types::any::PyAnyMethods, Bound, BoundObject, FromPyObject, PyAny, - PyResult, Python, + conversion::IntoPyObject, types::any::PyAnyMethods, BoundObject, FromPyObject, PyAny, Python, }; +use crate::{Borrowed, Bound}; impl<'py, T> IntoPyObject<'py> for Option where @@ -37,11 +37,13 @@ where } } -impl<'py, T> FromPyObject<'py> for Option +impl<'a, 'py, T> FromPyObject<'a, 'py> for Option where - T: FromPyObject<'py>, + T: FromPyObject<'a, 'py>, { - fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { + type Error = T::Error; + + fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result { if obj.is_none() { Ok(None) } else { diff --git a/src/conversions/std/osstr.rs b/src/conversions/std/osstr.rs index acc6f2bab7f..3c480526d83 100644 --- a/src/conversions/std/osstr.rs +++ b/src/conversions/std/osstr.rs @@ -2,7 +2,7 @@ use crate::conversion::IntoPyObject; use crate::ffi_ptr_ext::FfiPtrExt; use crate::instance::Bound; use crate::types::PyString; -use crate::{ffi, FromPyObject, PyAny, PyResult, Python}; +use crate::{ffi, Borrowed, FromPyObject, PyAny, PyErr, Python}; use std::borrow::Cow; use std::convert::Infallible; use std::ffi::{OsStr, OsString}; @@ -70,8 +70,10 @@ impl<'py> IntoPyObject<'py> for &&OsStr { // 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 -impl FromPyObject<'_> for OsString { - fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { +impl FromPyObject<'_, '_> for OsString { + type Error = PyErr; + + fn extract(ob: Borrowed<'_, '_, PyAny>) -> Result { let pystring = ob.cast::()?; #[cfg(not(windows))] @@ -97,8 +99,6 @@ impl FromPyObject<'_> for OsString { #[cfg(windows)] { - use crate::types::string::PyStringMethods; - // Take the quick and easy shortcut if UTF-8 if let Ok(utf8_string) = pystring.to_cow() { return Ok(utf8_string.into_owned().into()); @@ -169,7 +169,7 @@ impl<'py> IntoPyObject<'py> for &OsString { #[cfg(test)] mod tests { - use crate::types::{PyAnyMethods, PyString, PyStringMethods}; + use crate::types::{PyString, PyStringMethods}; use crate::{BoundObject, IntoPyObject, Python}; use std::fmt::Debug; use std::{ @@ -181,6 +181,7 @@ mod tests { #[cfg(not(windows))] fn test_non_utf8_conversion() { Python::attach(|py| { + use crate::types::PyAnyMethods; #[cfg(not(target_os = "wasi"))] use std::os::unix::ffi::OsStrExt; #[cfg(target_os = "wasi")] diff --git a/src/conversions/std/path.rs b/src/conversions/std/path.rs index 17cad8f2694..4d60facd186 100644 --- a/src/conversions/std/path.rs +++ b/src/conversions/std/path.rs @@ -1,17 +1,18 @@ use crate::conversion::IntoPyObject; use crate::ffi_ptr_ext::FfiPtrExt; -use crate::instance::Bound; use crate::sync::PyOnceLock; use crate::types::any::PyAnyMethods; -use crate::{ffi, FromPyObject, Py, PyAny, PyErr, PyResult, Python}; +use crate::{ffi, Borrowed, Bound, FromPyObject, Py, PyAny, PyErr, Python}; use std::borrow::Cow; use std::ffi::OsString; use std::path::{Path, PathBuf}; // See osstr.rs for why there's no FromPyObject impl for &Path -impl FromPyObject<'_> for PathBuf { - fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { +impl FromPyObject<'_, '_> for PathBuf { + type Error = PyErr; + + fn extract(ob: Borrowed<'_, '_, PyAny>) -> Result { // We use os.fspath to get the underlying path as bytes or str let path = unsafe { ffi::PyOS_FSPath(ob.as_ptr()).assume_owned_or_err(ob.py())? }; Ok(path.extract::()?.into()) @@ -99,6 +100,7 @@ mod tests { #[cfg(not(windows))] fn test_non_utf8_conversion() { Python::attach(|py| { + use crate::types::PyAnyMethods; use std::ffi::OsStr; #[cfg(not(target_os = "wasi"))] use std::os::unix::ffi::OsStrExt; diff --git a/src/conversions/std/set.rs b/src/conversions/std/set.rs index 4d4073101a0..58c38f6ca1c 100644 --- a/src/conversions/std/set.rs +++ b/src/conversions/std/set.rs @@ -3,15 +3,14 @@ use std::{cmp, collections, hash}; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; use crate::{ - conversion::IntoPyObject, - instance::Bound, + conversion::{FromPyObjectOwned, IntoPyObject}, types::{ any::PyAnyMethods, frozenset::PyFrozenSetMethods, set::{try_new_from_iter, PySetMethods}, PyFrozenSet, PySet, }, - FromPyObject, PyAny, PyErr, PyResult, Python, + Borrowed, Bound, FromPyObject, PyAny, PyErr, Python, }; impl<'py, K, S> IntoPyObject<'py> for collections::HashSet @@ -53,17 +52,25 @@ where } } -impl<'py, K, S> FromPyObject<'py> for collections::HashSet +impl<'py, K, S> FromPyObject<'_, 'py> for collections::HashSet where - K: FromPyObject<'py> + cmp::Eq + hash::Hash, + K: FromPyObjectOwned<'py> + cmp::Eq + hash::Hash, S: hash::BuildHasher + Default, { - fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { + type Error = PyErr; + + fn extract(ob: Borrowed<'_, 'py, PyAny>) -> Result { match ob.cast::() { - Ok(set) => set.iter().map(|any| any.extract()).collect(), + Ok(set) => set + .iter() + .map(|any| any.extract().map_err(Into::into)) + .collect(), Err(err) => { if let Ok(frozen_set) = ob.cast::() { - frozen_set.iter().map(|any| any.extract()).collect() + frozen_set + .iter() + .map(|any| any.extract().map_err(Into::into)) + .collect() } else { Err(PyErr::from(err)) } @@ -114,16 +121,24 @@ where } } -impl<'py, K> FromPyObject<'py> for collections::BTreeSet +impl<'py, K> FromPyObject<'_, 'py> for collections::BTreeSet where - K: FromPyObject<'py> + cmp::Ord, + K: FromPyObjectOwned<'py> + cmp::Ord, { - fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { + type Error = PyErr; + + fn extract(ob: Borrowed<'_, 'py, PyAny>) -> Result { match ob.cast::() { - Ok(set) => set.iter().map(|any| any.extract()).collect(), + Ok(set) => set + .iter() + .map(|any| any.extract().map_err(Into::into)) + .collect(), Err(err) => { if let Ok(frozen_set) = ob.cast::() { - frozen_set.iter().map(|any| any.extract()).collect() + frozen_set + .iter() + .map(|any| any.extract().map_err(Into::into)) + .collect() } else { Err(PyErr::from(err)) } diff --git a/src/conversions/std/slice.rs b/src/conversions/std/slice.rs index dca72b8fdf7..00c072e588f 100644 --- a/src/conversions/std/slice.rs +++ b/src/conversions/std/slice.rs @@ -5,7 +5,7 @@ use crate::inspect::types::TypeInfo; use crate::{ conversion::IntoPyObject, types::{PyByteArray, PyByteArrayMethods, PyBytes}, - Bound, PyAny, PyErr, PyResult, Python, + Bound, DowncastError, PyAny, PyErr, Python, }; impl<'a, 'py, T> IntoPyObject<'py> for &'a [T] @@ -35,8 +35,10 @@ where } } -impl<'a> crate::conversion::FromPyObjectBound<'a, '_> for &'a [u8] { - fn from_py_object_bound(obj: crate::Borrowed<'a, '_, PyAny>) -> PyResult { +impl<'a, 'py> crate::conversion::FromPyObject<'a, 'py> for &'a [u8] { + type Error = DowncastError<'a, 'py>; + + fn extract(obj: crate::Borrowed<'a, 'py, PyAny>) -> Result { Ok(obj.cast::()?.as_bytes()) } @@ -51,8 +53,10 @@ 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`. -impl<'a> crate::conversion::FromPyObjectBound<'a, '_> for Cow<'a, [u8]> { - fn from_py_object_bound(ob: crate::Borrowed<'a, '_, PyAny>) -> PyResult { +impl<'a, 'py> crate::conversion::FromPyObject<'a, 'py> for Cow<'a, [u8]> { + type Error = DowncastError<'a, 'py>; + + fn extract(ob: crate::Borrowed<'a, 'py, PyAny>) -> Result { if let Ok(bytes) = ob.cast::() { return Ok(Cow::Borrowed(bytes.as_bytes())); } diff --git a/src/conversions/std/string.rs b/src/conversions/std/string.rs index e4c804facd2..3c6a300ecdf 100644 --- a/src/conversions/std/string.rs +++ b/src/conversions/std/string.rs @@ -3,10 +3,8 @@ use std::{borrow::Cow, convert::Infallible}; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; use crate::{ - conversion::IntoPyObject, - instance::Bound, - types::{string::PyStringMethods, PyString}, - FromPyObject, PyAny, PyResult, Python, + conversion::IntoPyObject, instance::Bound, types::PyString, Borrowed, FromPyObject, PyAny, + PyErr, Python, }; impl<'py> IntoPyObject<'py> for &str { @@ -161,11 +159,13 @@ impl<'py> IntoPyObject<'py> for &String { } #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] -impl<'a> crate::conversion::FromPyObjectBound<'a, '_> for &'a str { +impl<'a> crate::conversion::FromPyObject<'a, '_> for &'a str { + type Error = PyErr; + #[cfg(feature = "experimental-inspect")] const INPUT_TYPE: &'static str = "str"; - fn from_py_object_bound(ob: crate::Borrowed<'a, '_, PyAny>) -> PyResult { + fn extract(ob: crate::Borrowed<'a, '_, PyAny>) -> Result { ob.cast::()?.to_str() } @@ -175,11 +175,13 @@ impl<'a> crate::conversion::FromPyObjectBound<'a, '_> for &'a str { } } -impl<'a> crate::conversion::FromPyObjectBound<'a, '_> for Cow<'a, str> { +impl<'a> crate::conversion::FromPyObject<'a, '_> for Cow<'a, str> { + type Error = PyErr; + #[cfg(feature = "experimental-inspect")] const INPUT_TYPE: &'static str = "str"; - fn from_py_object_bound(ob: crate::Borrowed<'a, '_, PyAny>) -> PyResult { + fn extract(ob: crate::Borrowed<'a, '_, PyAny>) -> Result { ob.cast::()?.to_cow() } @@ -191,11 +193,13 @@ impl<'a> crate::conversion::FromPyObjectBound<'a, '_> for Cow<'a, str> { /// Allows extracting strings from Python objects. /// Accepts Python `str` and `unicode` objects. -impl FromPyObject<'_> for String { +impl FromPyObject<'_, '_> for String { + type Error = PyErr; + #[cfg(feature = "experimental-inspect")] const INPUT_TYPE: &'static str = "str"; - fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { + fn extract(obj: Borrowed<'_, '_, PyAny>) -> Result { obj.cast::()?.to_cow().map(Cow::into_owned) } @@ -205,11 +209,13 @@ impl FromPyObject<'_> for String { } } -impl FromPyObject<'_> for char { +impl FromPyObject<'_, '_> for char { + type Error = PyErr; + #[cfg(feature = "experimental-inspect")] const INPUT_TYPE: &'static str = "str"; - fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { + fn extract(obj: Borrowed<'_, '_, PyAny>) -> Result { let s = obj.cast::()?.to_cow()?; let mut iter = s.chars(); if let (Some(ch), None) = (iter.next(), iter.next()) { diff --git a/src/conversions/std/time.rs b/src/conversions/std/time.rs index d2e2ed7c237..18a7e7dec1c 100644 --- a/src/conversions/std/time.rs +++ b/src/conversions/std/time.rs @@ -12,8 +12,10 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH}; const SECONDS_PER_DAY: u64 = 24 * 60 * 60; -impl FromPyObject<'_> for Duration { - fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { +impl FromPyObject<'_, '_> for Duration { + type Error = PyErr; + + fn extract(obj: Borrowed<'_, '_, PyAny>) -> Result { let delta = obj.cast::()?; #[cfg(not(Py_LIMITED_API))] let (days, seconds, microseconds) = { @@ -87,8 +89,10 @@ impl<'py> IntoPyObject<'py> for &Duration { // // TODO: it might be nice to investigate using timestamps anyway, at least when the datetime is a safe range. -impl FromPyObject<'_> for SystemTime { - fn extract_bound(obj: &Bound<'_, PyAny>) -> PyResult { +impl FromPyObject<'_, '_> for SystemTime { + type Error = PyErr; + + fn extract(obj: Borrowed<'_, '_, PyAny>) -> Result { let duration_since_unix_epoch: Duration = obj.sub(unix_epoch_py(obj.py())?)?.extract()?; UNIX_EPOCH .checked_add(duration_since_unix_epoch) diff --git a/src/conversions/time.rs b/src/conversions/time.rs index d9161f9553a..562eebd353e 100644 --- a/src/conversions/time.rs +++ b/src/conversions/time.rs @@ -58,7 +58,7 @@ use crate::types::datetime::{PyDateAccess, PyDeltaAccess}; use crate::types::{PyAnyMethods, PyDate, PyDateTime, PyDelta, PyNone, PyTime, PyTzInfo}; #[cfg(not(Py_LIMITED_API))] use crate::types::{PyTimeAccess, PyTzInfoAccess}; -use crate::{Bound, FromPyObject, IntoPyObject, PyAny, PyErr, PyResult, Python}; +use crate::{Borrowed, Bound, FromPyObject, IntoPyObject, PyAny, PyErr, PyResult, Python}; use time::{ Date, Duration, Month, OffsetDateTime, PrimitiveDateTime, Time, UtcDateTime, UtcOffset, }; @@ -180,8 +180,10 @@ impl<'py> IntoPyObject<'py> for Duration { } } -impl FromPyObject<'_> for Duration { - fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { +impl FromPyObject<'_, '_> for Duration { + type Error = PyErr; + + fn extract(ob: Borrowed<'_, '_, PyAny>) -> Result { #[cfg(not(Py_LIMITED_API))] let (days, seconds, microseconds) = { let delta = ob.cast::()?; @@ -223,8 +225,10 @@ impl<'py> IntoPyObject<'py> for Date { } } -impl FromPyObject<'_> for Date { - fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult { +impl FromPyObject<'_, '_> for Date { + type Error = PyErr; + + fn extract(ob: Borrowed<'_, '_, PyAny>) -> Result { let (year, month, day) = { #[cfg(not(Py_LIMITED_API))] { @@ -264,8 +268,10 @@ impl<'py> IntoPyObject<'py> for Time { } } -impl FromPyObject<'_> for Time { - fn extract_bound(ob: &Bound<'_, PyAny>) -> PyResult
Click to expand -The names for these APIs were created when the global interpreter lock (GIL) was mandatory. With the introduction of free-threading in Python 3.13 this is no longer the case, and the naming does not have no universal meaning anymore. +The names for these APIs were created when the global interpreter lock (GIL) was mandatory. With the introduction of free-threading in Python 3.13 this is no longer the case, and the naming has no universal meaning anymore. For this reason, we chose to rename these to more modern terminology introduced in free-threading: - `Python::with_gil` is now called `Python::attach`, it attaches a Python thread-state to the current thread. In GIL enabled builds there can only be 1 thread attached to the interpreter, in free-threading there can be more. From c585999ed3c37e96a34632696f42540060a3b65d Mon Sep 17 00:00:00 2001 From: Thomas Tanon Date: Wed, 3 Sep 2025 10:38:33 +0200 Subject: [PATCH 838/936] DateTime: implement PytypeInfo even using the stable ABI (#5388) --- newsfragments/5388.added.md | 1 + src/types/datetime.rs | 112 +++++++++++------------------------- 2 files changed, 36 insertions(+), 77 deletions(-) create mode 100644 newsfragments/5388.added.md diff --git a/newsfragments/5388.added.md b/newsfragments/5388.added.md new file mode 100644 index 00000000000..b62dfc6b9fc --- /dev/null +++ b/newsfragments/5388.added.md @@ -0,0 +1 @@ +Implement `PyTypeInfo` on `datetime.*` types even when the limited API is enabled \ No newline at end of file diff --git a/src/types/datetime.rs b/src/types/datetime.rs index 601440dc11e..53a3395e33e 100644 --- a/src/types/datetime.rs +++ b/src/types/datetime.rs @@ -18,12 +18,14 @@ use crate::ffi::{ }; #[cfg(all(Py_3_10, not(Py_LIMITED_API)))] use crate::ffi::{PyDateTime_DATE_GET_TZINFO, PyDateTime_TIME_GET_TZINFO, Py_IsNone}; +#[cfg(Py_LIMITED_API)] +use crate::types::typeobject::PyTypeMethods; +#[cfg(Py_LIMITED_API)] +use crate::types::IntoPyDict; use crate::types::{any::PyAnyMethods, PyString, PyType}; #[cfg(not(Py_LIMITED_API))] use crate::{ffi_ptr_ext::FfiPtrExt, py_result_ext::PyResultExt, types::PyTuple, BoundObject}; use crate::{sync::PyOnceLock, Py}; -#[cfg(Py_LIMITED_API)] -use crate::{types::IntoPyDict, PyTypeCheck}; use crate::{Borrowed, Bound, IntoPyObject, PyAny, PyErr, Python}; #[cfg(not(Py_LIMITED_API))] use std::ffi::c_int; @@ -234,21 +236,11 @@ pyobject_native_type!( pyobject_subclassable_native_type!(PyDate, crate::ffi::PyDateTime_Date); #[cfg(Py_LIMITED_API)] -pyobject_native_type_named!(PyDate); - -#[cfg(Py_LIMITED_API)] -impl PyTypeCheck for PyDate { - const NAME: &'static str = "PyDate"; - #[cfg(feature = "experimental-inspect")] - const PYTHON_TYPE: &'static str = "datetime.date"; - - fn type_check(object: &Bound<'_, PyAny>) -> bool { - let py = object.py(); - DatetimeTypes::try_get(py) - .and_then(|module| object.is_instance(module.date.bind(py))) - .unwrap_or_default() - } -} +pyobject_native_type_core!( + PyDate, + |py| type_to_ptr(&DatetimeTypes::try_get(py).unwrap().date, py), + #module=Some("datetime") +); impl PyDate { /// Creates a new `datetime.date`. @@ -335,21 +327,11 @@ pyobject_native_type!( pyobject_subclassable_native_type!(PyDateTime, crate::ffi::PyDateTime_DateTime); #[cfg(Py_LIMITED_API)] -pyobject_native_type_named!(PyDateTime); - -#[cfg(Py_LIMITED_API)] -impl PyTypeCheck for PyDateTime { - const NAME: &'static str = "PyDateTime"; - #[cfg(feature = "experimental-inspect")] - const PYTHON_TYPE: &'static str = "datetime.datetime"; - - fn type_check(object: &Bound<'_, PyAny>) -> bool { - let py = object.py(); - DatetimeTypes::try_get(py) - .and_then(|module| object.is_instance(module.datetime.bind(py))) - .unwrap_or_default() - } -} +pyobject_native_type_core!( + PyDateTime, + |py| type_to_ptr(&DatetimeTypes::try_get(py).unwrap().datetime, py), + #module=Some("datetime") +); impl PyDateTime { /// Creates a new `datetime.datetime` object. @@ -586,21 +568,11 @@ pyobject_native_type!( pyobject_subclassable_native_type!(PyTime, crate::ffi::PyDateTime_Time); #[cfg(Py_LIMITED_API)] -pyobject_native_type_named!(PyTime); - -#[cfg(Py_LIMITED_API)] -impl PyTypeCheck for PyTime { - const NAME: &'static str = "PyTime"; - #[cfg(feature = "experimental-inspect")] - const PYTHON_TYPE: &'static str = "datetime.time"; - - fn type_check(object: &Bound<'_, PyAny>) -> bool { - let py = object.py(); - DatetimeTypes::try_get(py) - .and_then(|module| object.is_instance(module.time.bind(py))) - .unwrap_or_default() - } -} +pyobject_native_type_core!( + PyTime, + |py| type_to_ptr(&DatetimeTypes::try_get(py).unwrap().time, py), + #module=Some("datetime") +); impl PyTime { /// Creates a new `datetime.time` object. @@ -772,21 +744,11 @@ pyobject_native_type!( pyobject_subclassable_native_type!(PyTzInfo, crate::ffi::PyObject); #[cfg(Py_LIMITED_API)] -pyobject_native_type_named!(PyTzInfo); - -#[cfg(Py_LIMITED_API)] -impl PyTypeCheck for PyTzInfo { - const NAME: &'static str = "PyTzInfo"; - #[cfg(feature = "experimental-inspect")] - const PYTHON_TYPE: &'static str = "datetime.tzinfo"; - - fn type_check(object: &Bound<'_, PyAny>) -> bool { - let py = object.py(); - DatetimeTypes::try_get(py) - .and_then(|module| object.is_instance(module.tzinfo.bind(py))) - .unwrap_or_default() - } -} +pyobject_native_type_core!( + PyTzInfo, + |py| type_to_ptr(&DatetimeTypes::try_get(py).unwrap().tzinfo, py), + #module=Some("datetime") +); impl PyTzInfo { /// Equivalent to `datetime.timezone.utc` @@ -888,21 +850,11 @@ pyobject_native_type!( pyobject_subclassable_native_type!(PyDelta, crate::ffi::PyDateTime_Delta); #[cfg(Py_LIMITED_API)] -pyobject_native_type_named!(PyDelta); - -#[cfg(Py_LIMITED_API)] -impl PyTypeCheck for PyDelta { - const NAME: &'static str = "PyDelta"; - #[cfg(feature = "experimental-inspect")] - const PYTHON_TYPE: &'static str = "datetime.timedelta"; - - fn type_check(object: &Bound<'_, PyAny>) -> bool { - let py = object.py(); - DatetimeTypes::try_get(py) - .and_then(|module| object.is_instance(module.timedelta.bind(py))) - .unwrap_or_default() - } -} +pyobject_native_type_core!( + PyDelta, + |py| type_to_ptr(&DatetimeTypes::try_get(py).unwrap().timedelta, py), + #module=Some("datetime") +); impl PyDelta { /// Creates a new `timedelta`. @@ -966,6 +918,12 @@ fn opt_to_pyobj(opt: Option<&Bound<'_, PyTzInfo>>) -> *mut ffi::PyObject { } } +// Utility to get pointer to the type object in order to feed PyTypeInfo::type_object_raw +#[cfg(Py_LIMITED_API)] +fn type_to_ptr(t: &Py, py: Python<'_>) -> *mut crate::ffi::PyTypeObject { + t.bind_borrowed(py).as_type_ptr() +} + #[cfg(test)] mod tests { use super::*; From 53a898d7ce56da2b6ca0db6df7e5910522dc191f Mon Sep 17 00:00:00 2001 From: Thomas Tanon Date: Wed, 3 Sep 2025 14:16:16 +0200 Subject: [PATCH 839/936] DateTime: small code cleanup (#5400) Make use of the new PyTypeInfo implementation and split the DatetimeTypes struct --- src/types/datetime.rs | 187 +++++++++++++++--------------------------- 1 file changed, 68 insertions(+), 119 deletions(-) diff --git a/src/types/datetime.rs b/src/types/datetime.rs index 53a3395e33e..76e51df7af2 100644 --- a/src/types/datetime.rs +++ b/src/types/datetime.rs @@ -3,6 +3,8 @@ //! For more details about these types, see the [Python //! documentation](https://docs.python.org/3/library/datetime.html) +#[cfg(not(Py_LIMITED_API))] +use crate::err::PyErr; use crate::err::PyResult; #[cfg(not(Py_3_9))] use crate::exceptions::PyImportError; @@ -19,6 +21,8 @@ use crate::ffi::{ #[cfg(all(Py_3_10, not(Py_LIMITED_API)))] use crate::ffi::{PyDateTime_DATE_GET_TZINFO, PyDateTime_TIME_GET_TZINFO, Py_IsNone}; #[cfg(Py_LIMITED_API)] +use crate::type_object::PyTypeInfo; +#[cfg(Py_LIMITED_API)] use crate::types::typeobject::PyTypeMethods; #[cfg(Py_LIMITED_API)] use crate::types::IntoPyDict; @@ -26,7 +30,7 @@ use crate::types::{any::PyAnyMethods, PyString, PyType}; #[cfg(not(Py_LIMITED_API))] use crate::{ffi_ptr_ext::FfiPtrExt, py_result_ext::PyResultExt, types::PyTuple, BoundObject}; use crate::{sync::PyOnceLock, Py}; -use crate::{Borrowed, Bound, IntoPyObject, PyAny, PyErr, Python}; +use crate::{Borrowed, Bound, IntoPyObject, PyAny, Python}; #[cfg(not(Py_LIMITED_API))] use std::ffi::c_int; @@ -48,38 +52,6 @@ fn expect_datetime_api(py: Python<'_>) -> &'static PyDateTime_CAPI { ensure_datetime_api(py).expect("failed to import `datetime` C API") } -#[cfg(Py_LIMITED_API)] -struct DatetimeTypes { - date: Py, - datetime: Py, - time: Py, - timedelta: Py, - timezone: Py, - tzinfo: Py, -} - -#[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: PyOnceLock = PyOnceLock::new(); - TYPES.get_or_try_init(py, || { - let datetime = py.import("datetime")?; - Ok::<_, PyErr>(Self { - date: datetime.getattr("date")?.cast_into()?.into(), - datetime: datetime.getattr("datetime")?.cast_into()?.into(), - time: datetime.getattr("time")?.cast_into()?.into(), - timedelta: datetime.getattr("timedelta")?.cast_into()?.into(), - timezone: datetime.getattr("timezone")?.cast_into()?.into(), - tzinfo: datetime.getattr("tzinfo")?.cast_into()?.into(), - }) - }) - } -} - // Type Check macros // // These are bindings around the C API typecheck macros, all of them return @@ -238,7 +210,10 @@ pyobject_subclassable_native_type!(PyDate, crate::ffi::PyDateTime_Date); #[cfg(Py_LIMITED_API)] pyobject_native_type_core!( PyDate, - |py| type_to_ptr(&DatetimeTypes::try_get(py).unwrap().date, py), + |py| { + static TYPE: PyOnceLock> = PyOnceLock::new(); + TYPE.import(py, "datetime", "date").unwrap().as_type_ptr() + }, #module=Some("datetime") ); @@ -255,13 +230,9 @@ impl PyDate { } } #[cfg(Py_LIMITED_API)] - unsafe { - Ok(DatetimeTypes::try_get(py)? - .date - .bind(py) - .call((year, month, day), None)? - .cast_into_unchecked()) - } + Ok(Self::type_object(py) + .call((year, month, day), None)? + .cast_into()?) } /// Construct a `datetime.date` from a POSIX timestamp @@ -283,13 +254,9 @@ impl PyDate { } #[cfg(Py_LIMITED_API)] - unsafe { - Ok(DatetimeTypes::try_get(py)? - .date - .bind(py) - .call_method1("fromtimestamp", (timestamp,))? - .cast_into_unchecked()) - } + Ok(Self::type_object(py) + .call_method1("fromtimestamp", (timestamp,))? + .cast_into()?) } } @@ -329,7 +296,10 @@ pyobject_subclassable_native_type!(PyDateTime, crate::ffi::PyDateTime_DateTime); #[cfg(Py_LIMITED_API)] pyobject_native_type_core!( PyDateTime, - |py| type_to_ptr(&DatetimeTypes::try_get(py).unwrap().datetime, py), + |py| { + static TYPE: PyOnceLock> = PyOnceLock::new(); + TYPE.import(py, "datetime", "datetime").unwrap().as_type_ptr() + }, #module=Some("datetime") ); @@ -368,16 +338,12 @@ impl PyDateTime { } #[cfg(Py_LIMITED_API)] - unsafe { - Ok(DatetimeTypes::try_get(py)? - .datetime - .bind(py) - .call( - (year, month, day, hour, minute, second, microsecond, tzinfo), - None, - )? - .cast_into_unchecked()) - } + Ok(Self::type_object(py) + .call( + (year, month, day, hour, minute, second, microsecond, tzinfo), + None, + )? + .cast_into()?) } /// Alternate constructor that takes a `fold` parameter. A `true` value for this parameter @@ -422,16 +388,12 @@ impl PyDateTime { } #[cfg(Py_LIMITED_API)] - unsafe { - Ok(DatetimeTypes::try_get(py)? - .datetime - .bind(py) - .call( - (year, month, day, hour, minute, second, microsecond, tzinfo), - Some(&[("fold", fold)].into_py_dict(py)?), - )? - .cast_into_unchecked()) - } + Ok(Self::type_object(py) + .call( + (year, month, day, hour, minute, second, microsecond, tzinfo), + Some(&[("fold", fold)].into_py_dict(py)?), + )? + .cast_into()?) } /// Construct a `datetime` object from a POSIX timestamp @@ -457,13 +419,9 @@ impl PyDateTime { } #[cfg(Py_LIMITED_API)] - unsafe { - Ok(DatetimeTypes::try_get(py)? - .datetime - .bind(py) - .call_method1("fromtimestamp", (timestamp, tzinfo))? - .cast_into_unchecked()) - } + Ok(Self::type_object(py) + .call_method1("fromtimestamp", (timestamp, tzinfo))? + .cast_into()?) } } @@ -570,7 +528,10 @@ pyobject_subclassable_native_type!(PyTime, crate::ffi::PyDateTime_Time); #[cfg(Py_LIMITED_API)] pyobject_native_type_core!( PyTime, - |py| type_to_ptr(&DatetimeTypes::try_get(py).unwrap().time, py), + |py| { + static TYPE: PyOnceLock> = PyOnceLock::new(); + TYPE.import(py, "datetime", "time").unwrap().as_type_ptr() + }, #module=Some("datetime") ); @@ -602,13 +563,9 @@ impl PyTime { } #[cfg(Py_LIMITED_API)] - unsafe { - Ok(DatetimeTypes::try_get(py)? - .time - .bind(py) - .call((hour, minute, second, microsecond, tzinfo), None)? - .cast_into_unchecked()) - } + Ok(Self::type_object(py) + .call((hour, minute, second, microsecond, tzinfo), None)? + .cast_into()?) } /// Alternate constructor that takes a `fold` argument. See [`PyDateTime::new_with_fold`]. @@ -640,17 +597,12 @@ impl PyTime { } #[cfg(Py_LIMITED_API)] - #[cfg(Py_LIMITED_API)] - unsafe { - Ok(DatetimeTypes::try_get(py)? - .time - .bind(py) - .call( - (hour, minute, second, microsecond, tzinfo), - Some(&[("fold", fold)].into_py_dict(py)?), - )? - .cast_into_unchecked()) - } + Ok(Self::type_object(py) + .call( + (hour, minute, second, microsecond, tzinfo), + Some(&[("fold", fold)].into_py_dict(py)?), + )? + .cast_into()?) } } @@ -746,7 +698,10 @@ pyobject_subclassable_native_type!(PyTzInfo, crate::ffi::PyObject); #[cfg(Py_LIMITED_API)] pyobject_native_type_core!( PyTzInfo, - |py| type_to_ptr(&DatetimeTypes::try_get(py).unwrap().tzinfo, py), + |py| { + static TYPE: PyOnceLock> = PyOnceLock::new(); + TYPE.import(py, "datetime", "tzinfo").unwrap().as_type_ptr() + }, #module=Some("datetime") ); @@ -765,9 +720,9 @@ impl PyTzInfo { { static UTC: PyOnceLock> = PyOnceLock::new(); UTC.get_or_try_init(py, || { - Ok(DatetimeTypes::get(py) - .timezone - .bind(py) + Ok(py + .import("datetime")? + .getattr("timezone")? .getattr("utc")? .cast_into()? .unbind()) @@ -813,12 +768,12 @@ impl PyTzInfo { } #[cfg(Py_LIMITED_API)] - unsafe { - Ok(DatetimeTypes::try_get(py)? - .timezone - .bind(py) + { + static TIMEZONE: PyOnceLock> = PyOnceLock::new(); + Ok(TIMEZONE + .import(py, "datetime", "timezone")? .call1((offset,))? - .cast_into_unchecked()) + .cast_into()?) } } } @@ -852,7 +807,10 @@ pyobject_subclassable_native_type!(PyDelta, crate::ffi::PyDateTime_Delta); #[cfg(Py_LIMITED_API)] pyobject_native_type_core!( PyDelta, - |py| type_to_ptr(&DatetimeTypes::try_get(py).unwrap().timedelta, py), + |py| { + static TYPE: PyOnceLock> = PyOnceLock::new(); + TYPE.import(py, "datetime", "timedelta").unwrap().as_type_ptr() + }, #module=Some("datetime") ); @@ -882,14 +840,11 @@ impl PyDelta { } #[cfg(Py_LIMITED_API)] - unsafe { - let _ = normalize; - Ok(DatetimeTypes::try_get(py)? - .timedelta - .bind(py) - .call1((days, seconds, microseconds))? - .cast_into_unchecked()) - } + let _ = normalize; + #[cfg(Py_LIMITED_API)] + Ok(Self::type_object(py) + .call1((days, seconds, microseconds))? + .cast_into()?) } } @@ -918,12 +873,6 @@ fn opt_to_pyobj(opt: Option<&Bound<'_, PyTzInfo>>) -> *mut ffi::PyObject { } } -// Utility to get pointer to the type object in order to feed PyTypeInfo::type_object_raw -#[cfg(Py_LIMITED_API)] -fn type_to_ptr(t: &Py, py: Python<'_>) -> *mut crate::ffi::PyTypeObject { - t.bind_borrowed(py).as_type_ptr() -} - #[cfg(test)] mod tests { use super::*; From 242c3ff81194639b580cebe25cdc19fbf300902d Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Wed, 3 Sep 2025 18:11:43 +0100 Subject: [PATCH 840/936] ci: stop running test suite on main (#5399) --- .github/workflows/ci-cache-warmup.yml | 34 +++++++++++++++ .github/workflows/ci.yml | 31 +++---------- noxfile.py | 63 +++++++++++++++++++++++++++ 3 files changed, 103 insertions(+), 25 deletions(-) create mode 100644 .github/workflows/ci-cache-warmup.yml diff --git a/.github/workflows/ci-cache-warmup.yml b/.github/workflows/ci-cache-warmup.yml new file mode 100644 index 00000000000..d3cb5c0577f --- /dev/null +++ b/.github/workflows/ci-cache-warmup.yml @@ -0,0 +1,34 @@ +name: CI Cache Warmup + +on: + push: + branches: + - main + +jobs: + cross-compilation-windows: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-python@v5 + with: + python-version: "3.13" + - uses: dtolnay/rust-toolchain@stable + with: + targets: x86_64-pc-windows-gnu,x86_64-pc-windows-msvc + components: rust-src + - uses: actions/cache/restore@v4 + with: + # https://github.com/PyO3/maturin/discussions/1953 + path: ~/.cache/cargo-xwin + key: cargo-xwin-cache + - name: Test cross compile to Windows + run: | + set -ex + sudo apt-get install -y mingw-w64 llvm + pip install nox + nox -s test-cross-compilation-windows + - uses: actions/cache/save@v4 + with: + path: ~/.cache/cargo-xwin + key: cargo-xwin-cache diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3891020fd73..31ff367fc71 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,9 +1,6 @@ name: CI on: - push: - branches: - - main pull_request: merge_group: types: [checks_requested] @@ -84,8 +81,6 @@ jobs: CARGO_BUILD_TARGET: x86_64-unknown-linux-gnu clippy: - # Don't run clippy on `main` because it's already run in the merge queue. - if: github.ref != 'refs/heads/main' needs: [fmt] runs-on: ubuntu-24.04-arm strategy: @@ -660,35 +655,21 @@ jobs: - uses: actions/setup-python@v5 with: python-version: "3.13" - - uses: Swatinem/rust-cache@v2 + - uses: dtolnay/rust-toolchain@stable with: - workspaces: examples/maturin-starter - save-if: ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'CI-save-pr-cache') }} + targets: x86_64-pc-windows-gnu,x86_64-pc-windows-msvc + components: rust-src + # load cache (prepared in ci-cache-warmup.yml) - uses: actions/cache/restore@v4 with: - # https://github.com/PyO3/maturin/discussions/1953 path: ~/.cache/cargo-xwin key: cargo-xwin-cache - name: Test cross compile to Windows - env: - XWIN_ARCH: x86_64 run: | set -ex sudo apt-get install -y mingw-w64 llvm - rustup target add x86_64-pc-windows-gnu x86_64-pc-windows-msvc - pip install cargo-xwin - # abi3 - cargo build --manifest-path examples/maturin-starter/Cargo.toml --features abi3 --target x86_64-pc-windows-gnu - cargo xwin build --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.13 - cargo build --manifest-path examples/maturin-starter/Cargo.toml --features generate-import-lib --target x86_64-pc-windows-gnu - 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: - path: ~/.cache/cargo-xwin - key: cargo-xwin-cache + pip install nox + nox -s test-cross-compilation-windows test-introspection: needs: [fmt] diff --git a/noxfile.py b/noxfile.py index d2d551b12f7..3cf76738135 100644 --- a/noxfile.py +++ b/noxfile.py @@ -440,6 +440,69 @@ def test_emscripten(session: nox.Session): ) +@nox.session(name="test-cross-compilation-windows") +def test_cross_compilation_windows(session: nox.Session): + session.install("cargo-xwin") + + env = os.environ.copy() + env["XWIN_ARCH"] = "x86_64" + + # abi3 + _run_cargo( + session, + "build", + "--manifest-path", + "examples/maturin-starter/Cargo.toml", + "--features", + "abi3", + "--target", + "x86_64-pc-windows-gnu", + env=env, + ) + _run_cargo( + session, + "xwin", + "build", + "--cross-compiler", + "clang", + "--manifest-path", + "examples/maturin-starter/Cargo.toml", + "--features", + "abi3", + "--target", + "x86_64-pc-windows-msvc", + env=env, + ) + + # non-abi3 + env["PYO3_CROSS_PYTHON_VERSION"] = "3.13" + _run_cargo( + session, + "build", + "--manifest-path", + "examples/maturin-starter/Cargo.toml", + "--features", + "generate-import-lib", + "--target", + "x86_64-pc-windows-gnu", + env=env, + ) + _run_cargo( + session, + "xwin", + "build", + "--cross-compiler", + "clang", + "--manifest-path", + "examples/maturin-starter/Cargo.toml", + "--features", + "generate-import-lib", + "--target", + "x86_64-pc-windows-msvc", + env=env, + ) + + @nox.session(venv_backend="none") def docs(session: nox.Session, nightly: bool = False, internal: bool = False) -> None: rustdoc_flags = ["-Dwarnings"] From a893a6b25b8573fe587985e7aab6c19b68a47526 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Wed, 3 Sep 2025 18:15:37 +0100 Subject: [PATCH 841/936] ci: use semver checks only to compare current PR changes (#5398) * ci: use semver checks only to compare current PR changes * fixup event_name --- .github/actions/fetch-merge-base/action.yml | 31 +++++++++++++++++++++ .github/workflows/ci.yml | 10 ++++++- .github/workflows/coverage-pr-base.yml | 23 +++++---------- 3 files changed, 47 insertions(+), 17 deletions(-) create mode 100644 .github/actions/fetch-merge-base/action.yml diff --git a/.github/actions/fetch-merge-base/action.yml b/.github/actions/fetch-merge-base/action.yml new file mode 100644 index 00000000000..7ddbce5b65e --- /dev/null +++ b/.github/actions/fetch-merge-base/action.yml @@ -0,0 +1,31 @@ +name: 'Fetch Merge Base' +description: 'Fetches the merge base between two branches, deepening the git checkout until complete' +inputs: + base_ref: + description: 'The base branch reference' + required: true + head_ref: + description: 'The head branch reference' + required: true +outputs: + merge_base: + description: 'The merge base commit SHA' + value: ${{ steps.fetch_merge_base.outputs.merge_base }} +runs: + using: "composite" + steps: + - name: Fetch Merge Base + id: fetch_merge_base + shell: bash + run: | + # fetch the merge commit between the PR base and head + git fetch -u --progress --depth=1 origin "+$BASE_REF:$BASE_REF" "+$HEAD_REF:$HEAD_REF" + while [ -z "$(git merge-base "$BASE_REF" "$HEAD_REF")" ]; do + git fetch -u -q --deepen="10" origin "$BASE_REF" "$HEAD_REF"; + done + + MERGE_BASE=$(git merge-base "$BASE_REF" "$HEAD_REF") + echo "merge_base=$MERGE_BASE" >> $GITHUB_OUTPUT + env: + BASE_REF: "${{ inputs.base_ref }}" + HEAD_REF: "${{ inputs.head_ref }}" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 31ff367fc71..989f2344c94 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,7 +45,7 @@ jobs: run: echo MSRV=`python -c 'import tomllib; print(tomllib.load(open("Cargo.toml", "rb"))["workspace"]["package"]["rust-version"])'` >> $GITHUB_OUTPUT semver-checks: - if: github.ref != 'refs/heads/main' + if: github.event_name == 'pull_request' needs: [fmt] runs-on: ubuntu-latest steps: @@ -53,7 +53,15 @@ jobs: - uses: actions/setup-python@v5 with: python-version: "3.13" + - name: Fetch merge base + id: fetch_merge_base + uses: ./.github/actions/fetch-merge-base + with: + base_ref: "refs/heads/${{ github.event.pull_request.base.ref }}" + head_ref: "refs/pull/${{ github.event.pull_request.number }}/merge" - uses: obi1kenobi/cargo-semver-checks-action@v2 + with: + baseline-rev: ${{ steps.fetch_merge_base.outputs.merge_base }} check-msrv: needs: [fmt, resolve] diff --git a/.github/workflows/coverage-pr-base.yml b/.github/workflows/coverage-pr-base.yml index 38417ab8e55..9c72567ed34 100644 --- a/.github/workflows/coverage-pr-base.yml +++ b/.github/workflows/coverage-pr-base.yml @@ -16,27 +16,18 @@ jobs: - uses: actions/setup-python@v5 with: python-version: '3.13' + - name: Fetch merge base + id: fetch_merge_base + uses: ./.github/actions/fetch-merge-base + with: + base_ref: "refs/heads/${{ github.event.pull_request.base.ref }}" + head_ref: "refs/pull/${{ github.event.pull_request.number }}/merge" - name: Set PR base on codecov run: | - # fetch the merge commit between the PR base and head - 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"; - 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 \ + --base-sha ${{ steps.fetch_merge_base.outputs.merge_base }} \ --pr ${{ github.event.number }} \ --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 a806a399e64330db5c00113f291b73ab8567b3be Mon Sep 17 00:00:00 2001 From: Thomas Tanon Date: Wed, 3 Sep 2025 19:40:37 +0200 Subject: [PATCH 842/936] Implement PyTypeInfo on PyCode when using the stable API (#5403) --- newsfragments/5403.added.md | 1 + src/types/code.rs | 29 ++++++++++------------------- 2 files changed, 11 insertions(+), 19 deletions(-) create mode 100644 newsfragments/5403.added.md diff --git a/newsfragments/5403.added.md b/newsfragments/5403.added.md new file mode 100644 index 00000000000..e2828417a12 --- /dev/null +++ b/newsfragments/5403.added.md @@ -0,0 +1 @@ +Implement `PyTypeInfo` on `PyCode` when using the stable ABI \ No newline at end of file diff --git a/src/types/code.rs b/src/types/code.rs index dbd12ee6075..7cbfa5262bc 100644 --- a/src/types/code.rs +++ b/src/types/code.rs @@ -2,6 +2,12 @@ use super::PyAnyMethods as _; use super::PyDict; use crate::ffi_ptr_ext::FfiPtrExt; use crate::py_result_ext::PyResultExt; +#[cfg(any(Py_LIMITED_API, PyPy))] +use crate::sync::PyOnceLock; +#[cfg(any(Py_LIMITED_API, PyPy))] +use crate::types::{PyType, PyTypeMethods}; +#[cfg(any(Py_LIMITED_API, PyPy))] +use crate::Py; use crate::{ffi, Bound, PyAny, PyErr, PyResult, Python}; use std::ffi::CStr; @@ -20,24 +26,10 @@ pyobject_native_type_core!( ); #[cfg(any(Py_LIMITED_API, PyPy))] -pyobject_native_type_named!(PyCode); - -#[cfg(any(Py_LIMITED_API, PyPy))] -impl crate::PyTypeCheck for PyCode { - const NAME: &'static str = "PyCode"; - #[cfg(feature = "experimental-inspect")] - const PYTHON_TYPE: &'static str = "types.CodeType"; - - fn type_check(object: &Bound<'_, PyAny>) -> bool { - let py = object.py(); - static TYPE: crate::sync::PyOnceLock> = - crate::sync::PyOnceLock::new(); - - TYPE.import(py, "types", "CodeType") - .and_then(|ty| object.is_instance(ty)) - .unwrap_or_default() - } -} +pyobject_native_type_core!(PyCode, |py| { + static TYPE: PyOnceLock> = PyOnceLock::new(); + TYPE.import(py, "types", "CodeType").unwrap().as_type_ptr() +}); /// Compilation mode of [`PyCode::compile`] pub enum PyCodeInput { @@ -145,7 +137,6 @@ impl<'py> PyCodeMethods<'py> for Bound<'py, PyCode> { #[cfg(test)] mod tests { #[test] - #[cfg(not(any(Py_LIMITED_API, PyPy)))] fn test_type_object() { use crate::types::PyTypeMethods; use crate::{PyTypeInfo, Python}; From f42c36546ca428c302d09fbf78b50069279c6229 Mon Sep 17 00:00:00 2001 From: Thomas Tanon Date: Wed, 3 Sep 2025 19:48:51 +0200 Subject: [PATCH 843/936] implement PyTypeInfo on PyWeakrefReference when using stable API (#5404) --- newsfragments/5404.added.md | 1 + src/types/weakref/reference.rs | 50 ++++++++++++++++++++++------------ 2 files changed, 34 insertions(+), 17 deletions(-) create mode 100644 newsfragments/5404.added.md diff --git a/newsfragments/5404.added.md b/newsfragments/5404.added.md new file mode 100644 index 00000000000..0d6d8492826 --- /dev/null +++ b/newsfragments/5404.added.md @@ -0,0 +1 @@ +Implement `PyTypeInfo` on `PyWeakrefReference` when using the stable ABI \ No newline at end of file diff --git a/src/types/weakref/reference.rs b/src/types/weakref/reference.rs index bf76fd706c7..492da7888b4 100644 --- a/src/types/weakref/reference.rs +++ b/src/types/weakref/reference.rs @@ -1,11 +1,16 @@ use crate::err::PyResult; use crate::ffi_ptr_ext::FfiPtrExt; use crate::py_result_ext::PyResultExt; +#[cfg(any(PyPy, GraalPy, Py_LIMITED_API))] +use crate::sync::PyOnceLock; use crate::types::any::PyAny; -use crate::{ffi, Borrowed, Bound, BoundObject, IntoPyObject, IntoPyObjectExt}; - #[cfg(any(PyPy, GraalPy, Py_LIMITED_API))] -use crate::type_object::PyTypeCheck; +use crate::types::typeobject::PyTypeMethods; +#[cfg(any(PyPy, GraalPy, Py_LIMITED_API))] +use crate::types::PyType; +#[cfg(any(PyPy, GraalPy, Py_LIMITED_API))] +use crate::Py; +use crate::{ffi, Borrowed, Bound, BoundObject, IntoPyObject, IntoPyObjectExt}; use super::PyWeakrefMethods; @@ -16,7 +21,7 @@ use super::PyWeakrefMethods; pub struct PyWeakrefReference(PyAny); #[cfg(not(any(PyPy, GraalPy, Py_LIMITED_API)))] -pyobject_subclassable_native_type!(PyWeakrefReference, crate::ffi::PyWeakReference); +pyobject_subclassable_native_type!(PyWeakrefReference, ffi::PyWeakReference); #[cfg(not(any(PyPy, GraalPy, Py_LIMITED_API)))] pyobject_native_type!( @@ -30,18 +35,17 @@ pyobject_native_type!( // When targeting 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))] -impl PyTypeCheck for PyWeakrefReference { - const NAME: &'static str = "weakref.ReferenceType"; - #[cfg(feature = "experimental-inspect")] - const PYTHON_TYPE: &'static str = "weakref.ReferenceType"; - - fn type_check(object: &Bound<'_, PyAny>) -> bool { - unsafe { ffi::PyWeakref_CheckRef(object.as_ptr()) > 0 } - } -} +pyobject_native_type_core!( + PyWeakrefReference, + |py| { + static TYPE: PyOnceLock> = PyOnceLock::new(); + TYPE.import(py, "weakref", "ref") + .unwrap() + .as_type_ptr() + }, + #module=Some("weakref"), + #checkfunction=ffi::PyWeakref_CheckRef +); impl PyWeakrefReference { /// Constructs a new Weak Reference (`weakref.ref`/`weakref.ReferenceType`) for the given object. @@ -238,7 +242,7 @@ mod tests { mod python_class { use super::*; - use crate::ffi; + use crate::{ffi, PyTypeInfo}; use crate::{py_result_ext::PyResultExt, types::PyType}; use std::ptr; @@ -372,6 +376,18 @@ mod tests { Ok(()) }) } + + #[test] + fn test_type_object() -> PyResult<()> { + Python::attach(|py| { + let class = get_type(py)?; + let object = class.call0()?; + let reference = PyWeakrefReference::new(&object)?; + + assert!(reference.is_instance(&PyWeakrefReference::type_object(py))?); + Ok(()) + }) + } } // under 'abi3-py37' and 'abi3-py38' PyClass cannot be weakreferencable. From c5c941e1afaf78e9791634ad0fddd4cfa79841d9 Mon Sep 17 00:00:00 2001 From: Thomas Tanon Date: Wed, 3 Sep 2025 20:05:34 +0200 Subject: [PATCH 844/936] PyIterator, PyMapping, PySequence: implement PyTypeInfo (#5402) Returns the collections.abc type --- newsfragments/5402.added.md | 1 + src/types/iterator.rs | 40 +++++++++++++------- src/types/mapping.rs | 67 +++++++++++++++++++-------------- src/types/sequence.rs | 75 +++++++++++++++++++++---------------- 4 files changed, 108 insertions(+), 75 deletions(-) create mode 100644 newsfragments/5402.added.md diff --git a/newsfragments/5402.added.md b/newsfragments/5402.added.md new file mode 100644 index 00000000000..88ff4a9c589 --- /dev/null +++ b/newsfragments/5402.added.md @@ -0,0 +1 @@ +Implement `PyTypeInfo` on `PyIterator`, `PyMapping` and `PySequence` \ No newline at end of file diff --git a/src/types/iterator.rs b/src/types/iterator.rs index 5424c8849c4..2d300c5d50e 100644 --- a/src/types/iterator.rs +++ b/src/types/iterator.rs @@ -1,7 +1,9 @@ use crate::ffi_ptr_ext::FfiPtrExt; use crate::instance::Borrowed; use crate::py_result_ext::PyResultExt; -use crate::{ffi, Bound, PyAny, PyErr, PyResult, PyTypeCheck}; +use crate::sync::PyOnceLock; +use crate::types::{PyType, PyTypeMethods}; +use crate::{ffi, Bound, Py, PyAny, PyErr, PyResult}; /// A Python iterator object. /// @@ -29,7 +31,18 @@ use crate::{ffi, Bound, PyAny, PyErr, PyResult, PyTypeCheck}; /// ``` #[repr(transparent)] pub struct PyIterator(PyAny); -pyobject_native_type_named!(PyIterator); + +pyobject_native_type_core!( + PyIterator, + |py| { + static TYPE: PyOnceLock> = PyOnceLock::new(); + TYPE.import(py, "collections.abc", "Iterator") + .unwrap() + .as_type_ptr() + }, + #module=Some("collections.abc"), + #checkfunction=ffi::PyIter_Check +); impl PyIterator { /// Builds an iterator for an iterable Python object; the equivalent of calling `iter(obj)` in Python. @@ -117,16 +130,6 @@ impl<'py> IntoIterator for &Bound<'py, PyIterator> { } } -impl PyTypeCheck for PyIterator { - const NAME: &'static str = "Iterator"; - #[cfg(feature = "experimental-inspect")] - const PYTHON_TYPE: &'static str = "collections.abc.Iterator"; - - fn type_check(object: &Bound<'_, PyAny>) -> bool { - unsafe { ffi::PyIter_Check(object.as_ptr()) != 0 } - } -} - #[cfg(test)] mod tests { use super::PyIterator; @@ -136,7 +139,7 @@ mod tests { #[cfg(all(not(PyPy), Py_3_10))] use crate::types::PyNone; use crate::types::{PyAnyMethods, PyDict, PyList, PyListMethods}; - use crate::{ffi, IntoPyObject, Python}; + use crate::{ffi, IntoPyObject, PyTypeInfo, Python}; #[test] fn vec_iter() { @@ -351,7 +354,7 @@ def fibonacci(target): assert_eq!( downcaster.borrow_mut(py).failed.take().unwrap().to_string(), - "TypeError: 'MySequence' object cannot be converted to 'Iterator'" + "TypeError: 'MySequence' object cannot be converted to 'PyIterator'" ); }); } @@ -391,4 +394,13 @@ def fibonacci(target): assert_eq!(hint, (3, None)); }); } + + #[test] + fn test_type_object() { + Python::attach(|py| { + let abc = PyIterator::type_object(py); + let iter = py.eval(ffi::c_str!("iter(())"), None, None).unwrap(); + assert!(iter.is_instance(&abc).unwrap()); + }) + } } diff --git a/src/types/mapping.rs b/src/types/mapping.rs index 8887f19db9e..af5b26ba463 100644 --- a/src/types/mapping.rs +++ b/src/types/mapping.rs @@ -6,8 +6,8 @@ use crate::py_result_ext::PyResultExt; use crate::sync::PyOnceLock; use crate::type_object::PyTypeInfo; use crate::types::any::PyAnyMethods; -use crate::types::{PyAny, PyDict, PyList, PyType}; -use crate::{ffi, Py, PyTypeCheck, Python}; +use crate::types::{PyAny, PyDict, PyList, PyType, PyTypeMethods}; +use crate::{ffi, Py, Python}; /// Represents a reference to a Python object supporting the mapping protocol. /// @@ -18,15 +18,43 @@ use crate::{ffi, Py, PyTypeCheck, Python}; /// [`Bound<'py, PyMapping>`][Bound]. #[repr(transparent)] pub struct PyMapping(PyAny); + pyobject_native_type_named!(PyMapping); +unsafe impl PyTypeInfo for PyMapping { + const NAME: &'static str = "Mapping"; + const MODULE: Option<&'static str> = Some("collections.abc"); + + #[inline] + #[allow(clippy::redundant_closure_call)] + fn type_object_raw(py: Python<'_>) -> *mut ffi::PyTypeObject { + static TYPE: PyOnceLock> = PyOnceLock::new(); + TYPE.import(py, "collections.abc", "Mapping") + .unwrap() + .as_type_ptr() + } + + #[inline] + fn is_type_of(object: &Bound<'_, PyAny>) -> bool { + // Using `is_instance` for `collections.abc.Mapping` is slow, so provide + // optimized case dict as a well-known mapping + PyDict::is_type_of(object) + || object + .is_instance(&Self::type_object(object.py()).into_any()) + .unwrap_or_else(|err| { + err.write_unraisable(object.py(), Some(object)); + false + }) + } +} + 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 castable from `PyAny` to `PyMapping`. pub fn register(py: Python<'_>) -> PyResult<()> { let ty = T::type_object(py); - get_mapping_abc(py)?.call_method1("register", (ty,))?; + Self::type_object(py).call_method1("register", (ty,))?; Ok(()) } } @@ -160,31 +188,6 @@ impl<'py> PyMappingMethods<'py> for Bound<'py, PyMapping> { } } -fn get_mapping_abc(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> { - static MAPPING_ABC: PyOnceLock> = PyOnceLock::new(); - - MAPPING_ABC.import(py, "collections.abc", "Mapping") -} - -impl PyTypeCheck for PyMapping { - const NAME: &'static str = "Mapping"; - #[cfg(feature = "experimental-inspect")] - const PYTHON_TYPE: &'static str = "collections.abc.Mapping"; - - #[inline] - fn type_check(object: &Bound<'_, PyAny>) -> bool { - // Using `is_instance` for `collections.abc.Mapping` is slow, so provide - // optimized case dict as a well-known mapping - PyDict::is_type_of(object) - || get_mapping_abc(object.py()) - .and_then(|abc| object.is_instance(abc)) - .unwrap_or_else(|err| { - err.write_unraisable(object.py(), Some(object)); - false - }) - } -} - #[cfg(test)] mod tests { use std::collections::HashMap; @@ -337,4 +340,12 @@ mod tests { assert_eq!(32 + 42 + 123, values_sum); }); } + + #[test] + fn test_type_object() { + Python::attach(|py| { + let abc = PyMapping::type_object(py); + assert!(PyDict::new(py).is_instance(&abc).unwrap()); + }) + } } diff --git a/src/types/sequence.rs b/src/types/sequence.rs index fb3d661e76d..ba82ef350ca 100644 --- a/src/types/sequence.rs +++ b/src/types/sequence.rs @@ -9,11 +9,8 @@ use crate::internal_tricks::get_ssize_index; use crate::py_result_ext::PyResultExt; use crate::sync::PyOnceLock; use crate::type_object::PyTypeInfo; -use crate::types::{any::PyAnyMethods, PyAny, PyList, PyString, PyTuple, PyType}; -use crate::{ - ffi, Borrowed, BoundObject, FromPyObject, IntoPyObject, IntoPyObjectExt, Py, PyTypeCheck, - Python, -}; +use crate::types::{any::PyAnyMethods, PyAny, PyList, PyString, PyTuple, PyType, PyTypeMethods}; +use crate::{ffi, Borrowed, BoundObject, FromPyObject, IntoPyObject, IntoPyObjectExt, Py, Python}; /// Represents a reference to a Python object supporting the sequence protocol. /// @@ -24,15 +21,44 @@ use crate::{ /// [`Bound<'py, PySequence>`][Bound]. #[repr(transparent)] pub struct PySequence(PyAny); + pyobject_native_type_named!(PySequence); +unsafe impl PyTypeInfo for PySequence { + const NAME: &'static str = "Sequence"; + const MODULE: Option<&'static str> = Some("collections.abc"); + + #[inline] + #[allow(clippy::redundant_closure_call)] + fn type_object_raw(py: Python<'_>) -> *mut ffi::PyTypeObject { + static TYPE: PyOnceLock> = PyOnceLock::new(); + TYPE.import(py, "collections.abc", "Sequence") + .unwrap() + .as_type_ptr() + } + + #[inline] + fn is_type_of(object: &Bound<'_, PyAny>) -> bool { + // Using `is_instance` for `collections.abc.Sequence` is slow, so provide + // optimized cases for list and tuples as common well-known sequences + PyList::is_type_of(object) + || PyTuple::is_type_of(object) + || object + .is_instance(&Self::type_object(object.py()).into_any()) + .unwrap_or_else(|err| { + err.write_unraisable(object.py(), Some(object)); + false + }) + } +} + 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 castable from `PyAny` to `PySequence`. pub fn register(py: Python<'_>) -> PyResult<()> { let ty = T::type_object(py); - get_sequence_abc(py)?.call_method1("register", (ty,))?; + Self::type_object(py).call_method1("register", (ty,))?; Ok(()) } } @@ -372,36 +398,10 @@ where Ok(v) } -fn get_sequence_abc(py: Python<'_>) -> PyResult<&Bound<'_, PyType>> { - static SEQUENCE_ABC: PyOnceLock> = PyOnceLock::new(); - - SEQUENCE_ABC.import(py, "collections.abc", "Sequence") -} - -impl PyTypeCheck for PySequence { - const NAME: &'static str = "Sequence"; - #[cfg(feature = "experimental-inspect")] - const PYTHON_TYPE: &'static str = "collections.abc.Sequence"; - - #[inline] - fn type_check(object: &Bound<'_, PyAny>) -> bool { - // Using `is_instance` for `collections.abc.Sequence` is slow, so provide - // optimized cases for list and tuples as common well-known sequences - PyList::is_type_of(object) - || PyTuple::is_type_of(object) - || get_sequence_abc(object.py()) - .and_then(|abc| object.is_instance(abc)) - .unwrap_or_else(|err| { - err.write_unraisable(object.py(), Some(object)); - false - }) - } -} - #[cfg(test)] mod tests { use crate::types::{PyAnyMethods, PyList, PySequence, PySequenceMethods, PyTuple}; - use crate::{ffi, IntoPyObject, Py, PyAny, Python}; + use crate::{ffi, IntoPyObject, Py, PyAny, PyTypeInfo, Python}; use std::ptr; fn get_object() -> Py { @@ -827,4 +827,13 @@ mod tests { assert!(seq_from.to_list().is_ok()); }); } + + #[test] + fn test_type_object() { + Python::attach(|py| { + let abc = PySequence::type_object(py); + assert!(PyList::empty(py).is_instance(&abc).unwrap()); + assert!(PyTuple::empty(py).is_instance(&abc).unwrap()); + }) + } } From 058824003a52f8966160058064a8e17931d58b8a Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Wed, 3 Sep 2025 21:43:30 +0100 Subject: [PATCH 845/936] ci: cache more in `benches.yml` (#5406) --- .github/workflows/benches.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/benches.yml b/.github/workflows/benches.yml index d10882f301a..8ce37097bdb 100644 --- a/.github/workflows/benches.yml +++ b/.github/workflows/benches.yml @@ -31,6 +31,7 @@ jobs: . pyo3-benches save-if: ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'CI-save-pr-cache') }} + cache-all-crates: 'true' - uses: taiki-e/install-action@v2 with: From 787fbe812389204c369c9fd1a3d5400e78ce6af2 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Thu, 4 Sep 2025 11:16:00 +0100 Subject: [PATCH 846/936] ci: cache even more benchmark target contents (#5410) --- .github/workflows/benches.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/benches.yml b/.github/workflows/benches.yml index 8ce37097bdb..8c46141bc14 100644 --- a/.github/workflows/benches.yml +++ b/.github/workflows/benches.yml @@ -32,6 +32,7 @@ jobs: pyo3-benches save-if: ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'CI-save-pr-cache') }} cache-all-crates: 'true' + cache-workspace-crates: 'true' - uses: taiki-e/install-action@v2 with: From ddb9b4d25f8ebcb498132a10acef43e874b2b050 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Thu, 4 Sep 2025 17:08:32 +0100 Subject: [PATCH 847/936] deprecate `pyfn` attribute (#5384) * deprecate `pyfn` attribute * add UI test --- guide/src/function.md | 59 ++-------- newsfragments/5384.changed.md | 1 + pyo3-macros-backend/src/module.rs | 17 ++- tests/test_compile_error.rs | 1 + tests/test_module.rs | 179 +++++++++++++++++++----------- tests/test_text_signature.rs | 1 + tests/ui/deprecated_pyfn.rs | 13 +++ tests/ui/deprecated_pyfn.stderr | 11 ++ 8 files changed, 161 insertions(+), 121 deletions(-) create mode 100644 newsfragments/5384.changed.md create mode 100644 tests/ui/deprecated_pyfn.rs create mode 100644 tests/ui/deprecated_pyfn.stderr diff --git a/guide/src/function.md b/guide/src/function.md index b129480559c..2a3d8f84240 100644 --- a/guide/src/function.md +++ b/guide/src/function.md @@ -26,7 +26,6 @@ This chapter of the guide explains full usage of the `#[pyfunction]` attribute. - [`#[pyo3(warn(message = "...", category = ...))]`](#warn) - [Per-argument options](#per-argument-options) - [Advanced function patterns](#advanced-function-patterns) -- [`#[pyfn]` shorthand](#pyfn-shorthand) There are also additional sections on the following topics: @@ -95,11 +94,11 @@ The `#[pyo3]` attribute can be used to modify properties of the generated Python ``` - `#[pyo3(warn(message = "...", category = ...))]` - This option is used to display a warning when the function is used in Python. It is equivalent to [`warnings.warn(message, category)`](https://docs.python.org/3/library/warnings.html#warnings.warn). - The `message` parameter is a string that will be displayed when the function is called, and the `category` parameter is optional and has to be a subclass of [`Warning`](https://docs.python.org/3/library/exceptions.html#Warning). + This option is used to display a warning when the function is used in Python. It is equivalent to [`warnings.warn(message, category)`](https://docs.python.org/3/library/warnings.html#warnings.warn). + The `message` parameter is a string that will be displayed when the function is called, and the `category` parameter is optional and has to be a subclass of [`Warning`](https://docs.python.org/3/library/exceptions.html#Warning). When the `category` parameter is not provided, the warning will be defaulted to [`UserWarning`](https://docs.python.org/3/library/exceptions.html#UserWarning). - > Note: when used with `#[pymethods]`, this attribute does not work with `#[classattr]` nor `__traverse__` magic method. + > Note: when used with `#[pymethods]`, this attribute does not work with `#[classattr]` nor `__traverse__` magic method. The following are examples of using the `#[pyo3(warn)]` attribute: @@ -110,20 +109,20 @@ The `#[pyo3]` attribute can be used to modify properties of the generated Python mod raising_warning_fn { use pyo3::prelude::pyfunction; use pyo3::exceptions::PyFutureWarning; - + #[pyfunction] #[pyo3(warn(message = "This is a warning message"))] fn function_with_warning() -> usize { 42 } - + #[pyfunction] #[pyo3(warn(message = "This function is warning with FutureWarning", category = PyFutureWarning))] fn function_with_warning_and_custom_category() -> usize { 42 } } - + # use pyo3::exceptions::{PyFutureWarning, PyUserWarning}; # use pyo3::types::{IntoPyDict, PyList}; # use pyo3::PyTypeInfo; @@ -142,7 +141,7 @@ The `#[pyo3]` attribute can be used to modify properties of the generated Python # .unwrap(); # Ok(()) # } - # + # # macro_rules! assert_warnings { # ($py:expr, $body:expr, [$(($category:ty, $message:literal)),+] $(,)? ) => { # catch_warning($py, |list| { @@ -159,7 +158,7 @@ The `#[pyo3]` attribute can be used to modify properties of the generated Python # }).unwrap(); # }; # } - # + # # Python::attach(|py| { # assert_warnings!( # py, @@ -181,7 +180,7 @@ The `#[pyo3]` attribute can be used to modify properties of the generated Python # }); ``` - When the functions are called as the following, warnings will be displayed. + When the functions are called as the following, warnings will be displayed. ```python import warnings @@ -197,7 +196,7 @@ The `#[pyo3]` attribute can be used to modify properties of the generated Python UserWarning: This is a warning message FutureWarning: This function is warning with FutureWarning ``` - + ## Per-argument options 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: @@ -268,42 +267,4 @@ arguments from the input `PyObject`s. The `wrap_pyfunction` macro can be used to directly get a `Bound` given a `#[pyfunction]` and a `Bound`: `wrap_pyfunction!(rust_fun, module)`. -## `#[pyfn]` shorthand - -There is a shorthand to `#[pyfunction]` and `wrap_pymodule!`: the function can be placed inside the module definition and -annotated with `#[pyfn]`. To simplify PyO3, it is expected that `#[pyfn]` may be removed in a future release (See [#694](https://github.com/PyO3/pyo3/issues/694)). - -An example of `#[pyfn]` is below: - -```rust,no_run -use pyo3::prelude::*; - -#[pymodule] -fn my_extension(m: &Bound<'_, PyModule>) -> PyResult<()> { - #[pyfn(m)] - fn double(x: usize) -> usize { - x * 2 - } - - Ok(()) -} -``` - -`#[pyfn(m)]` is just syntactic sugar for `#[pyfunction]`, and takes all the same options -documented in the rest of this chapter. The code above is expanded to the following: - -```rust,no_run -use pyo3::prelude::*; - -#[pymodule] -fn my_extension(m: &Bound<'_, PyModule>) -> PyResult<()> { - #[pyfunction] - fn double(x: usize) -> usize { - x * 2 - } - - m.add_function(wrap_pyfunction!(double, m)?) -} -``` - [`inspect.signature`]: https://docs.python.org/3/library/inspect.html#inspect.signature diff --git a/newsfragments/5384.changed.md b/newsfragments/5384.changed.md new file mode 100644 index 00000000000..fe7377d7de8 --- /dev/null +++ b/newsfragments/5384.changed.md @@ -0,0 +1 @@ +Deprecate `pyfn` attribute. diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index 1888f564df5..dc504949e2f 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -558,15 +558,20 @@ fn process_functions_in_module(options: &PyModuleOptions, func: &mut syn::ItemFn 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)? { + if let Some((pyfn_span, pyfn_args)) = get_pyfn_attr(&mut func.attrs)? { let module_name = pyfn_args.modname; let wrapped_function = impl_wrap_pyfunction(func, pyfn_args.options)?; let name = &func.sig.ident; - let statements: Vec = syn::parse_quote! { + let statements: Vec = syn::parse_quote_spanned! { + pyfn_span => #wrapped_function { use #pyo3_path::types::PyModuleMethods; #module_name.add_function(#pyo3_path::wrap_pyfunction!(#name, #module_name.as_borrowed())?)?; + #[deprecated(note = "`pyfn` will be removed in a future PyO3 version, use declarative `#[pymodule]` with `mod` instead")] + #[allow(dead_code)] + const PYFN_ATTRIBUTE: () = (); + const _: () = PYFN_ATTRIBUTE; } }; stmts.extend(statements); @@ -607,8 +612,8 @@ impl Parse for PyFnArgs { } /// Extracts the data from the #[pyfn(...)] attribute of a function -fn get_pyfn_attr(attrs: &mut Vec) -> syn::Result> { - let mut pyfn_args: Option = None; +fn get_pyfn_attr(attrs: &mut Vec) -> syn::Result> { + let mut pyfn_args: Option<(Span, PyFnArgs)> = None; take_attributes(attrs, |attr| { if attr.path().is_ident("pyfn") { @@ -616,14 +621,14 @@ fn get_pyfn_attr(attrs: &mut Vec) -> syn::Result "`#[pyfn] may only be specified once" ); - pyfn_args = Some(attr.parse_args()?); + pyfn_args = Some((attr.path().span(), attr.parse_args()?)); Ok(true) } else { Ok(false) } })?; - if let Some(pyfn_args) = &mut pyfn_args { + if let Some((_, pyfn_args)) = &mut pyfn_args { pyfn_args .options .add_attributes(take_pyo3_options(attrs)?)?; diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index 6d60d9cb0f5..97439769600 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -5,6 +5,7 @@ fn test_compile_errors() { let t = trybuild::TestCases::new(); + t.compile_fail("tests/ui/deprecated_pyfn.rs"); #[cfg(not(feature = "experimental-inspect"))] t.compile_fail("tests/ui/invalid_property_args.rs"); t.compile_fail("tests/ui/invalid_proto_pymethods.rs"); diff --git a/tests/test_module.rs b/tests/test_module.rs index 5e6244420e0..20174f9baeb 100644 --- a/tests/test_module.rs +++ b/tests/test_module.rs @@ -5,7 +5,6 @@ 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; mod test_utils; @@ -35,41 +34,44 @@ fn double(x: usize) -> usize { x * 2 } -/// This module is implemented in Rust. -#[pymodule(gil_used = false)] -fn module_with_functions(m: &Bound<'_, PyModule>) -> PyResult<()> { - #[pyfn(m)] - #[pyo3(name = "no_parameters")] - fn function_with_name() -> usize { - 42 - } +#[test] +fn test_module_with_functions() { + use pyo3::wrap_pymodule; - #[pyfn(m)] - #[pyo3(pass_module)] - fn with_module<'py>(module: &Bound<'py, PyModule>) -> PyResult> { - module.name() - } + /// This module is implemented in Rust. + #[pymodule(gil_used = false)] + mod module_with_functions { + use super::*; - #[pyfn(m)] - fn double_value(v: &ValueClass) -> usize { - v.value * 2 - } + #[pymodule_export] + use super::{AnonClass, ValueClass}; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - - m.add("foo", "bar")?; + #[pymodule_init] + fn init_module(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_class::()?; + m.add("foo", "bar")?; + m.add_function(wrap_pyfunction!(double, m)?)?; + m.add("also_double", wrap_pyfunction!(double, m)?)?; + Ok(()) + } - m.add_function(wrap_pyfunction!(double, m)?)?; - m.add("also_double", wrap_pyfunction!(double, m)?)?; + #[pyfunction] + #[pyo3(name = "no_parameters")] + fn function_with_name() -> usize { + 42 + } - Ok(()) -} + #[pyfunction] + #[pyo3(pass_module)] + fn with_module<'py>(module: &Bound<'py, PyModule>) -> PyResult> { + module.name() + } -#[test] -fn test_module_with_functions() { - use pyo3::wrap_pymodule; + #[pyfunction] + fn double_value(v: &ValueClass) -> usize { + v.value * 2 + } + } Python::attach(|py| { let d = [( @@ -118,6 +120,87 @@ fn test_module_with_functions() { }); } +#[test] +#[allow(deprecated)] +fn test_module_with_pyfn() { + use pyo3::wrap_pymodule; + + /// This module is implemented in Rust. + #[pymodule(gil_used = false)] + fn module_with_pyfn(m: &Bound<'_, PyModule>) -> PyResult<()> { + #[pyfn(m)] + #[pyo3(name = "no_parameters")] + fn function_with_name() -> usize { + 42 + } + + #[pyfn(m)] + #[pyo3(pass_module)] + fn with_module<'py>(module: &Bound<'py, PyModule>) -> PyResult> { + module.name() + } + + #[pyfn(m)] + fn double_value(v: &ValueClass) -> usize { + v.value * 2 + } + + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + + m.add("foo", "bar")?; + + m.add_function(wrap_pyfunction!(double, m)?)?; + m.add("also_double", wrap_pyfunction!(double, m)?)?; + + Ok(()) + } + + Python::attach(|py| { + let d = [("module_with_pyfn", wrap_pymodule!(module_with_pyfn)(py))] + .into_py_dict(py) + .unwrap(); + + py_assert!( + py, + *d, + "module_with_pyfn.__doc__ == 'This module is implemented in Rust.'" + ); + py_assert!(py, *d, "module_with_pyfn.no_parameters() == 42"); + py_assert!(py, *d, "module_with_pyfn.foo == 'bar'"); + py_assert!(py, *d, "module_with_pyfn.AnonClass != None"); + py_assert!(py, *d, "module_with_pyfn.LocatedClass != None"); + py_assert!( + py, + *d, + "module_with_pyfn.LocatedClass.__module__ == 'module'" + ); + py_assert!(py, *d, "module_with_pyfn.double(3) == 6"); + py_assert!( + py, + *d, + "module_with_pyfn.double.__doc__ == 'Doubles the given value'" + ); + py_assert!(py, *d, "module_with_pyfn.also_double(3) == 6"); + py_assert!( + py, + *d, + "module_with_pyfn.also_double.__doc__ == 'Doubles the given value'" + ); + py_assert!( + py, + *d, + "module_with_pyfn.double_value(module_with_pyfn.ValueClass(1)) == 2" + ); + py_assert!( + py, + *d, + "module_with_pyfn.with_module() == 'module_with_pyfn'" + ); + }); +} + /// This module uses a legacy two-argument module function. #[pymodule] fn module_with_explicit_py_arg(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> { @@ -319,42 +402,6 @@ 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>) -> PyResult> { - [ - a.into_pyobject(py)?.into_any().into_bound(), - args.as_any().clone(), - ] - .into_pyobject(py) - .map(Bound::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>) -> PyResult> { - ext_vararg_fn(py, a, args) - } - - m.add_function(wrap_pyfunction!(ext_vararg_fn, m)?).unwrap(); - Ok(()) -} - -#[test] -fn test_vararg_module() { - Python::attach(|py| { - let m = pyo3::wrap_pymodule!(vararg_module)(py); - - py_assert!(py, m, "m.ext_vararg_fn() == [5, ()]"); - py_assert!(py, m, "m.ext_vararg_fn(1, 2) == [1, (2,)]"); - - py_assert!(py, m, "m.int_vararg_fn() == [5, ()]"); - py_assert!(py, m, "m.int_vararg_fn(1, 2) == [1, (2,)]"); - }); -} - #[test] fn test_module_with_constant() { // Regression test for #1102 diff --git a/tests/test_text_signature.rs b/tests/test_text_signature.rs index 88057b05b6a..b4815d692a1 100644 --- a/tests/test_text_signature.rs +++ b/tests/test_text_signature.rs @@ -333,6 +333,7 @@ fn test_auto_test_signature_opt_out() { } #[test] +#[allow(deprecated)] fn test_pyfn() { #[pymodule] fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> { diff --git a/tests/ui/deprecated_pyfn.rs b/tests/ui/deprecated_pyfn.rs new file mode 100644 index 00000000000..1c5edbafd31 --- /dev/null +++ b/tests/ui/deprecated_pyfn.rs @@ -0,0 +1,13 @@ +#![deny(deprecated)] + +use pyo3::prelude::*; + +#[pymodule] +fn module_with_pyfn(m: &Bound<'_, PyModule>) -> PyResult<()> { + #[pyfn(m)] + fn foo() {} + + Ok(()) +} + +fn main() {} diff --git a/tests/ui/deprecated_pyfn.stderr b/tests/ui/deprecated_pyfn.stderr new file mode 100644 index 00000000000..c5dd0b3b4ac --- /dev/null +++ b/tests/ui/deprecated_pyfn.stderr @@ -0,0 +1,11 @@ +error: use of deprecated constant `module_with_pyfn::PYFN_ATTRIBUTE`: `pyfn` will be removed in a future PyO3 version, use declarative `#[pymodule]` with `mod` instead + --> tests/ui/deprecated_pyfn.rs:7:7 + | +7 | #[pyfn(m)] + | ^^^^ + | +note: the lint level is defined here + --> tests/ui/deprecated_pyfn.rs:1:9 + | +1 | #![deny(deprecated)] + | ^^^^^^^^^^ From 2bd4ad2e0466455b32f1f93e57b84ceb60e805c7 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Thu, 4 Sep 2025 17:12:20 +0100 Subject: [PATCH 848/936] ci: add benchmarks for extracting vec from python bytes (#5411) --- pyo3-benches/Cargo.toml | 4 +- pyo3-benches/benches/bench_frompyobject.rs | 72 ++++++++++++++++++- pyo3-benches/benches/bench_list.rs | 2 +- .../{bench_pyobject.rs => bench_py.rs} | 4 +- pyo3-benches/benches/bench_set.rs | 2 +- src/impl_/extract_argument.rs | 3 +- 6 files changed, 79 insertions(+), 8 deletions(-) rename pyo3-benches/benches/{bench_pyobject.rs => bench_py.rs} (97%) diff --git a/pyo3-benches/Cargo.toml b/pyo3-benches/Cargo.toml index 982b21c1c5c..510deacbb4d 100644 --- a/pyo3-benches/Cargo.toml +++ b/pyo3-benches/Cargo.toml @@ -60,11 +60,11 @@ name = "bench_list" harness = false [[bench]] -name = "bench_pyclass" +name = "bench_py" harness = false [[bench]] -name = "bench_pyobject" +name = "bench_pyclass" harness = false [[bench]] diff --git a/pyo3-benches/benches/bench_frompyobject.rs b/pyo3-benches/benches/bench_frompyobject.rs index 045678f33c4..43e128b62ec 100644 --- a/pyo3-benches/benches/bench_frompyobject.rs +++ b/pyo3-benches/benches/bench_frompyobject.rs @@ -4,7 +4,7 @@ use codspeed_criterion_compat::{criterion_group, criterion_main, Bencher, Criter use pyo3::{ prelude::*, - types::{PyList, PyString}, + types::{PyByteArray, PyBytes, PyList, PyString}, }; #[derive(FromPyObject)] @@ -73,6 +73,50 @@ fn not_a_list_via_extract_enum(b: &mut Bencher<'_>) { }) } +fn bench_vec_from_py_bytes(b: &mut Bencher<'_>, data: &[u8]) { + Python::attach(|py| { + let any = PyBytes::new(py, data).into_any(); + + b.iter(|| black_box(&any).extract::>().unwrap()); + }) +} + +fn vec_bytes_from_py_bytes_small(b: &mut Bencher<'_>) { + bench_vec_from_py_bytes(b, &[]); +} + +fn vec_bytes_from_py_bytes_medium(b: &mut Bencher<'_>) { + let data = (0..u8::MAX).collect::>(); + bench_vec_from_py_bytes(b, &data); +} + +fn vec_bytes_from_py_bytes_large(b: &mut Bencher<'_>) { + let data = vec![10u8; 100_000]; + bench_vec_from_py_bytes(b, &data); +} + +fn bench_vec_from_py_bytearray(b: &mut Bencher<'_>, data: &[u8]) { + Python::attach(|py| { + let any = PyByteArray::new(py, data).into_any(); + + b.iter(|| black_box(&any).extract::>().unwrap()); + }) +} + +fn vec_bytes_from_py_bytearray_small(b: &mut Bencher<'_>) { + bench_vec_from_py_bytearray(b, &[]); +} + +fn vec_bytes_from_py_bytearray_medium(b: &mut Bencher<'_>) { + let data = (0..u8::MAX).collect::>(); + bench_vec_from_py_bytearray(b, &data); +} + +fn vec_bytes_from_py_bytearray_large(b: &mut Bencher<'_>) { + let data = vec![10u8; 100_000]; + bench_vec_from_py_bytearray(b, &data); +} + fn criterion_benchmark(c: &mut Criterion) { c.bench_function("enum_from_pyobject", enum_from_pyobject); @@ -83,6 +127,32 @@ fn criterion_benchmark(c: &mut Criterion) { c.bench_function("not_a_list_via_cast", not_a_list_via_cast); c.bench_function("not_a_list_via_extract", not_a_list_via_extract); c.bench_function("not_a_list_via_extract_enum", not_a_list_via_extract_enum); + + c.bench_function( + "vec_bytes_from_py_bytes_small", + vec_bytes_from_py_bytes_small, + ); + c.bench_function( + "vec_bytes_from_py_bytes_medium", + vec_bytes_from_py_bytes_medium, + ); + c.bench_function( + "vec_bytes_from_py_bytes_large", + vec_bytes_from_py_bytes_large, + ); + + c.bench_function( + "vec_bytes_from_py_bytearray_small", + vec_bytes_from_py_bytearray_small, + ); + c.bench_function( + "vec_bytes_from_py_bytearray_medium", + vec_bytes_from_py_bytearray_medium, + ); + c.bench_function( + "vec_bytes_from_py_bytearray_large", + vec_bytes_from_py_bytearray_large, + ); } criterion_group!(benches, criterion_benchmark); diff --git a/pyo3-benches/benches/bench_list.rs b/pyo3-benches/benches/bench_list.rs index fe36b92ca23..5f83ff442f9 100644 --- a/pyo3-benches/benches/bench_list.rs +++ b/pyo3-benches/benches/bench_list.rs @@ -65,7 +65,7 @@ fn list_nth_back(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::attach(|py| { const LEN: usize = 50_000; diff --git a/pyo3-benches/benches/bench_pyobject.rs b/pyo3-benches/benches/bench_py.rs similarity index 97% rename from pyo3-benches/benches/bench_pyobject.rs rename to pyo3-benches/benches/bench_py.rs index f15097fb26f..fa022a6186d 100644 --- a/pyo3-benches/benches/bench_pyobject.rs +++ b/pyo3-benches/benches/bench_py.rs @@ -22,7 +22,7 @@ fn drop_many_objects(b: &mut Bencher<'_>) { fn drop_many_objects_without_gil(b: &mut Bencher<'_>) { b.iter_batched( - || Python::attach(|py| (0..1000).map(|_| py.None()).collect::>()), + || Python::attach(|py| (0..1000).map(|_| py.None()).collect::>>()), |objs| { drop(objs); @@ -71,7 +71,7 @@ fn drop_many_objects_multiple_threads(b: &mut Bencher<'_>) { let objs = Python::attach(|py| { (0..1000 / THREADS) .map(|_| py.None()) - .collect::>() + .collect::>>() }); sender.send(objs).unwrap(); diff --git a/pyo3-benches/benches/bench_set.rs b/pyo3-benches/benches/bench_set.rs index 4352917231e..0c9fedba687 100644 --- a/pyo3-benches/benches/bench_set.rs +++ b/pyo3-benches/benches/bench_set.rs @@ -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_any(py).unwrap()).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()); }); } diff --git a/src/impl_/extract_argument.rs b/src/impl_/extract_argument.rs index 8b2c84ec2e9..c88c704b590 100644 --- a/src/impl_/extract_argument.rs +++ b/src/impl_/extract_argument.rs @@ -254,8 +254,9 @@ pub unsafe fn unwrap_required_argument<'a, 'py>( Some(value) => value, #[cfg(debug_assertions)] None => unreachable!("required method argument was not extracted"), + // SAFETY: invariant of calling this function. Enforced by the macros. #[cfg(not(debug_assertions))] - None => std::hint::unreachable_unchecked(), + None => unsafe { std::hint::unreachable_unchecked() }, } } From ea2d483ab06505cc971b4d7fd545779f4cade3a8 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Thu, 4 Sep 2025 17:41:44 +0100 Subject: [PATCH 849/936] ci: stop running on macos-13 (#5316) * ci: stop running on macos-13 * try to install 3.9 from uv * disable 3.9 uv build for now, fix windows x86 --- .github/workflows/build.yml | 11 +++++++- .github/workflows/ci.yml | 55 ++++++++++--------------------------- 2 files changed, 25 insertions(+), 41 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1f496892ae2..6a7d627f0f8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -43,7 +43,9 @@ jobs: # with the commit diff, because the merge may affect line numbers. ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} - - name: Set up Python ${{ inputs.python-version }} + # installs using setup-python do not work for arm macOS 3.9 and below + - if: ${{ !(inputs.os == 'macos-latest' && contains(fromJSON('["3.7", "3.8", "3.9"]'), inputs.python-version) && inputs.python-architecture == 'x64') }} + name: Set up Python ${{ inputs.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ inputs.python-version }} @@ -51,6 +53,13 @@ jobs: # PyPy can have FFI changes within Python versions, which creates pain in CI check-latest: ${{ startsWith(inputs.python-version, 'pypy') }} + # workaround for the above, only available for 3.9 + - if: ${{ inputs.os == 'macos-latest' && contains(fromJSON('["3.9"]'), inputs.python-version) && inputs.python-architecture == 'x64' }} + name: Set up Python ${{ inputs.python-version }} + uses: astral-sh/setup-uv@v5 + with: + python-version: cpython-${{ inputs.python-version }}-macos-x86-64 + - name: Install nox run: python -m pip install --upgrade pip && pip install nox[uv] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 989f2344c94..fe814d42665 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -146,17 +146,13 @@ jobs: matrix: rust: [stable] python-version: ["3.13"] - platform: [ + platform: + [ { - os: "macos-latest", # first available arm macos runner + os: "macos-latest", python-architecture: "arm64", rust-target: "aarch64-apple-darwin", }, - { - os: "macos-13", # last available x86_64 macos runner - python-architecture: "x64", - rust-target: "x86_64-apple-darwin", - }, { os: "ubuntu-latest", python-architecture: "x64", @@ -316,7 +312,7 @@ jobs: python-version: "3.13" platform: { - os: "macos-13", + os: "macos-latest", python-architecture: "x64", rust-target: "x86_64-apple-darwin", } @@ -330,32 +326,17 @@ jobs: 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 - - 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", - } + # arm64 macOS Python not available on GitHub Actions until 3.10, + # and 3.7 & 3.8 not available to install from uv, but can backfill 3.9 with x64 from uv + # FIXME: setup-uv issue prevents this from working, https://github.com/astral-sh/setup-uv/issues/554 + # - rust: stable + # python-version: "3.9" + # platform: + # { + # os: "macos-latest", + # python-architecture: "x64", + # rust-target: "x86_64-apple-darwin", + # } # arm64 Linux runner is in public preview, so test 3.13 on it - rust: stable python-version: "3.13" @@ -618,12 +599,6 @@ jobs: - os: "ubuntu-latest" target: "x86_64-pc-windows-gnu" flags: "-i python3.13 --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" From a4a202d951551d1b63b23d3b365409c3489a8410 Mon Sep 17 00:00:00 2001 From: Matthijs Brobbel Date: Sat, 6 Sep 2025 21:26:26 +0200 Subject: [PATCH 850/936] Remove note in crate docs about deprecated `PyObject` (#5415) --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 7b1d957d104..fdb05cac785 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -61,7 +61,7 @@ //! //! The type parameter `T` in these smart pointers can be filled by: //! - [`PyAny`], e.g. `Py` or `Bound<'py, PyAny>`, where the Python object type is not -//! known. `Py` is so common it has a type alias [`PyObject`]. +//! known. //! - Concrete Python types like [`PyList`](types::PyList) or [`PyTuple`](types::PyTuple). //! - Rust types which are exposed to Python using the [`#[pyclass]`](macro@pyclass) macro. //! From ceeb1dc5d05d3a99ea5fdcfd717b321a7627d55d Mon Sep 17 00:00:00 2001 From: Thomas Tanon Date: Sun, 7 Sep 2025 22:02:51 +0200 Subject: [PATCH 851/936] AddTypeToModule: fetch name dynamically (#5414) Avoids using names like "PyDict" --- newsfragments/5414.changed.md | 1 + src/impl_/pymodule.rs | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 newsfragments/5414.changed.md diff --git a/newsfragments/5414.changed.md b/newsfragments/5414.changed.md new file mode 100644 index 00000000000..91e3bee5c55 --- /dev/null +++ b/newsfragments/5414.changed.md @@ -0,0 +1 @@ +`AddTypeToModule`: fetch name dynamically \ No newline at end of file diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index 027ce2982ee..75a0598e145 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -20,6 +20,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; #[cfg(not(any(PyPy, GraalPy)))] use crate::exceptions::PyImportError; +use crate::prelude::PyTypeMethods; #[cfg(all(not(Py_LIMITED_API), Py_GIL_DISABLED))] use crate::PyErr; use crate::{ @@ -180,7 +181,8 @@ impl AddTypeToModule { impl PyAddToModule for AddTypeToModule { fn add_to_module(&'static self, module: &Bound<'_, PyModule>) -> PyResult<()> { - module.add(T::NAME, T::type_object(module.py())) + let object = T::type_object(module.py()); + module.add(object.name()?, object) } } From ee29399364ae9626815a21524d93f9a7d93d8591 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sun, 7 Sep 2025 23:23:16 +0200 Subject: [PATCH 852/936] use custom error type in pyclass extraction (#5413) --- newsfragments/5413.changed.md | 1 + src/conversion.rs | 36 +++++++++------ src/pycell.rs | 12 +++++ src/pyclass.rs | 4 +- src/pyclass/guard.rs | 87 +++++++++++++++++++++++++++++++++-- 5 files changed, 120 insertions(+), 20 deletions(-) create mode 100644 newsfragments/5413.changed.md diff --git a/newsfragments/5413.changed.md b/newsfragments/5413.changed.md new file mode 100644 index 00000000000..74cec2205c8 --- /dev/null +++ b/newsfragments/5413.changed.md @@ -0,0 +1 @@ +`PyClassGuard(Mut)` and `PyRef(Mut)` extraction now returns an opaque Rust error \ No newline at end of file diff --git a/src/conversion.rs b/src/conversion.rs index 3fdb0c47060..0462844f230 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -3,8 +3,11 @@ use crate::err::PyResult; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; use crate::pyclass::boolean_struct::False; +use crate::pyclass::{PyClassGuardError, PyClassGuardMutError}; use crate::types::PyTuple; -use crate::{Borrowed, Bound, BoundObject, Py, PyAny, PyClass, PyErr, PyRef, PyRefMut, Python}; +use crate::{ + Borrowed, Bound, BoundObject, Py, PyAny, PyClass, PyClassGuard, PyErr, PyRef, PyRefMut, Python, +}; use std::convert::Infallible; /// Defines a conversion from a Rust type to a Python object, which may fail. @@ -460,46 +463,51 @@ pub trait FromPyObject<'a, 'py>: Sized { pub trait FromPyObjectOwned<'py>: for<'a> FromPyObject<'a, 'py> {} impl<'py, T> FromPyObjectOwned<'py> for T where T: for<'a> FromPyObject<'a, 'py> {} -impl FromPyObject<'_, '_> for T +impl<'a, 'py, T> FromPyObject<'a, 'py> for T where T: PyClass + Clone, { - type Error = PyErr; + type Error = PyClassGuardError<'a, 'py>; #[cfg(feature = "experimental-inspect")] const INPUT_TYPE: &'static str = ::TYPE_NAME; - fn extract(obj: Borrowed<'_, '_, PyAny>) -> Result { - let bound = obj.cast::()?; - Ok(bound.try_borrow()?.clone()) + fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result { + Ok(obj.extract::>()?.clone()) } } -impl<'py, T> FromPyObject<'_, 'py> for PyRef<'py, T> +impl<'a, 'py, T> FromPyObject<'a, 'py> for PyRef<'py, T> where T: PyClass, { - type Error = PyErr; + type Error = PyClassGuardError<'a, 'py>; #[cfg(feature = "experimental-inspect")] const INPUT_TYPE: &'static str = ::TYPE_NAME; - fn extract(obj: Borrowed<'_, 'py, PyAny>) -> Result { - obj.cast::()?.try_borrow().map_err(Into::into) + fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result { + obj.cast::() + .map_err(|e| PyClassGuardError(Some(e)))? + .try_borrow() + .map_err(|_| PyClassGuardError(None)) } } -impl<'py, T> FromPyObject<'_, 'py> for PyRefMut<'py, T> +impl<'a, 'py, T> FromPyObject<'a, 'py> for PyRefMut<'py, T> where T: PyClass, { - type Error = PyErr; + type Error = PyClassGuardMutError<'a, 'py>; #[cfg(feature = "experimental-inspect")] const INPUT_TYPE: &'static str = ::TYPE_NAME; - fn extract(obj: Borrowed<'_, 'py, PyAny>) -> Result { - obj.cast::()?.try_borrow_mut().map_err(Into::into) + fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result { + obj.cast::() + .map_err(|e| PyClassGuardMutError(Some(e)))? + .try_borrow_mut() + .map_err(|_| PyClassGuardMutError(None)) } } diff --git a/src/pycell.rs b/src/pycell.rs index fed6b69013f..c1f9606e40c 100644 --- a/src/pycell.rs +++ b/src/pycell.rs @@ -687,6 +687,12 @@ pub struct PyBorrowError { _private: (), } +impl PyBorrowError { + pub(crate) fn new() -> Self { + Self { _private: () } + } +} + impl fmt::Debug for PyBorrowError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("PyBorrowError").finish() @@ -712,6 +718,12 @@ pub struct PyBorrowMutError { _private: (), } +impl PyBorrowMutError { + pub(crate) fn new() -> Self { + Self { _private: () } + } +} + impl fmt::Debug for PyBorrowMutError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("PyBorrowMutError").finish() diff --git a/src/pyclass.rs b/src/pyclass.rs index cd21f8876ed..f5a3fd5e4eb 100644 --- a/src/pyclass.rs +++ b/src/pyclass.rs @@ -9,7 +9,9 @@ mod guard; pub(crate) use self::create_type_object::{create_type_object, PyClassTypeObject}; pub use self::gc::{PyTraverseError, PyVisit}; -pub use self::guard::{PyClassGuard, PyClassGuardMap, PyClassGuardMut}; +pub use self::guard::{ + PyClassGuard, PyClassGuardError, PyClassGuardMap, PyClassGuardMut, PyClassGuardMutError, +}; /// Types that can be used as Python classes. /// diff --git a/src/pyclass/guard.rs b/src/pyclass/guard.rs index 315b5fe6e0a..cc2b949e42e 100644 --- a/src/pyclass/guard.rs +++ b/src/pyclass/guard.rs @@ -2,8 +2,9 @@ use crate::impl_::pycell::{PyClassObject, PyClassObjectLayout as _}; use crate::pycell::PyBorrowMutError; use crate::pycell::{impl_::PyClassBorrowChecker, PyBorrowError}; use crate::pyclass::boolean_struct::False; -use crate::{ffi, Borrowed, FromPyObject, IntoPyObject, Py, PyClass, PyErr}; +use crate::{ffi, Borrowed, DowncastError, FromPyObject, IntoPyObject, Py, PyClass, PyErr}; use std::convert::Infallible; +use std::fmt; use std::marker::PhantomData; use std::ops::{Deref, DerefMut}; use std::ptr::NonNull; @@ -277,10 +278,15 @@ impl Deref for PyClassGuard<'_, T> { } impl<'a, 'py, T: PyClass> FromPyObject<'a, 'py> for PyClassGuard<'a, T> { - type Error = PyErr; + type Error = PyClassGuardError<'a, 'py>; fn extract(obj: Borrowed<'a, 'py, crate::PyAny>) -> Result { - Self::try_from_class_object(obj.cast()?.get_class_object()).map_err(Into::into) + Self::try_from_class_object( + obj.cast() + .map_err(|e| PyClassGuardError(Some(e)))? + .get_class_object(), + ) + .map_err(|_| PyClassGuardError(None)) } } @@ -327,6 +333,39 @@ unsafe impl crate::marker::Ungil for PyClassGuard<'_, T> {} unsafe impl Send for PyClassGuard<'_, T> {} unsafe impl Sync for PyClassGuard<'_, T> {} +/// Custom error type for extracting a [PyClassGuard] +pub struct PyClassGuardError<'a, 'py>(pub(crate) Option>); + +impl fmt::Debug for PyClassGuardError<'_, '_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(e) = &self.0 { + write!(f, "{e:?}") + } else { + write!(f, "{:?}", PyBorrowError::new()) + } + } +} + +impl fmt::Display for PyClassGuardError<'_, '_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(e) = &self.0 { + write!(f, "{e}") + } else { + write!(f, "{}", PyBorrowError::new()) + } + } +} + +impl From> for PyErr { + fn from(value: PyClassGuardError<'_, '_>) -> Self { + if let Some(e) = value.0 { + e.into() + } else { + PyBorrowError::new().into() + } + } +} + /// A wrapper type for a mutably borrowed value from a `PyClass` /// /// # When *not* to use [`PyClassGuardMut`] @@ -642,10 +681,15 @@ impl> DerefMut for PyClassGuardMut<'_, T> { } impl<'a, 'py, T: PyClass> FromPyObject<'a, 'py> for PyClassGuardMut<'a, T> { - type Error = PyErr; + type Error = PyClassGuardMutError<'a, 'py>; fn extract(obj: Borrowed<'a, 'py, crate::PyAny>) -> Result { - Self::try_from_class_object(obj.cast()?.get_class_object()).map_err(Into::into) + Self::try_from_class_object( + obj.cast() + .map_err(|e| PyClassGuardMutError(Some(e)))? + .get_class_object(), + ) + .map_err(|_| PyClassGuardMutError(None)) } } @@ -690,6 +734,39 @@ unsafe impl> crate::marker::Ungil for PyClassGuardMut unsafe impl + Send + Sync> Send for PyClassGuardMut<'_, T> {} unsafe impl + Sync> Sync for PyClassGuardMut<'_, T> {} +/// Custom error type for extracting a [PyClassGuardMut] +pub struct PyClassGuardMutError<'a, 'py>(pub(crate) Option>); + +impl fmt::Debug for PyClassGuardMutError<'_, '_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(e) = &self.0 { + write!(f, "{e:?}") + } else { + write!(f, "{:?}", PyBorrowMutError::new()) + } + } +} + +impl fmt::Display for PyClassGuardMutError<'_, '_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if let Some(e) = &self.0 { + write!(f, "{e}") + } else { + write!(f, "{}", PyBorrowMutError::new()) + } + } +} + +impl From> for PyErr { + fn from(value: PyClassGuardMutError<'_, '_>) -> Self { + if let Some(e) = value.0 { + e.into() + } else { + PyBorrowMutError::new().into() + } + } +} + /// Wraps a borrowed reference `U` to a value stored inside of a pyclass `T` /// /// See [`PyClassGuard::map`] and [`PyClassGuardMut::map`] From 5eca3ac0376ac77ec43b35dfeea14e0a716700bf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Sep 2025 08:21:18 +0200 Subject: [PATCH 853/936] build(deps): bump actions/setup-python from 5 to 6 (#5426) Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6. - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/setup-python dependency-version: '6' 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 +- .github/workflows/build.yml | 2 +- .github/workflows/changelog.yml | 2 +- .github/workflows/ci-cache-warmup.yml | 2 +- .github/workflows/ci.yml | 26 +++++++++++++------------- .github/workflows/coverage-pr-base.yml | 2 +- .github/workflows/netlify-build.yml | 2 +- 7 files changed, 19 insertions(+), 19 deletions(-) diff --git a/.github/workflows/benches.yml b/.github/workflows/benches.yml index 8c46141bc14..65898bb0802 100644 --- a/.github/workflows/benches.yml +++ b/.github/workflows/benches.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v5 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: '3.13' - uses: dtolnay/rust-toolchain@stable diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6a7d627f0f8..1020d5b068a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -46,7 +46,7 @@ jobs: # installs using setup-python do not work for arm macOS 3.9 and below - if: ${{ !(inputs.os == 'macos-latest' && contains(fromJSON('["3.7", "3.8", "3.9"]'), inputs.python-version) && inputs.python-architecture == 'x64') }} name: Set up Python ${{ inputs.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ inputs.python-version }} architecture: ${{ inputs.python-architecture }} diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml index 808f899964b..760918ce40a 100644 --- a/.github/workflows/changelog.yml +++ b/.github/workflows/changelog.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: '3.13' - run: python -m pip install --upgrade pip && pip install nox[uv] diff --git a/.github/workflows/ci-cache-warmup.yml b/.github/workflows/ci-cache-warmup.yml index d3cb5c0577f..2b05a1e98c0 100644 --- a/.github/workflows/ci-cache-warmup.yml +++ b/.github/workflows/ci-cache-warmup.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: "3.13" - uses: dtolnay/rust-toolchain@stable diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fe814d42665..9cb1c2e105f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: "3.13" - run: python -m pip install --upgrade pip && pip install nox[uv] @@ -37,7 +37,7 @@ jobs: verbose: ${{ runner.debug == '1' }} steps: - uses: actions/checkout@v5 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: "3.13" - name: resolve MSRV @@ -50,7 +50,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: "3.13" - name: Fetch merge base @@ -72,7 +72,7 @@ jobs: with: toolchain: ${{ needs.resolve.outputs.MSRV }} components: rust-src - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: "3.13" - uses: Swatinem/rust-cache@v2 @@ -390,7 +390,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: "3.13" - uses: Swatinem/rust-cache@v2 @@ -411,7 +411,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: "3.13" - uses: Swatinem/rust-cache@v2 @@ -433,7 +433,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: "3.13" - uses: Swatinem/rust-cache@v2 @@ -451,7 +451,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: # TODO bump emscripten builds to test on 3.13 python-version: 3.11 @@ -536,7 +536,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: "3.13" - uses: Swatinem/rust-cache@v2 @@ -560,7 +560,7 @@ jobs: - rust: ${{ needs.resolve.outputs.MSRV }} steps: - uses: actions/checkout@v5 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: "3.13" - uses: Swatinem/rust-cache@v2 @@ -605,7 +605,7 @@ jobs: flags: "-i python3.13 --features generate-import-lib" steps: - uses: actions/checkout@v5 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: "3.13" - uses: Swatinem/rust-cache@v2 @@ -635,7 +635,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: "3.13" - uses: dtolnay/rust-toolchain@stable @@ -689,7 +689,7 @@ jobs: with: targets: ${{ matrix.platform.rust-target }} components: rust-src - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: "3.13" architecture: ${{ matrix.platform.python-architecture }} diff --git a/.github/workflows/coverage-pr-base.yml b/.github/workflows/coverage-pr-base.yml index 9c72567ed34..0f4b359e41e 100644 --- a/.github/workflows/coverage-pr-base.yml +++ b/.github/workflows/coverage-pr-base.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: '3.13' - name: Fetch merge base diff --git a/.github/workflows/netlify-build.yml b/.github/workflows/netlify-build.yml index 3af4a6e1599..9fbb28886a2 100644 --- a/.github/workflows/netlify-build.yml +++ b/.github/workflows/netlify-build.yml @@ -22,7 +22,7 @@ jobs: tag_name: ${{ steps.prepare_tag.outputs.tag_name }} steps: - uses: actions/checkout@v5 - - uses: actions/setup-python@v5 + - uses: actions/setup-python@v6 with: python-version: "3.13" From 71ec68693ae87d092dcc5fcfa2f4431a1301bba4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Sep 2025 08:21:30 +0200 Subject: [PATCH 854/936] build(deps): bump actions/github-script from 7 to 8 (#5424) Bumps [actions/github-script](https://github.com/actions/github-script) from 7 to 8. - [Release notes](https://github.com/actions/github-script/releases) - [Commits](https://github.com/actions/github-script/compare/v7...v8) --- updated-dependencies: - dependency-name: actions/github-script dependency-version: '8' 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/netlify-deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/netlify-deploy.yml b/.github/workflows/netlify-deploy.yml index c7e2b97daf5..0985e3a6584 100644 --- a/.github/workflows/netlify-deploy.yml +++ b/.github/workflows/netlify-deploy.yml @@ -51,7 +51,7 @@ jobs: - name: Post Netlify Preview Status to PR if: ${{ github.event.workflow_run.event == 'pull_request' }} - uses: actions/github-script@v7 + uses: actions/github-script@v8 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | From 133633b7cd8f0b0955fabbfc641f8ba1e935f309 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Sep 2025 08:21:39 +0200 Subject: [PATCH 855/936] build(deps): bump astral-sh/setup-uv from 5 to 6 (#5423) Bumps [astral-sh/setup-uv](https://github.com/astral-sh/setup-uv) from 5 to 6. - [Release notes](https://github.com/astral-sh/setup-uv/releases) - [Commits](https://github.com/astral-sh/setup-uv/compare/v5...v6) --- updated-dependencies: - dependency-name: astral-sh/setup-uv dependency-version: '6' 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/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1020d5b068a..ff8f4939812 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -56,7 +56,7 @@ jobs: # workaround for the above, only available for 3.9 - if: ${{ inputs.os == 'macos-latest' && contains(fromJSON('["3.9"]'), inputs.python-version) && inputs.python-architecture == 'x64' }} name: Set up Python ${{ inputs.python-version }} - uses: astral-sh/setup-uv@v5 + uses: astral-sh/setup-uv@v6 with: python-version: cpython-${{ inputs.python-version }}-macos-x86-64 From 3751248ef9329c14e9f3253f2913c046db817a98 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Sep 2025 08:21:46 +0200 Subject: [PATCH 856/936] build(deps): bump actions/setup-node from 4 to 5 (#5422) Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 5. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/setup-node dependency-version: '5' 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/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9cb1c2e105f..7e4c19e610e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -461,7 +461,7 @@ jobs: with: targets: wasm32-unknown-emscripten components: rust-src - - uses: actions/setup-node@v4 + - uses: actions/setup-node@v5 with: node-version: 18 - run: python -m pip install --upgrade pip && pip install nox[uv] From f7e181e63ab7f10f101875aef2789cde637c1e9c Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Tue, 9 Sep 2025 09:24:25 +0100 Subject: [PATCH 857/936] simplify `extract_argument` code for `&T: PyClass` (#5408) * simplify `extract_argument` code for `&T: PyClass` * simplify further * update UI test * remove `extract_optional_argument` * review feedback * fixup * fixup UI test --- guide/src/class.md | 26 ---- pyo3-macros-backend/src/introspection.rs | 10 +- pyo3-macros-backend/src/params.rs | 53 +++----- pyo3-macros-backend/src/pyclass.rs | 66 ++-------- pyo3-macros-backend/src/pymethod.rs | 14 +-- pyo3-macros-backend/src/utils.rs | 5 +- src/impl_/extract_argument.rs | 118 ++++++++++++------ src/impl_/pyclass/probes.rs | 9 +- tests/test_compile_error.rs | 4 +- tests/ui/invalid_cancel_handle.stderr | 62 +++++---- tests/ui/invalid_frozen_pyclass_borrow.stderr | 4 +- tests/ui/invalid_pyfunction_argument.rs | 9 ++ tests/ui/invalid_pyfunction_argument.stderr | 47 +++++++ tests/ui/invalid_pymethod_enum.stderr | 8 +- 14 files changed, 216 insertions(+), 219 deletions(-) create mode 100644 tests/ui/invalid_pyfunction_argument.rs create mode 100644 tests/ui/invalid_pyfunction_argument.stderr diff --git a/guide/src/class.md b/guide/src/class.md index 942effed147..e149565146a 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -1407,32 +1407,6 @@ impl pyo3::PyClass for MyClass { type Frozen = pyo3::pyclass::boolean_struct::False; } -impl<'a, 'holder, 'py> pyo3::impl_::extract_argument::PyFunctionArgument<'a, 'holder, 'py, false> for &'holder MyClass -{ - type Holder = ::std::option::Option>; - type Error = pyo3::PyErr; - #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = "MyClass"; - - #[inline] - fn extract(obj: &'a pyo3::Bound<'py, PyAny>, holder: &'holder mut Self::Holder) -> pyo3::PyResult { - pyo3::impl_::extract_argument::extract_pyclass_ref(obj, holder) - } -} - -impl<'a, 'holder, 'py> pyo3::impl_::extract_argument::PyFunctionArgument<'a, 'holder, 'py, false> for &'holder mut MyClass -{ - type Holder = ::std::option::Option>; - type Error = pyo3::PyErr; - #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = "MyClass"; - - #[inline] - fn extract(obj: &'a pyo3::Bound<'py, PyAny>, holder: &'holder mut Self::Holder) -> pyo3::PyResult { - pyo3::impl_::extract_argument::extract_pyclass_ref_mut(obj, holder) - } -} - impl pyo3::impl_::pyclass::PyClassImpl for MyClass { const IS_BASETYPE: bool = false; const IS_SUBCLASS: bool = false; diff --git a/pyo3-macros-backend/src/introspection.rs b/pyo3-macros-backend/src/introspection.rs index ace5163c5be..83bd12d1056 100644 --- a/pyo3-macros-backend/src/introspection.rs +++ b/pyo3-macros-backend/src/introspection.rs @@ -390,7 +390,15 @@ impl IntrospectionNode<'_> { nullable, } => { content.push_str("\""); - content.push_tokens(quote! { <#rust_type as #pyo3_crate_path::impl_::extract_argument::PyFunctionArgument>::INPUT_TYPE.as_bytes() }); + content.push_tokens(quote! { + <#rust_type as #pyo3_crate_path::impl_::extract_argument::PyFunctionArgument< + { + #[allow(unused_imports)] + use #pyo3_crate_path::impl_::pyclass::Probe as _; + #pyo3_crate_path::impl_::pyclass::IsFromPyObject::<#rust_type>::VALUE + } + >>::INPUT_TYPE.as_bytes() + }); if nullable { content.push_str(" | None"); } diff --git a/pyo3-macros-backend/src/params.rs b/pyo3-macros-backend/src/params.rs index 087e97f500c..9e71630346b 100644 --- a/pyo3-macros-backend/src/params.rs +++ b/pyo3-macros-backend/src/params.rs @@ -1,4 +1,4 @@ -use crate::utils::{Ctx, TypeExt as _}; +use crate::utils::Ctx; use crate::{ attributes::FromPyWithAttribute, method::{FnArg, FnSpec, RegularArg}, @@ -198,7 +198,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::<_, false>( + #pyo3_path::impl_::extract_argument::extract_argument( &_args, &mut #holder, #name_str @@ -209,7 +209,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::<_, false>( + #pyo3_path::impl_::extract_argument::extract_argument_with_default( _kwargs.as_deref(), &mut #holder, #name_str, @@ -253,7 +253,6 @@ 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 } @@ -282,46 +281,22 @@ pub(crate) fn impl_regular_arg_param( } } else if let Some(default) = default { let holder = holders.push_holder(arg.name.span()); - 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_::pyclass::IsOption::<#arg_ty>::VALUE } - >( - #arg_value, - &mut #holder, - #name_str, - #[allow(clippy::redundant_closure)] - { - || #default - } - )? - } - } else { - quote_arg_span! { - #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 - } - )? - } + 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()); 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_::pyclass::IsOption::<#arg_ty>::VALUE } - >( + #pyo3_path::impl_::extract_argument::extract_argument( #unwrap, &mut #holder, #name_str diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 91f9117fc9e..0f8eea038d9 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -1121,7 +1121,6 @@ fn impl_complex_enum( let pyclass_impls: TokenStream = [ impl_builder.impl_pyclass(ctx), - impl_builder.impl_extractext(ctx), enum_into_pyobject_impl, impl_builder.impl_pyclassimpl(ctx)?, impl_builder.impl_add_to_module(ctx), @@ -1939,11 +1938,20 @@ fn impl_pytypeinfo(cls: &syn::Ident, attr: &PyClassArgs, ctx: &Ctx) -> TokenStre quote! { ::core::option::Option::None } }; + let python_type = if cfg!(feature = "experimental-inspect") { + let full_name = get_class_python_module_and_name(cls, attr); + quote! { const PYTHON_TYPE: &'static str = #full_name; } + } else { + quote! {} + }; + quote! { unsafe impl #pyo3_path::type_object::PyTypeInfo for #cls { const NAME: &'static str = #cls_name; const MODULE: ::std::option::Option<&'static str> = #module; + #python_type + #[inline] fn type_object_raw(py: #pyo3_path::Python<'_>) -> *mut #pyo3_path::ffi::PyTypeObject { use #pyo3_path::prelude::PyTypeMethods; @@ -2285,7 +2293,6 @@ impl<'a> PyClassImplsBuilder<'a> { fn impl_all(&self, ctx: &Ctx) -> Result { Ok([ self.impl_pyclass(ctx), - self.impl_extractext(ctx), self.impl_into_py(ctx), self.impl_pyclassimpl(ctx)?, self.impl_add_to_module(ctx), @@ -2312,61 +2319,6 @@ impl<'a> PyClassImplsBuilder<'a> { } } } - fn impl_extractext(&self, ctx: &Ctx) -> TokenStream { - let Ctx { pyo3_path, .. } = ctx; - let cls = self.cls; - - let input_type = if cfg!(feature = "experimental-inspect") { - let full_name = get_class_python_module_and_name(cls, self.attr); - quote! { const INPUT_TYPE: &'static str = #full_name; } - } else { - quote! {} - }; - if self.attr.options.frozen.is_some() { - quote! { - impl<'a, 'holder, 'py> #pyo3_path::impl_::extract_argument::PyFunctionArgument<'a, 'holder, 'py, false> for &'holder #cls - { - type Holder = ::std::option::Option<#pyo3_path::PyClassGuard<'a, #cls>>; - type Error = #pyo3_path::PyErr; - - #input_type - - #[inline] - fn extract(obj: &'a #pyo3_path::Bound<'py, #pyo3_path::PyAny>, holder: &'holder mut Self::Holder) -> #pyo3_path::PyResult { - #pyo3_path::impl_::extract_argument::extract_pyclass_ref(obj, holder) - } - } - } - } else { - quote! { - impl<'a, 'holder, 'py> #pyo3_path::impl_::extract_argument::PyFunctionArgument<'a, 'holder, 'py, false> for &'holder #cls - { - type Holder = ::std::option::Option<#pyo3_path::PyClassGuard<'a, #cls>>; - type Error = #pyo3_path::PyErr; - - #input_type - - #[inline] - fn extract(obj: &'a #pyo3_path::Bound<'py, #pyo3_path::PyAny>, holder: &'holder mut Self::Holder) -> #pyo3_path::PyResult { - #pyo3_path::impl_::extract_argument::extract_pyclass_ref(obj, holder) - } - } - - impl<'a, 'holder, 'py> #pyo3_path::impl_::extract_argument::PyFunctionArgument<'a, 'holder, 'py, false> for &'holder mut #cls - { - type Holder = ::std::option::Option<#pyo3_path::PyClassGuardMut<'a, #cls>>; - type Error =#pyo3_path::PyErr; - - #input_type - - #[inline] - fn extract(obj: &'a #pyo3_path::Bound<'py, #pyo3_path::PyAny>, holder: &'holder mut Self::Holder) -> #pyo3_path::PyResult { - #pyo3_path::impl_::extract_argument::extract_pyclass_ref_mut(obj, holder) - } - } - } - } - } fn impl_into_py(&self, ctx: &Ctx) -> TokenStream { let Ctx { pyo3_path, .. } = ctx; diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index b2b5ad20375..d08220490da 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -7,8 +7,8 @@ use crate::introspection::unique_element_id; use crate::method::{CallingConvention, ExtractErrorMode, PyArg}; use crate::params::{impl_regular_arg_param, Holders}; use crate::pyfunction::WarningFactory; +use crate::utils::PythonDoc; use crate::utils::{Ctx, LitCStr}; -use crate::utils::{PythonDoc, TypeExt as _}; use crate::{ method::{FnArg, FnSpec, FnType, SelfType}, pyfunction::PyFunctionOptions, @@ -739,14 +739,10 @@ pub fn impl_py_setter_def( .unwrap_or_default(); let holder = holders.push_holder(span); - let ty = field.ty.clone().elide_lifetimes(); quote! { #[allow(unused_imports)] 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)?; + let _val = #pyo3_path::impl_::extract_argument::extract_argument(_value.into(), &mut #holder, #name)?; } } }; @@ -1245,14 +1241,10 @@ fn extract_object( } } else { let holder = holders.push_holder(Span::call_site()); - let ty = arg.ty().clone().elide_lifetimes(); quote! {{ #[allow(unused_imports)] use #pyo3_path::impl_::pyclass::Probe as _; - #pyo3_path::impl_::extract_argument::extract_argument::< - _, - { #pyo3_path::impl_::pyclass::IsOption::<#ty>::VALUE } - >( + #pyo3_path::impl_::extract_argument::extract_argument( unsafe { #pyo3_path::impl_::pymethods::BoundRef::#ref_from_method(py, &#source_ptr).0 }, &mut #holder, #name diff --git a/pyo3-macros-backend/src/utils.rs b/pyo3-macros-backend/src/utils.rs index 0df0b75e431..91b6006add9 100644 --- a/pyo3-macros-backend/src/utils.rs +++ b/pyo3-macros-backend/src/utils.rs @@ -3,7 +3,6 @@ use proc_macro2::{Span, TokenStream}; use quote::{quote, quote_spanned, ToTokens}; use std::ffi::CString; use syn::spanned::Spanned; -use syn::visit_mut::VisitMut; use syn::{punctuated::Punctuated, Token}; /// Macro inspired by `anyhow::anyhow!` to create a compiler error with the given span. @@ -340,6 +339,7 @@ pub(crate) fn has_attribute_with_namespace( }) } +#[cfg(feature = "experimental-inspect")] pub(crate) trait TypeExt { /// Replaces all explicit lifetimes in `self` with elided (`'_`) lifetimes /// @@ -348,8 +348,11 @@ pub(crate) trait TypeExt { fn elide_lifetimes(self) -> Self; } +#[cfg(feature = "experimental-inspect")] impl TypeExt for syn::Type { fn elide_lifetimes(mut self) -> Self { + use syn::visit_mut::VisitMut; + struct ElideLifetimesVisitor; impl VisitMut for ElideLifetimesVisitor { diff --git a/src/impl_/extract_argument.rs b/src/impl_/extract_argument.rs index c88c704b590..424b3ef998f 100644 --- a/src/impl_/extract_argument.rs +++ b/src/impl_/extract_argument.rs @@ -12,6 +12,25 @@ use crate::{ /// (Function argument extraction borrows input arguments.) type PyArg<'py> = Borrowed<'py, 'py, PyAny>; +/// Seals `PyFunctionArgument` so that types outside PyO3 cannot implement it. +/// +/// The public API is `FromPyObject`. +mod function_argument { + use crate::{ + impl_::extract_argument::PyFunctionArgument, pyclass::boolean_struct::False, FromPyObject, + PyClass, PyTypeCheck, + }; + + pub trait Sealed {} + impl<'a, 'py, T: FromPyObject<'a, 'py>> Sealed for T {} + impl<'py, T: PyTypeCheck + 'py> Sealed for &'_ crate::Bound<'py, T> {} + impl<'a, 'holder, 'py, T: PyFunctionArgument<'a, 'holder, 'py, false>> Sealed for Option {} + #[cfg(all(Py_LIMITED_API, not(Py_3_10)))] + impl Sealed for &'_ str {} + impl Sealed for &'_ T {} + impl> Sealed for &'_ mut T {} +} + /// A trait which is used to help PyO3 macros extract function arguments. /// /// `#[pyclass]` structs need to extract as `PyRef` and `PyRefMut` @@ -20,7 +39,22 @@ 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, 'holder, 'py, const IS_OPTION: bool>: Sized { +/// +/// The const generic arg `IMPLEMENTS_FROMPYOBJECT` allows for const generic specialization of +/// some additional types which don't implement `FromPyObject`, such as `&T` for `#[pyclass]` types. +/// All types should only implement this trait once; either by the `FromPyObject` blanket or one +/// of the specialized implementations which needs a `Holder`. +#[cfg_attr( + diagnostic_namespace, + diagnostic::on_unimplemented( + message = "`{Self}` cannot be used as a Python function argument", + note = "implement `FromPyObject` to enable using `{Self}` as a function argument", + note = "`Python<'py>` is also a valid argument type to pass the Python token into `#[pyfunction]`s and `#[pymethods]`" + ) +)] +pub trait PyFunctionArgument<'a, 'holder, 'py, const IMPLEMENTS_FROMPYOBJECT: bool>: + Sized + function_argument::Sealed +{ type Holder: FunctionArgumentHolder; type Error: Into; @@ -34,7 +68,7 @@ pub trait PyFunctionArgument<'a, 'holder, 'py, const IS_OPTION: bool>: Sized { ) -> Result; } -impl<'a, 'holder, 'py, T> PyFunctionArgument<'a, 'holder, 'py, false> for T +impl<'a, 'py, T> PyFunctionArgument<'a, '_, 'py, true> for T where T: FromPyObject<'a, 'py>, { @@ -45,12 +79,12 @@ where const INPUT_TYPE: &'static str = T::INPUT_TYPE; #[inline] - fn extract(obj: &'a Bound<'py, PyAny>, _: &'holder mut ()) -> Result { + fn extract(obj: &'a Bound<'py, PyAny>, _: &'_ mut ()) -> Result { obj.extract() } } -impl<'a, 'holder, 'py, T: 'py> PyFunctionArgument<'a, 'holder, 'py, false> for &'a Bound<'py, T> +impl<'a, 'py, T: 'py> PyFunctionArgument<'a, '_, 'py, false> for &'a Bound<'py, T> where T: PyTypeCheck, { @@ -61,14 +95,15 @@ where const INPUT_TYPE: &'static str = T::PYTHON_TYPE; #[inline] - fn extract(obj: &'a Bound<'py, PyAny>, _: &'holder mut ()) -> Result { + fn extract(obj: &'a Bound<'py, PyAny>, _: &'_ mut ()) -> Result { obj.cast() } } -impl<'a, 'holder, 'py, T> PyFunctionArgument<'a, 'holder, 'py, true> for Option +/// Allow `Option` to be a function argument also for types which don't implement `FromPyObject` +impl<'a, 'holder, 'py, T> PyFunctionArgument<'a, 'holder, 'py, false> for Option where - T: PyFunctionArgument<'a, 'holder, 'py, false>, // inner `Option`s will use `FromPyObject` + T: PyFunctionArgument<'a, 'holder, 'py, false>, { type Holder = T::Holder; type Error = T::Error; @@ -120,17 +155,45 @@ impl FunctionArgumentHolder for Option { const INIT: Self = None; } +impl<'a, 'holder, T: PyClass> PyFunctionArgument<'a, 'holder, '_, false> for &'holder T { + type Holder = ::std::option::Option>; + type Error = PyErr; + + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = T::PYTHON_TYPE; + + #[inline] + fn extract(obj: &'a Bound<'_, PyAny>, holder: &'holder mut Self::Holder) -> PyResult { + extract_pyclass_ref(obj, holder) + } +} + +impl<'a, 'holder, T: PyClass> PyFunctionArgument<'a, 'holder, '_, false> + for &'holder mut T +{ + type Holder = ::std::option::Option>; + type Error = PyErr; + + #[cfg(feature = "experimental-inspect")] + const INPUT_TYPE: &'static str = T::PYTHON_TYPE; + + #[inline] + fn extract(obj: &'a Bound<'_, PyAny>, holder: &'holder mut Self::Holder) -> PyResult { + extract_pyclass_ref_mut(obj, holder) + } +} + #[inline] -pub fn extract_pyclass_ref<'a, 'holder, 'py, T: PyClass>( - obj: &'a Bound<'py, PyAny>, +pub fn extract_pyclass_ref<'a, 'holder, T: PyClass>( + obj: &'a Bound<'_, PyAny>, holder: &'holder mut Option>, ) -> PyResult<&'holder T> { Ok(&*holder.insert(PyClassGuard::try_borrow(obj.downcast()?.as_unbound())?)) } #[inline] -pub fn extract_pyclass_ref_mut<'a, 'holder, 'py, T: PyClass>( - obj: &'a Bound<'py, PyAny>, +pub fn extract_pyclass_ref_mut<'a, 'holder, T: PyClass>( + obj: &'a Bound<'_, PyAny>, holder: &'holder mut Option>, ) -> PyResult<&'holder mut T> { Ok(&mut *holder.insert(PyClassGuardMut::try_borrow_mut( @@ -140,13 +203,13 @@ pub fn extract_pyclass_ref_mut<'a, 'holder, 'py, T: PyClass>( /// The standard implementation of how PyO3 extracts a `#[pyfunction]` or `#[pymethod]` function argument. #[doc(hidden)] -pub fn extract_argument<'a, 'holder, 'py, T, const IS_OPTION: bool>( +pub fn extract_argument<'a, 'holder, 'py, T, const IMPLEMENTS_FROMPYOBJECT: bool>( obj: &'a Bound<'py, PyAny>, holder: &'holder mut T::Holder, arg_name: &str, ) -> PyResult where - T: PyFunctionArgument<'a, 'holder, 'py, IS_OPTION>, + T: PyFunctionArgument<'a, 'holder, 'py, IMPLEMENTS_FROMPYOBJECT>, { match PyFunctionArgument::extract(obj, holder) { Ok(value) => Ok(value), @@ -154,41 +217,16 @@ 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, 'holder, 'py, T, const IS_OPTION: bool>( - obj: Option<&'a Bound<'py, PyAny>>, - holder: &'holder mut T::Holder, - arg_name: &str, - default: fn() -> Option, -) -> PyResult> -where - T: PyFunctionArgument<'a, 'holder, 'py, IS_OPTION>, -{ - match obj { - Some(obj) => { - if obj.is_none() { - // Explicit `None` will result in None being used as the function argument - Ok(None) - } else { - extract_argument(obj, holder, arg_name).map(Some) - } - } - _ => Ok(default()), - } -} - /// 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, 'holder, 'py, T, const IS_OPTION: bool>( +pub fn extract_argument_with_default<'a, 'holder, 'py, T, const IMPLEMENTS_FROMPYOBJECT: bool>( obj: Option<&'a Bound<'py, PyAny>>, holder: &'holder mut T::Holder, arg_name: &str, default: fn() -> T, ) -> PyResult where - T: PyFunctionArgument<'a, 'holder, 'py, IS_OPTION>, + T: PyFunctionArgument<'a, 'holder, 'py, IMPLEMENTS_FROMPYOBJECT>, { 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 6e3804aaec8..ac254085dd1 100644 --- a/src/impl_/pyclass/probes.rs +++ b/src/impl_/pyclass/probes.rs @@ -1,6 +1,6 @@ use std::marker::PhantomData; -use crate::{conversion::IntoPyObject, Py}; +use crate::{conversion::IntoPyObject, FromPyObject, Py}; /// Trait used to combine with zero-sized types to calculate at compile time /// some property of a type. @@ -58,9 +58,12 @@ impl IsSync { pub const VALUE: bool = true; } -probe!(IsOption); +probe!(IsFromPyObject); -impl IsOption> { +impl<'a, 'py, T> IsFromPyObject +where + T: FromPyObject<'a, 'py>, +{ pub const VALUE: bool = true; } diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index 97439769600..68e6b06cfc7 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -17,8 +17,10 @@ fn test_compile_errors() { t.compile_fail("tests/ui/invalid_pyclass_generic.rs"); #[cfg(Py_3_9)] t.compile_fail("tests/ui/pyclass_generic_enum.rs"); - t.compile_fail("tests/ui/invalid_pyfunction_signatures.rs"); + #[cfg(not(feature = "experimental-inspect"))] + t.compile_fail("tests/ui/invalid_pyfunction_argument.rs"); t.compile_fail("tests/ui/invalid_pyfunction_definition.rs"); + 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 diff --git a/tests/ui/invalid_cancel_handle.stderr b/tests/ui/invalid_cancel_handle.stderr index ff6e7fb2e42..8fdea4c951a 100644 --- a/tests/ui/invalid_cancel_handle.stderr +++ b/tests/ui/invalid_cancel_handle.stderr @@ -38,66 +38,60 @@ 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<'_, '_, '_, false>` is not satisfied +error[E0277]: `CancelHandle` cannot be used as a Python function argument --> 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` | + = note: implement `FromPyObject` to enable using `CancelHandle` as a function argument + = note: `Python<'py>` is also a valid argument type to pass the Python token into `#[pyfunction]`s and `#[pymethods]` = help: the trait `PyClass` is implemented for `pyo3::coroutine::Coroutine` = note: required for `CancelHandle` to implement `FromPyObject<'_, '_>` - = note: required for `CancelHandle` to implement `PyFunctionArgument<'_, '_, '_, false>` + = note: required for `CancelHandle` to implement `PyFunctionArgument<'_, '_, '_, true>` note: required by a bound in `extract_argument` --> src/impl_/extract_argument.rs | - | pub fn extract_argument<'a, 'holder, 'py, T, const IS_OPTION: bool>( + | pub fn extract_argument<'a, 'holder, 'py, T, const IMPLEMENTS_FROMPYOBJECT: bool>( | ---------------- required by a bound in this function ... - | T: PyFunctionArgument<'a, 'holder, 'py, IS_OPTION>, - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `extract_argument` + | T: PyFunctionArgument<'a, 'holder, 'py, IMPLEMENTS_FROMPYOBJECT>, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `extract_argument` -error[E0277]: the trait bound `CancelHandle: PyFunctionArgument<'_, '_, '_, false>` is not satisfied +error[E0277]: `CancelHandle` cannot be used as a Python function argument --> 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, 'holder, 'py, IS_OPTION>`: - `&'a pyo3::Bound<'py, T>` implements `PyFunctionArgument<'a, 'holder, 'py, false>` - `&'holder mut pyo3::coroutine::Coroutine` implements `PyFunctionArgument<'a, 'holder, 'py, false>` - `&'holder pyo3::coroutine::Coroutine` implements `PyFunctionArgument<'a, 'holder, 'py, false>` - `Option` implements `PyFunctionArgument<'a, 'holder, 'py, true>` + = note: implement `FromPyObject` to enable using `CancelHandle` as a function argument + = note: `Python<'py>` is also a valid argument type to pass the Python token into `#[pyfunction]`s and `#[pymethods]` + = help: the following other types implement trait `PyFunctionArgument<'a, 'holder, 'py, IMPLEMENTS_FROMPYOBJECT>`: + `&'a pyo3::Bound<'py, T>` implements `PyFunctionArgument<'a, '_, 'py, false>` + `&'holder T` implements `PyFunctionArgument<'a, 'holder, '_, false>` + `&'holder mut T` implements `PyFunctionArgument<'a, 'holder, '_, false>` + `Option` implements `PyFunctionArgument<'a, 'holder, 'py, false>` = note: required for `CancelHandle` to implement `FromPyObject<'_, '_>` - = note: required for `CancelHandle` to implement `PyFunctionArgument<'_, '_, '_, false>` + = note: required for `CancelHandle` to implement `PyFunctionArgument<'_, '_, '_, true>` note: required by a bound in `extract_argument` --> src/impl_/extract_argument.rs | - | pub fn extract_argument<'a, 'holder, 'py, T, const IS_OPTION: bool>( + | pub fn extract_argument<'a, 'holder, 'py, T, const IMPLEMENTS_FROMPYOBJECT: bool>( | ---------------- required by a bound in this function ... - | T: PyFunctionArgument<'a, 'holder, 'py, IS_OPTION>, - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `extract_argument` + | T: PyFunctionArgument<'a, 'holder, 'py, IMPLEMENTS_FROMPYOBJECT>, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `extract_argument` -error[E0277]: the trait bound `CancelHandle: PyFunctionArgument<'_, '_, '_, false>` is not satisfied +error[E0277]: `CancelHandle` cannot be used as a Python function argument --> 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` + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `PyFunctionArgument<'_, '_, '_, false>` is not implemented for `CancelHandle` | - = help: the trait `PyClass` is implemented for `pyo3::coroutine::Coroutine` - = note: required for `CancelHandle` to implement `FromPyObject<'_, '_>` - = note: required for `CancelHandle` to implement `PyFunctionArgument<'_, '_, '_, false>` - -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, 'holder, 'py, IS_OPTION>`: - `&'a pyo3::Bound<'py, T>` implements `PyFunctionArgument<'a, 'holder, 'py, false>` - `&'holder mut pyo3::coroutine::Coroutine` implements `PyFunctionArgument<'a, 'holder, 'py, false>` - `&'holder pyo3::coroutine::Coroutine` implements `PyFunctionArgument<'a, 'holder, 'py, false>` - `Option` implements `PyFunctionArgument<'a, 'holder, 'py, true>` - = note: required for `CancelHandle` to implement `FromPyObject<'_, '_>` - = note: required for `CancelHandle` to implement `PyFunctionArgument<'_, '_, '_, false>` + = note: implement `FromPyObject` to enable using `CancelHandle` as a function argument + = note: `Python<'py>` is also a valid argument type to pass the Python token into `#[pyfunction]`s and `#[pymethods]` + = help: the following other types implement trait `PyFunctionArgument<'a, 'holder, 'py, IMPLEMENTS_FROMPYOBJECT>`: + `&'a pyo3::Bound<'py, T>` implements `PyFunctionArgument<'a, '_, 'py, false>` + `&'holder T` implements `PyFunctionArgument<'a, 'holder, '_, false>` + `&'holder mut T` implements `PyFunctionArgument<'a, 'holder, '_, false>` + `Option` implements `PyFunctionArgument<'a, 'holder, 'py, false>` diff --git a/tests/ui/invalid_frozen_pyclass_borrow.stderr b/tests/ui/invalid_frozen_pyclass_borrow.stderr index b7a06dc9c67..ae81251c24c 100644 --- a/tests/ui/invalid_frozen_pyclass_borrow.stderr +++ b/tests/ui/invalid_frozen_pyclass_borrow.stderr @@ -18,8 +18,8 @@ note: expected this to be `False` note: required by a bound in `extract_pyclass_ref_mut` --> src/impl_/extract_argument.rs | - | pub fn extract_pyclass_ref_mut<'a, 'holder, 'py, T: PyClass>( - | ^^^^^^^^^^^^^^ required by this bound in `extract_pyclass_ref_mut` + | pub fn extract_pyclass_ref_mut<'a, 'holder, T: PyClass>( + | ^^^^^^^^^^^^^^ required by this bound in `extract_pyclass_ref_mut` = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0271]: type mismatch resolving `::Frozen == False` diff --git a/tests/ui/invalid_pyfunction_argument.rs b/tests/ui/invalid_pyfunction_argument.rs new file mode 100644 index 00000000000..585d8d5d082 --- /dev/null +++ b/tests/ui/invalid_pyfunction_argument.rs @@ -0,0 +1,9 @@ +use pyo3::prelude::*; +use std::sync::atomic::AtomicPtr; + +#[pyfunction] +fn invalid_pyfunction_argument(arg: AtomicPtr<()>) { + let _ = arg; +} + +fn main() {} diff --git a/tests/ui/invalid_pyfunction_argument.stderr b/tests/ui/invalid_pyfunction_argument.stderr new file mode 100644 index 00000000000..3bd9b4bc77c --- /dev/null +++ b/tests/ui/invalid_pyfunction_argument.stderr @@ -0,0 +1,47 @@ +error[E0277]: `AtomicPtr<()>` cannot be used as a Python function argument + --> tests/ui/invalid_pyfunction_argument.rs:5:37 + | +5 | fn invalid_pyfunction_argument(arg: AtomicPtr<()>) { + | ^^^^^^^^^ the trait `PyClass` is not implemented for `AtomicPtr<()>` + | + = note: implement `FromPyObject` to enable using `AtomicPtr<()>` as a function argument + = note: `Python<'py>` is also a valid argument type to pass the Python token into `#[pyfunction]`s and `#[pymethods]` + = help: the following other types implement trait `PyFunctionArgument<'a, 'holder, 'py, IMPLEMENTS_FROMPYOBJECT>`: + `&'a pyo3::Bound<'py, T>` implements `PyFunctionArgument<'a, '_, 'py, false>` + `&'holder T` implements `PyFunctionArgument<'a, 'holder, '_, false>` + `&'holder mut T` implements `PyFunctionArgument<'a, 'holder, '_, false>` + `Option` implements `PyFunctionArgument<'a, 'holder, 'py, false>` + = note: required for `AtomicPtr<()>` to implement `FromPyObject<'_, '_>` + = note: required for `AtomicPtr<()>` to implement `PyFunctionArgument<'_, '_, '_, true>` +note: required by a bound in `extract_argument` + --> src/impl_/extract_argument.rs + | + | pub fn extract_argument<'a, 'holder, 'py, T, const IMPLEMENTS_FROMPYOBJECT: bool>( + | ---------------- required by a bound in this function +... + | T: PyFunctionArgument<'a, 'holder, 'py, IMPLEMENTS_FROMPYOBJECT>, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `extract_argument` + +error[E0277]: `AtomicPtr<()>` cannot be used as a Python function argument + --> tests/ui/invalid_pyfunction_argument.rs:5:37 + | +5 | fn invalid_pyfunction_argument(arg: AtomicPtr<()>) { + | ^^^^^^^^^ the trait `Clone` is not implemented for `AtomicPtr<()>` + | + = note: implement `FromPyObject` to enable using `AtomicPtr<()>` as a function argument + = note: `Python<'py>` is also a valid argument type to pass the Python token into `#[pyfunction]`s and `#[pymethods]` + = help: the following other types implement trait `PyFunctionArgument<'a, 'holder, 'py, IMPLEMENTS_FROMPYOBJECT>`: + `&'a pyo3::Bound<'py, T>` implements `PyFunctionArgument<'a, '_, 'py, false>` + `&'holder T` implements `PyFunctionArgument<'a, 'holder, '_, false>` + `&'holder mut T` implements `PyFunctionArgument<'a, 'holder, '_, false>` + `Option` implements `PyFunctionArgument<'a, 'holder, 'py, false>` + = note: required for `AtomicPtr<()>` to implement `FromPyObject<'_, '_>` + = note: required for `AtomicPtr<()>` to implement `PyFunctionArgument<'_, '_, '_, true>` +note: required by a bound in `extract_argument` + --> src/impl_/extract_argument.rs + | + | pub fn extract_argument<'a, 'holder, 'py, T, const IMPLEMENTS_FROMPYOBJECT: bool>( + | ---------------- required by a bound in this function +... + | T: PyFunctionArgument<'a, 'holder, 'py, IMPLEMENTS_FROMPYOBJECT>, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `extract_argument` diff --git a/tests/ui/invalid_pymethod_enum.stderr b/tests/ui/invalid_pymethod_enum.stderr index e93f8fe64ca..996c3983900 100644 --- a/tests/ui/invalid_pymethod_enum.stderr +++ b/tests/ui/invalid_pymethod_enum.stderr @@ -12,8 +12,8 @@ note: expected this to be `False` note: required by a bound in `extract_pyclass_ref_mut` --> src/impl_/extract_argument.rs | - | pub fn extract_pyclass_ref_mut<'a, 'holder, 'py, T: PyClass>( - | ^^^^^^^^^^^^^^ required by this bound in `extract_pyclass_ref_mut` + | pub fn extract_pyclass_ref_mut<'a, 'holder, T: PyClass>( + | ^^^^^^^^^^^^^^ required by this bound in `extract_pyclass_ref_mut` = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0271]: type mismatch resolving `::Frozen == False` @@ -48,8 +48,8 @@ note: expected this to be `False` note: required by a bound in `extract_pyclass_ref_mut` --> src/impl_/extract_argument.rs | - | pub fn extract_pyclass_ref_mut<'a, 'holder, 'py, T: PyClass>( - | ^^^^^^^^^^^^^^ required by this bound in `extract_pyclass_ref_mut` + | pub fn extract_pyclass_ref_mut<'a, 'holder, T: PyClass>( + | ^^^^^^^^^^^^^^ required by this bound in `extract_pyclass_ref_mut` = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0271]: type mismatch resolving `::Frozen == False` From 0e6e4f4e1dc61a9a49e4114fe477a744c0ecfd0b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 9 Sep 2025 08:46:29 +0000 Subject: [PATCH 858/936] build(deps): bump CodSpeedHQ/action from 3 to 4 (#5425) * build(deps): bump CodSpeedHQ/action from 3 to 4 Bumps [CodSpeedHQ/action](https://github.com/codspeedhq/action) from 3 to 4. - [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/v3...v4) --- updated-dependencies: - dependency-name: CodSpeedHQ/action dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] * add `mode` parameter --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: David Hewitt --- .github/workflows/benches.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/benches.yml b/.github/workflows/benches.yml index 65898bb0802..19dd9d145c4 100644 --- a/.github/workflows/benches.yml +++ b/.github/workflows/benches.yml @@ -20,7 +20,7 @@ jobs: - uses: actions/checkout@v5 - uses: actions/setup-python@v6 with: - python-version: '3.13' + python-version: "3.13" - uses: dtolnay/rust-toolchain@stable with: components: rust-src @@ -31,8 +31,8 @@ jobs: . pyo3-benches save-if: ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'CI-save-pr-cache') }} - cache-all-crates: 'true' - cache-workspace-crates: 'true' + cache-all-crates: "true" + cache-workspace-crates: "true" - uses: taiki-e/install-action@v2 with: @@ -42,7 +42,8 @@ jobs: run: pip install nox[uv] - name: Run the benchmarks - uses: CodSpeedHQ/action@v3 + uses: CodSpeedHQ/action@v4 with: run: nox -s codspeed token: ${{ secrets.CODSPEED_TOKEN }} + mode: instrumentation From 1ca6d3c2f68d2b3eb0b384e6061bfd661845d0d3 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sun, 14 Sep 2025 11:47:20 +0100 Subject: [PATCH 859/936] docs: fix link to `PyGenericAlias` from the guide (#5440) --- guide/src/python-typing-hints.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/guide/src/python-typing-hints.md b/guide/src/python-typing-hints.md index d2c755b0712..ad0219247de 100644 --- a/guide/src/python-typing-hints.md +++ b/guide/src/python-typing-hints.md @@ -208,7 +208,7 @@ Stub files (`pyi`) are only useful for static type checkers and ignored at runti PyO3 classes do not inherit from `typing.Generic` even if specified in the stub files. This can cause some runtime issues, as annotating a variable like `f1_car: Car[AlloyWheel] = ...` -can make Python call magic methods that are not defined. +can make Python call magic methods that are not defined. To overcome this limitation, implementers can pass the `generic` parameter to `pyclass` in Rust: @@ -236,8 +236,8 @@ impl MyClass { } ``` -Note that [`pyo3::types::PyGenericAlias`][pygenericalias] can be helfpul when implementing -`__class_geitem__` as it can create [`types.GenericAlias`][genericalias] objects from Rust. +Note that [`pyo3::types::PyGenericAlias`][pygenericalias] can be helpful when implementing +`__class_getitem__` as it can create [`types.GenericAlias`][genericalias] objects from Rust. -[pygenericalias]: {{#PYO3_DOCS_URL}}/pyo3/types/struct.pygenericalias -[genericalias]: https://docs.python.org/3/library/types.html#types.GenericAlias \ No newline at end of file +[pygenericalias]: {{#PYO3_DOCS_URL}}/pyo3/types/struct.PyGenericAlias.html +[genericalias]: https://docs.python.org/3/library/types.html#types.GenericAlias From 3a097f7f33658fff238316f942dcb72b160ee7bc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 14 Sep 2025 11:51:46 +0000 Subject: [PATCH 860/936] build(deps): update hashbrown requirement from >= 0.15.0, < 0.16 to >= 0.15.0, < 0.17 (#5428) * build(deps): update hashbrown requirement from >= 0.15.0, < 0.16 to >= 0.15.0, < 0.17 Updates the requirements on [hashbrown](https://github.com/rust-lang/hashbrown) to permit the latest version. - [Release notes](https://github.com/rust-lang/hashbrown/releases) - [Changelog](https://github.com/rust-lang/hashbrown/blob/master/CHANGELOG.md) - [Commits](https://github.com/rust-lang/hashbrown/compare/v0.15.0...v0.16.0) --- updated-dependencies: - dependency-name: hashbrown dependency-version: 0.16.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] * build(deps): update hashbrown requirement in /pyo3-benches Updates the requirements on [hashbrown](https://github.com/rust-lang/hashbrown) to permit the latest version. - [Release notes](https://github.com/rust-lang/hashbrown/releases) - [Changelog](https://github.com/rust-lang/hashbrown/blob/master/CHANGELOG.md) - [Commits](https://github.com/rust-lang/hashbrown/compare/v0.15.0...v0.16.0) --- updated-dependencies: - dependency-name: hashbrown dependency-version: 0.16.0 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 | 2 +- newsfragments/5428.packaging.md | 1 + pyo3-benches/Cargo.toml | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 newsfragments/5428.packaging.md diff --git a/Cargo.toml b/Cargo.toml index a6e0ed838c2..6ecdb9e50b4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,7 +48,7 @@ chrono = { version = "0.4.25", 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.6.8, < 0.7", optional = true } -hashbrown = { version = ">= 0.15.0, < 0.16", optional = true, default-features = false } +hashbrown = { version = ">= 0.15.0, < 0.17", optional = true, default-features = false } 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 } diff --git a/newsfragments/5428.packaging.md b/newsfragments/5428.packaging.md new file mode 100644 index 00000000000..a6d0c1e6183 --- /dev/null +++ b/newsfragments/5428.packaging.md @@ -0,0 +1 @@ +Extend range of supported versions of `hashbrown` optional dependency to include version 0.16. diff --git a/pyo3-benches/Cargo.toml b/pyo3-benches/Cargo.toml index 510deacbb4d..bd4899263ae 100644 --- a/pyo3-benches/Cargo.toml +++ b/pyo3-benches/Cargo.toml @@ -17,7 +17,7 @@ codspeed-criterion-compat = "3.0" criterion = "0.7.0" num-bigint = "0.4.3" rust_decimal = { version = "1.0.0", default-features = false } -hashbrown = "0.15" +hashbrown = "0.16" [[bench]] name = "bench_any" From 1549dbb2f27d0a9fbdcd25c29e356786aa9e4850 Mon Sep 17 00:00:00 2001 From: Thomas Tanon Date: Sun, 14 Sep 2025 14:27:59 +0200 Subject: [PATCH 861/936] Macros: move elide_lifetimes into introspection module (#5433) Only used for introspection --- pyo3-macros-backend/src/frompyobject.rs | 7 +++--- pyo3-macros-backend/src/intopyobject.rs | 7 +++--- pyo3-macros-backend/src/introspection.rs | 30 ++++++++++++++++++------ pyo3-macros-backend/src/utils.rs | 27 --------------------- 4 files changed, 29 insertions(+), 42 deletions(-) diff --git a/pyo3-macros-backend/src/frompyobject.rs b/pyo3-macros-backend/src/frompyobject.rs index 27b83673982..d3232e90777 100644 --- a/pyo3-macros-backend/src/frompyobject.rs +++ b/pyo3-macros-backend/src/frompyobject.rs @@ -1,9 +1,7 @@ use crate::attributes::{DefaultAttribute, FromPyWithAttribute, RenamingRule}; use crate::derive_attributes::{ContainerAttributes, FieldAttributes, FieldGetter}; #[cfg(feature = "experimental-inspect")] -use crate::introspection::ConcatenationBuilder; -#[cfg(feature = "experimental-inspect")] -use crate::utils::TypeExt; +use crate::introspection::{elide_lifetimes, ConcatenationBuilder}; use crate::utils::{self, Ctx}; use proc_macro2::TokenStream; use quote::{format_ident, quote, quote_spanned, ToTokens}; @@ -496,7 +494,8 @@ impl<'a> Container<'a> { // We don't know what from_py_with is doing builder.push_str("_typeshed.Incomplete") } else { - let ty = ty.clone().elide_lifetimes(); + let mut ty = ty.clone(); + elide_lifetimes(&mut ty); let pyo3_crate_path = &ctx.pyo3_path; builder.push_tokens( quote! { <#ty as #pyo3_crate_path::FromPyObject<'_, '_>>::INPUT_TYPE.as_bytes() }, diff --git a/pyo3-macros-backend/src/intopyobject.rs b/pyo3-macros-backend/src/intopyobject.rs index 811e05569cb..a49aaaae81d 100644 --- a/pyo3-macros-backend/src/intopyobject.rs +++ b/pyo3-macros-backend/src/intopyobject.rs @@ -1,9 +1,7 @@ use crate::attributes::{IntoPyWithAttribute, RenamingRule}; use crate::derive_attributes::{ContainerAttributes, FieldAttributes}; #[cfg(feature = "experimental-inspect")] -use crate::introspection::ConcatenationBuilder; -#[cfg(feature = "experimental-inspect")] -use crate::utils::TypeExt; +use crate::introspection::{elide_lifetimes, ConcatenationBuilder}; use crate::utils::{self, Ctx}; use proc_macro2::{Span, TokenStream}; use quote::{format_ident, quote, quote_spanned, ToTokens}; @@ -402,7 +400,8 @@ impl<'a, const REF: bool> Container<'a, REF> { // We don't know what into_py_with is doing builder.push_str("_typeshed.Incomplete") } else { - let ty = ty.clone().elide_lifetimes(); + let mut ty = ty.clone(); + elide_lifetimes(&mut ty); let pyo3_crate_path = &ctx.pyo3_path; builder.push_tokens( quote! { <#ty as #pyo3_crate_path::IntoPyObject<'_>>::OUTPUT_TYPE.as_bytes() }, diff --git a/pyo3-macros-backend/src/introspection.rs b/pyo3-macros-backend/src/introspection.rs index 83bd12d1056..bcece0e8a70 100644 --- a/pyo3-macros-backend/src/introspection.rs +++ b/pyo3-macros-backend/src/introspection.rs @@ -10,7 +10,7 @@ use crate::method::{FnArg, RegularArg}; use crate::pyfunction::FunctionSignature; -use crate::utils::{PyO3CratePath, TypeExt}; +use crate::utils::PyO3CratePath; use proc_macro2::{Span, TokenStream}; use quote::{format_ident, quote, ToTokens}; use std::borrow::Cow; @@ -20,7 +20,7 @@ use std::hash::{Hash, Hasher}; use std::mem::take; use std::sync::atomic::{AtomicUsize, Ordering}; use syn::visit_mut::{visit_type_mut, VisitMut}; -use syn::{Attribute, Ident, ReturnType, Type, TypePath}; +use syn::{Attribute, Ident, Lifetime, ReturnType, Type, TypePath}; static GLOBAL_COUNTER_FOR_UNIQUE_NAMES: AtomicUsize = AtomicUsize::new(0); @@ -114,7 +114,7 @@ pub fn function_introspection_code( if let Some(class_type) = parent { replace_self(&mut ty, class_type); } - ty = ty.elide_lifetimes(); + elide_lifetimes(&mut ty); IntrospectionNode::OutputType { rust_type: ty, is_final: false, @@ -168,7 +168,7 @@ pub fn attribute_introspection_code( if let Some(parent) = parent { replace_self(&mut rust_type, parent); } - rust_type = rust_type.elide_lifetimes(); + elide_lifetimes(&mut rust_type); desc.insert( "annotation", IntrospectionNode::OutputType { @@ -313,7 +313,7 @@ fn argument_introspection_data<'a>( if let Some(class_type) = class_type { replace_self(&mut ty, class_type); } - ty = ty.elide_lifetimes(); + elide_lifetimes(&mut ty); params.insert( "annotation", IntrospectionNode::InputType { @@ -326,7 +326,7 @@ fn argument_introspection_data<'a>( if let Some(class_type) = class_type { replace_self(&mut ty, class_type); } - ty = ty.elide_lifetimes(); + elide_lifetimes(&mut ty); params.insert( "annotation", IntrospectionNode::InputType { @@ -574,6 +574,22 @@ fn ident_to_type(ident: &Ident) -> Cow<'static, Type> { ) } +/// Replaces all explicit lifetimes in `self` with elided (`'_`) lifetimes +/// +/// This is useful if `Self` is used in `const` context, where explicit +/// lifetimes are not allowed (yet). +pub fn elide_lifetimes(ty: &mut Type) { + struct ElideLifetimesVisitor; + + impl VisitMut for ElideLifetimesVisitor { + fn visit_lifetime_mut(&mut self, l: &mut syn::Lifetime) { + *l = Lifetime::new("'_", l.span()); + } + } + + ElideLifetimesVisitor.visit_type_mut(ty); +} + // Replace Self in types with the given type fn replace_self(ty: &mut Type, self_target: &Type) { struct SelfReplacementVisitor<'a> { @@ -582,7 +598,7 @@ fn replace_self(ty: &mut Type, self_target: &Type) { impl VisitMut for SelfReplacementVisitor<'_> { fn visit_type_mut(&mut self, ty: &mut Type) { - if let syn::Type::Path(type_path) = ty { + if let Type::Path(type_path) = ty { if type_path.qself.is_none() && type_path.path.segments.len() == 1 && type_path.path.segments[0].ident == "Self" diff --git a/pyo3-macros-backend/src/utils.rs b/pyo3-macros-backend/src/utils.rs index 91b6006add9..3e553acfdbd 100644 --- a/pyo3-macros-backend/src/utils.rs +++ b/pyo3-macros-backend/src/utils.rs @@ -339,33 +339,6 @@ pub(crate) fn has_attribute_with_namespace( }) } -#[cfg(feature = "experimental-inspect")] -pub(crate) trait TypeExt { - /// Replaces all explicit lifetimes in `self` with elided (`'_`) lifetimes - /// - /// This is useful if `Self` is used in `const` context, where explicit - /// lifetimes are not allowed (yet). - fn elide_lifetimes(self) -> Self; -} - -#[cfg(feature = "experimental-inspect")] -impl TypeExt for syn::Type { - fn elide_lifetimes(mut self) -> Self { - use syn::visit_mut::VisitMut; - - struct ElideLifetimesVisitor; - - impl VisitMut for ElideLifetimesVisitor { - fn visit_lifetime_mut(&mut self, l: &mut syn::Lifetime) { - *l = syn::Lifetime::new("'_", l.span()); - } - } - - ElideLifetimesVisitor.visit_type_mut(&mut self); - self - } -} - pub fn expr_to_python(expr: &syn::Expr) -> String { match expr { // literal values From b31a2580c0653c4a10dae453ce5118b89ac29020 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sun, 14 Sep 2025 19:48:56 +0100 Subject: [PATCH 862/936] ci: fix coverage reporting for main (#5441) --- .github/workflows/build.yml | 12 ++++-------- .github/workflows/coverage-pr-base.yml | 2 +- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ff8f4939812..e8422e72e65 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -126,12 +126,10 @@ jobs: 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 - - if: ${{ github.event_name != 'merge_group' }} - name: Install cargo-llvm-cov + - name: Install cargo-llvm-cov uses: taiki-e/install-action@cargo-llvm-cov - - if: ${{ github.event_name != 'merge_group' }} - name: Prepare coverage environment + - name: Prepare coverage environment run: | cargo llvm-cov clean --workspace --profraw-only nox -s set-coverage-env @@ -148,8 +146,7 @@ jobs: env: CARGO_TARGET_DIR: ${{ github.workspace }}/target - - if: ${{ github.event_name != 'merge_group' }} - name: Generate coverage report + - name: Generate coverage report # needs investigation why llvm-cov fails on windows-11-arm continue-on-error: ${{ inputs.os == 'windows-11-arm' }} run: cargo llvm-cov @@ -160,8 +157,7 @@ jobs: --package=pyo3-ffi report --codecov --output-path coverage.json - - if: ${{ github.event_name != 'merge_group' }} - name: Upload coverage report + - name: Upload coverage report uses: codecov/codecov-action@v5 # needs investigation why llvm-cov fails on windows-11-arm continue-on-error: ${{ inputs.os == 'windows-11-arm' }} diff --git a/.github/workflows/coverage-pr-base.yml b/.github/workflows/coverage-pr-base.yml index 0f4b359e41e..7b21b399266 100644 --- a/.github/workflows/coverage-pr-base.yml +++ b/.github/workflows/coverage-pr-base.yml @@ -21,7 +21,7 @@ jobs: uses: ./.github/actions/fetch-merge-base with: base_ref: "refs/heads/${{ github.event.pull_request.base.ref }}" - head_ref: "refs/pull/${{ github.event.pull_request.number }}/merge" + head_ref: "refs/pull/${{ github.event.pull_request.number }}/head" - name: Set PR base on codecov run: | pip install codecov-cli From 0ee6eacd87f698a62a9f07f53b0cf59fb7a0ef10 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Mon, 15 Sep 2025 14:02:27 +0100 Subject: [PATCH 863/936] improve debug representation of `PyBuffer` (#5442) * improve debug representation of `PyBuffer` * newsfragment * fixup tests --- newsfragments/5442.changed.md | 1 + src/buffer.rs | 31 +++++++++++++++---------------- 2 files changed, 16 insertions(+), 16 deletions(-) create mode 100644 newsfragments/5442.changed.md diff --git a/newsfragments/5442.changed.md b/newsfragments/5442.changed.md new file mode 100644 index 00000000000..4355627ea0a --- /dev/null +++ b/newsfragments/5442.changed.md @@ -0,0 +1 @@ +Improve `Debug` representation of `PyBuffer`. diff --git a/src/buffer.rs b/src/buffer.rs index bdd0ee214fe..f2b42aa6ea4 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -48,10 +48,10 @@ impl Debug for PyBuffer { .field("itemsize", &self.0.itemsize) .field("readonly", &self.0.readonly) .field("ndim", &self.0.ndim) - .field("format", &self.0.format) - .field("shape", &self.0.shape) - .field("strides", &self.0.strides) - .field("suboffsets", &self.0.suboffsets) + .field("format", &self.format()) + .field("shape", &self.shape()) + .field("strides", &self.strides()) + .field("suboffsets", &self.suboffsets()) .field("internal", &self.0.internal) .finish() } @@ -228,7 +228,9 @@ impl PyBuffer { Ok(buf) } } +} +impl PyBuffer { /// Gets the pointer to the start of the buffer memory. /// /// Warning: the buffer memory can be mutated by other code (including @@ -367,7 +369,9 @@ impl PyBuffer { pub fn is_fortran_contiguous(&self) -> bool { unsafe { ffi::PyBuffer_IsContiguous(&*self.0, b'F' as std::ffi::c_char) != 0 } } +} +impl PyBuffer { /// Gets the buffer memory as a slice. /// /// This function succeeds if: @@ -703,29 +707,24 @@ mod tests { use crate::ffi; use crate::types::any::PyAnyMethods; + use crate::types::PyBytes; use crate::Python; #[test] fn test_debug() { Python::attach(|py| { - let bytes = py.eval(ffi::c_str!("b'abcde'"), None, None).unwrap(); + let bytes = PyBytes::new(py, b"abcde"); let buffer: PyBuffer = PyBuffer::get(&bytes).unwrap(); let expected = format!( concat!( "PyBuffer {{ buf: {:?}, obj: {:?}, ", "len: 5, itemsize: 1, readonly: 1, ", - "ndim: 1, format: {:?}, shape: {:?}, ", - "strides: {:?}, suboffsets: {:?}, internal: {:?} }}", + "ndim: 1, format: \"B\", shape: [5], ", + "strides: [1], suboffsets: None, internal: {:?} }}", ), - buffer.0.buf, - buffer.0.obj, - buffer.0.format, - buffer.0.shape, - buffer.0.strides, - buffer.0.suboffsets, - buffer.0.internal + buffer.0.buf, buffer.0.obj, buffer.0.internal ); - let debug_repr = format!("{buffer:?}"); + let debug_repr = format!("{:?}", buffer); assert_eq!(debug_repr, expected); }); } @@ -867,7 +866,7 @@ mod tests { #[test] fn test_bytes_buffer() { Python::attach(|py| { - let bytes = py.eval(ffi::c_str!("b'abcde'"), None, None).unwrap(); + let bytes = PyBytes::new(py, b"abcde"); let buffer = PyBuffer::get(&bytes).unwrap(); assert_eq!(buffer.dimensions(), 1); assert_eq!(buffer.item_count(), 5); From 6f02125cbfd1603e076fc8707982a5549229d53e Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Mon, 15 Sep 2025 14:39:46 +0100 Subject: [PATCH 864/936] remove uneeded `Pin` from `PyBuffer` (#5417) --- src/buffer.rs | 29 +++++++++-------------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index f2b42aa6ea4..dedea35c582 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -25,14 +25,12 @@ use std::ffi::{ c_ushort, c_void, }; use std::marker::PhantomData; -use std::pin::Pin; -use std::{cell, mem, ptr, slice}; +use std::{cell, mem, slice}; use std::{ffi::CStr, fmt::Debug}; /// Allows access to the underlying buffer used by a python object such as `bytes`, `bytearray` or `array.array`. -// use Pin because Python expects that the Py_buffer struct has a stable memory address #[repr(transparent)] -pub struct PyBuffer(Pin>, PhantomData); +pub struct PyBuffer(Box, PhantomData); // PyBuffer is thread-safe: the shape of the buffer is immutable while a Py_buffer exists. // Accessing the buffer contents is protected using the GIL. @@ -208,7 +206,7 @@ impl PyBuffer { }; // Create PyBuffer immediately so that if validation checks fail, the PyBuffer::drop code // will call PyBuffer_Release (thus avoiding any leaks). - let buf = PyBuffer(Pin::from(buf), PhantomData); + let buf = PyBuffer(buf, PhantomData); if buf.0.shape.is_null() { Err(PyBufferError::new_err("shape is null")) @@ -618,26 +616,17 @@ impl PyBuffer { /// /// This will automatically be called on drop. pub fn release(self, _py: Python<'_>) { - // First move self into a ManuallyDrop, so that PyBuffer::drop will - // never be called. (It would acquire the GIL and call PyBuffer_Release - // again.) - let mut mdself = mem::ManuallyDrop::new(self); - unsafe { - // Next, make the actual PyBuffer_Release call. - ffi::PyBuffer_Release(&mut *mdself.0); - - // Finally, drop the contained Pin> in place, to free the - // Box memory. - let inner: *mut Pin> = &mut mdself.0; - ptr::drop_in_place(inner); - } + // SAFETY: Self is `repr(transparent)` around a Box + let mut inner: Box = unsafe { std::mem::transmute(self) }; + // SAFETY: the ffi::Py_buffer structure is valid until this release call + unsafe { ffi::PyBuffer_Release(&mut *inner) }; } } impl Drop for PyBuffer { fn drop(&mut self) { - fn inner(buf: &mut Pin>) { - if Python::try_attach(|_| unsafe { ffi::PyBuffer_Release(&mut **buf) }).is_none() + fn inner(buf: &mut Box) { + if Python::try_attach(|_| unsafe { ffi::PyBuffer_Release(buf.as_mut()) }).is_none() && crate::internal::state::is_in_gc_traversal() { eprintln!("Warning: PyBuffer dropped while in GC traversal, this is a bug and will leak memory."); From e0fe042d52555760e382667e442111b77ca7aa63 Mon Sep 17 00:00:00 2001 From: elbaro Date: Tue, 16 Sep 2025 18:11:56 +0900 Subject: [PATCH 865/936] Add PyString::from_bytes (#5437) * Add PyString::from_bytes * add newsfragment * gate the test behind not(any(Py_LIMITED_API, PyPy, GraalPy)) * Update the docstring * Fix PyMemoryError not in scope * style --- newsfragments/5437.added.md | 1 + src/types/string.rs | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 newsfragments/5437.added.md diff --git a/newsfragments/5437.added.md b/newsfragments/5437.added.md new file mode 100644 index 00000000000..f828546a05d --- /dev/null +++ b/newsfragments/5437.added.md @@ -0,0 +1 @@ +Add PyString::from_bytes. This saves a redundant UTF-8 validation check because Python internally validates the bytes again. diff --git a/src/types/string.rs b/src/types/string.rs index 1baa6a18eeb..e61d37d541d 100644 --- a/src/types/string.rs +++ b/src/types/string.rs @@ -168,6 +168,20 @@ impl PyString { } } + /// Creates a new Python string object from bytes. + /// + /// Returns PyMemoryError if out of memory. + /// Returns [PyUnicodeDecodeError] if the slice is not a valid UTF-8 string. + pub fn from_bytes<'py>(py: Python<'py>, s: &[u8]) -> PyResult> { + let ptr = s.as_ptr().cast(); + let len = s.len() as ffi::Py_ssize_t; + unsafe { + ffi::PyUnicode_FromStringAndSize(ptr, len) + .assume_owned_or_err(py) + .cast_into_unchecked() + } + } + /// Intern the given string /// /// This will return a reference to the same Python string object if called repeatedly with the same string. @@ -828,6 +842,20 @@ mod tests { }); } + #[test] + #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] + fn test_pystring_from_bytes() { + Python::attach(|py| { + let result = PyString::from_bytes(py, "\u{2122}".as_bytes()); + assert!(result.is_ok()); + let result = PyString::from_bytes(py, b"\x80"); + assert!(result + .unwrap_err() + .get_type(py) + .is(py.get_type::())); + }); + } + #[test] fn test_intern_string() { Python::attach(|py| { From 98c56da1bb8fdb6a1e4d976ac3a5eb7805702158 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Thu, 18 Sep 2025 11:58:47 +0100 Subject: [PATCH 866/936] optimize conversion from bytes for Vec and [u8; N] (#5244) * optimize conversion from bytes for `Vec` and `[u8; N]` * rewrite to support bytearray cleanly * newsfragment * tidy specialized implementations * fix MSRV * fix clippy --- newsfragments/5244.changed.md | 1 + pyo3-build-config/src/lib.rs | 1 + src/conversion.rs | 64 +++++++++++++++++++ src/conversions/std/array.rs | 52 +++++++++++++++- src/conversions/std/num.rs | 96 ++++++++++++++++++++++++++++- src/conversions/std/vec.rs | 112 +++++++++++++++++++++++++++++++++- src/types/bytearray.rs | 2 +- src/types/sequence.rs | 97 +---------------------------- 8 files changed, 324 insertions(+), 101 deletions(-) create mode 100644 newsfragments/5244.changed.md diff --git a/newsfragments/5244.changed.md b/newsfragments/5244.changed.md new file mode 100644 index 00000000000..dcbf400c20d --- /dev/null +++ b/newsfragments/5244.changed.md @@ -0,0 +1 @@ +Optimize `FromPyObject` implementations for `Vec` and `[u8; N]` from `bytes` and `bytearray` diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index 9b28d4937c3..257cb125d26 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -181,6 +181,7 @@ fn print_feature_cfg(minor_version_required: u32, cfg: &str) { /// so this function is unstable. #[doc(hidden)] pub fn print_feature_cfgs() { + print_feature_cfg(75, "return_position_impl_trait_in_traits"); 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 diff --git a/src/conversion.rs b/src/conversion.rs index 0462844f230..8b829d21aed 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -9,6 +9,8 @@ use crate::{ Borrowed, Bound, BoundObject, Py, PyAny, PyClass, PyClassGuard, PyErr, PyRef, PyRefMut, Python, }; use std::convert::Infallible; +#[cfg(return_position_impl_trait_in_traits)] +use std::marker::PhantomData; /// Defines a conversion from a Rust type to a Python object, which may fail. /// @@ -408,8 +410,70 @@ pub trait FromPyObject<'a, 'py>: Sized { fn type_input() -> TypeInfo { TypeInfo::Any } + + /// Specialization hook for extracting sequences for types like `Vec` and `[u8; N]`, + /// where the bytes can be directly copied from some python objects without going through + /// iteration. + #[doc(hidden)] + #[inline(always)] + #[cfg(return_position_impl_trait_in_traits)] + fn sequence_extractor( + _obj: Borrowed<'_, 'py, PyAny>, + _: private::Token, + ) -> Option> { + struct NeverASequence(PhantomData); + + impl FromPyObjectSequence for NeverASequence { + type Target = T; + + fn to_vec(&self) -> Vec { + unreachable!() + } + + fn to_array(&self) -> PyResult<[Self::Target; N]> { + unreachable!() + } + } + + Option::>::None + } + + /// Equivalent to the above for MSRV < 1.75, which pays an additional allocation cost. + #[doc(hidden)] + #[inline(always)] + #[cfg(not(return_position_impl_trait_in_traits))] + fn sequence_extractor<'b>( + _obj: Borrowed<'b, 'b, PyAny>, + _: private::Token, + ) -> Option + 'b>> { + None + } } +mod from_py_object_sequence { + use crate::PyResult; + + /// Private trait for implementing specialized sequence extraction for `Vec` and `[u8; N]` + #[doc(hidden)] + pub trait FromPyObjectSequence { + type Target; + + fn to_vec(&self) -> Vec; + + #[cfg(return_position_impl_trait_in_traits)] + fn to_array(&self) -> PyResult<[Self::Target; N]>; + + /// Fills an uninit slice with values from the object. + /// + /// on success, `out` is fully initialized, on failure, `out` should be considered uninitialized. + #[cfg(not(return_position_impl_trait_in_traits))] + fn fill_slice(&self, out: &mut [std::mem::MaybeUninit]) -> PyResult<()>; + } +} + +// Only reachable / implementable inside PyO3 itself. +pub(crate) use from_py_object_sequence::FromPyObjectSequence; + /// A data structure that can be extracted without borrowing any data from the input. /// /// This is primarily useful for trait bounds. For example a [`FromPyObject`] implementation of a diff --git a/src/conversions/std/array.rs b/src/conversions/std/array.rs index 7da9f13dc8b..b34c4f5b59e 100644 --- a/src/conversions/std/array.rs +++ b/src/conversions/std/array.rs @@ -42,7 +42,25 @@ where { type Error = PyErr; - fn extract(obj: Borrowed<'_, 'py, PyAny>) -> Result { + fn extract(obj: Borrowed<'_, 'py, PyAny>) -> PyResult { + if let Some(extractor) = T::sequence_extractor(obj, crate::conversion::private::Token) { + #[cfg(return_position_impl_trait_in_traits)] + { + use crate::conversion::FromPyObjectSequence; + return extractor.to_array(); + } + + #[cfg(not(return_position_impl_trait_in_traits))] + { + use std::array; + + let mut out = array::from_fn(|_| std::mem::MaybeUninit::uninit()); + extractor.fill_slice(&mut out)?; + // Safety: `out` is fully initialized by successful `fill_slice` + return Ok(out.map(|x| unsafe { x.assume_init() })); + } + } + create_array_from_obj(obj) } } @@ -112,7 +130,7 @@ where } } -fn invalid_sequence_length(expected: usize, actual: usize) -> PyErr { +pub(crate) fn invalid_sequence_length(expected: usize, actual: usize) -> PyErr { exceptions::PyValueError::new_err(format!( "expected a sequence of length {expected} (got {actual})" )) @@ -153,6 +171,36 @@ mod tests { assert_eq!(DROP_COUNTER.load(Ordering::SeqCst), 2); } + #[test] + fn test_extract_bytes_to_array() { + Python::attach(|py| { + let v: [u8; 33] = py + .eval( + ffi::c_str!("b'abcabcabcabcabcabcabcabcabcabcabc'"), + None, + None, + ) + .unwrap() + .extract() + .unwrap(); + assert!(&v == b"abcabcabcabcabcabcabcabcabcabcabc"); + }) + } + + #[test] + fn test_extract_bytes_wrong_length() { + Python::attach(|py| { + let v: PyResult<[u8; 3]> = py + .eval(ffi::c_str!("b'abcdefg'"), None, None) + .unwrap() + .extract(); + assert_eq!( + v.unwrap_err().to_string(), + "ValueError: expected a sequence of length 3 (got 7)" + ); + }) + } + #[test] fn test_extract_bytearray_to_array() { Python::attach(|py| { diff --git a/src/conversions/std/num.rs b/src/conversions/std/num.rs index 986179ad6d3..80517d5c0e5 100644 --- a/src/conversions/std/num.rs +++ b/src/conversions/std/num.rs @@ -1,17 +1,20 @@ use crate::conversion::private::Reference; -use crate::conversion::IntoPyObject; +use crate::conversion::{FromPyObjectSequence, IntoPyObject}; use crate::ffi_ptr_ext::FfiPtrExt; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; -use crate::types::{PyBytes, PyInt}; +use crate::types::{PyByteArray, PyByteArrayMethods, PyBytes, PyInt}; use crate::{exceptions, ffi, Borrowed, Bound, FromPyObject, PyAny, PyErr, PyResult, Python}; use std::convert::Infallible; use std::ffi::c_long; +use std::mem::MaybeUninit; use std::num::{ NonZeroI128, NonZeroI16, NonZeroI32, NonZeroI64, NonZeroI8, NonZeroIsize, NonZeroU128, NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU8, NonZeroUsize, }; +use super::array::invalid_sequence_length; + macro_rules! int_fits_larger_int { ($rust_type:ty, $larger_type:ty) => { impl<'py> IntoPyObject<'py> for $rust_type { @@ -292,6 +295,95 @@ impl<'py> FromPyObject<'_, 'py> for u8 { fn type_input() -> TypeInfo { Self::type_output() } + + #[cfg(return_position_impl_trait_in_traits)] + #[inline] + fn sequence_extractor( + obj: Borrowed<'_, 'py, PyAny>, + _: crate::conversion::private::Token, + ) -> Option> { + if let Ok(bytes) = obj.cast::() { + Some(BytesSequenceExtractor::Bytes(bytes)) + } else if let Ok(byte_array) = obj.cast::() { + Some(BytesSequenceExtractor::ByteArray(byte_array)) + } else { + None + } + } + + #[cfg(not(return_position_impl_trait_in_traits))] + #[inline] + fn sequence_extractor<'b>( + obj: Borrowed<'b, 'b, PyAny>, + _: crate::conversion::private::Token, + ) -> Option + 'b>> { + if let Ok(bytes) = obj.cast::() { + Some(Box::new(BytesSequenceExtractor::Bytes(bytes))) + } else if let Ok(byte_array) = obj.cast::() { + Some(Box::new(BytesSequenceExtractor::ByteArray(byte_array))) + } else { + None + } + } +} + +pub(crate) enum BytesSequenceExtractor<'a, 'py> { + Bytes(Borrowed<'a, 'py, PyBytes>), + ByteArray(Borrowed<'a, 'py, PyByteArray>), +} + +impl BytesSequenceExtractor<'_, '_> { + fn fill_slice(&self, out: &mut [MaybeUninit]) -> PyResult<()> { + let mut copy_slice = |slice: &[u8]| { + if slice.len() != out.len() { + return Err(invalid_sequence_length(out.len(), slice.len())); + } + // Safety: `slice` and `out` are guaranteed not to overlap due to `&mut` reference on `out`. + unsafe { + std::ptr::copy_nonoverlapping(slice.as_ptr(), out.as_mut_ptr().cast(), out.len()) + }; + Ok(()) + }; + + match self { + BytesSequenceExtractor::Bytes(b) => copy_slice(b.as_bytes()), + BytesSequenceExtractor::ByteArray(b) => crate::sync::with_critical_section(b, || { + // Safety: b is protected by a critical section + copy_slice(unsafe { b.as_bytes() }) + }), + } + } +} + +impl FromPyObjectSequence for BytesSequenceExtractor<'_, '_> { + type Target = u8; + + fn to_vec(&self) -> Vec { + match self { + BytesSequenceExtractor::Bytes(b) => b.as_bytes().to_vec(), + BytesSequenceExtractor::ByteArray(b) => b.to_vec(), + } + } + + #[cfg(return_position_impl_trait_in_traits)] + fn to_array(&self) -> PyResult<[u8; N]> { + let mut out: MaybeUninit<[u8; N]> = MaybeUninit::uninit(); + + // Safety: `[u8; N]` has the same layout as `[MaybeUninit; N]` + let slice = unsafe { + std::slice::from_raw_parts_mut(out.as_mut_ptr().cast::>(), N) + }; + + self.fill_slice(slice)?; + + // Safety: `out` is fully initialized + Ok(unsafe { out.assume_init() }) + } + + #[cfg(not(return_position_impl_trait_in_traits))] + fn fill_slice(&self, out: &mut [MaybeUninit]) -> PyResult<()> { + self.fill_slice(out) + } } int_fits_c_long!(i8); diff --git a/src/conversions/std/vec.rs b/src/conversions/std/vec.rs index 6ed71e44840..f638b6387d7 100644 --- a/src/conversions/std/vec.rs +++ b/src/conversions/std/vec.rs @@ -1,6 +1,12 @@ -use crate::conversion::IntoPyObject; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; +use crate::{ + conversion::{FromPyObject, FromPyObjectOwned, IntoPyObject}, + exceptions::PyTypeError, + ffi, + types::{PyAnyMethods, PySequence, PyString}, + Borrowed, DowncastError, PyResult, +}; use crate::{Bound, PyAny, PyErr, Python}; impl<'py, T> IntoPyObject<'py> for Vec @@ -49,11 +55,58 @@ where } } +impl<'py, T> FromPyObject<'_, 'py> for Vec +where + T: FromPyObjectOwned<'py>, +{ + type Error = PyErr; + + fn extract(obj: Borrowed<'_, 'py, PyAny>) -> PyResult { + if let Some(extractor) = T::sequence_extractor(obj, crate::conversion::private::Token) { + #[cfg(return_position_impl_trait_in_traits)] + use crate::conversion::FromPyObjectSequence; + return Ok(extractor.to_vec()); + } + + if obj.is_instance_of::() { + return Err(PyTypeError::new_err("Can't extract `str` to `Vec`")); + } + + extract_sequence(obj) + } + + #[cfg(feature = "experimental-inspect")] + fn type_input() -> TypeInfo { + TypeInfo::sequence_of(T::type_input()) + } +} + +fn extract_sequence<'py, T>(obj: Borrowed<'_, 'py, PyAny>) -> PyResult> +where + T: FromPyObjectOwned<'py>, +{ + // Types that pass `PySequence_Check` usually implement enough of the sequence protocol + // to support this function and if not, we will only fail extraction safely. + let seq = unsafe { + if ffi::PySequence_Check(obj.as_ptr()) != 0 { + obj.downcast_unchecked::() + } else { + return Err(DowncastError::new_from_borrowed(obj, "Sequence").into()); + } + }; + + let mut v = Vec::with_capacity(seq.len().unwrap_or(0)); + for item in seq.try_iter()? { + v.push(item?.extract::().map_err(Into::into)?); + } + Ok(v) +} + #[cfg(test)] mod tests { use crate::conversion::IntoPyObject; use crate::types::{PyAnyMethods, PyBytes, PyBytesMethods, PyList}; - use crate::Python; + use crate::{ffi, Python}; #[test] fn test_vec_intopyobject_impl() { @@ -84,4 +137,59 @@ mod tests { assert!(obj.is_instance_of::()); }); } + + #[test] + fn test_strings_cannot_be_extracted_to_vec() { + Python::attach(|py| { + let v = "London Calling"; + let ob = v.into_pyobject(py).unwrap(); + + assert!(ob.extract::>().is_err()); + assert!(ob.extract::>().is_err()); + }); + } + + #[test] + fn test_extract_bytes_to_vec() { + Python::attach(|py| { + let v: Vec = PyBytes::new(py, b"abc").extract().unwrap(); + assert_eq!(v, b"abc"); + }); + } + + #[test] + fn test_extract_tuple_to_vec() { + Python::attach(|py| { + let v: Vec = py + .eval(ffi::c_str!("(1, 2)"), None, None) + .unwrap() + .extract() + .unwrap(); + assert!(v == [1, 2]); + }); + } + + #[test] + fn test_extract_range_to_vec() { + Python::attach(|py| { + let v: Vec = py + .eval(ffi::c_str!("range(1, 5)"), None, None) + .unwrap() + .extract() + .unwrap(); + assert!(v == [1, 2, 3, 4]); + }); + } + + #[test] + fn test_extract_bytearray_to_vec() { + Python::attach(|py| { + let v: Vec = py + .eval(ffi::c_str!("bytearray(b'abc')"), None, None) + .unwrap() + .extract() + .unwrap(); + assert!(v == b"abc"); + }); + } } diff --git a/src/types/bytearray.rs b/src/types/bytearray.rs index c85c20f12f4..b1f1370f87c 100644 --- a/src/types/bytearray.rs +++ b/src/types/bytearray.rs @@ -299,7 +299,7 @@ impl<'a> Borrowed<'a, '_, PyByteArray> { } #[allow(clippy::wrong_self_convention)] - unsafe fn as_bytes(self) -> &'a [u8] { + pub(crate) unsafe fn as_bytes(self) -> &'a [u8] { unsafe { slice::from_raw_parts(self.data(), self.len()) } } diff --git a/src/types/sequence.rs b/src/types/sequence.rs index ba82ef350ca..6b9e864ef59 100644 --- a/src/types/sequence.rs +++ b/src/types/sequence.rs @@ -1,16 +1,12 @@ -use crate::conversion::FromPyObjectOwned; -use crate::err::{self, DowncastError, PyErr, PyResult}; -use crate::exceptions::PyTypeError; +use crate::err::{self, PyErr, PyResult}; use crate::ffi_ptr_ext::FfiPtrExt; -#[cfg(feature = "experimental-inspect")] -use crate::inspect::types::TypeInfo; use crate::instance::Bound; use crate::internal_tricks::get_ssize_index; use crate::py_result_ext::PyResultExt; use crate::sync::PyOnceLock; use crate::type_object::PyTypeInfo; -use crate::types::{any::PyAnyMethods, PyAny, PyList, PyString, PyTuple, PyType, PyTypeMethods}; -use crate::{ffi, Borrowed, BoundObject, FromPyObject, IntoPyObject, IntoPyObjectExt, Py, Python}; +use crate::types::{any::PyAnyMethods, PyAny, PyList, PyTuple, PyType, PyTypeMethods}; +use crate::{ffi, Borrowed, BoundObject, IntoPyObject, IntoPyObjectExt, Py, Python}; /// Represents a reference to a Python object supporting the sequence protocol. /// @@ -358,46 +354,6 @@ impl<'py> PySequenceMethods<'py> for Bound<'py, PySequence> { } } -impl<'py, T> FromPyObject<'_, 'py> for Vec -where - T: FromPyObjectOwned<'py>, -{ - type Error = PyErr; - - fn extract(obj: Borrowed<'_, 'py, PyAny>) -> Result { - if obj.is_instance_of::() { - return Err(PyTypeError::new_err("Can't extract `str` to `Vec`")); - } - extract_sequence(obj) - } - - #[cfg(feature = "experimental-inspect")] - fn type_input() -> TypeInfo { - TypeInfo::sequence_of(T::type_input()) - } -} - -fn extract_sequence<'py, T>(obj: Borrowed<'_, 'py, PyAny>) -> PyResult> -where - T: FromPyObjectOwned<'py>, -{ - // Types that pass `PySequence_Check` usually implement enough of the sequence protocol - // to support this function and if not, we will only fail extraction safely. - let seq = unsafe { - if ffi::PySequence_Check(obj.as_ptr()) != 0 { - obj.cast_unchecked::() - } else { - return Err(DowncastError::new_from_borrowed(obj, "Sequence").into()); - } - }; - - let mut v = Vec::with_capacity(seq.len().unwrap_or(0)); - for item in seq.try_iter()? { - v.push(item?.extract::().map_err(Into::into)?); - } - Ok(v) -} - #[cfg(test)] mod tests { use crate::types::{PyAnyMethods, PyList, PySequence, PySequenceMethods, PyTuple}; @@ -429,17 +385,6 @@ mod tests { }); } - #[test] - fn test_strings_cannot_be_extracted_to_vec() { - Python::attach(|py| { - let v = "London Calling"; - let ob = v.into_pyobject(py).unwrap(); - - assert!(ob.extract::>().is_err()); - assert!(ob.extract::>().is_err()); - }); - } - #[test] fn test_seq_empty() { Python::attach(|py| { @@ -780,42 +725,6 @@ mod tests { }); } - #[test] - fn test_extract_tuple_to_vec() { - Python::attach(|py| { - let v: Vec = py - .eval(ffi::c_str!("(1, 2)"), None, None) - .unwrap() - .extract() - .unwrap(); - assert!(v == [1, 2]); - }); - } - - #[test] - fn test_extract_range_to_vec() { - Python::attach(|py| { - let v: Vec = py - .eval(ffi::c_str!("range(1, 5)"), None, None) - .unwrap() - .extract() - .unwrap(); - assert!(v == [1, 2, 3, 4]); - }); - } - - #[test] - fn test_extract_bytearray_to_vec() { - Python::attach(|py| { - let v: Vec = py - .eval(ffi::c_str!("bytearray(b'abc')"), None, None) - .unwrap() - .extract() - .unwrap(); - assert!(v == b"abc"); - }); - } - #[test] fn test_seq_cast_unchecked() { Python::attach(|py| { From 26d5a239181749af4b455da7043c03390efd80dd Mon Sep 17 00:00:00 2001 From: Thomas Tanon Date: Thu, 18 Sep 2025 13:09:36 +0200 Subject: [PATCH 867/936] Guide: focus on the module way of creating python modules (#5430) * Guide: focus on the module way of creating python modules * Update guide/src/module.md Co-authored-by: David Hewitt * Code review --------- Co-authored-by: David Hewitt --- README.md | 23 ++++---- guide/src/module.md | 140 +++++++++++++++++++++----------------------- 2 files changed, 77 insertions(+), 86 deletions(-) diff --git a/README.md b/README.md index 7508d851dbc..805784c6aa7 100644 --- a/README.md +++ b/README.md @@ -77,21 +77,18 @@ pyo3 = { version = "0.26.0", features = ["extension-module"] } **`src/lib.rs`** ```rust -use pyo3::prelude::*; - -/// Formats the sum of two numbers as string. -#[pyfunction] -fn sum_as_string(a: usize, b: usize) -> PyResult { - Ok((a + b).to_string()) -} - -/// A Python module implemented in Rust. The name of this function must match +/// A Python module implemented in Rust. The name of this module must match /// the `lib.name` setting in the `Cargo.toml`, else Python will not be able to /// import the module. -#[pymodule] -fn string_sum(m: &Bound<'_, PyModule>) -> PyResult<()> { - m.add_function(wrap_pyfunction!(sum_as_string, m)?)?; - Ok(()) +#[pyo3::pymodule] +mod string_sum { + use pyo3::prelude::*; + + /// Formats the sum of two numbers as string. + #[pyfunction] + fn sum_as_string(a: usize, b: usize) -> PyResult { + Ok((a + b).to_string()) + } } ``` diff --git a/guide/src/module.md b/guide/src/module.md index e1d77d4adb5..84ee25e7252 100644 --- a/guide/src/module.md +++ b/guide/src/module.md @@ -3,6 +3,7 @@ You can create a module using `#[pymodule]`: ```rust,no_run +# mod declarative_module_basic_test { use pyo3::prelude::*; #[pyfunction] @@ -12,18 +13,28 @@ 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)?) +mod my_extension { + use pyo3::prelude::*; + + #[pymodule_export] + use super::double; // The double function is made available from Python, works also with classes + + #[pyfunction] // Inline definition of a pyfunction, also made availlable to Python + fn triple(x: usize) -> usize { + x * 3 + } } +# } ``` -The `#[pymodule]` procedural macro takes care of exporting the initialization function of your -module to Python. +The `#[pymodule]` procedural macro takes care of creating the initialization function of your +module and exposing it to Python. -The module's name defaults to the name of the Rust function. You can override the module name by +The module's name defaults to the name of the Rust module. You can override the module name by using `#[pyo3(name = "custom_name")]`: ```rust,no_run +# mod declarative_module_custom_name_test { use pyo3::prelude::*; #[pyfunction] @@ -32,9 +43,11 @@ fn double(x: usize) -> usize { } #[pymodule(name = "custom_name")] -fn my_extension(m: &Bound<'_, PyModule>) -> PyResult<()> { - m.add_function(wrap_pyfunction!(double, m)?) +mod my_extension { + #[pymodule_export] + use super::double; } +# } ``` The name of the module must match the name of the `.so` or `.pyd` @@ -48,8 +61,7 @@ To import the module, either: ## Documentation -The [Rust doc comments](https://doc.rust-lang.org/stable/book/ch03-04-comments.html) of the module -initialization function will be applied automatically as the Python docstring of your module. +The [Rust doc comments](https://doc.rust-lang.org/stable/book/ch03-04-comments.html) of the Rust module will be applied automatically as the Python docstring of your module. For example, building off of the above code, this will print `This module is implemented in Rust.`: @@ -61,39 +73,40 @@ print(my_extension.__doc__) ## Python submodules -You can create a module hierarchy within a single extension module by using -[`Bound<'_, PyModule>::add_submodule()`]({{#PYO3_DOCS_URL}}/pyo3/prelude/trait.PyModuleMethods.html#tymethod.add_submodule). -For example, you could define the modules `parent_module` and `parent_module.child_module`. +You can create a module hierarchy within a single extension module by just `use`ing modules like functions or classes. +For example, you could define the modules `parent_module` and `parent_module.child_module`: ```rust use pyo3::prelude::*; #[pymodule] -fn parent_module(m: &Bound<'_, PyModule>) -> PyResult<()> { - register_child_module(m)?; - Ok(()) +mod parent_module { + #[pymodule_export] + use super::child_module; } -fn register_child_module(parent_module: &Bound<'_, PyModule>) -> PyResult<()> { - 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) +#[pymodule] +mod child_module { + #[pymodule_export] + use super::func; } #[pyfunction] fn func() -> String { "func".to_string() } - -# Python::attach(|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).unwrap(); # -# py.run(c_str!("assert parent_module.child_module.func() == 'func'"), None, Some(&ctx)).unwrap(); -# }) +# fn main() { +# Python::attach(|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).unwrap(); +# +# py.run(c_str!("assert parent_module.child_module.func() == 'func'"), None, Some(&ctx)).unwrap(); +# }) +} ``` Note that this does not define a package, so this won’t allow Python code to directly import @@ -101,35 +114,25 @@ submodules by using `from parent_module import child_module`. For more informati [#759](https://github.com/PyO3/pyo3/issues/759) and [#1517](https://github.com/PyO3/pyo3/issues/1517#issuecomment-808664021). -It is not necessary to add `#[pymodule]` on nested modules, which is only required on the top-level module. +You can provide the `submodule` argument to `#[pymodule()]` for modules that are not top-level modules in order for them to properly generate the `#[pyclass]` `module` attribute automatically. -## Declarative modules +## Inline declaration -Another syntax based on Rust inline modules is also available to declare modules. +It is possible to declare functions, classes, sub-modules and constants inline in a module: For example: ```rust,no_run # mod declarative_module_test { -use pyo3::prelude::*; - -#[pyfunction] -fn double(x: usize) -> usize { - x * 2 -} - -#[pymodule] +#[pyo3::pymodule] mod my_extension { - use super::*; - - #[pymodule_export] - use super::double; // Exports the double function as part of the module + use pyo3::prelude::*; #[pymodule_export] const PI: f64 = std::f64::consts::PI; // Exports PI constant as part of the module #[pyfunction] // This will be part of the module - fn triple(x: usize) -> usize { - x * 3 + fn double(x: usize) -> usize { + x * 2 } #[pyclass] // This will be part of the module @@ -138,47 +141,38 @@ mod my_extension { #[pymodule] mod submodule { // This is a submodule - } + use pyo3::prelude::*; - #[pymodule_init] - fn init(m: &Bound<'_, PyModule>) -> PyResult<()> { - // Arbitrary code to run at the module initialization - m.add("double2", m.getattr("double")?) + #[pyclass] + struct Nested; } } # } ``` -The `#[pymodule]` macro automatically sets the `module` attribute of the `#[pyclass]` macros declared inside of it with its name. +In this case, `#[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,no_run -# mod declarative_module_module_attr_test { -use pyo3::prelude::*; +In the previous example, the `Nested` class will have for `module` `my_extension.submodule`. -#[pyclass] -struct Ext; +## Procedural initialization -#[pymodule] +If the macros provided by PyO3 are not enough, it is possible to run code at the module initialization: +```rust,no_run +# mod procedural_module_test { +#[pyo3::pymodule] mod my_extension { - use super::*; - - #[pymodule_export] - use super::Ext; + use pyo3::prelude::*; - #[pymodule] - mod submodule { - use super::*; - // This is a submodule + #[pyfunction] + fn double(x: usize) -> usize { + x * 2 + } - #[pyclass] // This will be part of the module - struct Unit; + #[pymodule_init] + fn init(m: &Bound<'_, PyModule>) -> PyResult<()> { + // Arbitrary code to run at the module initialization + m.add("double2", m.getattr("double")?) } } # } ``` -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]`. From 4e0174c6555b8216b50d9278c6b133e6067882e1 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers <7943856+bschoenmaeckers@users.noreply.github.com> Date: Fri, 19 Sep 2025 14:59:38 +0200 Subject: [PATCH 868/936] Updates for Rust 1.90 (#5448) * updates for Rust 1.90 * fix ui tests * Disable introspection test --------- Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> --- .github/workflows/ci.yml | 3 ++- tests/ui/invalid_closure.stderr | 4 ++-- tests/ui/invalid_frozen_pyclass_borrow.stderr | 4 ++-- tests/ui/invalid_pyclass_generic.stderr | 16 ++++++---------- tests/ui/invalid_pymethod_enum.stderr | 2 +- tests/ui/not_send2.stderr | 6 +++--- 6 files changed, 16 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7e4c19e610e..d5261de2472 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -697,7 +697,8 @@ jobs: with: save-if: ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'CI-save-pr-cache') }} - run: python -m pip install --upgrade pip && pip install nox[uv] - - run: nox -s test-introspection + # TODO test will be fixed in https://github.com/PyO3/pyo3/pull/5450 +# - run: nox -s test-introspection env: CARGO_BUILD_TARGET: ${{ matrix.platform.rust-target }} diff --git a/tests/ui/invalid_closure.stderr b/tests/ui/invalid_closure.stderr index 4791954f3e1..7864a898c28 100644 --- a/tests/ui/invalid_closure.stderr +++ b/tests/ui/invalid_closure.stderr @@ -1,9 +1,9 @@ error[E0597]: `local_data` does not live long enough --> tests/ui/invalid_closure.rs:7:27 | -6 | let local_data = vec![0, 1, 2, 3, 4]; + 6 | let local_data = vec![0, 1, 2, 3, 4]; | ---------- binding `local_data` declared here -7 | let ref_: &[u8] = &local_data; + 7 | let ref_: &[u8] = &local_data; | ^^^^^^^^^^^ borrowed value does not live long enough ... 14 | PyCFunction::new_closure(py, None, None, closure_fn) diff --git a/tests/ui/invalid_frozen_pyclass_borrow.stderr b/tests/ui/invalid_frozen_pyclass_borrow.stderr index ae81251c24c..5bfa9ee0808 100644 --- a/tests/ui/invalid_frozen_pyclass_borrow.stderr +++ b/tests/ui/invalid_frozen_pyclass_borrow.stderr @@ -13,7 +13,7 @@ error[E0271]: type mismatch resolving `::Frozen == False` note: expected this to be `False` --> tests/ui/invalid_frozen_pyclass_borrow.rs:3:1 | -3 | #[pyclass(frozen)] + 3 | #[pyclass(frozen)] | ^^^^^^^^^^^^^^^^^^ note: required by a bound in `extract_pyclass_ref_mut` --> src/impl_/extract_argument.rs @@ -49,7 +49,7 @@ error[E0271]: type mismatch resolving `::Frozen == False` note: expected this to be `False` --> tests/ui/invalid_frozen_pyclass_borrow.rs:3:1 | -3 | #[pyclass(frozen)] + 3 | #[pyclass(frozen)] | ^^^^^^^^^^^^^^^^^^ note: required by a bound in `pyo3::Bound::<'py, T>::borrow_mut` --> src/instance.rs diff --git a/tests/ui/invalid_pyclass_generic.stderr b/tests/ui/invalid_pyclass_generic.stderr index 1d6233102e7..9fb5db15b0a 100644 --- a/tests/ui/invalid_pyclass_generic.stderr +++ b/tests/ui/invalid_pyclass_generic.stderr @@ -12,7 +12,7 @@ error[E0592]: duplicate definitions with name `__pymethod___class_getitem____` error[E0592]: duplicate definitions with name `__class_getitem__` --> tests/ui/invalid_pyclass_generic.rs:4:1 | -4 | #[pyclass(generic)] + 4 | #[pyclass(generic)] | ^^^^^^^^^^^^^^^^^^^ duplicate definitions for `__class_getitem__` ... 15 | / pub fn __class_getitem__( @@ -44,13 +44,13 @@ note: candidate #2 is defined in an impl for the type `ClassRedefinesClassGetIte error[E0034]: multiple applicable items in scope --> tests/ui/invalid_pyclass_generic.rs:4:1 | -4 | #[pyclass(generic)] + 4 | #[pyclass(generic)] | ^^^^^^^^^^^^^^^^^^^ multiple `__class_getitem__` found | note: candidate #1 is defined in an impl for the type `ClassRedefinesClassGetItem` --> tests/ui/invalid_pyclass_generic.rs:4:1 | -4 | #[pyclass(generic)] + 4 | #[pyclass(generic)] | ^^^^^^^^^^^^^^^^^^^ note: candidate #2 is defined in an impl for the type `ClassRedefinesClassGetItem` --> tests/ui/invalid_pyclass_generic.rs:15:5 @@ -70,8 +70,6 @@ error[E0034]: multiple applicable items in scope | = note: candidate #1 is defined in an impl for the type `IntoPyObjectConverter>` = note: candidate #2 is defined in an impl for the type `IntoPyObjectConverter` - = note: candidate #3 is defined in an impl for the type `UnknownReturnResultType>` - = note: candidate #4 is defined in an impl for the type `UnknownReturnType` = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0308]: mismatched types @@ -94,12 +92,12 @@ error[E0034]: multiple applicable items in scope note: candidate #1 is defined in an impl for the type `ClassRedefinesClassGetItem` --> tests/ui/invalid_pyclass_generic.rs:4:1 | -4 | #[pyclass(generic)] + 4 | #[pyclass(generic)] | ^^^^^^^^^^^^^^^^^^^ note: candidate #2 is defined in an impl for the type `ClassRedefinesClassGetItem` --> tests/ui/invalid_pyclass_generic.rs:7:1 | -7 | #[pymethods] + 7 | #[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) @@ -112,7 +110,7 @@ error[E0034]: multiple applicable items in scope note: candidate #1 is defined in an impl for the type `ClassRedefinesClassGetItem` --> tests/ui/invalid_pyclass_generic.rs:4:1 | -4 | #[pyclass(generic)] + 4 | #[pyclass(generic)] | ^^^^^^^^^^^^^^^^^^^ note: candidate #2 is defined in an impl for the type `ClassRedefinesClassGetItem` --> tests/ui/invalid_pyclass_generic.rs:15:5 @@ -132,5 +130,3 @@ error[E0034]: multiple applicable items in scope | = note: candidate #1 is defined in an impl for the type `IntoPyObjectConverter>` = note: candidate #2 is defined in an impl for the type `IntoPyObjectConverter` - = note: candidate #3 is defined in an impl for the type `UnknownReturnResultType>` - = note: candidate #4 is defined in an impl for the type `UnknownReturnType` diff --git a/tests/ui/invalid_pymethod_enum.stderr b/tests/ui/invalid_pymethod_enum.stderr index 996c3983900..3c574320cb5 100644 --- a/tests/ui/invalid_pymethod_enum.stderr +++ b/tests/ui/invalid_pymethod_enum.stderr @@ -7,7 +7,7 @@ error[E0271]: type mismatch resolving `::Frozen == False note: expected this to be `False` --> tests/ui/invalid_pymethod_enum.rs:3:1 | -3 | #[pyclass] + 3 | #[pyclass] | ^^^^^^^^^^ note: required by a bound in `extract_pyclass_ref_mut` --> src/impl_/extract_argument.rs diff --git a/tests/ui/not_send2.stderr b/tests/ui/not_send2.stderr index 3bc3c297527..3d76b5ebc11 100644 --- a/tests/ui/not_send2.stderr +++ b/tests/ui/not_send2.stderr @@ -1,11 +1,11 @@ error[E0277]: `*mut pyo3::Python<'static>` cannot be shared between threads safely --> tests/ui/not_send2.rs:8:19 | -8 | py.detach(|| { + 8 | py.detach(|| { | ____________------_^ | | | | | required by a bound introduced by this call -9 | | println!("{:?}", string); + 9 | | println!("{:?}", string); 10 | | }); | |_________^ `*mut pyo3::Python<'static>` cannot be shared between threads safely | @@ -39,7 +39,7 @@ note: required because it appears within the type `pyo3::Bound<'_, PyString>` note: required because it's used within this closure --> tests/ui/not_send2.rs:8:19 | -8 | py.detach(|| { + 8 | py.detach(|| { | ^^ = note: required for `{closure@$DIR/tests/ui/not_send2.rs:8:19: 8:21}` to implement `Ungil` note: required by a bound in `pyo3::Python::<'py>::detach` From 5e10ea173651f12b7bc4549c84e72512aef5f174 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers <7943856+bschoenmaeckers@users.noreply.github.com> Date: Fri, 19 Sep 2025 15:07:33 +0200 Subject: [PATCH 869/936] fix `OsStr` conversion for non-utf8 strings on windows (#5444) --- newsfragments/5444.fixed.md | 1 + src/conversions/std/osstr.rs | 37 ++++++++++++++++++++++++++++++++++-- 2 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 newsfragments/5444.fixed.md diff --git a/newsfragments/5444.fixed.md b/newsfragments/5444.fixed.md new file mode 100644 index 00000000000..86055f8f1be --- /dev/null +++ b/newsfragments/5444.fixed.md @@ -0,0 +1 @@ +fix `OsStr` conversion for non-utf8 strings on windows diff --git a/src/conversions/std/osstr.rs b/src/conversions/std/osstr.rs index 3c480526d83..eb132a58f69 100644 --- a/src/conversions/std/osstr.rs +++ b/src/conversions/std/osstr.rs @@ -110,6 +110,12 @@ impl FromPyObject<'_, '_> for OsString { unsafe { ffi::PyUnicode_AsWideChar(pystring.as_ptr(), std::ptr::null_mut(), 0) }; crate::err::error_on_minusone(ob.py(), size)?; + debug_assert!( + size > 0, + "PyUnicode_AsWideChar should return at least 1 for null terminator" + ); + let size = size - 1; // exclude null terminator + let mut buffer = vec![0; size as usize]; let bytes_read = unsafe { ffi::PyUnicode_AsWideChar(pystring.as_ptr(), buffer.as_mut_ptr(), size) }; @@ -169,7 +175,7 @@ impl<'py> IntoPyObject<'py> for &OsString { #[cfg(test)] mod tests { - use crate::types::{PyString, PyStringMethods}; + use crate::types::{PyAnyMethods, PyString, PyStringMethods}; use crate::{BoundObject, IntoPyObject, Python}; use std::fmt::Debug; use std::{ @@ -181,7 +187,6 @@ mod tests { #[cfg(not(windows))] fn test_non_utf8_conversion() { Python::attach(|py| { - use crate::types::PyAnyMethods; #[cfg(not(target_os = "wasi"))] use std::os::unix::ffi::OsStrExt; #[cfg(target_os = "wasi")] @@ -219,4 +224,32 @@ mod tests { test_roundtrip::(py, os_str.to_os_string()); }); } + + #[test] + #[cfg(windows)] + fn test_windows_non_utf8_osstring_roundtrip() { + use std::os::windows::ffi::{OsStrExt, OsStringExt}; + + Python::attach(|py| { + // Example: Unpaired surrogate (0xD800) is not valid UTF-8, but valid in Windows OsString + let wide: &[u16] = &['A' as u16, 0xD800, 'B' as u16]; // 'A', unpaired surrogate, 'B' + let os_str = OsString::from_wide(wide); + + assert_eq!(os_str.to_string_lossy(), "A�B"); + + // This cannot be represented as UTF-8, so .to_str() would return None + assert!(os_str.to_str().is_none()); + + // Convert to Python and back + let py_str = os_str.as_os_str().into_pyobject(py).unwrap(); + let os_str_2 = py_str.extract::().unwrap(); + + // The roundtrip should preserve the original wide data + assert_eq!(os_str, os_str_2); + + // Show that encode_wide is necessary: direct UTF-8 conversion would lose information + let encoded: Vec = os_str.encode_wide().collect(); + assert_eq!(encoded, wide); + }); + } } From cb9111b608a1714ff7ce878103a931e37e9149f7 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers <7943856+bschoenmaeckers@users.noreply.github.com> Date: Fri, 19 Sep 2025 15:07:40 +0200 Subject: [PATCH 870/936] Implement `AsRef<[u8]>` for `PyBytes` (#5445) --- newsfragments/5445.added.md | 1 + src/types/bytes.rs | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 newsfragments/5445.added.md diff --git a/newsfragments/5445.added.md b/newsfragments/5445.added.md new file mode 100644 index 00000000000..af00d098f85 --- /dev/null +++ b/newsfragments/5445.added.md @@ -0,0 +1 @@ +Implement `AsRef<[u8]>` for `PyBytes` diff --git a/src/types/bytes.rs b/src/types/bytes.rs index 136f6361b27..c6fa1d65329 100644 --- a/src/types/bytes.rs +++ b/src/types/bytes.rs @@ -275,6 +275,20 @@ impl PartialEq> for &'_ [u8] { } } +impl<'a> AsRef<[u8]> for Borrowed<'a, '_, PyBytes> { + #[inline] + fn as_ref(&self) -> &'a [u8] { + self.as_bytes() + } +} + +impl AsRef<[u8]> for Bound<'_, PyBytes> { + #[inline] + fn as_ref(&self) -> &[u8] { + self.as_bytes() + } +} + #[cfg(test)] mod tests { use super::*; @@ -381,4 +395,17 @@ mod tests { } }) } + + #[test] + fn test_as_ref_slice() { + Python::attach(|py| { + let b = b"hello, world"; + let py_bytes = PyBytes::new(py, b); + let ref_bound: &[u8] = py_bytes.as_ref(); + assert_eq!(ref_bound, b); + let py_bytes_borrowed = py_bytes.as_borrowed(); + let ref_borrowed: &[u8] = py_bytes_borrowed.as_ref(); + assert_eq!(ref_borrowed, b); + }) + } } From 84291e50bab48dffe45ab0d249cab20e2a96ba64 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 19 Sep 2025 20:00:15 +0100 Subject: [PATCH 871/936] Put `Pin` back into `PyBuffer` (#5453) * Revert "remove uneeded `Pin` from `PyBuffer` (#5417)" This reverts commit 6f02125cbfd1603e076fc8707982a5549229d53e. * use `PhantomPinned` to encode buffer pinning * clippy --- src/buffer.rs | 159 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 105 insertions(+), 54 deletions(-) diff --git a/src/buffer.rs b/src/buffer.rs index dedea35c582..6b0e424a9cc 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -24,13 +24,26 @@ use std::ffi::{ c_char, c_int, c_long, c_longlong, c_schar, c_short, c_uchar, c_uint, c_ulong, c_ulonglong, c_ushort, c_void, }; -use std::marker::PhantomData; -use std::{cell, mem, slice}; +use std::marker::{PhantomData, PhantomPinned}; +use std::pin::Pin; +use std::{cell, mem, ptr, slice}; use std::{ffi::CStr, fmt::Debug}; /// Allows access to the underlying buffer used by a python object such as `bytes`, `bytearray` or `array.array`. #[repr(transparent)] -pub struct PyBuffer(Box, PhantomData); +pub struct PyBuffer( + // It is common for exporters filling `Py_buffer` struct to make it self-referential, e.g. see + // implementation of + // [`PyBuffer_FillInfo`](https://github.com/python/cpython/blob/2fd43a1ffe4ff1f6c46f6045bc327d6085c40fbf/Objects/abstract.c#L798-L802). + // + // Therefore we use `Pin>` to ensure that the memory address of the `Py_buffer` is stable + Pin>, + PhantomData, +); + +/// Wrapper around `ffi::Py_buffer` to be `!Unpin`. +#[repr(transparent)] +struct RawBuffer(ffi::Py_buffer, PhantomPinned); // PyBuffer is thread-safe: the shape of the buffer is immutable while a Py_buffer exists. // Accessing the buffer contents is protected using the GIL. @@ -39,18 +52,19 @@ unsafe impl Sync for PyBuffer {} impl Debug for PyBuffer { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let raw = self.raw(); f.debug_struct("PyBuffer") - .field("buf", &self.0.buf) - .field("obj", &self.0.obj) - .field("len", &self.0.len) - .field("itemsize", &self.0.itemsize) - .field("readonly", &self.0.readonly) - .field("ndim", &self.0.ndim) + .field("buf", &raw.buf) + .field("obj", &raw.obj) + .field("len", &raw.len) + .field("itemsize", &raw.itemsize) + .field("readonly", &raw.readonly) + .field("ndim", &raw.ndim) .field("format", &self.format()) .field("shape", &self.shape()) .field("strides", &self.strides()) .field("suboffsets", &self.suboffsets()) - .field("internal", &self.0.internal) + .field("internal", &raw.internal) .finish() } } @@ -195,10 +209,15 @@ 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 our MSRV is 1.82 - let mut buf = Box::new(mem::MaybeUninit::uninit()); - let buf: Box = { + 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) + ffi::PyObject_GetBuffer( + obj.as_ptr(), + // SAFETY: RawBuffer is `#[repr(transparent)]` around FFI struct + buf.as_mut_ptr().cast::(), + ffi::PyBUF_FULL_RO, + ) })?; // Safety: buf is initialized by PyObject_GetBuffer. // TODO: use nightly API Box::assume_init() once our MSRV is 1.82 @@ -206,18 +225,19 @@ impl PyBuffer { }; // Create PyBuffer immediately so that if validation checks fail, the PyBuffer::drop code // will call PyBuffer_Release (thus avoiding any leaks). - let buf = PyBuffer(buf, PhantomData); + let buf = PyBuffer(Pin::from(buf), PhantomData); + let raw = buf.raw(); - if buf.0.shape.is_null() { + if raw.shape.is_null() { Err(PyBufferError::new_err("shape is null")) - } else if buf.0.strides.is_null() { + } else if raw.strides.is_null() { Err(PyBufferError::new_err("strides is null")) } else if mem::size_of::() != buf.item_size() || !T::is_compatible_format(buf.format()) { Err(PyBufferError::new_err(format!( "buffer contents are not compatible with {}", std::any::type_name::() ))) - } else if buf.0.buf.align_offset(mem::align_of::()) != 0 { + } else if raw.buf.align_offset(mem::align_of::()) != 0 { Err(PyBufferError::new_err(format!( "buffer contents are insufficiently aligned for {}", std::any::type_name::() @@ -240,7 +260,7 @@ impl PyBuffer { /// [this blog post]: https://alexgaynor.net/2022/oct/23/buffers-on-the-edge/ #[inline] pub fn buf_ptr(&self) -> *mut c_void { - self.0.buf + self.raw().buf } /// Gets a pointer to the specified item. @@ -254,10 +274,10 @@ impl PyBuffer { unsafe { ffi::PyBuffer_GetPointer( #[cfg(Py_3_11)] - &*self.0, + self.raw(), #[cfg(not(Py_3_11))] { - &*self.0 as *const ffi::Py_buffer as *mut ffi::Py_buffer + self.raw() as *const ffi::Py_buffer as *mut ffi::Py_buffer }, #[cfg(Py_3_11)] { @@ -274,20 +294,20 @@ impl PyBuffer { /// Gets whether the underlying buffer is read-only. #[inline] pub fn readonly(&self) -> bool { - self.0.readonly != 0 + self.raw().readonly != 0 } /// Gets the size of a single element, in bytes. /// Important exception: when requesting an unformatted buffer, item_size still has the value #[inline] pub fn item_size(&self) -> usize { - self.0.itemsize as usize + self.raw().itemsize as usize } /// Gets the total number of items. #[inline] pub fn item_count(&self) -> usize { - (self.0.len as usize) / (self.0.itemsize as usize) + (self.raw().len as usize) / (self.raw().itemsize as usize) } /// `item_size() * item_count()`. @@ -295,7 +315,7 @@ impl PyBuffer { /// For non-contiguous arrays, it is the length that the logical structure would have if it were copied to a contiguous representation. #[inline] pub fn len_bytes(&self) -> usize { - self.0.len as usize + self.raw().len as usize } /// Gets the number of dimensions. @@ -303,7 +323,7 @@ impl PyBuffer { /// May be 0 to indicate a single scalar value. #[inline] pub fn dimensions(&self) -> usize { - self.0.ndim as usize + self.raw().ndim as usize } /// Returns an array of length `dimensions`. `shape()[i]` is the length of the array in dimension number `i`. @@ -315,7 +335,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.cast(), self.0.ndim as usize) } + unsafe { slice::from_raw_parts(self.raw().shape.cast(), self.raw().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. @@ -324,7 +344,7 @@ impl PyBuffer { /// but a consumer MUST be able to handle the case `strides[n] <= 0`. #[inline] pub fn strides(&self) -> &[isize] { - unsafe { slice::from_raw_parts(self.0.strides, self.0.ndim as usize) } + unsafe { slice::from_raw_parts(self.raw().strides, self.raw().ndim as usize) } } /// An array of length ndim. @@ -335,12 +355,12 @@ impl PyBuffer { #[inline] pub fn suboffsets(&self) -> Option<&[isize]> { unsafe { - if self.0.suboffsets.is_null() { + if self.raw().suboffsets.is_null() { None } else { Some(slice::from_raw_parts( - self.0.suboffsets, - self.0.ndim as usize, + self.raw().suboffsets, + self.raw().ndim as usize, )) } } @@ -349,23 +369,27 @@ impl PyBuffer { /// A NUL terminated string in struct module style syntax describing the contents of a single item. #[inline] pub fn format(&self) -> &CStr { - if self.0.format.is_null() { + if self.raw().format.is_null() { ffi::c_str!("B") } else { - unsafe { CStr::from_ptr(self.0.format) } + unsafe { CStr::from_ptr(self.raw().format) } } } /// 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, b'C' as std::ffi::c_char) != 0 } + unsafe { ffi::PyBuffer_IsContiguous(self.raw(), b'C' as std::ffi::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, b'F' as std::ffi::c_char) != 0 } + unsafe { ffi::PyBuffer_IsContiguous(self.raw(), b'F' as std::ffi::c_char) != 0 } + } + + fn raw(&self) -> &ffi::Py_buffer { + &self.0 .0 } } @@ -383,7 +407,7 @@ impl PyBuffer { if self.is_c_contiguous() { unsafe { Some(slice::from_raw_parts( - self.0.buf as *mut ReadOnlyCell, + self.raw().buf as *mut ReadOnlyCell, self.item_count(), )) } @@ -406,7 +430,7 @@ impl PyBuffer { if !self.readonly() && self.is_c_contiguous() { unsafe { Some(slice::from_raw_parts( - self.0.buf as *mut cell::Cell, + self.raw().buf as *mut cell::Cell, self.item_count(), )) } @@ -428,7 +452,7 @@ impl PyBuffer { if mem::size_of::() == self.item_size() && self.is_fortran_contiguous() { unsafe { Some(slice::from_raw_parts( - self.0.buf as *mut ReadOnlyCell, + self.raw().buf as *mut ReadOnlyCell, self.item_count(), )) } @@ -451,7 +475,7 @@ impl PyBuffer { if !self.readonly() && self.is_fortran_contiguous() { unsafe { Some(slice::from_raw_parts( - self.0.buf as *mut cell::Cell, + self.raw().buf as *mut cell::Cell, self.item_count(), )) } @@ -499,12 +523,12 @@ impl PyBuffer { ffi::PyBuffer_ToContiguous( target.as_mut_ptr().cast(), #[cfg(Py_3_11)] - &*self.0, + self.raw(), #[cfg(not(Py_3_11))] { - &*self.0 as *const ffi::Py_buffer as *mut ffi::Py_buffer + self.raw() as *const ffi::Py_buffer as *mut ffi::Py_buffer }, - self.0.len, + self.raw().len, fort as std::ffi::c_char, ) }) @@ -536,12 +560,12 @@ impl PyBuffer { ffi::PyBuffer_ToContiguous( vec.as_ptr() as *mut c_void, #[cfg(Py_3_11)] - &*self.0, + self.raw(), #[cfg(not(Py_3_11))] { - &*self.0 as *const ffi::Py_buffer as *mut ffi::Py_buffer + self.raw() as *const ffi::Py_buffer as *mut ffi::Py_buffer }, - self.0.len, + self.raw().len, fort as std::ffi::c_char, ) })?; @@ -592,10 +616,10 @@ impl PyBuffer { err::error_on_minusone(py, unsafe { ffi::PyBuffer_FromContiguous( #[cfg(Py_3_11)] - &*self.0, + self.raw(), #[cfg(not(Py_3_11))] { - &*self.0 as *const ffi::Py_buffer as *mut ffi::Py_buffer + self.raw() as *const ffi::Py_buffer as *mut ffi::Py_buffer }, #[cfg(Py_3_11)] { @@ -605,7 +629,7 @@ impl PyBuffer { { source.as_ptr() as *mut c_void }, - self.0.len, + self.raw().len, fort as std::ffi::c_char, ) }) @@ -616,17 +640,42 @@ impl PyBuffer { /// /// This will automatically be called on drop. pub fn release(self, _py: Python<'_>) { - // SAFETY: Self is `repr(transparent)` around a Box - let mut inner: Box = unsafe { std::mem::transmute(self) }; - // SAFETY: the ffi::Py_buffer structure is valid until this release call - unsafe { ffi::PyBuffer_Release(&mut *inner) }; + // First move self into a ManuallyDrop, so that PyBuffer::drop will + // never be called. (It would acquire the GIL and call PyBuffer_Release + // again.) + let mut mdself = mem::ManuallyDrop::new(self); + unsafe { + // Next, make the actual PyBuffer_Release call. + // Fine to get a mutable reference to the inner ffi::Py_buffer here, as we're destroying it. + mdself.0.release(); + + // Finally, drop the contained Pin> in place, to free the + // Box memory. + ptr::drop_in_place::>>(&mut mdself.0); + } + } +} + +impl RawBuffer { + /// Release the contents of this pinned buffer. + /// + /// # Safety + /// + /// - The buffer must not be used after calling this function. + /// - This function can only be called once. + /// - Must be attached to the interpreter. + /// + unsafe fn release(self: &mut Pin>) { + unsafe { + ffi::PyBuffer_Release(&mut Pin::get_unchecked_mut(self.as_mut()).0); + } } } impl Drop for PyBuffer { fn drop(&mut self) { - fn inner(buf: &mut Box) { - if Python::try_attach(|_| unsafe { ffi::PyBuffer_Release(buf.as_mut()) }).is_none() + fn inner(buf: &mut Pin>) { + if Python::try_attach(|_| unsafe { buf.release() }).is_none() && crate::internal::state::is_in_gc_traversal() { eprintln!("Warning: PyBuffer dropped while in GC traversal, this is a bug and will leak memory."); @@ -711,7 +760,9 @@ mod tests { "ndim: 1, format: \"B\", shape: [5], ", "strides: [1], suboffsets: None, internal: {:?} }}", ), - buffer.0.buf, buffer.0.obj, buffer.0.internal + buffer.raw().buf, + buffer.raw().obj, + buffer.raw().internal ); let debug_repr = format!("{:?}", buffer); assert_eq!(debug_repr, expected); From 5ad0723fdaf1e2b43f868fb88b8ceb3756366061 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 19 Sep 2025 20:21:00 +0100 Subject: [PATCH 872/936] add benchmark for function which takes many keyword arguments (#5452) * add benchmark for function which takes many keyword arguments * clippy --- pytests/src/pyfunctions.rs | 50 ++++++++++++++++++++++++++-- pytests/stubs/pyfunctions.pyi | 19 +++++++++++ pytests/tests/test_pyfunctions.py | 54 ++++++++++++++++++++++++++++++- 3 files changed, 120 insertions(+), 3 deletions(-) diff --git a/pytests/src/pyfunctions.rs b/pytests/src/pyfunctions.rs index 8d3da83cee3..ce7494f7c5f 100644 --- a/pytests/src/pyfunctions.rs +++ b/pytests/src/pyfunctions.rs @@ -87,11 +87,57 @@ fn with_custom_type_annotations<'py>( a } +#[allow(clippy::too_many_arguments)] +#[pyfunction( + signature = ( + *, + ant = None, + bear = None, + cat = None, + dog = None, + elephant = None, + fox = None, + goat = None, + horse = None, + iguana = None, + jaguar = None, + koala = None, + lion = None, + monkey = None, + newt = None, + owl = None, + penguin = None + ) +)] +fn many_keyword_arguments<'py>( + ant: Option<&'_ Bound<'py, PyAny>>, + bear: Option<&'_ Bound<'py, PyAny>>, + cat: Option<&'_ Bound<'py, PyAny>>, + dog: Option<&'_ Bound<'py, PyAny>>, + elephant: Option<&'_ Bound<'py, PyAny>>, + fox: Option<&'_ Bound<'py, PyAny>>, + goat: Option<&'_ Bound<'py, PyAny>>, + horse: Option<&'_ Bound<'py, PyAny>>, + iguana: Option<&'_ Bound<'py, PyAny>>, + jaguar: Option<&'_ Bound<'py, PyAny>>, + koala: Option<&'_ Bound<'py, PyAny>>, + lion: Option<&'_ Bound<'py, PyAny>>, + monkey: Option<&'_ Bound<'py, PyAny>>, + newt: Option<&'_ Bound<'py, PyAny>>, + owl: Option<&'_ Bound<'py, PyAny>>, + penguin: Option<&'_ Bound<'py, PyAny>>, +) { + std::hint::black_box(( + ant, bear, cat, dog, elephant, fox, goat, horse, iguana, jaguar, koala, lion, monkey, newt, + owl, penguin, + )); +} + #[pymodule] pub mod pyfunctions { #[pymodule_export] use super::{ - args_kwargs, none, positional_only, simple, simple_args, simple_args_kwargs, simple_kwargs, - with_custom_type_annotations, with_typed_args, + args_kwargs, many_keyword_arguments, none, positional_only, simple, simple_args, + simple_args_kwargs, simple_kwargs, with_custom_type_annotations, with_typed_args, }; } diff --git a/pytests/stubs/pyfunctions.pyi b/pytests/stubs/pyfunctions.pyi index 251ff160409..d2a4d78d8d9 100644 --- a/pytests/stubs/pyfunctions.pyi +++ b/pytests/stubs/pyfunctions.pyi @@ -1,6 +1,25 @@ import typing def args_kwargs(*args, **kwargs) -> typing.Any: ... +def many_keyword_arguments( + *, + ant: typing.Any | None = None, + bear: typing.Any | None = None, + cat: typing.Any | None = None, + dog: typing.Any | None = None, + elephant: typing.Any | None = None, + fox: typing.Any | None = None, + goat: typing.Any | None = None, + horse: typing.Any | None = None, + iguana: typing.Any | None = None, + jaguar: typing.Any | None = None, + koala: typing.Any | None = None, + lion: typing.Any | None = None, + monkey: typing.Any | None = None, + newt: typing.Any | None = None, + owl: typing.Any | None = None, + penguin: typing.Any | None = None, +) -> typing.Any: ... def none() -> None: ... def positional_only(a: typing.Any, /, b: typing.Any) -> typing.Any: ... def simple( diff --git a/pytests/tests/test_pyfunctions.py b/pytests/tests/test_pyfunctions.py index b3897f289c6..e33d4d6f102 100644 --- a/pytests/tests/test_pyfunctions.py +++ b/pytests/tests/test_pyfunctions.py @@ -1,4 +1,4 @@ -from typing import Tuple +from typing import Any, Tuple from pyo3_pytests import pyfunctions @@ -118,3 +118,55 @@ def test_with_typed_args_rs(benchmark): rust = benchmark(pyfunctions.with_typed_args, True, 1, 1.2, "foo") py = with_typed_args_py(True, 1, 1.2, "foo") assert rust == py + + +def many_keyword_arguments_py( + *, + ant: Any = None, + bear: Any = None, + cat: Any = None, + dog: Any = None, + elephant: Any = None, + fox: Any = None, + goat: Any = None, + horse: Any = None, + iguana: Any = None, + jaguar: Any = None, + koala: Any = None, + lion: Any = None, + monkey: Any = None, + newt: Any = None, + owl: Any = None, + penguin: Any = None, +): ... + + +def call_with_many_keyword_arguments(f) -> Any: + return f( + ant=True, + bear=1, + cat=1.2, + dog="foo", + elephant=None, + fox=8, + goat=9, + horse=10, + iguana=None, + jaguar=None, + koala=None, + lion=11, + owl=None, + penguin=None, + ) + + +def test_many_keyword_arguments_py(benchmark): + benchmark(call_with_many_keyword_arguments, many_keyword_arguments_py) + + +def test_many_keyword_arguments_rs(benchmark): + rust = benchmark( + call_with_many_keyword_arguments, pyfunctions.many_keyword_arguments + ) + py = call_with_many_keyword_arguments(many_keyword_arguments_py) + assert rust == py From ae64e57330427e5ef969f4637c59222d25b1d840 Mon Sep 17 00:00:00 2001 From: Thomas Tanon Date: Sat, 20 Sep 2025 15:58:03 +0200 Subject: [PATCH 873/936] Introspection: emit messages inline in the binaries instead of introducing a ptr+len indirection (#5450) * Cleanly fail if the ELF contains too many sections * Introspection: emit messages inline in the binaries instead of introducing a ptr+len indirection Significantly simplifies the parsing I dropped the old code path, we can add it back if we want to maintain backward compatibility * Encode slices as length + value * Fixes expected stubs file --- .github/workflows/ci.yml | 3 +- newsfragments/5450.changed.md | 1 + pyo3-introspection/src/introspection.rs | 80 ++++++++---------------- pyo3-macros-backend/src/introspection.rs | 36 +++++++---- pytests/stubs/pyfunctions.pyi | 2 +- src/impl_/introspection.rs | 6 ++ tests/test_compile_error.rs | 2 +- 7 files changed, 62 insertions(+), 68 deletions(-) create mode 100644 newsfragments/5450.changed.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d5261de2472..7e4c19e610e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -697,8 +697,7 @@ jobs: with: save-if: ${{ github.ref == 'refs/heads/main' || contains(github.event.pull_request.labels.*.name, 'CI-save-pr-cache') }} - run: python -m pip install --upgrade pip && pip install nox[uv] - # TODO test will be fixed in https://github.com/PyO3/pyo3/pull/5450 -# - run: nox -s test-introspection + - run: nox -s test-introspection env: CARGO_BUILD_TARGET: ${{ matrix.platform.rust-target }} diff --git a/newsfragments/5450.changed.md b/newsfragments/5450.changed.md new file mode 100644 index 00000000000..deb249d35fd --- /dev/null +++ b/newsfragments/5450.changed.md @@ -0,0 +1 @@ +Introspection: change the way introspection data is emitted in the binaries to avoid a pointer indirection and simplify parsing. \ No newline at end of file diff --git a/pyo3-introspection/src/introspection.rs b/pyo3-introspection/src/introspection.rs index c089292c1cb..2a5b94931f9 100644 --- a/pyo3-introspection/src/introspection.rs +++ b/pyo3-introspection/src/introspection.rs @@ -1,7 +1,8 @@ use crate::model::{ Argument, Arguments, Attribute, Class, Function, Module, VariableLengthArgument, }; -use anyhow::{bail, ensure, Context, Result}; +use anyhow::{anyhow, bail, ensure, Context, Result}; +use goblin::elf::section_header::SHN_XINDEX; use goblin::elf::Elf; use goblin::mach::load_command::CommandVariant; use goblin::mach::symbols::{NO_SECT, N_SECT}; @@ -11,8 +12,8 @@ use goblin::Object; use serde::Deserialize; use std::cmp::Ordering; use std::collections::HashMap; -use std::fs; use std::path::Path; +use std::{fs, str}; /// Introspect a cdylib built with PyO3 and returns the definition of a Python module. /// @@ -268,13 +269,12 @@ fn find_introspection_chunks_in_elf(elf: &Elf<'_>, library_content: &[u8]) -> Re let mut chunks = Vec::new(); for sym in &elf.syms { if is_introspection_symbol(elf.strtab.get_at(sym.st_name).unwrap_or_default()) { + ensure!(u32::try_from(sym.st_shndx)? != SHN_XINDEX, "Section names length is greater than SHN_LORESERVE in ELF, this is not supported by PyO3 yet"); let section_header = &elf.section_headers[sym.st_shndx]; let data_offset = sym.st_value + section_header.sh_offset - section_header.sh_addr; - chunks.push(read_symbol_value_with_ptr_and_len( + chunks.push(deserialize_chunk( &library_content[usize::try_from(data_offset).context("File offset overflow")?..], - 0, - library_content, - elf.is_64, + elf.little_endian, )?); } } @@ -311,11 +311,9 @@ fn find_introspection_chunks_in_macho( { let section = §ions[nlist.n_sect - 1]; // Sections are counted from 1 let data_offset = nlist.n_value + u64::from(section.offset) - section.addr; - chunks.push(read_symbol_value_with_ptr_and_len( + chunks.push(deserialize_chunk( &library_content[usize::try_from(data_offset).context("File offset overflow")?..], - 0, - library_content, - macho.is_64, + macho.little_endian, )?); } } @@ -323,61 +321,37 @@ fn find_introspection_chunks_in_macho( } fn find_introspection_chunks_in_pe(pe: &PE<'_>, library_content: &[u8]) -> Result> { - let rdata_data_section = pe - .sections - .iter() - .find(|section| section.name().unwrap_or_default() == ".rdata") - .context("No .rdata section found")?; - let rdata_shift = usize::try_from(pe.image_base).context("image_base overflow")? - + usize::try_from(rdata_data_section.virtual_address) - .context(".rdata virtual_address overflow")? - - usize::try_from(rdata_data_section.pointer_to_raw_data) - .context(".rdata pointer_to_raw_data overflow")?; - let mut chunks = Vec::new(); for export in &pe.exports { if is_introspection_symbol(export.name.unwrap_or_default()) { - chunks.push(read_symbol_value_with_ptr_and_len( + chunks.push(deserialize_chunk( &library_content[export.offset.context("No symbol offset")?..], - rdata_shift, - library_content, - pe.is_64, + true, )?); } } Ok(chunks) } -fn read_symbol_value_with_ptr_and_len( - value_slice: &[u8], - shift: usize, - full_library_content: &[u8], - is_64: bool, +fn deserialize_chunk( + content_with_chunk_at_the_beginning: &[u8], + is_little_endian: bool, ) -> Result { - let (ptr, len) = if is_64 { - let (ptr, len) = value_slice[..16].split_at(8); - let ptr = usize::try_from(u64::from_le_bytes( - ptr.try_into().context("Too short symbol value")?, - )) - .context("Pointer overflow")?; - let len = usize::try_from(u64::from_le_bytes( - len.try_into().context("Too short symbol value")?, - )) - .context("Length overflow")?; - (ptr, len) + let length = content_with_chunk_at_the_beginning + .split_at(4) + .0 + .try_into() + .context("The introspection chunk must contain a length")?; + let length = if is_little_endian { + u32::from_le_bytes(length) } else { - let (ptr, len) = value_slice[..8].split_at(4); - let ptr = usize::try_from(u32::from_le_bytes( - ptr.try_into().context("Too short symbol value")?, - )) - .context("Pointer overflow")?; - let len = usize::try_from(u32::from_le_bytes( - len.try_into().context("Too short symbol value")?, - )) - .context("Length overflow")?; - (ptr, len) + u32::from_be_bytes(length) }; - let chunk = &full_library_content[ptr - shift..ptr - shift + len]; + let chunk = content_with_chunk_at_the_beginning + .get(4..4 + length as usize) + .ok_or_else(|| { + anyhow!("The introspection chunk length {length} is greater that the binary size") + })?; serde_json::from_slice(chunk).with_context(|| { format!( "Failed to parse introspection chunk: '{}'", @@ -389,7 +363,7 @@ fn read_symbol_value_with_ptr_and_len( fn is_introspection_symbol(name: &str) -> bool { name.strip_prefix('_') .unwrap_or(name) - .starts_with("PYO3_INTROSPECTION_0_") + .starts_with("PYO3_INTROSPECTION_1_") } #[derive(Deserialize)] diff --git a/pyo3-macros-backend/src/introspection.rs b/pyo3-macros-backend/src/introspection.rs index bcece0e8a70..8ca21beedbe 100644 --- a/pyo3-macros-backend/src/introspection.rs +++ b/pyo3-macros-backend/src/introspection.rs @@ -353,17 +353,10 @@ impl IntrospectionNode<'_> { fn emit(self, pyo3_crate_path: &PyO3CratePath) -> TokenStream { let mut content = ConcatenationBuilder::default(); self.add_to_serialization(&mut content, pyo3_crate_path); - let content = content.into_token_stream(pyo3_crate_path); - - let static_name = format_ident!("PYO3_INTROSPECTION_0_{}", unique_element_id()); - // #[no_mangle] is required to make sure some linkers like Linux ones do not mangle the section name too. - quote! { - const _: () = { - #[used] - #[no_mangle] - static #static_name: &'static [u8] = #content; - }; - } + content.into_static( + pyo3_crate_path, + format_ident!("PYO3_INTROSPECTION_1_{}", unique_element_id()), + ) } fn add_to_serialization( @@ -530,6 +523,27 @@ impl ConcatenationBuilder { } } } + + fn into_static(self, pyo3_crate_path: &PyO3CratePath, ident: Ident) -> TokenStream { + let mut elements = self.elements; + if !self.current_string.is_empty() { + elements.push(ConcatenationBuilderElement::String(self.current_string)); + } + + // #[no_mangle] is required to make sure some linkers like Linux ones do not mangle the section name too. + quote! { + const _: () = { + const PIECES: &[&[u8]] = &[#(#elements , )*]; + const PIECES_LEN: usize = #pyo3_crate_path::impl_::concat::combined_len(PIECES); + #[used] + #[no_mangle] + static #ident: #pyo3_crate_path::impl_::introspection::SerializedIntrospectionFragment = #pyo3_crate_path::impl_::introspection::SerializedIntrospectionFragment { + length: PIECES_LEN as u32, + fragment: #pyo3_crate_path::impl_::concat::combine_to_array::(PIECES) + }; + }; + } + } } enum ConcatenationBuilderElement { diff --git a/pytests/stubs/pyfunctions.pyi b/pytests/stubs/pyfunctions.pyi index d2a4d78d8d9..369119cd96f 100644 --- a/pytests/stubs/pyfunctions.pyi +++ b/pytests/stubs/pyfunctions.pyi @@ -19,7 +19,7 @@ def many_keyword_arguments( newt: typing.Any | None = None, owl: typing.Any | None = None, penguin: typing.Any | None = None, -) -> typing.Any: ... +) -> None: ... def none() -> None: ... def positional_only(a: typing.Any, /, b: typing.Any) -> typing.Any: ... def simple( diff --git a/src/impl_/introspection.rs b/src/impl_/introspection.rs index cd00a55162b..a0f3ec81942 100644 --- a/src/impl_/introspection.rs +++ b/src/impl_/introspection.rs @@ -15,3 +15,9 @@ impl<'a, T: IntoPyObject<'a>> PyReturnType for T { impl PyReturnType for Result { const OUTPUT_TYPE: &'static str = T::OUTPUT_TYPE; } + +#[repr(C)] +pub struct SerializedIntrospectionFragment { + pub length: u32, + pub fragment: [u8; LEN], +} diff --git a/tests/test_compile_error.rs b/tests/test_compile_error.rs index 68e6b06cfc7..ec22d65a672 100644 --- a/tests/test_compile_error.rs +++ b/tests/test_compile_error.rs @@ -69,7 +69,7 @@ fn test_compile_errors() { t.compile_fail("tests/ui/invalid_pymodule_glob.rs"); t.compile_fail("tests/ui/invalid_pymodule_trait.rs"); t.compile_fail("tests/ui/invalid_pymodule_two_pymodule_init.rs"); - #[cfg(feature = "experimental-async")] + #[cfg(all(feature = "experimental-async", not(feature = "experimental-inspect")))] #[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"); From 6a6ed99de36768afdc6d09273c83c0f4fd07740c Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sat, 20 Sep 2025 15:45:10 +0100 Subject: [PATCH 874/936] remove `pybuilddir.txt` from git repository (#5456) * remove `pybuilddir.txt` from git repository * newsfragment --- emscripten/pybuilddir.txt | 1 - newsfragments/5456.fixed.md | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 emscripten/pybuilddir.txt create mode 100644 newsfragments/5456.fixed.md diff --git a/emscripten/pybuilddir.txt b/emscripten/pybuilddir.txt deleted file mode 100644 index 59f2a4a7546..00000000000 --- a/emscripten/pybuilddir.txt +++ /dev/null @@ -1 +0,0 @@ -build/lib.linux-x86_64-3.11 \ No newline at end of file diff --git a/newsfragments/5456.fixed.md b/newsfragments/5456.fixed.md new file mode 100644 index 00000000000..51be2e4200b --- /dev/null +++ b/newsfragments/5456.fixed.md @@ -0,0 +1 @@ +Fix issue with `cargo vendor` caused by gitignored build artifact `emscripten/pybuilddir.txt`. From 28db16c1956646b58a8ac57e118806c06b67f497 Mon Sep 17 00:00:00 2001 From: Thomas Tanon Date: Sat, 27 Sep 2025 10:39:11 +0200 Subject: [PATCH 875/936] Fetch type name dynamically on cast errors instead of using `PyTypeInfo::NAME` (#5387) * Fetch type name dynamically on cast errors * Lazily fetch type name * Derive Debug * Introduce PyTypeCheck::classinfo_object and make use of it * Add back PyTypeCheck::NAME * Fixes Python 3.9 tests --- newsfragments/5387.added.md | 1 + newsfragments/5387.changed.md | 2 + src/conversions/chrono.rs | 14 ++-- src/conversions/jiff.rs | 14 ++-- src/conversions/smallvec.rs | 9 ++- src/conversions/std/array.rs | 8 ++- src/conversions/std/vec.rs | 8 ++- src/err/mod.rs | 95 +++++++++++++++++++------ src/instance.rs | 21 ++++-- src/pybacked.rs | 17 +++-- src/type_object.rs | 16 ++++- src/types/iterator.rs | 2 +- src/types/weakref/anyref.rs | 70 +++++++++++++++++- src/types/weakref/proxy.rs | 58 +++++++++++++-- tests/test_frompyobject.rs | 14 ++-- tests/test_pyerr_debug_unformattable.rs | 2 +- tests/test_pyfunction.rs | 8 +-- 17 files changed, 286 insertions(+), 73 deletions(-) create mode 100644 newsfragments/5387.added.md create mode 100644 newsfragments/5387.changed.md diff --git a/newsfragments/5387.added.md b/newsfragments/5387.added.md new file mode 100644 index 00000000000..9b96ac46353 --- /dev/null +++ b/newsfragments/5387.added.md @@ -0,0 +1 @@ +Add `PyTypeCheck::classinfo_object` that returns an object that can be used as parameter in `isinstance` or `issubclass` \ No newline at end of file diff --git a/newsfragments/5387.changed.md b/newsfragments/5387.changed.md new file mode 100644 index 00000000000..8c737793ef0 --- /dev/null +++ b/newsfragments/5387.changed.md @@ -0,0 +1,2 @@ +- Fetch type name dynamically on cast errors instead of using `PyTypeCheck::NAME` +- Deprecate `PyTypeCheck::NAME`, please `TypeTypeCheck::classinfo_object` to get the expected type and format it at runtime. \ No newline at end of file diff --git a/src/conversions/chrono.rs b/src/conversions/chrono.rs index d250cfc03d5..0a2a56c3530 100644 --- a/src/conversions/chrono.rs +++ b/src/conversions/chrono.rs @@ -692,11 +692,11 @@ mod tests { let none = py.None().into_bound(py); assert_eq!( none.extract::().unwrap_err().to_string(), - "TypeError: 'NoneType' object cannot be converted to 'PyDelta'" + "TypeError: 'NoneType' object cannot be converted to 'timedelta'" ); assert_eq!( none.extract::().unwrap_err().to_string(), - "TypeError: 'NoneType' object cannot be converted to 'PyTzInfo'" + "TypeError: 'NoneType' object cannot be converted to 'tzinfo'" ); assert_eq!( none.extract::().unwrap_err().to_string(), @@ -704,25 +704,25 @@ mod tests { ); assert_eq!( none.extract::().unwrap_err().to_string(), - "TypeError: 'NoneType' object cannot be converted to 'PyTime'" + "TypeError: 'NoneType' object cannot be converted to 'time'" ); assert_eq!( none.extract::().unwrap_err().to_string(), - "TypeError: 'NoneType' object cannot be converted to 'PyDate'" + "TypeError: 'NoneType' object cannot be converted to 'date'" ); assert_eq!( none.extract::().unwrap_err().to_string(), - "TypeError: 'NoneType' object cannot be converted to 'PyDateTime'" + "TypeError: 'NoneType' object cannot be converted to 'datetime'" ); assert_eq!( none.extract::>().unwrap_err().to_string(), - "TypeError: 'NoneType' object cannot be converted to 'PyDateTime'" + "TypeError: 'NoneType' object cannot be converted to 'datetime'" ); assert_eq!( none.extract::>() .unwrap_err() .to_string(), - "TypeError: 'NoneType' object cannot be converted to 'PyDateTime'" + "TypeError: 'NoneType' object cannot be converted to 'datetime'" ); }); } diff --git a/src/conversions/jiff.rs b/src/conversions/jiff.rs index 7ad84516f47..42d31527841 100644 --- a/src/conversions/jiff.rs +++ b/src/conversions/jiff.rs @@ -553,31 +553,31 @@ mod tests { let none = py.None().into_bound(py); assert_eq!( none.extract::().unwrap_err().to_string(), - "TypeError: 'NoneType' object cannot be converted to 'PyDelta'" + "TypeError: 'NoneType' object cannot be converted to 'timedelta'" ); assert_eq!( none.extract::().unwrap_err().to_string(), - "TypeError: 'NoneType' object cannot be converted to 'PyTzInfo'" + "TypeError: 'NoneType' object cannot be converted to 'tzinfo'" ); assert_eq!( none.extract::().unwrap_err().to_string(), - "TypeError: 'NoneType' object cannot be converted to 'PyTzInfo'" + "TypeError: 'NoneType' object cannot be converted to 'tzinfo'" ); assert_eq!( none.extract::
+## `PyTypeCheck` is now an `unsafe trait` + +Because `PyTypeCheck` is the trait used to guard the `.cast()` functions to treat Python objects as specific concrete types, the trait is `unsafe` to implement. + +This should always have been the case, it was an unfortunate omission from its original implementation which is being corrected in this release. + ## from 0.25.* to 0.26 ### Rename of `Python::with_gil`, `Python::allow_threads`, and `pyo3::prepare_freethreaded_python`
diff --git a/newsfragments/5473.changed.md b/newsfragments/5473.changed.md new file mode 100644 index 00000000000..71c54298f55 --- /dev/null +++ b/newsfragments/5473.changed.md @@ -0,0 +1 @@ +Make `PyTypeCheck` an `unsafe trait`. diff --git a/src/type_object.rs b/src/type_object.rs index fd25d519e07..f7f8d4dbfd1 100644 --- a/src/type_object.rs +++ b/src/type_object.rs @@ -35,6 +35,10 @@ pub trait PySizedLayout: PyLayout + Sized {} /// /// Implementations must provide an implementation for `type_object_raw` which infallibly produces a /// non-null pointer to the corresponding Python type object. +/// +/// `is_type_of` must only return true for objects which can safely be treated as instances of `Self`. +/// +/// `is_exact_type_of` must only return true for objects whose type is exactly `Self`. pub unsafe trait PyTypeInfo: Sized { /// Class name. const NAME: &'static str; @@ -85,7 +89,13 @@ pub unsafe trait PyTypeInfo: Sized { } /// Implemented by types which can be used as a concrete Python type inside `Py` smart pointers. -pub trait PyTypeCheck { +/// +/// # Safety +/// +/// This trait is used to determine whether [`Bound::cast`] and similar functions can safely cast +/// to a concrete type. The implementor is responsible for ensuring that `type_check` only returns +/// true for objects which can safely be treated as Python instances of `Self`. +pub unsafe trait PyTypeCheck { /// Name of self. This is used in error messages, for example. #[deprecated( since = "0.27.0", @@ -108,7 +118,7 @@ pub trait PyTypeCheck { fn classinfo_object(py: Python<'_>) -> Bound<'_, PyAny>; } -impl PyTypeCheck for T +unsafe impl PyTypeCheck for T where T: PyTypeInfo, { diff --git a/src/types/weakref/anyref.rs b/src/types/weakref/anyref.rs index 1fcb96a6918..4c5772f98f2 100644 --- a/src/types/weakref/anyref.rs +++ b/src/types/weakref/anyref.rs @@ -18,7 +18,7 @@ pyobject_native_type_named!(PyWeakref); // #[cfg(not(Py_LIMITED_API))] // pyobject_native_type_sized!(PyWeakref, ffi::PyWeakReference); -impl PyTypeCheck for PyWeakref { +unsafe impl PyTypeCheck for PyWeakref { const NAME: &'static str = "weakref"; #[cfg(feature = "experimental-inspect")] diff --git a/src/types/weakref/proxy.rs b/src/types/weakref/proxy.rs index 85823aa1428..6f2bc1d57ac 100644 --- a/src/types/weakref/proxy.rs +++ b/src/types/weakref/proxy.rs @@ -20,7 +20,7 @@ pyobject_native_type_named!(PyWeakrefProxy); // #[cfg(not(Py_LIMITED_API))] // pyobject_native_type_sized!(PyWeakrefProxy, ffi::PyWeakReference); -impl PyTypeCheck for PyWeakrefProxy { +unsafe impl PyTypeCheck for PyWeakrefProxy { const NAME: &'static str = "weakref.ProxyTypes"; #[cfg(feature = "experimental-inspect")] From bdc17fe75d0e5369f39ea0ca86b963f8408277ca Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Sun, 28 Sep 2025 15:02:04 +0200 Subject: [PATCH 881/936] remove `doc_auto_cfg` feature, merged into `doc_cfg` (#5476) https://github.com/rust-lang/rust/pull/138907 --- pyo3-ffi/src/lib.rs | 2 +- pyo3-macros-backend/src/lib.rs | 2 +- pyo3-macros/src/lib.rs | 2 +- src/lib.rs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyo3-ffi/src/lib.rs b/pyo3-ffi/src/lib.rs index 2ee446598f5..986ceabef32 100644 --- a/pyo3-ffi/src/lib.rs +++ b/pyo3-ffi/src/lib.rs @@ -1,4 +1,4 @@ -#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#![cfg_attr(docsrs, feature(doc_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. diff --git a/pyo3-macros-backend/src/lib.rs b/pyo3-macros-backend/src/lib.rs index f9e3cb865db..5a99f499f19 100644 --- a/pyo3-macros-backend/src/lib.rs +++ b/pyo3-macros-backend/src/lib.rs @@ -1,7 +1,7 @@ //! This crate contains the implementation of the proc macro attributes #![warn(elided_lifetimes_in_paths, unused_lifetimes)] -#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#![cfg_attr(docsrs, feature(doc_cfg))] #![recursion_limit = "1024"] // Listed first so that macros in this module are available in the rest of the crate. diff --git a/pyo3-macros/src/lib.rs b/pyo3-macros/src/lib.rs index c2e42faf61b..6e4a46ee95e 100644 --- a/pyo3-macros/src/lib.rs +++ b/pyo3-macros/src/lib.rs @@ -1,7 +1,7 @@ //! This crate declares only the proc macro attributes, as a crate defining proc macro attributes //! must not contain any other public items. -#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#![cfg_attr(docsrs, feature(doc_cfg))] use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; use pyo3_macros_backend::{ diff --git a/src/lib.rs b/src/lib.rs index fdb05cac785..9103bf38a54 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,7 +3,7 @@ feature = "nightly", feature(auto_traits, negative_impls, try_trait_v2, iter_advance_by) )] -#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] +#![cfg_attr(docsrs, feature(doc_cfg))] #![warn(unsafe_op_in_unsafe_fn)] // Deny some lints in doctests. // Use `#[allow(...)]` locally to override. From 62280e189b4e7bfaec88ef318791c5b1caf6080b Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sun, 28 Sep 2025 18:32:51 +0100 Subject: [PATCH 882/936] docs: replace "GIL token" with "Python token" (#5477) --- guide/src/migration.md | 4 ++-- src/conversion.rs | 2 +- src/err/err_state.rs | 2 +- src/impl_/pymethods.rs | 10 ++++------ src/instance.rs | 2 +- src/marker.rs | 2 +- 6 files changed, 10 insertions(+), 12 deletions(-) diff --git a/guide/src/migration.md b/guide/src/migration.md index f525a0d81fd..c2c67039c47 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -1280,7 +1280,7 @@ However, if the `anyhow::Error` or `eyre::Report` has a source, then the origina
Click to expand -While the API provided by [`Python::acquire_gil`](https://docs.rs/pyo3/0.18.3/pyo3/marker/struct.Python.html#method.acquire_gil) seems convenient, it is somewhat brittle as the design of the GIL token [`Python`](https://docs.rs/pyo3/0.18.3/pyo3/marker/struct.Python.html) relies on proper nesting and panics if not used correctly, e.g. +While the API provided by [`Python::acquire_gil`](https://docs.rs/pyo3/0.18.3/pyo3/marker/struct.Python.html#method.acquire_gil) seems convenient, it is somewhat brittle as the design of the [`Python`](https://docs.rs/pyo3/0.18.3/pyo3/marker/struct.Python.html) token relies on proper nesting and panics if not used correctly, e.g. ```rust,ignore # #![allow(dead_code, deprecated)] @@ -1346,7 +1346,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/0.22.5/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 Python token supplied by `Python::with_gil`, the problem is avoided as the Python 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 Python 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 diff --git a/src/conversion.rs b/src/conversion.rs index 8b829d21aed..fbb1eae2150 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -17,7 +17,7 @@ use std::marker::PhantomData; /// 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) +/// It functions similarly to std's [`TryInto`] trait, but requires a [`Python<'py>`] token /// as an argument. /// /// The [`into_pyobject`][IntoPyObject::into_pyobject] method is designed for maximum flexibility and efficiency; it diff --git a/src/err/err_state.rs b/src/err/err_state.rs index d2285a7a1ca..e2583a863da 100644 --- a/src/err/err_state.rs +++ b/src/err/err_state.rs @@ -85,8 +85,8 @@ impl PyErrState { #[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. + // - The `Once` ensure that only one thread will do the write. // Guard against re-entrant normalization, because `Once` does not provide // re-entrancy guarantees. diff --git a/src/impl_/pymethods.rs b/src/impl_/pymethods.rs index 32abadcf8c4..37d54e92166 100644 --- a/src/impl_/pymethods.rs +++ b/src/impl_/pymethods.rs @@ -275,13 +275,11 @@ pub unsafe fn _call_traverse( where T: PyClass, { - // It is important the implementation of `__traverse__` cannot safely access the GIL, - // c.f. https://github.com/PyO3/pyo3/issues/3165, and hence we do not expose our GIL - // token to the user code and lock safe methods for acquiring the GIL. + // It is important the implementation of `__traverse__` cannot safely access the interpreter, + // c.f. https://github.com/PyO3/pyo3/issues/3165, and hence we do not expose our Python + // token to the user code and forbid safe methods for attaching. // (This includes enforcing the `&self` method receiver as e.g. `PyRef` could - // reconstruct a GIL token via `PyRef::py`.) - // 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`. + // reconstruct a Python token via `PyRef::py`.) let trap = PanicTrap::new("uncaught panic inside __traverse__ handler"); let lock = ForbidAttaching::during_traverse(); diff --git a/src/instance.rs b/src/instance.rs index 9131df6d08c..1514350b9fb 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -789,7 +789,7 @@ impl Drop for Bound<'_, T> { } impl<'py, T> Bound<'py, T> { - /// Returns the GIL token associated with this object. + /// Returns the [`Python``] token associated with this object. #[inline] pub fn py(&self) -> Python<'py> { self.0 diff --git a/src/marker.rs b/src/marker.rs index 52262d4119a..ad545780929 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -219,7 +219,7 @@ mod nightly { /// }); /// ``` /// - /// This applies to the GIL token `Python` itself as well, e.g. + /// This applies to the [`Python`] token itself as well, e.g. /// /// ```compile_fail /// # use pyo3::prelude::*; From 8d51cf27ff8d5388abfa1745efc216f50bd88b10 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Tue, 30 Sep 2025 19:55:10 +0200 Subject: [PATCH 883/936] expose `Borrowed::cast` and `Borrowed::cast_unchecked` as public api (#5475) --- newsfragments/5475.added.md | 1 + src/instance.rs | 116 +++++++++++++++++++++++++++--------- src/types/complex.rs | 2 - 3 files changed, 88 insertions(+), 31 deletions(-) create mode 100644 newsfragments/5475.added.md diff --git a/newsfragments/5475.added.md b/newsfragments/5475.added.md new file mode 100644 index 00000000000..29e37a413f7 --- /dev/null +++ b/newsfragments/5475.added.md @@ -0,0 +1 @@ +added `Borrowed::cast`, `Borrowed::cast_exact` and `Borrowed::cast_unchecked` \ No newline at end of file diff --git a/src/instance.rs b/src/instance.rs index 1514350b9fb..f25b44fe0df 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -890,12 +890,26 @@ impl<'py, T> BoundObject<'py, T> for Bound<'py, T> { } } -/// A borrowed equivalent to `Bound`. +/// 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 -/// is already a pointer to an `ffi::PyObject``. +/// [`Borrowed<'a, 'py, T>`] is an advanced type used just occasionally at the edge of interaction +/// with the Python interpreter. It can be thought of as analogous to the shared reference `&'a +/// Bound<'py, T>`, similarly this type is `Copy` and `Clone`. The difference is that [`Borrowed<'a, +/// 'py, T>`] is just a smart pointer rather than a reference-to-a-smart-pointer. For one this +/// reduces one level of pointer indirection, but additionally it removes the implicit lifetime +/// relation that `'py` has to outlive `'a` (`'py: 'a`). This opens the possibility to borrow from +/// the underlying Python object without necessarily requiring attachment to the interpreter for +/// that duration. Within PyO3 this is used for example for the byte slice (`&[u8]`) extraction. /// -/// Similarly, this type is `Copy` and `Clone`, like a shared reference (`&T`). +/// [`Borrowed<'a, 'py, T>`] dereferences to [`Bound<'py, T>`], so all methods on [`Bound<'py, T>`] +/// are available on [`Borrowed<'a, 'py, T>`]. +/// +/// Some Python C APIs also return "borrowed" pointers, which need to be increfd by the caller to +/// keep them alive. This can also be modelled using [`Borrowed`]. However with free-threading these +/// APIs are gradually replaced, because in absense of the GIL it is very hard to guarantee that the +/// referred to object is not deallocated between receiving the pointer and incrementing the +/// reference count. When possible APIs which return a "strong" reference (modelled by [`Bound`]) +/// should be using instead and otherwise great care needs to be taken to ensure safety. #[repr(transparent)] pub struct Borrowed<'a, 'py, T>(NonNull, PhantomData<&'a Py>, Python<'py>); @@ -954,6 +968,75 @@ impl<'a, 'py, T> Borrowed<'a, 'py, T> { { FromPyObject::extract(self.to_any()) } + + /// Cast this to a concrete Python type or pyclass. + /// + /// This performs a runtime type check using the equivalent of Python's + /// `isinstance(self, U)`. + #[inline] + pub fn cast(self) -> Result, DowncastError<'a, 'py>> + where + U: PyTypeCheck, + { + fn inner<'a, 'py, U>( + any: Borrowed<'a, 'py, PyAny>, + ) -> Result, DowncastError<'a, 'py>> + where + U: PyTypeCheck, + { + if U::type_check(&any) { + // Safety: type_check is responsible for ensuring that the type is correct + Ok(unsafe { any.cast_unchecked() }) + } else { + Err(DowncastError::new_from_type( + any, + U::classinfo_object(any.py()), + )) + } + } + inner(self.to_any()) + } + + /// Cast this to a concrete Python type or pyclass (but not a subclass of it). + /// + /// It is almost always better to use [`cast`](Self::cast) because it accounts for Python + /// subtyping. Use this method only when you do not want to allow subtypes. + /// + /// The advantage of this method over [`cast`](Self::cast) is that it is faster. The + /// implementation of `cast_exact` uses the equivalent of the Python expression `type(self) is + /// U`, whereas `cast` uses `isinstance(self, U)`. + #[inline] + pub fn cast_exact(self) -> Result, DowncastError<'a, 'py>> + where + U: PyTypeInfo, + { + fn inner<'a, 'py, U>( + any: Borrowed<'a, 'py, PyAny>, + ) -> Result, DowncastError<'a, 'py>> + where + U: PyTypeInfo, + { + if any.is_exact_instance_of::() { + // Safety: is_exact_instance_of is responsible for ensuring that the type is correct + Ok(unsafe { any.cast_unchecked() }) + } else { + Err(DowncastError::new_from_type( + any, + U::classinfo_object(any.py()), + )) + } + } + inner(self.to_any()) + } + + /// Converts this 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 cast_unchecked(self) -> Borrowed<'a, 'py, U> { + Borrowed(self.0, PhantomData, self.2) + } } impl<'a, T: PyClass> Borrowed<'a, '_, T> { @@ -1042,31 +1125,6 @@ impl<'a, 'py> Borrowed<'a, 'py, PyAny> { pub(crate) unsafe fn from_non_null(py: Python<'py>, ptr: NonNull) -> Self { Self(ptr, PhantomData, py) } - - #[inline] - pub(crate) fn cast(self) -> Result, DowncastError<'a, 'py>> - where - T: PyTypeCheck, - { - if T::type_check(&self) { - // Safety: type_check is responsible for ensuring that the type is correct - Ok(unsafe { self.cast_unchecked() }) - } else { - Err(DowncastError::new_from_type( - self, - T::classinfo_object(self.py()), - )) - } - } - - /// Converts this `PyAny` to a concrete Python type without checking validity. - /// - /// # Safety - /// Callers must ensure that the type is valid or risk type confusion. - #[inline] - pub(crate) unsafe fn cast_unchecked(self) -> Borrowed<'a, 'py, T> { - Borrowed(self.0, PhantomData, self.2) - } } impl<'a, 'py, T> From<&'a Bound<'py, T>> for Borrowed<'a, 'py, T> { diff --git a/src/types/complex.rs b/src/types/complex.rs index d2624875733..7f9ac4e6191 100644 --- a/src/types/complex.rs +++ b/src/types/complex.rs @@ -257,8 +257,6 @@ mod tests { #[test] fn test_from_double() { - use assert_approx_eq::assert_approx_eq; - Python::attach(|py| { let complex = PyComplex::from_doubles(py, 3.0, 1.2); assert_approx_eq!(complex.real(), 3.0); From 63430bf4302f64e28c4d633606c563cfb3999f77 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers <7943856+bschoenmaeckers@users.noreply.github.com> Date: Wed, 1 Oct 2025 21:25:44 +0200 Subject: [PATCH 884/936] Add conversions for `jiff::civil::ISOWeekDate` (#5478) --- guide/src/features.md | 1 + newsfragments/5478.added.md | 1 + src/conversions/jiff.rs | 47 ++++++++++++++++++++++++++++++++++++- 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 newsfragments/5478.added.md diff --git a/guide/src/features.md b/guide/src/features.md index efb0b90646b..da3f79d8752 100644 --- a/guide/src/features.md +++ b/guide/src/features.md @@ -172,6 +172,7 @@ Adds a dependency on [jiff@0.2](https://docs.rs/jiff/0.2) and requires MSRV 1.70 - [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) +- [ISOWeekDate](https://docs.rs/jiff/0.2/jiff/civil/struct.ISOWeekDate.html) -> [`PyDate`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyDate.html) ### `lock_api` diff --git a/newsfragments/5478.added.md b/newsfragments/5478.added.md new file mode 100644 index 00000000000..87c59d425e2 --- /dev/null +++ b/newsfragments/5478.added.md @@ -0,0 +1 @@ +Add conversions for `jiff::civil::ISOWeekDate` diff --git a/src/conversions/jiff.rs b/src/conversions/jiff.rs index 42d31527841..817b50df79d 100644 --- a/src/conversions/jiff.rs +++ b/src/conversions/jiff.rs @@ -52,7 +52,7 @@ use crate::types::{PyDate, PyDateTime, PyDelta, PyTime, PyTzInfo, PyTzInfoAccess #[cfg(not(Py_LIMITED_API))] use crate::types::{PyDateAccess, PyDeltaAccess, PyTimeAccess}; use crate::{intern, Borrowed, Bound, FromPyObject, IntoPyObject, PyAny, PyErr, PyResult, Python}; -use jiff::civil::{Date, DateTime, Time}; +use jiff::civil::{Date, DateTime, ISOWeekDate, Time}; use jiff::tz::{Offset, TimeZone}; use jiff::{SignedDuration, Span, Timestamp, Zoned}; #[cfg(feature = "jiff-02")] @@ -477,6 +477,34 @@ impl<'py> FromPyObject<'_, 'py> for Span { } } +impl<'py> IntoPyObject<'py> for ISOWeekDate { + type Target = PyDate; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + self.date().into_pyobject(py) + } +} + +impl<'py> IntoPyObject<'py> for &ISOWeekDate { + type Target = PyDate; + type Output = Bound<'py, Self::Target>; + type Error = PyErr; + + fn into_pyobject(self, py: Python<'py>) -> Result { + (*self).into_pyobject(py) + } +} + +impl FromPyObject<'_, '_> for ISOWeekDate { + type Error = PyErr; + + fn extract(ob: Borrowed<'_, '_, PyAny>) -> PyResult { + Ok(ob.extract::()?.iso_week_date()) + } +} + impl From for PyErr { fn from(e: jiff::Error) -> Self { PyValueError::new_err(e.to_string()) @@ -1081,6 +1109,23 @@ mod tests { })?; } + #[test] + fn test_weekdate_roundtrip( + year in 1i16..=9999i16, + month in 1i8..=12i8, + day in 1i8..=31i8 + ) { + // Test roundtrip conversion rust->python->rust for all allowed + // python dates (from year 1 to year 9999) + Python::attach(|py| { + let weekdate = try_date(year, month, day)?.iso_week_date(); + let py_date = weekdate.into_pyobject(py).unwrap(); + let roundtripped = py_date.extract::().expect("Round trip"); + prop_assert_eq!(weekdate, roundtripped); + Ok(()) + })?; + } + #[test] fn test_naive_time_roundtrip( hour in 0i8..=23i8, From 9fd849b0b9eb3a028165be6cf2a99da732befbb9 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Thu, 2 Oct 2025 17:42:31 +0100 Subject: [PATCH 885/936] deprecate `import_exception_bound` (#5480) * deprecate `import_exception_bound` * newsfragment --- newsfragments/5480.changed.md | 1 + src/exceptions.rs | 54 +++++------------------------------ 2 files changed, 8 insertions(+), 47 deletions(-) create mode 100644 newsfragments/5480.changed.md diff --git a/newsfragments/5480.changed.md b/newsfragments/5480.changed.md new file mode 100644 index 00000000000..5a0792946cf --- /dev/null +++ b/newsfragments/5480.changed.md @@ -0,0 +1 @@ +Deprecate `import_exception_bound` in favour of `import_exception`. diff --git a/src/exceptions.rs b/src/exceptions.rs index fca1cd63766..a0936eedfbf 100644 --- a/src/exceptions.rs +++ b/src/exceptions.rs @@ -17,16 +17,6 @@ use std::ops; #[doc(hidden)] #[macro_export] macro_rules! impl_exception_boilerplate { - ($name: ident) => { - $crate::impl_exception_boilerplate_bound!($name); - - impl $crate::ToPyErr for $name {} - }; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! impl_exception_boilerplate_bound { ($name: ident) => { impl $name { /// Creates a new [`PyErr`] of this type. @@ -41,6 +31,8 @@ macro_rules! impl_exception_boilerplate_bound { $crate::PyErr::new::<$name, A>(args) } } + + impl $crate::ToPyErr for $name {} }; } @@ -102,44 +94,12 @@ macro_rules! import_exception { }; } -/// Variant of [`import_exception`](crate::import_exception) that does not emit code needed to -/// use the imported exception type as a GIL Ref. -/// -/// This is useful only during migration as a way to avoid generating needless code. +/// Deprecated name for `import_exception!`. #[macro_export] +#[deprecated(since = "0.27.0", note = "renamed to `import_exception!` instead")] macro_rules! import_exception_bound { ($module: expr, $name: ident) => { - /// A Rust type representing an exception defined in Python code. - /// - /// This type was created by the [`pyo3::import_exception_bound!`] macro - see its documentation - /// for more information. - /// - /// [`pyo3::import_exception_bound!`]: https://docs.rs/pyo3/latest/pyo3/macro.import_exception.html "import_exception in pyo3" - #[repr(transparent)] - #[allow(non_camel_case_types)] // E.g. `socket.herror` - pub struct $name($crate::PyAny); - - $crate::impl_exception_boilerplate_bound!($name); - - $crate::pyobject_native_type_info!( - $name, - $name::type_object_raw, - ::std::option::Option::Some(stringify!($module)) - ); - - impl $crate::types::DerefToPyAny for $name {} - - impl $name { - fn type_object_raw(py: $crate::Python<'_>) -> *mut $crate::ffi::PyTypeObject { - use $crate::types::PyTypeMethods; - static TYPE_OBJECT: $crate::impl_::exceptions::ImportedExceptionTypeObject = - $crate::impl_::exceptions::ImportedExceptionTypeObject::new( - stringify!($module), - stringify!($name), - ); - TYPE_OBJECT.get(py).as_type_ptr() - } - } + $crate::import_exception!($module, $name); }; } @@ -821,8 +781,8 @@ mod tests { use crate::types::{IntoPyDict, PyDict}; use crate::PyErr; - import_exception_bound!(socket, gaierror); - import_exception_bound!(email.errors, MessageError); + import_exception!(socket, gaierror); + import_exception!(email.errors, MessageError); #[test] fn test_check_exception() { From 2f4083ef19abfce8165319b1e3172a5965652be4 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers <7943856+bschoenmaeckers@users.noreply.github.com> Date: Fri, 3 Oct 2025 15:51:06 +0200 Subject: [PATCH 886/936] Add conversion for `&Cstr`, `Cstring` and `Cow` (#5482) * Add conversion for `&Cstr`, `Cstring` and `Cow` * Add `Cow` roundtrip test * fix test * Cast to pystring before converting * fix MSRV * Safety comment --- newsfragments/5482.added.md | 1 + src/conversions/std/cstring.rs | 188 +++++++++++++++++++++++++++++++++ src/conversions/std/mod.rs | 1 + 3 files changed, 190 insertions(+) create mode 100644 newsfragments/5482.added.md create mode 100644 src/conversions/std/cstring.rs diff --git a/newsfragments/5482.added.md b/newsfragments/5482.added.md new file mode 100644 index 00000000000..b856a03a2ae --- /dev/null +++ b/newsfragments/5482.added.md @@ -0,0 +1 @@ +Add conversion for `&Cstr`, `Cstring` and `Cow` diff --git a/src/conversions/std/cstring.rs b/src/conversions/std/cstring.rs new file mode 100644 index 00000000000..3e13619f0c5 --- /dev/null +++ b/src/conversions/std/cstring.rs @@ -0,0 +1,188 @@ +use crate::types::PyString; +use crate::{Borrowed, Bound, FromPyObject, IntoPyObject, PyAny, PyErr, Python}; +use std::borrow::Cow; +use std::ffi::{CStr, CString}; +use std::str::Utf8Error; +#[cfg(any(Py_3_10, not(Py_LIMITED_API)))] +use { + crate::{exceptions::PyValueError, ffi}, + std::slice, +}; + +impl<'py> IntoPyObject<'py> for &CStr { + type Target = PyString; + type Output = Bound<'py, Self::Target>; + type Error = Utf8Error; + + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + self.to_str()?.into_pyobject(py).map_err(|err| match err {}) + } +} + +impl<'py> IntoPyObject<'py> for CString { + type Target = PyString; + type Output = Bound<'py, Self::Target>; + type Error = Utf8Error; + + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + (&*self).into_pyobject(py) + } +} + +impl<'py> IntoPyObject<'py> for &CString { + type Target = PyString; + type Output = Bound<'py, Self::Target>; + type Error = Utf8Error; + + #[inline] + fn into_pyobject(self, py: Python<'py>) -> Result { + (&**self).into_pyobject(py) + } +} + +impl<'py> IntoPyObject<'py> for Cow<'_, CStr> { + type Target = PyString; + type Output = Bound<'py, Self::Target>; + type Error = Utf8Error; + + fn into_pyobject(self, py: Python<'py>) -> Result { + (*self).into_pyobject(py) + } +} + +impl<'py> IntoPyObject<'py> for &Cow<'_, CStr> { + type Target = PyString; + type Output = Bound<'py, Self::Target>; + type Error = Utf8Error; + + fn into_pyobject(self, py: Python<'py>) -> Result { + (&**self).into_pyobject(py) + } +} + +#[cfg(any(Py_3_10, not(Py_LIMITED_API)))] +impl<'a> FromPyObject<'a, '_> for &'a CStr { + type Error = PyErr; + + fn extract(obj: Borrowed<'a, '_, PyAny>) -> Result { + let obj = obj.cast::()?; + let mut size = 0; + // SAFETY: obj is a PyString so we can safely call PyUnicode_AsUTF8AndSize + let ptr = unsafe { ffi::PyUnicode_AsUTF8AndSize(obj.as_ptr(), &mut size) }; + + if ptr.is_null() { + return Err(PyErr::fetch(obj.py())); + } + + // SAFETY: PyUnicode_AsUTF8AndSize always returns a NUL-terminated string but size does not + // include the NUL terminator. So we add 1 to the size to include it. + let slice = unsafe { slice::from_raw_parts(ptr.cast(), size as usize + 1) }; + + CStr::from_bytes_with_nul(slice).map_err(|err| PyValueError::new_err(err.to_string())) + } +} + +impl<'a> FromPyObject<'a, '_> for Cow<'a, CStr> { + type Error = PyErr; + + fn extract(obj: Borrowed<'a, '_, PyAny>) -> Result { + #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] + { + Ok(Cow::Borrowed(obj.extract::<&CStr>()?)) + } + + #[cfg(not(any(Py_3_10, not(Py_LIMITED_API))))] + { + Ok(Cow::Owned(obj.extract::()?)) + } + } +} +impl FromPyObject<'_, '_> for CString { + type Error = PyErr; + + fn extract(obj: Borrowed<'_, '_, PyAny>) -> Result { + #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] + { + Ok(obj.extract::<&CStr>()?.to_owned()) + } + + #[cfg(not(any(Py_3_10, not(Py_LIMITED_API))))] + { + CString::new(&*obj.cast::()?.to_cow()?).map_err(Into::into) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::types::string::PyStringMethods; + use crate::types::PyAnyMethods; + use crate::Python; + + #[test] + fn test_into_pyobject() { + Python::attach(|py| { + let s = "Hello, Python!"; + let cstr = CString::new(s).unwrap(); + + let py_string = cstr.as_c_str().into_pyobject(py).unwrap(); + assert_eq!(py_string.to_cow().unwrap(), s); + + let py_string = cstr.into_pyobject(py).unwrap(); + assert_eq!(py_string.to_cow().unwrap(), s); + }) + } + + #[test] + fn test_extract_with_nul_error() { + Python::attach(|py| { + let s = "Hello\0Python"; + let py_string = s.into_pyobject(py).unwrap(); + + #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] + { + let err = py_string.extract::<&CStr>(); + assert!(err.is_err()); + } + + let err = py_string.extract::(); + assert!(err.is_err()); + }) + } + + #[test] + fn test_extract_cstr_and_cstring() { + Python::attach(|py| { + let s = "Hello, world!"; + let cstr = CString::new(s).unwrap(); + let py_string = cstr.as_c_str().into_pyobject(py).unwrap(); + + #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] + { + let extracted_cstr: &CStr = py_string.extract().unwrap(); + assert_eq!(extracted_cstr.to_str().unwrap(), s); + } + + let extracted_cstring: CString = py_string.extract().unwrap(); + assert_eq!(extracted_cstring.to_str().unwrap(), s); + }) + } + + #[test] + fn test_cow_roundtrip() { + Python::attach(|py| { + let s = "Hello, world!"; + let cstr = CString::new(s).unwrap(); + let cow: Cow<'_, CStr> = Cow::Borrowed(cstr.as_c_str()); + + let py_string = cow.into_pyobject(py).unwrap(); + assert_eq!(py_string.to_cow().unwrap(), s); + + let roundtripped: Cow<'_, CStr> = py_string.extract().unwrap(); + assert_eq!(roundtripped.as_ref(), cstr.as_c_str()); + }) + } +} diff --git a/src/conversions/std/mod.rs b/src/conversions/std/mod.rs index 305344b1284..d2f640dc467 100644 --- a/src/conversions/std/mod.rs +++ b/src/conversions/std/mod.rs @@ -1,5 +1,6 @@ mod array; mod cell; +mod cstring; mod ipaddr; mod map; mod num; From d5409d35a049ab05c8d1287f2fdc5412401e198c Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sun, 5 Oct 2025 15:18:51 +0100 Subject: [PATCH 887/936] ci: fix rust nightly unused macro warning (#5491) --- tests/test_utils/mod.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_utils/mod.rs b/tests/test_utils/mod.rs index 523bc1ede6f..235adc2f99d 100644 --- a/tests/test_utils/mod.rs +++ b/tests/test_utils/mod.rs @@ -223,6 +223,7 @@ mod inner { }}; } + #[allow(unused_imports)] // not all tests use this macro pub(crate) use assert_warnings; pub fn generate_unique_module_name(base: &str) -> std::ffi::CString { From 5152711458f1993967e0fa7d4ce98afafeec0f4f Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Sun, 5 Oct 2025 20:19:47 +0200 Subject: [PATCH 888/936] Added add_note method to PyErr (#5489) --- guide/src/function/error-handling.md | 3 +++ newsfragments/5489.added.md | 1 + src/err/mod.rs | 33 ++++++++++++++++++++++++++++ 3 files changed, 37 insertions(+) create mode 100644 newsfragments/5489.added.md diff --git a/guide/src/function/error-handling.md b/guide/src/function/error-handling.md index 990dc787dac..0d5d24ca704 100644 --- a/guide/src/function/error-handling.md +++ b/guide/src/function/error-handling.md @@ -231,6 +231,9 @@ fn wrapped_get_x() -> Result { # } ``` +## Notes + +In Python 3.11 and up, notes can be added to Python exceptions to provide additional debugging information when printing the exception. In PyO3, you can use the `add_note` method on `PyErr` to accomplish this functionality. [`From`]: https://doc.rust-lang.org/stable/std/convert/trait.From.html [`Result`]: https://doc.rust-lang.org/stable/std/result/enum.Result.html diff --git a/newsfragments/5489.added.md b/newsfragments/5489.added.md new file mode 100644 index 00000000000..dd8c76d8d99 --- /dev/null +++ b/newsfragments/5489.added.md @@ -0,0 +1 @@ +Added `PyErr::add_note` diff --git a/src/err/mod.rs b/src/err/mod.rs index 7f959b03d0e..6575263aeeb 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -1,7 +1,11 @@ use crate::instance::Bound; +#[cfg(Py_3_11)] +use crate::intern; use crate::panic::PanicException; use crate::type_object::PyTypeInfo; use crate::types::any::PyAnyMethods; +#[cfg(Py_3_11)] +use crate::types::PyString; use crate::types::{ string::PyStringMethods, traceback::PyTracebackMethods, typeobject::PyTypeMethods, PyTraceback, PyTuple, PyTupleMethods, PyType, @@ -655,6 +659,18 @@ impl PyErr { } } + /// Equivalent to calling `add_note` on the exception in Python. + #[cfg(Py_3_11)] + pub fn add_note IntoPyObject<'py, Target = PyString>>( + &self, + py: Python<'_>, + note: N, + ) -> PyResult<()> { + self.value(py) + .call_method1(intern!(py, "add_note"), (note,))?; + Ok(()) + } + #[inline] fn from_state(state: PyErrState) -> PyErr { PyErr { state } @@ -1152,4 +1168,21 @@ mod tests { warnings.call_method0("resetwarnings").unwrap(); }); } + + #[test] + #[cfg(Py_3_11)] + fn test_add_note() { + use crate::types::any::PyAnyMethods; + Python::attach(|py| { + let err = PyErr::new::("original error"); + err.add_note(py, "additional context").unwrap(); + + let notes = err.value(py).getattr("__notes__").unwrap(); + assert_eq!(notes.len().unwrap(), 1); + assert_eq!( + notes.get_item(0).unwrap().extract::().unwrap(), + "additional context" + ); + }); + } } From b4cad774ea7472cbff0088a92d6ef8bee5da9cb9 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Sun, 5 Oct 2025 21:45:14 +0200 Subject: [PATCH 889/936] docs: fixed several links to the wrong cpython functions (#5493) --- src/types/datetime.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types/datetime.rs b/src/types/datetime.rs index 76e51df7af2..8c57aa8a43a 100644 --- a/src/types/datetime.rs +++ b/src/types/datetime.rs @@ -138,12 +138,12 @@ pub trait PyDeltaAccess { /// Returns the number of seconds, as an int from 0 through 86399. /// /// Implementations should conform to the upstream documentation: - /// + /// fn get_seconds(&self) -> i32; /// Returns the number of microseconds, as an int from 0 through 999999. /// /// Implementations should conform to the upstream documentation: - /// + /// fn get_microseconds(&self) -> i32; } From e0d0e8ae9b910a35079c7bf56df6bd4ddda0af85 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sun, 5 Oct 2025 21:53:41 +0100 Subject: [PATCH 890/936] add `SAFETY` comments in `instance.rs`, simplify implementations (#5484) * add `SAFETY` comments in `instance.rs`, simplify implementations * Apply suggestions from code review Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> * review feedback * coverage of `Py::is_truthy` --------- Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> --- Cargo.toml | 3 + src/instance.rs | 213 +++++++++++++++++++++++++++++++----------------- 2 files changed, 140 insertions(+), 76 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6ecdb9e50b4..c47ecf0affb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -203,6 +203,9 @@ let_unit_value = "warn" manual_assert = "warn" manual_ok_or = "warn" todo = "warn" +# TODO: make this "warn" +# https://github.com/PyO3/pyo3/issues/5487 +undocumented_unsafe_blocks = "allow" unnecessary_wraps = "warn" useless_transmute = "warn" used_underscore_binding = "warn" diff --git a/src/instance.rs b/src/instance.rs index f25b44fe0df..3f80c6616f1 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -1,6 +1,8 @@ +#![warn(clippy::undocumented_unsafe_blocks)] // TODO: remove this when the top-level is "warn" - https://github.com/PyO3/pyo3/issues/5487 + use crate::call::PyCallArgs; use crate::conversion::IntoPyObject; -use crate::err::{self, PyErr, PyResult}; +use crate::err::{PyErr, PyResult}; use crate::impl_::pycell::PyClassObject; use crate::internal_tricks::ptr_from_ref; use crate::pycell::{PyBorrowError, PyBorrowMutError}; @@ -308,6 +310,7 @@ impl<'py, T> Bound<'py, T> { /// Callers must ensure that the type is valid or risk type confusion. #[inline] pub unsafe fn cast_unchecked(&self) -> &Bound<'py, U> { + // SAFETY: caller has upheld the safety contract, all `Bound` have the same layout unsafe { NonNull::from(self).cast().as_ref() } } @@ -318,6 +321,7 @@ impl<'py, T> Bound<'py, T> { /// Callers must ensure that the type is valid or risk type confusion. #[inline] pub unsafe fn cast_into_unchecked(self) -> Bound<'py, U> { + // SAFETY: caller has upheld the safety contract, all `Bound` have the same layout unsafe { std::mem::transmute(self) } } } @@ -332,10 +336,8 @@ 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(unsafe { Py::from_owned_ptr(py, ptr) }), - ) + // SAFETY: caller has upheld the safety contract + unsafe { Py::from_owned_ptr(py, ptr) }.into_bound(py) } /// Constructs a new `Bound<'py, PyAny>` from a pointer. Returns `None` if `ptr` is null. @@ -346,7 +348,8 @@ 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 { - unsafe { Py::from_owned_ptr_or_opt(py, ptr) }.map(|obj| Self(py, ManuallyDrop::new(obj))) + // SAFETY: caller has upheld the safety contract + unsafe { Py::from_owned_ptr_or_opt(py, ptr) }.map(|obj| obj.into_bound(py)) } /// Constructs a new `Bound<'py, PyAny>` from a pointer. Returns an `Err` by calling `PyErr::fetch` @@ -361,7 +364,8 @@ impl<'py> Bound<'py, PyAny> { py: Python<'py>, ptr: *mut ffi::PyObject, ) -> PyResult { - unsafe { Py::from_owned_ptr_or_err(py, ptr) }.map(|obj| Self(py, ManuallyDrop::new(obj))) + // SAFETY: caller has upheld the safety contract + unsafe { Py::from_owned_ptr_or_err(py, ptr) }.map(|obj| obj.into_bound(py)) } /// Constructs a new `Bound<'py, PyAny>` from a pointer without checking for null. @@ -374,10 +378,8 @@ impl<'py> Bound<'py, PyAny> { py: Python<'py>, ptr: *mut ffi::PyObject, ) -> Self { - Self( - py, - ManuallyDrop::new(unsafe { Py::from_owned_ptr_unchecked(ptr) }), - ) + // SAFETY: caller has upheld the safety contract + unsafe { Py::from_owned_ptr_unchecked(ptr) }.into_bound(py) } /// Constructs a new `Bound<'py, PyAny>` from a pointer by creating a new Python reference. @@ -389,7 +391,8 @@ impl<'py> Bound<'py, PyAny> { #[inline] #[track_caller] pub unsafe fn from_borrowed_ptr(py: Python<'py>, ptr: *mut ffi::PyObject) -> Self { - unsafe { Self(py, ManuallyDrop::new(Py::from_borrowed_ptr(py, ptr))) } + // SAFETY: caller has upheld the safety contract + unsafe { Py::from_borrowed_ptr(py, ptr) }.into_bound(py) } /// Constructs a new `Bound<'py, PyAny>` from a pointer by creating a new Python reference. @@ -403,7 +406,8 @@ impl<'py> Bound<'py, PyAny> { py: Python<'py>, ptr: *mut ffi::PyObject, ) -> Option { - unsafe { Py::from_borrowed_ptr_or_opt(py, ptr).map(|obj| Self(py, ManuallyDrop::new(obj))) } + // SAFETY: caller has upheld the safety contract + unsafe { Py::from_borrowed_ptr_or_opt(py, ptr) }.map(|obj| obj.into_bound(py)) } /// Constructs a new `Bound<'py, PyAny>` from a pointer by creating a new Python reference. @@ -417,7 +421,8 @@ impl<'py> Bound<'py, PyAny> { py: Python<'py>, ptr: *mut ffi::PyObject, ) -> PyResult { - unsafe { Py::from_borrowed_ptr_or_err(py, ptr).map(|obj| Self(py, ManuallyDrop::new(obj))) } + // SAFETY: caller has upheld the safety contract + unsafe { Py::from_borrowed_ptr_or_err(py, ptr) }.map(|obj| obj.into_bound(py)) } /// This slightly strange method is used to obtain `&Bound` from a pointer in macro code @@ -435,6 +440,8 @@ impl<'py> Bound<'py, PyAny> { _py: Python<'py>, ptr: &'a *mut ffi::PyObject, ) -> &'a Self { + // SAFETY: caller has upheld the safety contract, + // and `Bound` is layout-compatible with `*mut ffi::PyObject`. unsafe { &*ptr_from_ref(ptr).cast::>() } } @@ -447,6 +454,8 @@ impl<'py> Bound<'py, PyAny> { _py: Python<'py>, ptr: &'a *mut ffi::PyObject, ) -> &'a Option { + // SAFETY: caller has upheld the safety contract, + // and `Option>` is layout-compatible with `*mut ffi::PyObject`. unsafe { &*ptr_from_ref(ptr).cast::>>() } } @@ -464,6 +473,8 @@ impl<'py> Bound<'py, PyAny> { _py: Python<'py>, ptr: &'a NonNull, ) -> &'a Self { + // SAFETY: caller has upheld the safety contract, + // and `Bound` is layout-compatible with `NonNull`. unsafe { NonNull::from(ptr).cast().as_ref() } } } @@ -653,8 +664,8 @@ where /// [`borrow`]: Bound::borrow #[inline] pub fn as_super(&self) -> &Bound<'py, T::BaseType> { - // a pyclass can always be safely "cast" to its base type - unsafe { self.as_any().cast_unchecked() } + // SAFETY: a pyclass can always be safely "cast" to its base type + unsafe { self.cast_unchecked() } } /// Upcast this `Bound` to its base type by value. @@ -705,7 +716,7 @@ where /// [`borrow`]: Bound::borrow #[inline] pub fn into_super(self) -> Bound<'py, T::BaseType> { - // a pyclass can always be safely "cast" to its base type + // SAFETY: a pyclass can always be safely "cast" to its base type unsafe { self.cast_into_unchecked() } } @@ -784,6 +795,8 @@ impl Clone for Bound<'_, T> { impl Drop for Bound<'_, T> { #[inline] fn drop(&mut self) { + // SAFETY: self is an owned reference and the `Bound` implies the thread + // is attached to the interpreter unsafe { ffi::Py_DECREF(self.as_ptr()) } } } @@ -837,20 +850,17 @@ impl<'py, T> Bound<'py, T> { /// Casts this `Bound` to a `Borrowed` smart pointer. #[inline] pub fn as_borrowed<'a>(&'a self) -> Borrowed<'a, 'py, T> { - Borrowed( - unsafe { NonNull::new_unchecked(self.as_ptr()) }, - PhantomData, - self.py(), - ) + // SAFETY: self is known to be a valid pointer to T and will be borrowed from the lifetime 'a + unsafe { Borrowed::from_non_null(self.py(), (self.1).0).cast_unchecked() } } /// Removes the connection for this `Bound` from the GIL, allowing /// it to cross thread boundaries. #[inline] pub fn unbind(self) -> Py { - // Safety: the type T is known to be correct and the ownership of the - // pointer is transferred to the new Py instance. let non_null = (ManuallyDrop::new(self).1).0; + // SAFETY: the type T is known to be correct and the `ManuallyDrop` ensures + // the ownership of the reference is transferred into the `Py`. unsafe { Py::from_non_null(non_null) } } @@ -1065,11 +1075,9 @@ impl<'a, 'py> Borrowed<'a, 'py, PyAny> { #[inline] #[track_caller] pub unsafe fn from_ptr(py: Python<'py>, ptr: *mut ffi::PyObject) -> Self { - Self( - NonNull::new(ptr).unwrap_or_else(|| crate::err::panic_after_error(py)), - PhantomData, - py, - ) + let non_null = NonNull::new(ptr).unwrap_or_else(|| crate::err::panic_after_error(py)); + // SAFETY: caller has upheld the safety contract + unsafe { Self::from_non_null(py, non_null) } } /// Constructs a new `Borrowed<'a, 'py, PyAny>` from a pointer. Returns `None` if `ptr` is null. @@ -1085,7 +1093,9 @@ impl<'a, 'py> Borrowed<'a, 'py, PyAny> { /// derived from is valid for the lifetime `'a`. #[inline] pub unsafe fn from_ptr_or_opt(py: Python<'py>, ptr: *mut ffi::PyObject) -> Option { - NonNull::new(ptr).map(|ptr| Self(ptr, PhantomData, py)) + NonNull::new(ptr).map(|ptr| + // SAFETY: caller has upheld the safety contract + unsafe { Self::from_non_null(py, ptr) }) } /// Constructs a new `Borrowed<'a, 'py, PyAny>` from a pointer. Returns an `Err` by calling `PyErr::fetch` @@ -1104,24 +1114,34 @@ impl<'a, 'py> Borrowed<'a, 'py, PyAny> { pub unsafe fn from_ptr_or_err(py: Python<'py>, ptr: *mut ffi::PyObject) -> PyResult { NonNull::new(ptr).map_or_else( || Err(PyErr::fetch(py)), - |ptr| Ok(Self(ptr, PhantomData, py)), + |ptr| { + Ok( + // SAFETY: ptr is known to be non-null, caller has upheld the safety contract + unsafe { Self::from_non_null(py, ptr) }, + ) + }, ) } /// # Safety - /// This is similar to `std::slice::from_raw_parts`, the lifetime `'a` is completely defined by - /// the caller and it's the caller's responsibility to ensure that the reference this is - /// derived from is valid for the lifetime `'a`. + /// + /// - `ptr` must be a valid pointer to a Python object. It must not be null. + /// - similar to `std::slice::from_raw_parts`, the lifetime `'a` is completely defined by + /// the caller and it is the caller's responsibility to ensure that the reference this is + /// derived from is valid for the lifetime `'a`. #[inline] pub(crate) unsafe fn from_ptr_unchecked(py: Python<'py>, ptr: *mut ffi::PyObject) -> Self { - Self(unsafe { NonNull::new_unchecked(ptr) }, PhantomData, py) + // SAFETY: caller has upheld the safety contract + unsafe { Self::from_non_null(py, NonNull::new_unchecked(ptr)) } } /// # Safety - /// This similar to `std::slice::from_raw_parts`, the lifetime `'a` is - /// completely defined by the caller and it is the caller's responsibility - /// to ensure that the reference this is derived from is valid for the - /// lifetime `'a`. + /// + /// - `ptr` must be a valid pointer to a Python object. + /// - similar to `std::slice::from_raw_parts`, the lifetime `'a` is completely defined by + /// the caller and it is the caller's responsibility to ensure that the reference this is + /// derived from is valid for the lifetime `'a`. + #[inline] pub(crate) unsafe fn from_non_null(py: Python<'py>, ptr: NonNull) -> Self { Self(ptr, PhantomData, py) } @@ -1399,7 +1419,14 @@ pub struct Py(NonNull, PhantomData); // The inner value is only accessed through ways that require proving the gil is held #[cfg(feature = "nightly")] unsafe impl crate::marker::Ungil for Py {} +// SAFETY: Python objects can be sent between threads unsafe impl Send for Py {} +// SAFETY: Python objects can be shared between threads. Any thread safety is +// implemented in the object type itself; `Py` only allows synchronized access +// to `T` through: +// - `borrow`/`borrow_mut` for `#[pyclass]` types +// - `get()` for frozen `#[pyclass(frozen)]` types +// - Python native types have their own thread safety mechanisms unsafe impl Sync for Py {} impl Py @@ -1635,8 +1662,8 @@ impl Py { /// Attaches this `Py` to the given Python context, allowing access to further Python APIs. #[inline] pub fn bind<'py>(&self, _py: Python<'py>) -> &Bound<'py, T> { - // Safety: `Bound` has the same layout as `Py` - unsafe { &*ptr_from_ref(self).cast() } + // SAFETY: `Bound` has the same layout as `Py` + unsafe { NonNull::from(self).cast().as_ref() } } /// Same as `bind` but takes ownership of `self`. @@ -1648,7 +1675,12 @@ impl Py { /// Same as `bind` but produces a `Borrowed` instead of a `Bound`. #[inline] pub fn bind_borrowed<'a, 'py>(&'a self, py: Python<'py>) -> Borrowed<'a, 'py, T> { - Borrowed(self.0, PhantomData, py) + // NB cannot go via `self.bind(py)` because the `&Bound` would imply `'a: 'py` + + // SAFETY: `self.0` is a valid pointer to a PyObject for the lifetime 'a + let borrowed = unsafe { Borrowed::from_non_null(py, self.0) }; + // SAFETY: object is known to be of type T + unsafe { borrowed.cast_unchecked() } } /// Returns whether `self` and `other` point to the same object. To compare @@ -1663,6 +1695,7 @@ impl Py { /// Gets the reference count of the `ffi::PyObject` pointer. #[inline] pub fn get_refcnt(&self, _py: Python<'_>) -> isize { + // SAFETY: Self is a valid pointer to a PyObject unsafe { ffi::Py_REFCNT(self.0.as_ptr()) } } @@ -1690,10 +1723,13 @@ impl Py { /// ``` #[inline] pub fn clone_ref(&self, _py: Python<'_>) -> Py { - unsafe { - ffi::Py_INCREF(self.as_ptr()); - Self::from_non_null(self.0) - } + // NB cannot use self.bind(py) because Bound::clone is implemented using Py::clone_ref + // (infinite recursion) + + // SAFETY: object is known to be valid + unsafe { ffi::Py_INCREF(self.0.as_ptr()) }; + // SAFETY: newly created reference is transferred to the new Py + unsafe { Self::from_non_null(self.0) } } /// Drops `self` and immediately decreases its reference count. @@ -1728,17 +1764,15 @@ impl Py { /// Returns whether the object is considered to be None. /// /// This is equivalent to the Python expression `self is None`. - pub fn is_none(&self, _py: Python<'_>) -> bool { - unsafe { ptr::eq(ffi::Py_None(), self.as_ptr()) } + pub fn is_none(&self, py: Python<'_>) -> bool { + self.bind(py).as_any().is_none() } /// 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, py: Python<'_>) -> PyResult { - let v = unsafe { ffi::PyObject_IsTrue(self.as_ptr()) }; - err::error_on_minusone(py, v)?; - Ok(v != 0) + self.bind(py).as_any().is_truthy() } /// Extracts some type from the Python object. @@ -1913,7 +1947,10 @@ impl Py { #[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), + Some(nonnull_ptr) => { + // SAFETY: caller has upheld the safety contract, ptr is known to be non-null + unsafe { Self::from_non_null(nonnull_ptr) } + } None => crate::err::panic_after_error(py), } } @@ -1930,7 +1967,10 @@ impl Py { ptr: *mut ffi::PyObject, ) -> PyResult> { match NonNull::new(ptr) { - Some(nonnull_ptr) => Ok(Py(nonnull_ptr, PhantomData)), + Some(nonnull_ptr) => Ok( + // SAFETY: caller has upheld the safety contract, ptr is known to be non-null + unsafe { Self::from_non_null(nonnull_ptr) }, + ), None => Err(PyErr::fetch(py)), } } @@ -1943,16 +1983,20 @@ impl Py { /// If non-null, `ptr` must be a pointer to a Python object of type T. #[inline] pub unsafe fn from_owned_ptr_or_opt(_py: Python<'_>, ptr: *mut ffi::PyObject) -> Option { - NonNull::new(ptr).map(|nonnull_ptr| Py(nonnull_ptr, PhantomData)) + NonNull::new(ptr).map(|nonnull_ptr| { + // SAFETY: caller has upheld the safety contract + unsafe { Self::from_non_null(nonnull_ptr) } + }) } /// 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`. + /// - `ptr` must be a non-null pointer to a Python object of type `T`. pub(crate) unsafe fn from_owned_ptr_unchecked(ptr: *mut ffi::PyObject) -> Self { - Py(unsafe { NonNull::new_unchecked(ptr) }, PhantomData) + // SAFETY: caller has upheld the safety contract + unsafe { Self::from_non_null(NonNull::new_unchecked(ptr)) } } /// Create a `Py` instance by creating a new reference from the given FFI pointer. @@ -1965,10 +2009,9 @@ impl Py { #[inline] #[track_caller] pub unsafe fn from_borrowed_ptr(py: Python<'_>, ptr: *mut ffi::PyObject) -> Py { - match unsafe { Self::from_borrowed_ptr_or_opt(py, ptr) } { - Some(slf) => slf, - None => crate::err::panic_after_error(py), - } + // SAFETY: caller has upheld the safety contract + unsafe { Self::from_borrowed_ptr_or_opt(py, ptr) } + .unwrap_or_else(|| crate::err::panic_after_error(py)) } /// Create a `Py` instance by creating a new reference from the given FFI pointer. @@ -1982,7 +2025,8 @@ impl Py { py: Python<'_>, ptr: *mut ffi::PyObject, ) -> PyResult { - unsafe { Self::from_borrowed_ptr_or_opt(py, ptr).ok_or_else(|| PyErr::fetch(py)) } + // SAFETY: caller has upheld the safety contract + 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. @@ -1990,24 +2034,26 @@ impl Py { /// If `ptr` is null then `None` is returned. /// /// # Safety - /// `ptr` must be a pointer to a Python object of type T. + /// `ptr` must be a pointer to a Python object of type T, or null. #[inline] pub unsafe fn from_borrowed_ptr_or_opt( _py: Python<'_>, ptr: *mut ffi::PyObject, ) -> Option { - unsafe { - NonNull::new(ptr).map(|nonnull_ptr| { - ffi::Py_INCREF(ptr); - Py(nonnull_ptr, PhantomData) - }) - } + NonNull::new(ptr).map(|nonnull_ptr| { + // SAFETY: ptr is a valid python object, thread is attached to the interpreter + unsafe { ffi::Py_INCREF(ptr) }; + // SAFETY: caller has upheld the safety contract, and object was just made owned + unsafe { Self::from_non_null(nonnull_ptr) } + }) } /// For internal conversions. /// /// # Safety - /// `ptr` must point to a Python object of type T. + /// + /// `ptr` must point to an owned Python object type T. + #[inline(always)] unsafe fn from_non_null(ptr: NonNull) -> Self { Self(ptr, PhantomData) } @@ -2053,20 +2099,22 @@ impl std::convert::From> for Py { } } -impl<'a, T> std::convert::From> for Py +impl<'py, T> std::convert::From> for Py where T: PyClass, { - fn from(pyref: PyRef<'a, T>) -> Self { + fn from(pyref: PyRef<'py, T>) -> Self { + // SAFETY: PyRef::as_ptr returns a borrowed reference to a valid object unsafe { Py::from_borrowed_ptr(pyref.py(), pyref.as_ptr()) } } } -impl<'a, T> std::convert::From> for Py +impl<'py, T> std::convert::From> for Py where T: PyClass, { - fn from(pyref: PyRefMut<'a, T>) -> Self { + fn from(pyref: PyRefMut<'py, T>) -> Self { + // SAFETY: PyRefMut::as_ptr returns a borrowed reference to a valid object unsafe { Py::from_borrowed_ptr(pyref.py(), pyref.as_ptr()) } } } @@ -2264,6 +2312,7 @@ impl Py { // FIXME(icxolu) deprecate in favor of `Py::cast_bound_unchecked` #[inline] pub unsafe fn downcast_bound_unchecked<'py, T>(&self, py: Python<'py>) -> &Bound<'py, T> { + // SAFETY: caller has upheld the safety contract unsafe { self.cast_bound_unchecked(py) } } } @@ -2334,6 +2383,7 @@ impl Py { /// Callers must ensure that the type is valid or risk type confusion. #[inline] pub unsafe fn cast_bound_unchecked<'py, U>(&self, py: Python<'py>) -> &Bound<'py, U> { + // Safety: caller has upheld the safety contract unsafe { self.bind(py).cast_unchecked() } } } @@ -2343,7 +2393,7 @@ mod tests { use super::{Bound, IntoPyObject, Py}; use crate::test_utils::generate_unique_module_name; use crate::types::{dict::IntoPyDict, PyAnyMethods, PyCapsule, PyDict, PyString}; - use crate::{ffi, Borrowed, PyAny, PyResult, Python}; + use crate::{ffi, Borrowed, IntoPyObjectExt, PyAny, PyResult, Python}; use pyo3_ffi::c_str; use std::ffi::CStr; @@ -2597,8 +2647,8 @@ a = A() } #[test] + #[allow(clippy::undocumented_unsafe_blocks)] // Doing evil things to try to make `Bound` blow up fn bound_from_borrowed_ptr_constructors() { - // More detailed tests of the underlying semantics in pycell.rs Python::attach(|py| { fn check_drop<'py>( py: Python<'py>, @@ -2636,8 +2686,8 @@ a = A() } #[test] + #[allow(clippy::undocumented_unsafe_blocks)] // Doing evil things to try to make `Borrowed` blow up fn borrowed_ptr_constructors() { - // More detailed tests of the underlying semantics in pycell.rs Python::attach(|py| { fn check_drop<'py>( py: Python<'py>, @@ -2688,6 +2738,17 @@ a = A() }); } + #[test] + fn test_py_is_truthy() { + Python::attach(|py| { + let yes = true.into_py_any(py).unwrap(); + let no = false.into_py_any(py).unwrap(); + + assert!(yes.is_truthy(py).unwrap()); + assert!(!no.is_truthy(py).unwrap()); + }); + } + #[cfg(feature = "macros")] mod using_macros { use super::*; From d2be5387a96e760772832983a1d51dc60ec85118 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sun, 5 Oct 2025 22:13:20 +0100 Subject: [PATCH 891/936] apply some more uses of `assume_borrowed_unchecked` (#5494) * apply some more uses of `assume_borrowed_unchecked` * newsfragment, safety docs --- newsfragments/5494.changed.md | 1 + src/types/boolobject.rs | 3 ++- src/types/dict.rs | 7 ++++++- src/types/ellipsis.rs | 7 ++++++- src/types/list.rs | 20 +++++++++++++------- src/types/none.rs | 7 ++++++- src/types/notimplemented.rs | 3 ++- src/types/tuple.rs | 18 ++++++++++++++---- 8 files changed, 50 insertions(+), 16 deletions(-) create mode 100644 newsfragments/5494.changed.md diff --git a/newsfragments/5494.changed.md b/newsfragments/5494.changed.md new file mode 100644 index 00000000000..a66b4aadd1d --- /dev/null +++ b/newsfragments/5494.changed.md @@ -0,0 +1 @@ +`PyList::get_item_unchecked`, `PyTuple::get_item_unchecked`, and `PyTuple::get_borrowed_item_unchecked` no longer check for null values at the provided index. diff --git a/src/types/boolobject.rs b/src/types/boolobject.rs index 454b6afb03c..de81684e035 100644 --- a/src/types/boolobject.rs +++ b/src/types/boolobject.rs @@ -31,9 +31,10 @@ impl PyBool { /// `False` singletons #[inline] pub fn new(py: Python<'_>, val: bool) -> Borrowed<'_, '_, Self> { + // SAFETY: `Py_True` and `Py_False` are global singletons which are known to be boolean objects unsafe { if val { ffi::Py_True() } else { ffi::Py_False() } - .assume_borrowed(py) + .assume_borrowed_unchecked(py) .cast_unchecked() } } diff --git a/src/types/dict.rs b/src/types/dict.rs index 15d31e06f49..926354cc684 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -718,7 +718,12 @@ mod borrowed_iter { // Safety: // - PyDict_Next returns borrowed values // - we have already checked that `PyDict_Next` succeeded, so we can assume these to be non-null - Some(unsafe { (key.assume_borrowed(py), value.assume_borrowed(py)) }) + Some(unsafe { + ( + key.assume_borrowed_unchecked(py), + value.assume_borrowed_unchecked(py), + ) + }) } else { None } diff --git a/src/types/ellipsis.rs b/src/types/ellipsis.rs index 5aaa19a8c4b..523e13bbf9a 100644 --- a/src/types/ellipsis.rs +++ b/src/types/ellipsis.rs @@ -16,7 +16,12 @@ impl PyEllipsis { /// Returns the `Ellipsis` object. #[inline] pub fn get(py: Python<'_>) -> Borrowed<'_, '_, PyEllipsis> { - unsafe { ffi::Py_Ellipsis().assume_borrowed(py).cast_unchecked() } + // SAFETY: `Py_Ellipsis` is a global singleton which is known to be the ellipsis object + unsafe { + ffi::Py_Ellipsis() + .assume_borrowed_unchecked(py) + .cast_unchecked() + } } } diff --git a/src/types/list.rs b/src/types/list.rs index 0a2a32a3760..ad3e0741c88 100644 --- a/src/types/list.rs +++ b/src/types/list.rs @@ -135,13 +135,18 @@ pub trait PyListMethods<'py>: crate::sealed::Sealed { /// ``` fn get_item(&self, index: usize) -> PyResult>; - /// Gets the list item at the specified index. Undefined behavior on bad index. Use with caution. + /// Gets the list item at the specified index. Undefined behavior on bad index, or if the list + /// contains a null pointer at the specified index. Use with caution. /// /// # Safety /// - /// Caller must verify that the index is within the bounds of the list. - /// 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. + /// - Caller must verify that the index is within the bounds of the list. + /// - A null pointer is only legal in a list which is in the process of being initialized, callers + /// can typically assume the list item is non-null unless they are knowingly filling an + /// uninitialized list. (If a list were to contain a null pointer element, accessing it from Python + /// typically causes a segfault.) + /// - 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>; @@ -278,12 +283,13 @@ impl<'py> PyListMethods<'py> for Bound<'py, PyList> { /// Caller must verify that the index is within the bounds of the list. #[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). + // SAFETY: caller has upheld the safety contract unsafe { ffi::PyList_GET_ITEM(self.as_ptr(), index as Py_ssize_t) - .assume_borrowed(self.py()) - .to_owned() + .assume_borrowed_unchecked(self.py()) } + // PyList_GET_ITEM return borrowed ptr; must make owned for safety (see #890). + .to_owned() } /// Takes the slice `self[low:high]` and returns it as a new list. diff --git a/src/types/none.rs b/src/types/none.rs index 23f58883ab4..dc2a6b0123b 100644 --- a/src/types/none.rs +++ b/src/types/none.rs @@ -14,7 +14,12 @@ impl PyNone { /// Returns the `None` object. #[inline] pub fn get(py: Python<'_>) -> Borrowed<'_, '_, PyNone> { - unsafe { ffi::Py_None().assume_borrowed(py).cast_unchecked() } + // SAFETY: `Py_None` is a global singleton which is known to be the None object + unsafe { + ffi::Py_None() + .assume_borrowed_unchecked(py) + .cast_unchecked() + } } } diff --git a/src/types/notimplemented.rs b/src/types/notimplemented.rs index 36a8edb2e8c..d89b2fa64a6 100644 --- a/src/types/notimplemented.rs +++ b/src/types/notimplemented.rs @@ -16,9 +16,10 @@ impl PyNotImplemented { /// Returns the `NotImplemented` object. #[inline] pub fn get(py: Python<'_>) -> Borrowed<'_, '_, PyNotImplemented> { + // SAFETY: `Py_NotImplemented` is a global singleton which is known to be the NotImplemented object unsafe { ffi::Py_NotImplemented() - .assume_borrowed(py) + .assume_borrowed_unchecked(py) .cast_unchecked() } } diff --git a/src/types/tuple.rs b/src/types/tuple.rs index 18f59398b57..b216451b431 100644 --- a/src/types/tuple.rs +++ b/src/types/tuple.rs @@ -159,11 +159,16 @@ pub trait PyTupleMethods<'py>: crate::sealed::Sealed { /// by avoiding a reference count change. fn get_borrowed_item<'a>(&'a self, index: usize) -> PyResult>; - /// Gets the tuple item at the specified index. Undefined behavior on bad index. Use with caution. + /// Gets the tuple item at the specified index. Undefined behavior on bad index, or if the tuple + /// contains a null pointer at the specified index. Use with caution. /// /// # Safety /// - /// Caller must verify that the index is within the bounds of the tuple. + /// - Caller must verify that the index is within the bounds of the tuple. + /// - A null pointer is only legal in a tuple which is in the process of being initialized, callers + /// can typically assume the tuple item is non-null unless they are knowingly filling an + /// uninitialized tuple. (If a tuple were to contain a null pointer element, accessing it from Python + /// typically causes a segfault.) #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] unsafe fn get_item_unchecked(&self, index: usize) -> Bound<'py, PyAny>; @@ -172,7 +177,7 @@ pub trait PyTupleMethods<'py>: crate::sealed::Sealed { /// /// # Safety /// - /// Caller must verify that the index is within the bounds of the tuple. + /// See [`get_item_unchecked`][PyTupleMethods::get_item_unchecked]. #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] unsafe fn get_borrowed_item_unchecked<'a>(&'a self, index: usize) -> Borrowed<'a, 'py, PyAny>; @@ -304,10 +309,15 @@ impl<'a, 'py> Borrowed<'a, 'py, PyTuple> { } } + /// # Safety + /// + /// See `get_item_unchecked` in `PyTupleMethods`. #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] unsafe fn get_borrowed_item_unchecked(self, index: usize) -> Borrowed<'a, 'py, PyAny> { + // SAFETY: caller has upheld the safety contract unsafe { - ffi::PyTuple_GET_ITEM(self.as_ptr(), index as Py_ssize_t).assume_borrowed(self.py()) + ffi::PyTuple_GET_ITEM(self.as_ptr(), index as Py_ssize_t) + .assume_borrowed_unchecked(self.py()) } } From df974a1cef7ac42ee40824d80abdffcfa3bb7994 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Oct 2025 22:45:11 +0100 Subject: [PATCH 892/936] build(deps): update codspeed-criterion-compat requirement (#5498) Updates the requirements on [codspeed-criterion-compat](https://github.com/CodSpeedHQ/codspeed-rust) to permit the latest version. - [Release notes](https://github.com/CodSpeedHQ/codspeed-rust/releases) - [Commits](https://github.com/CodSpeedHQ/codspeed-rust/compare/v3.0.0...v4.0.2) --- updated-dependencies: - dependency-name: codspeed-criterion-compat dependency-version: 4.0.2 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pyo3-benches/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyo3-benches/Cargo.toml b/pyo3-benches/Cargo.toml index bd4899263ae..6927bfa237d 100644 --- a/pyo3-benches/Cargo.toml +++ b/pyo3-benches/Cargo.toml @@ -13,7 +13,7 @@ pyo3 = { path = "../", features = ["auto-initialize", "full"] } pyo3-build-config = { path = "../pyo3-build-config" } [dev-dependencies] -codspeed-criterion-compat = "3.0" +codspeed-criterion-compat = "4.0" criterion = "0.7.0" num-bigint = "0.4.3" rust_decimal = { version = "1.0.0", default-features = false } From bcab64d4e9c52c775fcaea922e7c449040dcfa47 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Wed, 8 Oct 2025 15:29:46 +0100 Subject: [PATCH 893/936] apply default lib name for `PYO3_CONFIG_FILE` after abi3 detection (#5503) * apply default lib name for `PYO3_CONFIG_FILE` after abi3 detection * apply lib name fixup after config file is loaded * remove needless type hint * undo whitespace change * don't try to use `TARGET` env var in `pyo3_build_config::get()` * fixups * allow testing default lib name fixup --- newsfragments/5503.fixed.md | 1 + pyo3-build-config/Cargo.toml | 3 + pyo3-build-config/src/impl_.rs | 139 +++++++++++++++++++++++++-------- pyo3-build-config/src/lib.rs | 85 ++++++++++++++------ pyo3-ffi/Cargo.toml | 2 +- pyo3-ffi/build.rs | 26 ++++-- 6 files changed, 191 insertions(+), 65 deletions(-) create mode 100644 newsfragments/5503.fixed.md diff --git a/newsfragments/5503.fixed.md b/newsfragments/5503.fixed.md new file mode 100644 index 00000000000..9fd8eab6274 --- /dev/null +++ b/newsfragments/5503.fixed.md @@ -0,0 +1 @@ +Fix failure to build for `abi3` interpreters on Windows using maturin's built-in sysconfig in combination with the `generate-import-lib` feature. diff --git a/pyo3-build-config/Cargo.toml b/pyo3-build-config/Cargo.toml index 719a42e7974..79d7e9b597c 100644 --- a/pyo3-build-config/Cargo.toml +++ b/pyo3-build-config/Cargo.toml @@ -29,6 +29,9 @@ resolve-config = [] # This feature is enabled by pyo3 when building an extension module. extension-module = [] +# Automatically generates `python3.dll` import libraries for Windows targets. +generate-import-lib = ["dep:python3-dll-a"] + # These features are enabled by pyo3 when building Stable ABI extension modules. abi3 = [] abi3-py37 = ["abi3-py38"] diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index ec582efba14..dd0fbb91a3e 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -2,7 +2,7 @@ //! and its build script. // Optional python3.dll import library generator for Windows -#[cfg(feature = "python3-dll-a")] +#[cfg(feature = "generate-import-lib")] #[path = "import_lib.rs"] mod import_lib; @@ -548,15 +548,6 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) 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, gil_disabled, &target) - } else { - None - } - }); Ok(InterpreterConfig { implementation, @@ -574,7 +565,25 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) }) } - #[cfg(feature = "python3-dll-a")] + /// Helper function to apply a default lib_name if none is set in `PYO3_CONFIG_FILE`. + /// + /// This requires knowledge of the final target, so cannot be done when the config file is + /// inlined into `pyo3-build-config` at build time and instead needs to be done when + /// resolving the build config for linking. + #[cfg(any(test, feature = "resolve-config"))] + pub(crate) fn apply_default_lib_name_to_config_file(&mut self, target: &Triple) { + if self.lib_name.is_none() { + self.lib_name = Some(default_lib_name_for_target( + self.version, + self.implementation, + self.abi3, + self.is_free_threaded(), + target, + )); + } + } + + #[cfg(feature = "generate-import-lib")] #[allow(clippy::unnecessary_wraps)] pub fn generate_import_libs(&mut self) -> Result<()> { // Auto generate python3.dll import libraries for Windows targets. @@ -603,7 +612,7 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED")) Ok(()) } - #[cfg(not(feature = "python3-dll-a"))] + #[cfg(not(feature = "generate-import-lib"))] #[allow(clippy::unnecessary_wraps)] pub fn generate_import_libs(&mut self) -> Result<()> { Ok(()) @@ -896,7 +905,7 @@ fn is_linking_libpython_for_target(target: &Triple) -> bool { /// /// Must be called from a PyO3 crate build script. fn require_libdir_for_target(target: &Triple) -> bool { - let is_generating_libpython = cfg!(feature = "python3-dll-a") + let is_generating_libpython = cfg!(feature = "generate-import-lib") && target.operating_system == OperatingSystem::Windows && is_abi3(); @@ -1572,7 +1581,7 @@ fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result Result Result Option { +) -> String { if target.operating_system == OperatingSystem::Windows { - 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, gil_disabled).unwrap()) + default_lib_name_windows(version, implementation, abi3, false, false, gil_disabled).unwrap() } else { - None + default_lib_name_unix(version, implementation, None, gil_disabled).unwrap() } } @@ -1963,7 +1959,7 @@ pub fn make_interpreter_config() -> Result { 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")] + #[cfg(feature = "generate-import-lib")] { let gil_disabled = interpreter_config .build_flags @@ -3222,4 +3218,79 @@ mod tests { // 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()))); } + + #[test] + fn test_apply_default_lib_name_to_config_file() { + let mut config = InterpreterConfig { + implementation: PythonImplementation::CPython, + version: PythonVersion { major: 3, minor: 9 }, + 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: None, + }; + + let unix = Triple::from_str("x86_64-unknown-linux-gnu").unwrap(); + let win_x64 = Triple::from_str("x86_64-pc-windows-msvc").unwrap(); + let win_arm64 = Triple::from_str("aarch64-pc-windows-msvc").unwrap(); + + config.apply_default_lib_name_to_config_file(&unix); + assert_eq!(config.lib_name, Some("python3.9".into())); + + config.lib_name = None; + config.apply_default_lib_name_to_config_file(&win_x64); + assert_eq!(config.lib_name, Some("python39".into())); + + config.lib_name = None; + config.apply_default_lib_name_to_config_file(&win_arm64); + assert_eq!(config.lib_name, Some("python39".into())); + + // PyPy + config.implementation = PythonImplementation::PyPy; + config.lib_name = None; + config.apply_default_lib_name_to_config_file(&unix); + assert_eq!(config.lib_name, Some("pypy3.9-c".into())); + + config.implementation = PythonImplementation::CPython; + + // Free-threaded + config.build_flags.0.insert(BuildFlag::Py_GIL_DISABLED); + config.version = PythonVersion { + major: 3, + minor: 13, + }; + config.lib_name = None; + config.apply_default_lib_name_to_config_file(&unix); + assert_eq!(config.lib_name, Some("python3.13t".into())); + + config.lib_name = None; + config.apply_default_lib_name_to_config_file(&win_x64); + assert_eq!(config.lib_name, Some("python313t".into())); + + config.lib_name = None; + config.apply_default_lib_name_to_config_file(&win_arm64); + assert_eq!(config.lib_name, Some("python313t".into())); + + config.build_flags.0.remove(&BuildFlag::Py_GIL_DISABLED); + + // abi3 + config.abi3 = true; + config.lib_name = None; + config.apply_default_lib_name_to_config_file(&unix); + assert_eq!(config.lib_name, Some("python3.13".into())); + + config.lib_name = None; + config.apply_default_lib_name_to_config_file(&win_x64); + assert_eq!(config.lib_name, Some("python3".into())); + + config.lib_name = None; + config.apply_default_lib_name_to_config_file(&win_arm64); + assert_eq!(config.lib_name, Some("python3".into())); + } } diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index 257cb125d26..3ce21ee1916 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -22,6 +22,7 @@ pub use impl_::{ cross_compiling_from_to, find_all_sysconfigdata, parse_sysconfigdata, BuildFlag, BuildFlags, CrossCompileConfig, InterpreterConfig, PythonImplementation, PythonVersion, Triple, }; + use target_lexicon::OperatingSystem; /// Adds all the [`#[cfg]` flags](index.html) to the current compilation. @@ -82,9 +83,8 @@ fn _add_extension_module_link_args(triple: &Triple, mut writer: impl std::io::Wr /// 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, + get(), &impl_::target_triple_from_env(), impl_::is_linking_libpython(), std::io::stdout(), @@ -119,12 +119,12 @@ 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 + // CONFIG_FILE is generated in build.rs, so its 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() { - InterpreterConfig::from_reader(Cursor::new(CONFIG_FILE)) + } else if let Some(interpreter_config) = config_from_pyo3_config_file_env() { + Ok(interpreter_config) } else if cross_compiling { InterpreterConfig::from_path(cross_compile_config_path.as_ref().unwrap()) } else { @@ -134,13 +134,26 @@ pub fn get() -> &'static InterpreterConfig { }) } -/// Build configuration provided by `PYO3_CONFIG_FILE`. May be empty if env var not set. -#[doc(hidden)] +/// Build configuration provided by `PYO3_CONFIG_FILE`, inlined into the `pyo3-build-config` binary. #[cfg(feature = "resolve-config")] -const CONFIG_FILE: &str = include_str!(concat!(env!("OUT_DIR"), "/pyo3-build-config-file.txt")); +fn config_from_pyo3_config_file_env() -> Option { + #[doc(hidden)] + const CONFIG_FILE: &str = include_str!(concat!(env!("OUT_DIR"), "/pyo3-build-config-file.txt")); + + // CONFIG_FILE is generated in build.rs, so its content can vary + // TODO: `unknown_lints` allow not needed on MSRV 1.79+ + #[allow(unknown_lints, clippy::const_is_empty)] + if !CONFIG_FILE.is_empty() { + let config = InterpreterConfig::from_reader(Cursor::new(CONFIG_FILE)) + .expect("contents of CONFIG_FILE should always be valid (generated by pyo3-build-config's build.rs)"); + Some(config) + } else { + None + } +} /// Build configuration discovered by `pyo3-build-config` build script. Not aware of -/// cross-compilation settings. +/// cross-compilation settings. Not generated if `PYO3_CONFIG_FILE` is set. #[doc(hidden)] #[cfg(feature = "resolve-config")] const HOST_CONFIG: &str = include_str!(concat!(env!("OUT_DIR"), "/pyo3-build-config.txt")); @@ -233,27 +246,48 @@ pub mod pyo3_build_script_impl { pub use crate::errors::*; } pub use crate::impl_::{ - cargo_env_var, env_var, is_linking_libpython, make_cross_compile_config, InterpreterConfig, - PythonVersion, + cargo_env_var, env_var, is_linking_libpython, make_cross_compile_config, + target_triple_from_env, InterpreterConfig, PythonVersion, }; - /// Gets the configuration for use from PyO3's build script. + pub enum BuildConfigSource { + /// Config was provided by `PYO3_CONFIG_FILE`. + ConfigFile, + /// Config was found by an interpreter on the host system. + Host, + /// Config was configured by cross-compilation settings. + CrossCompile, + } + + pub struct BuildConfig { + pub interpreter_config: InterpreterConfig, + pub source: BuildConfigSource, + } + + /// Gets the configuration for use from `pyo3-ffi`'s build script. + /// + /// Differs from `.get()` in three ways: + /// 1. The cargo_dep_env config is not yet available (exported by `pyo3-ffi`'s build script). + /// 1. If `PYO3_CONFIG_FILE` is set, lib name is fixed up and the windows import libs might be generated. + /// 2. The cross-compile config file is generated if necessary. /// - /// Differs from .get() above only in the cross-compile case, where PyO3's build script is - /// required to generate a new config (as it's the first build script which has access to the - /// correct value for CARGO_CFG_TARGET_OS). + /// Steps 2 and 3 are necessary because `pyo3-ffi`'s build script is the first code run which knows + /// the correct target triple. #[cfg(feature = "resolve-config")] - pub fn resolve_interpreter_config() -> Result { + pub fn resolve_build_config(target: &Triple) -> 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 interpreter_config = InterpreterConfig::from_reader(Cursor::new(CONFIG_FILE))?; + if let Some(mut interpreter_config) = config_from_pyo3_config_file_env() { + interpreter_config.apply_default_lib_name_to_config_file(target); interpreter_config.generate_import_libs()?; - Ok(interpreter_config) + Ok(BuildConfig { + interpreter_config, + source: BuildConfigSource::ConfigFile, + }) } else if let Some(interpreter_config) = make_cross_compile_config()? { // This is a cross compile and need to write the config file. let path = resolve_cross_compile_config_path() - .expect("resolve_interpreter_config() must be called from a build script"); + .expect("resolve_build_config() must be called from a build script"); let parent_dir = path.parent().ok_or_else(|| { format!( "failed to resolve parent directory of config file {}", @@ -269,9 +303,16 @@ pub mod pyo3_build_script_impl { interpreter_config.to_writer(&mut std::fs::File::create(&path).with_context( || format!("failed to create config file at {}", path.display()), )?)?; - Ok(interpreter_config) + Ok(BuildConfig { + interpreter_config, + source: BuildConfigSource::CrossCompile, + }) } else { - InterpreterConfig::from_reader(Cursor::new(HOST_CONFIG)) + let interpreter_config = InterpreterConfig::from_reader(Cursor::new(HOST_CONFIG))?; + Ok(BuildConfig { + interpreter_config, + source: BuildConfigSource::Host, + }) } } } diff --git a/pyo3-ffi/Cargo.toml b/pyo3-ffi/Cargo.toml index 78197c7c263..5c4fe70ee6f 100644 --- a/pyo3-ffi/Cargo.toml +++ b/pyo3-ffi/Cargo.toml @@ -38,7 +38,7 @@ abi3-py313 = ["abi3-py314", "pyo3-build-config/abi3-py313"] abi3-py314 = ["abi3", "pyo3-build-config/abi3-py314"] # Automatically generates `python3.dll` import libraries for Windows targets. -generate-import-lib = ["pyo3-build-config/python3-dll-a"] +generate-import-lib = ["pyo3-build-config/generate-import-lib"] [dev-dependencies] paste = "1" diff --git a/pyo3-ffi/build.rs b/pyo3-ffi/build.rs index 8c935501954..6118e81a46f 100644 --- a/pyo3-ffi/build.rs +++ b/pyo3-ffi/build.rs @@ -1,8 +1,8 @@ use pyo3_build_config::{ bail, ensure, print_feature_cfgs, pyo3_build_script_impl::{ - cargo_env_var, env_var, errors::Result, is_linking_libpython, resolve_interpreter_config, - InterpreterConfig, PythonVersion, + cargo_env_var, env_var, errors::Result, is_linking_libpython, resolve_build_config, + target_triple_from_env, BuildConfig, BuildConfigSource, InterpreterConfig, PythonVersion, }, warn, PythonImplementation, }; @@ -149,7 +149,8 @@ fn ensure_target_pointer_width(interpreter_config: &InterpreterConfig) -> Result Ok(()) } -fn emit_link_config(interpreter_config: &InterpreterConfig) -> Result<()> { +fn emit_link_config(build_config: &BuildConfig) -> Result<()> { + let interpreter_config = &build_config.interpreter_config; let target_os = cargo_env_var("CARGO_CFG_TARGET_OS").unwrap(); println!( @@ -171,6 +172,13 @@ fn emit_link_config(interpreter_config: &InterpreterConfig) -> Result<()> { if let Some(lib_dir) = &interpreter_config.lib_dir { println!("cargo:rustc-link-search=native={lib_dir}"); + } else if matches!(build_config.source, BuildConfigSource::CrossCompile) { + warn!( + "The output binary will link to libpython, \ + but PYO3_CROSS_LIB_DIR environment variable is not set. \ + Ensure that the target Python library directory is \ + in the rustc native library search path." + ); } Ok(()) @@ -184,20 +192,22 @@ fn emit_link_config(interpreter_config: &InterpreterConfig) -> Result<()> { /// Emits the cargo configuration based on this config as well as a few checks of the Rust compiler /// version to enable features which aren't supported on MSRV. fn configure_pyo3() -> Result<()> { - let interpreter_config = resolve_interpreter_config()?; + let target = target_triple_from_env(); + let build_config = resolve_build_config(&target)?; + let interpreter_config = &build_config.interpreter_config; if env_var("PYO3_PRINT_CONFIG").is_some_and(|os_str| os_str == "1") { - print_config_and_exit(&interpreter_config); + print_config_and_exit(interpreter_config); } - ensure_python_version(&interpreter_config)?; - ensure_target_pointer_width(&interpreter_config)?; + ensure_python_version(interpreter_config)?; + ensure_target_pointer_width(interpreter_config)?; // Serialize the whole interpreter config into DEP_PYTHON_PYO3_CONFIG env var. interpreter_config.to_cargo_dep_env()?; if is_linking_libpython() && !interpreter_config.suppress_build_script_link_lines { - emit_link_config(&interpreter_config)?; + emit_link_config(&build_config)?; } for cfg in interpreter_config.build_script_outputs() { From cfe0a256fb9e14053db4b9eb2a9f25eb8faf9545 Mon Sep 17 00:00:00 2001 From: Icxolu <10486322+Icxolu@users.noreply.github.com> Date: Wed, 8 Oct 2025 16:32:58 +0200 Subject: [PATCH 894/936] move away from the `FromPyObject` blanket for `PyClass + Clone` (#5488) * move `FromPyObject` pyclass blanket into macro * add opt-out option * add tests * add newsfragments * fix typo Co-authored-by: David Hewitt --------- Co-authored-by: David Hewitt --- guide/pyclass-parameters.md | 1 + guide/src/conversions/traits.md | 23 +++++++++++ newsfragments/5488.added.md | 1 + pyo3-macros-backend/src/attributes.rs | 1 + pyo3-macros-backend/src/pyclass.rs | 15 +++++++ src/conversion.rs | 38 ++++++++++++++++- src/impl_/pyclass.rs | 2 + tests/ui/invalid_pyclass_args.stderr | 4 +- tests/ui/invalid_pyfunction_argument.rs | 9 ++++ tests/ui/invalid_pyfunction_argument.stderr | 46 ++++++++++++++++++++- 10 files changed, 136 insertions(+), 4 deletions(-) create mode 100644 newsfragments/5488.added.md diff --git a/guide/pyclass-parameters.md b/guide/pyclass-parameters.md index 8fdb2122ff8..0f60a7599b2 100644 --- a/guide/pyclass-parameters.md +++ b/guide/pyclass-parameters.md @@ -21,6 +21,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. | +| `skip_from_py_object` | Prevents this PyClass from participating in the `FromPyObject: PyClass + Clone` blanket implementation. This allows a custom `FromPyObject` impl, even if `self` is `Clone`. | | `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. | | `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. | diff --git a/guide/src/conversions/traits.md b/guide/src/conversions/traits.md index 704a43d9aab..98bd2864809 100644 --- a/guide/src/conversions/traits.md +++ b/guide/src/conversions/traits.md @@ -535,6 +535,29 @@ struct RustyStruct { # } ``` +#### ⚠ Phase-Out of `FromPyObject` blanket implementation for cloneable PyClasses ⚠ +Historically PyO3 has provided a blanket implementation for `#[pyclass]` types that also implement `Clone`, to allow extraction of such types by value. Over time this has turned out problematic for a few reasons, the major one being the prevention of custom conversions by downstream crates if their type is `Clone`. Over the next few releases the blanket implementation is gradually phased out, and eventually replaced by an opt-in option. As a first step of this migration a new `skip_from_py_object` option for `#[pyclass]` was introduced, to opt-out of the blanket implementation and allow downstream users to provide their own implementation: +```rust +# #![allow(dead_code)] +# use pyo3::prelude::*; + +#[pyclass(skip_from_py_object)] // opt-out of the PyO3 FromPyObject blanket +#[derive(Clone)] +struct Number(i32); + +impl<'py> FromPyObject<'_, 'py> for Number { + type Error = PyErr; + + fn extract(obj: pyo3::Borrowed<'_, 'py, pyo3::PyAny>) -> Result { + if let Ok(obj) = obj.cast::() { // first try extraction via class object + Ok(obj.borrow().clone()) + } else { + obj.extract::().map(Self) // otherwise try integer directly + } + } +} +``` + ### `IntoPyObject` 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`. diff --git a/newsfragments/5488.added.md b/newsfragments/5488.added.md new file mode 100644 index 00000000000..06b6a46bb7b --- /dev/null +++ b/newsfragments/5488.added.md @@ -0,0 +1 @@ +add `skip_from_py_object` pyclass option, to opt-out of the `FromPyObject: PyClass + Clone` blanket impl diff --git a/pyo3-macros-backend/src/attributes.rs b/pyo3-macros-backend/src/attributes.rs index 0355d5d4bec..64420176a52 100644 --- a/pyo3-macros-backend/src/attributes.rs +++ b/pyo3-macros-backend/src/attributes.rs @@ -53,6 +53,7 @@ pub mod kw { syn::custom_keyword!(warn); syn::custom_keyword!(message); syn::custom_keyword!(category); + syn::custom_keyword!(skip_from_py_object); } fn take_int(read: &mut &str, tracker: &mut usize) -> String { diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 0f8eea038d9..bb8a863cb65 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -90,6 +90,7 @@ pub struct PyClassPyO3Options { pub unsendable: Option, pub weakref: Option, pub generic: Option, + pub skip_from_py_object: Option, } pub enum PyClassPyO3Option { @@ -115,6 +116,7 @@ pub enum PyClassPyO3Option { Unsendable(kw::unsendable), Weakref(kw::weakref), Generic(kw::generic), + SkipFromPyObject(kw::skip_from_py_object), } impl Parse for PyClassPyO3Option { @@ -164,6 +166,8 @@ impl Parse for PyClassPyO3Option { input.parse().map(PyClassPyO3Option::Weakref) } else if lookahead.peek(attributes::kw::generic) { input.parse().map(PyClassPyO3Option::Generic) + } else if lookahead.peek(attributes::kw::skip_from_py_object) { + input.parse().map(PyClassPyO3Option::SkipFromPyObject) } else { Err(lookahead.error()) } @@ -243,6 +247,9 @@ impl PyClassPyO3Options { set_option!(weakref); } PyClassPyO3Option::Generic(generic) => set_option!(generic), + PyClassPyO3Option::SkipFromPyObject(skip_from_py_object) => { + set_option!(skip_from_py_object) + } } Ok(()) } @@ -2497,7 +2504,15 @@ impl<'a> PyClassImplsBuilder<'a> { quote! {} }; + let extract_pyclass_with_clone = if self.attr.options.skip_from_py_object.is_none() { + quote!( impl #pyo3_path::impl_::pyclass::ExtractPyClassWithClone for #cls {} ) + } else { + TokenStream::new() + }; + Ok(quote! { + #extract_pyclass_with_clone + #assertions #pyclass_base_type_impl diff --git a/src/conversion.rs b/src/conversion.rs index fbb1eae2150..77aa078a201 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -1,5 +1,6 @@ //! Defines conversions between Rust and Python types. use crate::err::PyResult; +use crate::impl_::pyclass::ExtractPyClassWithClone; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; use crate::pyclass::boolean_struct::False; @@ -529,7 +530,7 @@ impl<'py, T> FromPyObjectOwned<'py> for T where T: for<'a> FromPyObject<'a, 'py> impl<'a, 'py, T> FromPyObject<'a, 'py> for T where - T: PyClass + Clone, + T: PyClass + Clone + ExtractPyClassWithClone, { type Error = PyClassGuardError<'a, 'py>; @@ -601,3 +602,38 @@ impl<'py> IntoPyObject<'py> for () { /// }) /// ``` mod test_no_clone {} + +#[cfg(test)] +mod tests { + #[test] + #[cfg(feature = "macros")] + fn test_pyclass_skip_from_py_object() { + use crate::{types::PyAnyMethods, FromPyObject, IntoPyObject, PyErr, Python}; + + #[crate::pyclass(crate = "crate", skip_from_py_object)] + #[derive(Clone)] + struct Foo(i32); + + impl<'py> FromPyObject<'_, 'py> for Foo { + type Error = PyErr; + + fn extract(obj: crate::Borrowed<'_, 'py, crate::PyAny>) -> Result { + if let Ok(obj) = obj.cast::() { + Ok(obj.borrow().clone()) + } else { + obj.extract::().map(Self) + } + } + } + Python::attach(|py| { + let foo1 = 42i32.into_pyobject(py)?; + assert_eq!(foo1.extract::()?.0, 42); + + let foo2 = Foo(0).into_pyobject(py)?; + assert_eq!(foo2.extract::()?.0, 0); + + Ok::<_, PyErr>(()) + }) + .unwrap(); + } +} diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index 3fda206eb5b..7666a883af7 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -1426,6 +1426,8 @@ impl ConvertField tests/ui/invalid_pyclass_args.rs:4:11 | 4 | #[pyclass(extend=pyo3::types::PyDict)] @@ -46,7 +46,7 @@ error: expected string literal 25 | #[pyclass(module = my_module)] | ^^^^^^^^^ -error: expected one of: `crate`, `dict`, `eq`, `eq_int`, `extends`, `freelist`, `frozen`, `get_all`, `hash`, `immutable_type`, `mapping`, `module`, `name`, `ord`, `rename_all`, `sequence`, `set_all`, `str`, `subclass`, `unsendable`, `weakref`, `generic` +error: expected one of: `crate`, `dict`, `eq`, `eq_int`, `extends`, `freelist`, `frozen`, `get_all`, `hash`, `immutable_type`, `mapping`, `module`, `name`, `ord`, `rename_all`, `sequence`, `set_all`, `str`, `subclass`, `unsendable`, `weakref`, `generic`, `skip_from_py_object` --> tests/ui/invalid_pyclass_args.rs:28:11 | 28 | #[pyclass(weakrev)] diff --git a/tests/ui/invalid_pyfunction_argument.rs b/tests/ui/invalid_pyfunction_argument.rs index 585d8d5d082..0505c2f0ad0 100644 --- a/tests/ui/invalid_pyfunction_argument.rs +++ b/tests/ui/invalid_pyfunction_argument.rs @@ -6,4 +6,13 @@ fn invalid_pyfunction_argument(arg: AtomicPtr<()>) { let _ = arg; } +#[pyclass(skip_from_py_object)] +#[derive(Clone)] +struct Foo; + +#[pyfunction] +fn skip_from_py_object_without_custom_from_py_object(arg: Foo) { + let _ = arg; +} + fn main() {} diff --git a/tests/ui/invalid_pyfunction_argument.stderr b/tests/ui/invalid_pyfunction_argument.stderr index 3bd9b4bc77c..fe867f6a797 100644 --- a/tests/ui/invalid_pyfunction_argument.stderr +++ b/tests/ui/invalid_pyfunction_argument.stderr @@ -6,6 +6,26 @@ error[E0277]: `AtomicPtr<()>` cannot be used as a Python function argument | = note: implement `FromPyObject` to enable using `AtomicPtr<()>` as a function argument = note: `Python<'py>` is also a valid argument type to pass the Python token into `#[pyfunction]`s and `#[pymethods]` + = help: the trait `PyClass` is implemented for `Foo` + = note: required for `AtomicPtr<()>` to implement `FromPyObject<'_, '_>` + = note: required for `AtomicPtr<()>` to implement `PyFunctionArgument<'_, '_, '_, true>` +note: required by a bound in `extract_argument` + --> src/impl_/extract_argument.rs + | + | pub fn extract_argument<'a, 'holder, 'py, T, const IMPLEMENTS_FROMPYOBJECT: bool>( + | ---------------- required by a bound in this function +... + | T: PyFunctionArgument<'a, 'holder, 'py, IMPLEMENTS_FROMPYOBJECT>, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `extract_argument` + +error[E0277]: `AtomicPtr<()>` cannot be used as a Python function argument + --> tests/ui/invalid_pyfunction_argument.rs:5:37 + | +5 | fn invalid_pyfunction_argument(arg: AtomicPtr<()>) { + | ^^^^^^^^^ the trait `Clone` is not implemented for `AtomicPtr<()>` + | + = note: implement `FromPyObject` to enable using `AtomicPtr<()>` as a function argument + = note: `Python<'py>` is also a valid argument type to pass the Python token into `#[pyfunction]`s and `#[pymethods]` = help: the following other types implement trait `PyFunctionArgument<'a, 'holder, 'py, IMPLEMENTS_FROMPYOBJECT>`: `&'a pyo3::Bound<'py, T>` implements `PyFunctionArgument<'a, '_, 'py, false>` `&'holder T` implements `PyFunctionArgument<'a, 'holder, '_, false>` @@ -26,7 +46,7 @@ error[E0277]: `AtomicPtr<()>` cannot be used as a Python function argument --> tests/ui/invalid_pyfunction_argument.rs:5:37 | 5 | fn invalid_pyfunction_argument(arg: AtomicPtr<()>) { - | ^^^^^^^^^ the trait `Clone` is not implemented for `AtomicPtr<()>` + | ^^^^^^^^^ the trait `ExtractPyClassWithClone` is not implemented for `AtomicPtr<()>` | = note: implement `FromPyObject` to enable using `AtomicPtr<()>` as a function argument = note: `Python<'py>` is also a valid argument type to pass the Python token into `#[pyfunction]`s and `#[pymethods]` @@ -45,3 +65,27 @@ note: required by a bound in `extract_argument` ... | T: PyFunctionArgument<'a, 'holder, 'py, IMPLEMENTS_FROMPYOBJECT>, | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `extract_argument` + +error[E0277]: `Foo` cannot be used as a Python function argument + --> tests/ui/invalid_pyfunction_argument.rs:14:59 + | +14 | fn skip_from_py_object_without_custom_from_py_object(arg: Foo) { + | ^^^ the trait `ExtractPyClassWithClone` is not implemented for `Foo` + | + = note: implement `FromPyObject` to enable using `Foo` as a function argument + = note: `Python<'py>` is also a valid argument type to pass the Python token into `#[pyfunction]`s and `#[pymethods]` + = help: the following other types implement trait `PyFunctionArgument<'a, 'holder, 'py, IMPLEMENTS_FROMPYOBJECT>`: + `&'a pyo3::Bound<'py, T>` implements `PyFunctionArgument<'a, '_, 'py, false>` + `&'holder T` implements `PyFunctionArgument<'a, 'holder, '_, false>` + `&'holder mut T` implements `PyFunctionArgument<'a, 'holder, '_, false>` + `Option` implements `PyFunctionArgument<'a, 'holder, 'py, false>` + = note: required for `Foo` to implement `FromPyObject<'_, '_>` + = note: required for `Foo` to implement `PyFunctionArgument<'_, '_, '_, true>` +note: required by a bound in `extract_argument` + --> src/impl_/extract_argument.rs + | + | pub fn extract_argument<'a, 'holder, 'py, T, const IMPLEMENTS_FROMPYOBJECT: bool>( + | ---------------- required by a bound in this function +... + | T: PyFunctionArgument<'a, 'holder, 'py, IMPLEMENTS_FROMPYOBJECT>, + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `extract_argument` From 4c56b0d25db53ec1fe70e9420b6e70ff542e0e60 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Wed, 8 Oct 2025 15:34:16 +0100 Subject: [PATCH 895/936] use `PyLong_FromNativeBytes` in `num-bigint` conversion (#5471) * use `PyLong_FromNativeBytes` in `num-bigint` conversion * `PyLong_AsNativeBytes` not available on stable abi * remove incorrect cfg * newsfragment, tidy name * Update Cargo.toml Co-authored-by: Bas Schoenmaeckers <7943856+bschoenmaeckers@users.noreply.github.com> * fixup * fixup GraalPy config * bump `num-bigint` minimal version --------- Co-authored-by: Bas Schoenmaeckers <7943856+bschoenmaeckers@users.noreply.github.com> --- Cargo.toml | 5 ++- newsfragments/5471.changed.md | 1 + newsfragments/5471.packaging.md | 1 + src/conversions/mod.rs | 2 +- src/conversions/num_bigint.rs | 68 +++++++++++++++--------------- src/conversions/std/mod.rs | 2 +- src/conversions/std/num.rs | 75 ++++++++++++++++----------------- 7 files changed, 80 insertions(+), 74 deletions(-) create mode 100644 newsfragments/5471.changed.md create mode 100644 newsfragments/5471.packaging.md diff --git a/Cargo.toml b/Cargo.toml index c47ecf0affb..5199667b7b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,9 +51,10 @@ eyre = { version = ">= 0.6.8, < 0.7", optional = true } hashbrown = { version = ">= 0.15.0, < 0.17", optional = true, default-features = false } 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-bigint = { version = "0.4.4", optional = true } num-complex = { version = ">= 0.4.6, < 0.5", optional = true } num-rational = { version = "0.4.1", optional = true } +num-traits = { version = "0.2.16", optional = true } ordered-float = { version = "5.0.0", default-features = false, optional = true } rust_decimal = { version = "1.15", default-features = false, optional = true } time = { version = "0.3.38", default-features = false, optional = true } @@ -135,10 +136,12 @@ py-clone = [] parking_lot = ["dep:parking_lot", "lock_api"] arc_lock = ["lock_api", "lock_api/arc_lock", "parking_lot?/arc_lock"] +num-bigint = ["dep:num-bigint", "dep:num-traits"] bigdecimal = ["dep:bigdecimal", "num-bigint"] chrono-local = ["chrono/clock", "dep:iana-time-zone"] + # Optimizes PyObject to Vec conversion and so on. nightly = [] diff --git a/newsfragments/5471.changed.md b/newsfragments/5471.changed.md new file mode 100644 index 00000000000..56f1d369156 --- /dev/null +++ b/newsfragments/5471.changed.md @@ -0,0 +1 @@ +Enable fast-path for 128-bit integer conversions on `GraalPy`. diff --git a/newsfragments/5471.packaging.md b/newsfragments/5471.packaging.md new file mode 100644 index 00000000000..0b4bba9e3fa --- /dev/null +++ b/newsfragments/5471.packaging.md @@ -0,0 +1 @@ +Bump optional `num-bigint` dependency minimum version to 0.4.4. diff --git a/src/conversions/mod.rs b/src/conversions/mod.rs index 05e2ea1d1d0..921e4ea40c6 100644 --- a/src/conversions/mod.rs +++ b/src/conversions/mod.rs @@ -17,6 +17,6 @@ pub mod ordered_float; pub mod rust_decimal; pub mod serde; pub mod smallvec; -mod std; +pub(crate) mod std; pub mod time; pub mod uuid; diff --git a/src/conversions/num_bigint.rs b/src/conversions/num_bigint.rs index 4384231c801..3344870dc7f 100644 --- a/src/conversions/num_bigint.rs +++ b/src/conversions/num_bigint.rs @@ -61,7 +61,7 @@ use num_bigint::Sign; // for identical functionality between BigInt and BigUint macro_rules! bigint_conversion { - ($rust_ty: ty, $is_signed: literal, $to_bytes: path) => { + ($rust_ty: ty, $is_signed: literal) => { #[cfg_attr(docsrs, doc(cfg(feature = "num-bigint")))] impl<'py> IntoPyObject<'py> for $rust_ty { type Target = PyInt; @@ -80,47 +80,49 @@ macro_rules! bigint_conversion { 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) - .cast_into_unchecked()) + use num_traits::ToBytes; + + #[cfg(all(not(Py_LIMITED_API), Py_3_13))] + { + use crate::conversions::std::num::int_from_ne_bytes; + let bytes = self.to_ne_bytes(); + Ok(int_from_ne_bytes::<{ $is_signed }>(py, &bytes)) } - } - #[cfg(Py_LIMITED_API)] - fn into_pyobject(self, py: Python<'py>) -> Result { - use $crate::py_result_ext::PyResultExt; - use $crate::types::any::PyAnyMethods; - 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::() - .call_method("from_bytes", (bytes_obj, "little"), kwargs.as_ref()) - .cast_into_unchecked() + #[cfg(all(not(Py_LIMITED_API), not(Py_3_13)))] + { + use crate::conversions::std::num::int_from_le_bytes; + let bytes = self.to_le_bytes(); + Ok(int_from_le_bytes::<{ $is_signed }>(py, &bytes)) + } + + #[cfg(Py_LIMITED_API)] + { + use $crate::py_result_ext::PyResultExt; + use $crate::types::any::PyAnyMethods; + let bytes = self.to_le_bytes(); + 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::() + .call_method("from_bytes", (bytes_obj, "little"), kwargs.as_ref()) + .cast_into_unchecked() + } } } } }; } -bigint_conversion!(BigUint, false, BigUint::to_bytes_le); -bigint_conversion!(BigInt, true, BigInt::to_signed_bytes_le); +bigint_conversion!(BigUint, false); +bigint_conversion!(BigInt, true); #[cfg_attr(docsrs, doc(cfg(feature = "num-bigint")))] impl<'py> FromPyObject<'_, 'py> for BigInt { diff --git a/src/conversions/std/mod.rs b/src/conversions/std/mod.rs index d2f640dc467..c82f2eba98f 100644 --- a/src/conversions/std/mod.rs +++ b/src/conversions/std/mod.rs @@ -3,7 +3,7 @@ mod cell; mod cstring; mod ipaddr; mod map; -mod num; +pub(crate) mod num; mod option; mod osstr; mod path; diff --git a/src/conversions/std/num.rs b/src/conversions/std/num.rs index 80517d5c0e5..a2ccf64f818 100644 --- a/src/conversions/std/num.rs +++ b/src/conversions/std/num.rs @@ -419,7 +419,7 @@ int_convert_u64_or_i64!( true ); -#[cfg(all(not(Py_LIMITED_API), not(GraalPy)))] +#[cfg(not(Py_LIMITED_API))] mod fast_128bit_int_conversion { use super::*; @@ -435,45 +435,15 @@ mod fast_128bit_int_conversion { const OUTPUT_TYPE: &'static str = "int"; fn into_pyobject(self, py: Python<'py>) -> Result { - #[cfg(not(Py_3_13))] - { - let bytes = self.to_le_bytes(); - unsafe { - Ok(ffi::_PyLong_FromByteArray( - bytes.as_ptr().cast(), - bytes.len(), - 1, - $is_signed.into(), - ) - .assume_owned(py) - .cast_into_unchecked()) - } - } #[cfg(Py_3_13)] { let bytes = self.to_ne_bytes(); - - if $is_signed { - unsafe { - Ok(ffi::PyLong_FromNativeBytes( - bytes.as_ptr().cast(), - bytes.len(), - ffi::Py_ASNATIVEBYTES_NATIVE_ENDIAN, - ) - .assume_owned(py) - .cast_into_unchecked()) - } - } else { - unsafe { - Ok(ffi::PyLong_FromUnsignedNativeBytes( - bytes.as_ptr().cast(), - bytes.len(), - ffi::Py_ASNATIVEBYTES_NATIVE_ENDIAN, - ) - .assume_owned(py) - .cast_into_unchecked()) - } - } + Ok(int_from_ne_bytes::<{ $is_signed }>(py, &bytes)) + } + #[cfg(not(Py_3_13))] + { + let bytes = self.to_le_bytes(); + Ok(int_from_le_bytes::<{ $is_signed }>(py, &bytes)) } } @@ -566,8 +536,37 @@ mod fast_128bit_int_conversion { int_convert_128!(u128, false); } +#[cfg(all(not(Py_LIMITED_API), not(Py_3_13)))] +pub(crate) fn int_from_le_bytes<'py, const IS_SIGNED: bool>( + py: Python<'py>, + bytes: &[u8], +) -> Bound<'py, PyInt> { + unsafe { + ffi::_PyLong_FromByteArray(bytes.as_ptr().cast(), bytes.len(), 1, IS_SIGNED.into()) + .assume_owned(py) + .cast_into_unchecked() + } +} + +#[cfg(all(Py_3_13, not(Py_LIMITED_API)))] +pub(crate) fn int_from_ne_bytes<'py, const IS_SIGNED: bool>( + py: Python<'py>, + bytes: &[u8], +) -> Bound<'py, PyInt> { + let flags = if IS_SIGNED { + ffi::Py_ASNATIVEBYTES_NATIVE_ENDIAN + } else { + ffi::Py_ASNATIVEBYTES_NATIVE_ENDIAN | ffi::Py_ASNATIVEBYTES_UNSIGNED_BUFFER + }; + unsafe { + ffi::PyLong_FromNativeBytes(bytes.as_ptr().cast(), bytes.len(), flags) + .assume_owned(py) + .cast_into_unchecked() + } +} + // For ABI3 we implement the conversion manually. -#[cfg(any(Py_LIMITED_API, GraalPy))] +#[cfg(Py_LIMITED_API)] mod slow_128bit_int_conversion { use super::*; use crate::types::any::PyAnyMethods as _; From 17ecd26c664d39b14484faa272ea53bd1375498d Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Wed, 8 Oct 2025 16:12:06 +0100 Subject: [PATCH 896/936] docs: remove left-to-right mark from `Contributing.md` (#5504) --- Contributing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Contributing.md b/Contributing.md index 63d51a6c697..03bbf4420e5 100644 --- a/Contributing.md +++ b/Contributing.md @@ -184,7 +184,7 @@ PyO3 supports all officially supported Python versions, as well as the latest Py If you plan to add support for a pre-release version of CPython, here's a (non-exhaustive) checklist: - [ ] Wait until the last alpha release (usually alpha7), since ABI is not guaranteed until the first beta release - - [ ] Add prerelease_ver-dev (e.g. `3.14-dev`) to `‎.github/workflows/ci.yml`, and bump version in `noxfile.py`, `pyo3-ffi/Cargo.toml` under `max-version` within `[package.metadata.cpython]`, and `max` within `pyo3-ffi/build.rs` + - [ ] Add prerelease_ver-dev (e.g. `3.14-dev`) to `.github/workflows/ci.yml`, and bump version in `noxfile.py`, `pyo3-ffi/Cargo.toml` under `max-version` within `[package.metadata.cpython]`, and `max` within `pyo3-ffi/build.rs` - [ ] Add a new abi3-prerelease feature for the version (e.g. `abi3-py314`) - In `pyo3-build-config/Cargo.toml`, set abi3-most_current_stable to ["abi3-prerelease"] and abi3-prerelease to ["abi3"] - In `pyo3-ffi/Cargo.toml`, set abi3-most_current_stable to ["abi3-prerelease", "pyo3-build-config/abi3-most_current_stable"] and abi3-prerelease to ["abi3", "pyo3-build-config/abi3-prerelease"] From 97437c77b77c01392d6068b3696bcd6835e6d2f7 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Wed, 8 Oct 2025 18:46:46 +0100 Subject: [PATCH 897/936] deprecate `downcast` functions in favour of `cast` functions (#5472) * deprecate `downcast` functions in favour of `cast` functions * newsfragment * fix some `.downcast()` uses -> `cast()` --- newsfragments/5472.changed.md | 1 + src/conversions/std/vec.rs | 2 +- src/err/mod.rs | 4 ++-- src/impl_/extract_argument.rs | 6 ++--- src/instance.rs | 9 ++++--- src/sync/once_lock.rs | 5 +--- src/types/any.rs | 44 +++++++++++++++++++++++++++-------- 7 files changed, 47 insertions(+), 24 deletions(-) create mode 100644 newsfragments/5472.changed.md diff --git a/newsfragments/5472.changed.md b/newsfragments/5472.changed.md new file mode 100644 index 00000000000..4634c5afea2 --- /dev/null +++ b/newsfragments/5472.changed.md @@ -0,0 +1 @@ +Deprecate `PyAnyMethods::downcast` functions in favour of `Bound::cast` functions diff --git a/src/conversions/std/vec.rs b/src/conversions/std/vec.rs index abe752dc5a7..74c796732ea 100644 --- a/src/conversions/std/vec.rs +++ b/src/conversions/std/vec.rs @@ -89,7 +89,7 @@ where // to support this function and if not, we will only fail extraction safely. let seq = unsafe { if ffi::PySequence_Check(obj.as_ptr()) != 0 { - obj.downcast_unchecked::() + obj.cast_unchecked::() } else { return Err(DowncastError::new_from_type( obj, diff --git a/src/err/mod.rs b/src/err/mod.rs index 6575263aeeb..a560d651f66 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -857,12 +857,12 @@ impl std::fmt::Display for TypeNameOrValue<'_> { match self { Self::Name(name) => name.fmt(f), Self::Value(t) => { - if let Ok(t) = t.downcast::() { + if let Ok(t) = t.cast::() { t.qualname() .map_err(|_| std::fmt::Error)? .to_string_lossy() .fmt(f) - } else if let Ok(t) = t.downcast::() { + } else if let Ok(t) = t.cast::() { for (i, t) in t.iter().enumerate() { if i > 0 { f.write_str(" | ")?; diff --git a/src/impl_/extract_argument.rs b/src/impl_/extract_argument.rs index 424b3ef998f..39023d021a2 100644 --- a/src/impl_/extract_argument.rs +++ b/src/impl_/extract_argument.rs @@ -188,7 +188,7 @@ pub fn extract_pyclass_ref<'a, 'holder, T: PyClass>( obj: &'a Bound<'_, PyAny>, holder: &'holder mut Option>, ) -> PyResult<&'holder T> { - Ok(&*holder.insert(PyClassGuard::try_borrow(obj.downcast()?.as_unbound())?)) + Ok(&*holder.insert(PyClassGuard::try_borrow(obj.cast()?.as_unbound())?)) } #[inline] @@ -196,9 +196,7 @@ pub fn extract_pyclass_ref_mut<'a, 'holder, T: PyClass>( obj: &'a Bound<'_, PyAny>, holder: &'holder mut Option>, ) -> PyResult<&'holder mut T> { - Ok(&mut *holder.insert(PyClassGuardMut::try_borrow_mut( - obj.downcast()?.as_unbound(), - )?)) + Ok(&mut *holder.insert(PyClassGuardMut::try_borrow_mut(obj.cast()?.as_unbound())?)) } /// The standard implementation of how PyO3 extracts a `#[pyfunction]` or `#[pymethod]` function argument. diff --git a/src/instance.rs b/src/instance.rs index 3f80c6616f1..37a05ccebc6 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -2254,6 +2254,7 @@ impl Py { /// # Example: Downcasting to a specific Python object /// /// ```rust + /// # #![allow(deprecated)] /// use pyo3::prelude::*; /// use pyo3::types::{PyDict, PyList}; /// @@ -2270,6 +2271,7 @@ impl Py { /// This is useful if you want to mutate a `Py` that might actually be a pyclass. /// /// ```rust + /// # #![allow(deprecated)] /// # fn main() -> Result<(), pyo3::PyErr> { /// use pyo3::prelude::*; /// @@ -2292,7 +2294,7 @@ impl Py { /// }) /// # } /// ``` - // FIXME(icxolu) deprecate in favor of `Py::cast_bound` + #[deprecated(since = "0.27.0", note = "use `Py::cast_bound` instead")] #[inline] pub fn downcast_bound<'py, T>( &self, @@ -2301,7 +2303,8 @@ impl Py { where T: PyTypeCheck, { - self.cast_bound(py) + #[allow(deprecated)] + self.bind(py).downcast() } /// Casts the `Py` to a concrete Python object type without checking validity. @@ -2309,7 +2312,7 @@ impl Py { /// # Safety /// /// Callers must ensure that the type is valid or risk type confusion. - // FIXME(icxolu) deprecate in favor of `Py::cast_bound_unchecked` + #[deprecated(since = "0.27.0", note = "use `Py::cast_bound_unchecked` instead")] #[inline] pub unsafe fn downcast_bound_unchecked<'py, T>(&self, py: Python<'py>) -> &Bound<'py, T> { // SAFETY: caller has upheld the safety contract diff --git a/src/sync/once_lock.rs b/src/sync/once_lock.rs index fd608215049..5612b556e38 100644 --- a/src/sync/once_lock.rs +++ b/src/sync/once_lock.rs @@ -162,10 +162,7 @@ where attr_name: &str, ) -> PyResult<&Bound<'py, T>> { self.get_or_try_init(py, || { - let type_object = py - .import(module_name)? - .getattr(attr_name)? - .downcast_into()?; + let type_object = py.import(module_name)?.getattr(attr_name)?.cast_into()?; Ok(type_object.unbind()) }) .map(|ty| ty.bind(py)) diff --git a/src/types/any.rs b/src/types/any.rs index 4970340a8e5..a20f0bebb99 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -772,7 +772,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// }) /// # } /// ``` - // FIXME(icxolu) deprecate in favor of `Bound::cast` + #[deprecated(since = "0.27.0", note = "use `Bound::cast` instead")] fn downcast(&self) -> Result<&Bound<'py, T>, DowncastError<'_, 'py>> where T: PyTypeCheck; @@ -800,7 +800,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// assert!(obj.downcast_into::().is_ok()); /// }) /// ``` - // FIXME(icxolu) deprecate in favor of `Bound::cast_into` + #[deprecated(since = "0.27.0", note = "use `Bound::cast_into` instead")] fn downcast_into(self) -> Result, DowncastIntoError<'py>> where T: PyTypeCheck; @@ -836,13 +836,13 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// assert!(any.downcast_exact::().is_ok()); /// }); /// ``` - // FIXME(icxolu) deprecate in favor of `Bound::cast_exact` + #[deprecated(since = "0.27.0", note = "use `Bound::cast_exact` instead")] fn downcast_exact(&self) -> Result<&Bound<'py, T>, DowncastError<'_, 'py>> where T: PyTypeInfo; /// Like `downcast_exact` but takes ownership of `self`. - // FIXME(icxolu) deprecate in favor of `Bound::cast_into_exact` + #[deprecated(since = "0.27.0", note = "use `Bound::cast_into_exact` instead")] fn downcast_into_exact(self) -> Result, DowncastIntoError<'py>> where T: PyTypeInfo; @@ -852,7 +852,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// # Safety /// /// Callers must ensure that the type is valid or risk type confusion. - // FIXME(icxolu) deprecate in favor of `Bound::cast_unchecked` + #[deprecated(since = "0.27.0", note = "use `Bound::cast_unchecked` instead")] unsafe fn downcast_unchecked(&self) -> &Bound<'py, T>; /// Like `downcast_unchecked` but takes ownership of `self`. @@ -860,7 +860,7 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// # Safety /// /// Callers must ensure that the type is valid or risk type confusion. - // FIXME(icxolu) deprecate in favor of `Bound::cast_into_unchecked` + #[deprecated(since = "0.27.0", note = "use `Bound::cast_into_unchecked` instead")] unsafe fn downcast_into_unchecked(self) -> Bound<'py, T>; /// Extracts some type from the Python object. @@ -1449,7 +1449,13 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { where T: PyTypeCheck, { - self.cast() + if T::type_check(self) { + // Safety: type_check is responsible for ensuring that the type is correct + Ok(unsafe { self.cast_unchecked() }) + } else { + #[allow(deprecated)] + Err(DowncastError::new(self, T::NAME)) + } } #[inline] @@ -1457,7 +1463,13 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { where T: PyTypeCheck, { - self.cast_into() + if T::type_check(&self) { + // Safety: type_check is responsible for ensuring that the type is correct + Ok(unsafe { self.cast_into_unchecked() }) + } else { + #[allow(deprecated)] + Err(DowncastIntoError::new(self, T::NAME)) + } } #[inline] @@ -1465,7 +1477,13 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { where T: PyTypeInfo, { - self.cast_exact() + if T::is_exact_type_of(self) { + // Safety: is_exact_type_of is responsible for ensuring that the type is correct + Ok(unsafe { self.cast_unchecked() }) + } else { + #[allow(deprecated)] + Err(DowncastError::new(self, T::NAME)) + } } #[inline] @@ -1473,7 +1491,13 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { where T: PyTypeInfo, { - self.cast_into_exact() + if T::is_exact_type_of(&self) { + // Safety: is_exact_type_of is responsible for ensuring that the type is correct + Ok(unsafe { self.cast_into_unchecked() }) + } else { + #[allow(deprecated)] + Err(DowncastIntoError::new(self, T::NAME)) + } } #[inline] From 558b8a16300637986ec3971fb68636f66b4751fd Mon Sep 17 00:00:00 2001 From: Marco Neumann Date: Thu, 9 Oct 2025 04:43:54 +0200 Subject: [PATCH 898/936] fix: compilation on wasip2 (#5368) WASI strings are UTF8, so we don't need the nightly-only APIs. See https://github.com/rust-lang/rust/issues/130323 as well. --- newsfragments/5368.fixed.md | 1 + src/conversions/std/osstr.rs | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 newsfragments/5368.fixed.md diff --git a/newsfragments/5368.fixed.md b/newsfragments/5368.fixed.md new file mode 100644 index 00000000000..837a7398e62 --- /dev/null +++ b/newsfragments/5368.fixed.md @@ -0,0 +1 @@ +Fix compilation on `wasm32-wasip2`. diff --git a/src/conversions/std/osstr.rs b/src/conversions/std/osstr.rs index eb132a58f69..b30a3a5cc36 100644 --- a/src/conversions/std/osstr.rs +++ b/src/conversions/std/osstr.rs @@ -23,7 +23,7 @@ impl<'py> IntoPyObject<'py> for &OsStr { #[cfg(not(windows))] { #[cfg(target_os = "wasi")] - let bytes = std::os::wasi::ffi::OsStrExt::as_bytes(self); + let bytes = self.to_str().expect("wasi strings are UTF8").as_bytes(); #[cfg(not(target_os = "wasi"))] let bytes = std::os::unix::ffi::OsStrExt::as_bytes(self); @@ -87,9 +87,11 @@ impl FromPyObject<'_, '_> for OsString { }; // Create an OsStr view into the raw bytes from Python + // + // For WASI: OS strings are UTF-8 by definition. #[cfg(target_os = "wasi")] let os_str: &OsStr = - std::os::wasi::ffi::OsStrExt::from_bytes(fs_encoded_bytes.as_bytes(ob.py())); + OsStr::new(std::str::from_utf8(fs_encoded_bytes.as_bytes(ob.py()))?); #[cfg(not(target_os = "wasi"))] let os_str: &OsStr = std::os::unix::ffi::OsStrExt::from_bytes(fs_encoded_bytes.as_bytes(ob.py())); From 60c7785d40917dd87ae50100f757fedd65c969ce Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Thu, 9 Oct 2025 03:45:22 +0100 Subject: [PATCH 899/936] ci: test windows-aarch64 on PR (#5500) --- .github/workflows/ci.yml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f12fd80a6aa..ae4fcb46eac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -159,7 +159,7 @@ jobs: rust-target: "x86_64-unknown-linux-gnu", }, { - os: "ubuntu-22.04-arm", + os: "ubuntu-24.04-arm", python-architecture: "arm64", rust-target: "aarch64-unknown-linux-gnu", }, @@ -173,6 +173,11 @@ jobs: python-architecture: "x86", rust-target: "i686-pc-windows-msvc", }, + { + os: "windows-11-arm", + python-architecture: "arm64", + rust-target: "aarch64-pc-windows-msvc", + }, ] include: # Test nightly Rust on PRs so that PR authors have a chance to fix nightly @@ -337,12 +342,12 @@ jobs: # python-architecture: "x64", # rust-target: "x86_64-apple-darwin", # } - # arm64 Linux runner is in public preview, so test 3.13 on it + # test latest Python on arm64 linux & windows runners - rust: stable python-version: "3.13" platform: { - os: "ubuntu-22.04-arm", + os: "ubuntu-24.04-arm", python-architecture: "arm64", rust-target: "aarch64-unknown-linux-gnu", } From 4906a617cd88da522adf0a424b5644f65a072fcc Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Thu, 9 Oct 2025 07:42:11 +0100 Subject: [PATCH 900/936] ci: test against 3.14 stable (#5499) * ci: test against 3.14 stable * newsfragment * don't export `PyObjectObFlagsAndRefcnt` on 32-bit arch * newsfragment --- .github/workflows/ci.yml | 42 ++++++++++----------------------- newsfragments/5499.fixed.md | 1 + newsfragments/5499.packaging.md | 1 + pyo3-ffi/src/object.rs | 14 +++++++++-- 4 files changed, 26 insertions(+), 32 deletions(-) create mode 100644 newsfragments/5499.fixed.md create mode 100644 newsfragments/5499.packaging.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ae4fcb46eac..e799406af6c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -145,7 +145,7 @@ jobs: fail-fast: ${{ !contains(github.event.pull_request.labels.*.name, 'CI-no-fail-fast') }} matrix: rust: [stable] - python-version: ["3.13"] + python-version: ["3.14"] platform: [ { @@ -183,7 +183,7 @@ jobs: # Test nightly Rust on PRs so that PR authors have a chance to fix nightly # failures, as nightly does not block merge. - rust: nightly - python-version: "3.13" + python-version: "3.14" platform: { os: "ubuntu-latest", @@ -193,25 +193,7 @@ jobs: # Also test free-threaded Python just for latest Python version, on ubuntu # (run for all OSes on build-full) - rust: stable - python-version: "3.13t" - platform: - { - os: "ubuntu-latest", - python-architecture: "x64", - rust-target: "x86_64-unknown-linux-gnu", - } - # As we approach 3.14 release date, let's also have testing for 3.14 and 3.14t - # on linux - - rust: stable - python-version: "3.14-dev" - platform: - { - os: "ubuntu-latest", - python-architecture: "x64", - rust-target: "x86_64-unknown-linux-gnu", - } - - rust: stable - python-version: "3.14t-dev" + python-version: "3.14t" platform: { os: "ubuntu-latest", @@ -248,8 +230,8 @@ jobs: "3.12", "3.13", "3.13t", - "3.14-dev", - "3.14t-dev", + "3.14", + "3.14t", "pypy3.9", "pypy3.10", "pypy3.11", @@ -276,7 +258,7 @@ jobs: include: # Test minimal supported Rust version - rust: ${{ needs.resolve.outputs.MSRV }} - python-version: "3.13" + python-version: "3.14" platform: { os: "ubuntu-latest", @@ -286,7 +268,7 @@ jobs: # Test the `nightly` feature - rust: nightly - python-version: "3.13" + python-version: "3.14" platform: { os: "ubuntu-latest", @@ -296,7 +278,7 @@ jobs: # Run rust beta to help catch toolchain regressions - rust: beta - python-version: "3.13" + python-version: "3.14" platform: { os: "ubuntu-latest", @@ -306,7 +288,7 @@ jobs: # Test 32-bit Windows and x64 macOS only with the latest Python version - rust: stable - python-version: "3.13" + python-version: "3.14" platform: { os: "windows-latest", @@ -314,7 +296,7 @@ jobs: rust-target: "i686-pc-windows-msvc", } - rust: stable - python-version: "3.13" + python-version: "3.14" platform: { os: "macos-latest", @@ -344,7 +326,7 @@ jobs: # } # test latest Python on arm64 linux & windows runners - rust: stable - python-version: "3.13" + python-version: "3.14" platform: { os: "ubuntu-24.04-arm", @@ -352,7 +334,7 @@ jobs: rust-target: "aarch64-unknown-linux-gnu", } - rust: stable - python-version: "3.13" + python-version: "3.14" platform: { os: "windows-11-arm", diff --git a/newsfragments/5499.fixed.md b/newsfragments/5499.fixed.md new file mode 100644 index 00000000000..0b50b0caf59 --- /dev/null +++ b/newsfragments/5499.fixed.md @@ -0,0 +1 @@ +Don't export definition of FFI struct PyObjectObFlagsAndRefcnt on 32-bit Python (doesn't exist). diff --git a/newsfragments/5499.packaging.md b/newsfragments/5499.packaging.md new file mode 100644 index 00000000000..fdd0e5d13ee --- /dev/null +++ b/newsfragments/5499.packaging.md @@ -0,0 +1 @@ +Test against Python 3.14 final release. diff --git a/pyo3-ffi/src/object.rs b/pyo3-ffi/src/object.rs index 8e93df176d8..1073fd13b5f 100644 --- a/pyo3-ffi/src/object.rs +++ b/pyo3-ffi/src/object.rs @@ -19,7 +19,12 @@ pub use crate::cpython::object::PyTypeObject; #[repr(C)] #[derive(Copy, Clone)] -#[cfg(all(Py_3_14, not(Py_GIL_DISABLED), target_endian = "big"))] +#[cfg(all( + target_pointer_width = "64", + Py_3_14, + not(Py_GIL_DISABLED), + target_endian = "big" +))] /// This struct is anonymous in CPython, so the name was given by PyO3 because /// Rust structs need a name. pub struct PyObjectObFlagsAndRefcnt { @@ -30,7 +35,12 @@ pub struct PyObjectObFlagsAndRefcnt { #[repr(C)] #[derive(Copy, Clone)] -#[cfg(all(Py_3_14, not(Py_GIL_DISABLED), target_endian = "little"))] +#[cfg(all( + target_pointer_width = "64", + Py_3_14, + not(Py_GIL_DISABLED), + target_endian = "little" +))] /// This struct is anonymous in CPython, so the name was given by PyO3 because /// Rust structs need a name. pub struct PyObjectObFlagsAndRefcnt { From d9e2c5021c6754152bb340543bb5f64e68769644 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers <7943856+bschoenmaeckers@users.noreply.github.com> Date: Thu, 9 Oct 2025 13:58:59 +0200 Subject: [PATCH 901/936] Add `FromPyObject` impl for `Cow` & `Cow` (#5497) * Add `FromPyObject` impl for `Cow` & `Cow` * Remove comments about `&OsStr` & `&Path` * Test if result is borrowed * Fix test on wasm --- newsfragments/5497.added.md | 1 + src/conversions/std/osstr.rs | 58 ++++++++++++++++++++++++++++--- src/conversions/std/path.rs | 67 +++++++++++++++++++++++++++++++----- 3 files changed, 114 insertions(+), 12 deletions(-) create mode 100644 newsfragments/5497.added.md diff --git a/newsfragments/5497.added.md b/newsfragments/5497.added.md new file mode 100644 index 00000000000..6fea77cdcec --- /dev/null +++ b/newsfragments/5497.added.md @@ -0,0 +1 @@ +Add `FromPyObject` impl for `Cow` & `Cow` diff --git a/src/conversions/std/osstr.rs b/src/conversions/std/osstr.rs index b30a3a5cc36..2c05d0fbff6 100644 --- a/src/conversions/std/osstr.rs +++ b/src/conversions/std/osstr.rs @@ -67,9 +67,6 @@ impl<'py> IntoPyObject<'py> for &&OsStr { } } -// 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 - impl FromPyObject<'_, '_> for OsString { type Error = PyErr; @@ -153,6 +150,19 @@ impl<'py> IntoPyObject<'py> for &Cow<'_, OsStr> { } } +impl<'a> FromPyObject<'a, '_> for Cow<'a, OsStr> { + type Error = PyErr; + + fn extract(obj: Borrowed<'a, '_, PyAny>) -> Result { + #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] + if let Ok(s) = obj.extract::<&str>() { + return Ok(Cow::Borrowed(s.as_ref())); + } + + obj.extract::().map(Cow::Owned) + } +} + impl<'py> IntoPyObject<'py> for OsString { type Target = PyString; type Output = Bound<'py, Self::Target>; @@ -178,8 +188,12 @@ impl<'py> IntoPyObject<'py> for &OsString { #[cfg(test)] mod tests { use crate::types::{PyAnyMethods, PyString, PyStringMethods}; - use crate::{BoundObject, IntoPyObject, Python}; + use crate::{Bound, BoundObject, IntoPyObject, Python}; use std::fmt::Debug; + #[cfg(unix)] + use std::os::unix::ffi::OsStringExt; + #[cfg(windows)] + use std::os::windows::ffi::OsStringExt; use std::{ borrow::Cow, ffi::{OsStr, OsString}, @@ -254,4 +268,40 @@ mod tests { assert_eq!(encoded, wide); }); } + + #[test] + fn test_extract_cow() { + Python::attach(|py| { + fn test_extract<'py, T>(py: Python<'py>, input: &T, is_borrowed: bool) + where + for<'a> &'a T: IntoPyObject<'py, Output = Bound<'py, PyString>>, + for<'a> <&'a T as IntoPyObject<'py>>::Error: Debug, + T: AsRef + ?Sized, + { + let pystring = input.into_pyobject(py).unwrap(); + let cow: Cow<'_, OsStr> = pystring.extract().unwrap(); + assert_eq!(cow, input.as_ref()); + assert_eq!(is_borrowed, matches!(cow, Cow::Borrowed(_))); + } + + // On Python 3.10+ or when not using the limited API, we can borrow strings from python + let can_borrow_str = cfg!(any(Py_3_10, not(Py_LIMITED_API))); + // This can be borrowed because it is valid UTF-8 + test_extract::(py, "Hello\0\n🐍", can_borrow_str); + test_extract::(py, "Hello, world!", can_borrow_str); + + #[cfg(windows)] + let os_str = { + // 'A', unpaired surrogate, 'B' + OsString::from_wide(&['A' as u16, 0xD800, 'B' as u16]) + }; + + #[cfg(unix)] + let os_str = { OsString::from_vec(vec![250, 251, 252, 253, 254, 255, 0, 255]) }; + + // This cannot be borrowed because it is not valid UTF-8 + #[cfg(any(windows, unix))] + test_extract::(py, &os_str, false); + }); + } } diff --git a/src/conversions/std/path.rs b/src/conversions/std/path.rs index 4d60facd186..7a92ca860c4 100644 --- a/src/conversions/std/path.rs +++ b/src/conversions/std/path.rs @@ -7,8 +7,6 @@ use std::borrow::Cow; use std::ffi::OsString; use std::path::{Path, PathBuf}; -// See osstr.rs for why there's no FromPyObject impl for &Path - impl FromPyObject<'_, '_> for PathBuf { type Error = PyErr; @@ -66,6 +64,19 @@ impl<'py> IntoPyObject<'py> for &Cow<'_, Path> { } } +impl<'a> FromPyObject<'a, '_> for Cow<'a, Path> { + type Error = PyErr; + + fn extract(obj: Borrowed<'a, '_, PyAny>) -> Result { + #[cfg(any(Py_3_10, not(Py_LIMITED_API)))] + if let Ok(s) = obj.extract::<&str>() { + return Ok(Cow::Borrowed(s.as_ref())); + } + + obj.extract::().map(Cow::Owned) + } +} + impl<'py> IntoPyObject<'py> for PathBuf { type Target = PyAny; type Output = Bound<'py, Self::Target>; @@ -90,18 +101,22 @@ impl<'py> IntoPyObject<'py> for &PathBuf { #[cfg(test)] mod tests { - use crate::types::{PyAnyMethods, PyString}; - use crate::{IntoPyObject, IntoPyObjectExt, Python}; - use std::borrow::Cow; + use super::*; + use crate::{ + types::{PyAnyMethods, PyString}, + IntoPyObjectExt, + }; + use std::ffi::OsStr; use std::fmt::Debug; - use std::path::{Path, PathBuf}; + #[cfg(unix)] + use std::os::unix::ffi::OsStringExt; + #[cfg(windows)] + use std::os::windows::ffi::OsStringExt; #[test] #[cfg(not(windows))] fn test_non_utf8_conversion() { Python::attach(|py| { - use crate::types::PyAnyMethods; - use std::ffi::OsStr; #[cfg(not(target_os = "wasi"))] use std::os::unix::ffi::OsStrExt; #[cfg(target_os = "wasi")] @@ -147,4 +162,40 @@ mod tests { assert_eq!(roundtrip, Path::new(path)); }); } + + #[test] + fn test_extract_cow() { + Python::attach(|py| { + fn test_extract<'py, T>(py: Python<'py>, path: &T, is_borrowed: bool) + where + for<'a> &'a T: IntoPyObject<'py, Output = Bound<'py, PyString>>, + for<'a> <&'a T as IntoPyObject<'py>>::Error: Debug, + T: AsRef + ?Sized, + { + let pystring = path.into_pyobject(py).unwrap(); + let cow: Cow<'_, Path> = pystring.extract().unwrap(); + assert_eq!(cow, path.as_ref()); + assert_eq!(is_borrowed, matches!(cow, Cow::Borrowed(_))); + } + + // On Python 3.10+ or when not using the limited API, we can borrow strings from python + let can_borrow_str = cfg!(any(Py_3_10, not(Py_LIMITED_API))); + // This can be borrowed because it is valid UTF-8 + test_extract::(py, "Hello\0\n🐍", can_borrow_str); + test_extract::(py, "Hello, world!", can_borrow_str); + + #[cfg(windows)] + let os_str = { + // 'A', unpaired surrogate, 'B' + OsString::from_wide(&['A' as u16, 0xD800, 'B' as u16]) + }; + + #[cfg(unix)] + let os_str = { OsString::from_vec(vec![250, 251, 252, 253, 254, 255, 0, 255]) }; + + // This cannot be borrowed because it is not valid UTF-8 + #[cfg(any(unix, windows))] + test_extract::(py, &os_str, false); + }); + } } From 254b29f0c63597a9ae6ba73ede79edb49d61a50c Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Thu, 9 Oct 2025 21:26:34 +0100 Subject: [PATCH 902/936] replace `DowncastError` with `CastError` (#5468) * replace `DowncastError` with `CastError` * newsfragment, CI * fixups * migration guide entry * fixup docs --- guide/src/conversions/traits.md | 2 +- guide/src/migration.md | 10 + newsfragments/5468.added.md | 1 + newsfragments/5468.changed.md | 1 + pyo3-macros-backend/src/method.rs | 6 +- src/conversions/bytes.rs | 4 +- src/conversions/chrono.rs | 14 +- src/conversions/jiff.rs | 14 +- src/conversions/smallvec.rs | 11 +- src/conversions/std/array.rs | 8 +- src/conversions/std/slice.rs | 6 +- src/conversions/std/vec.rs | 8 +- src/err/cast_error.rs | 239 ++++++++++++++++++++++++ src/err/downcast_error.rs | 183 ++++++++++++++++++ src/err/mod.rs | 196 +------------------ src/impl_/extract_argument.rs | 6 +- src/impl_/pymethods.rs | 9 +- src/instance.rs | 58 +++--- src/lib.rs | 4 +- src/pybacked.rs | 7 +- src/pyclass/guard.rs | 6 +- src/types/any.rs | 12 +- src/types/iterator.rs | 2 +- src/types/weakref/anyref.rs | 2 +- src/types/weakref/proxy.rs | 2 +- tests/test_frompyobject.rs | 20 +- tests/test_pyerr_debug_unformattable.rs | 2 +- tests/test_pyfunction.rs | 8 +- 28 files changed, 545 insertions(+), 296 deletions(-) create mode 100644 newsfragments/5468.added.md create mode 100644 newsfragments/5468.changed.md create mode 100644 src/err/cast_error.rs create mode 100644 src/err/downcast_error.rs diff --git a/guide/src/conversions/traits.md b/guide/src/conversions/traits.md index 98bd2864809..be9d3f7e6fe 100644 --- a/guide/src/conversions/traits.md +++ b/guide/src/conversions/traits.md @@ -464,7 +464,7 @@ enum RustyEnum { ``` If the input is neither a string nor an integer, the error message will be: -`"'' cannot be converted to 'str | int'"`. +`"'' cannot be cast as 'str | int'"`. #### `#[derive(FromPyObject)]` Container Attributes - `pyo3(transparent)` diff --git a/guide/src/migration.md b/guide/src/migration.md index c2c67039c47..099c06bbe29 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -133,6 +133,16 @@ This is very similar to `serde`s [`Deserialize`] and [`DeserializeOwned`] traits [`DeserializeOwned`]: https://docs.rs/serde/latest/serde/de/trait.DeserializeOwned.html
+## `.downcast()` and `DowncastError` replaced with `.cast()` and `CastError` + +The `.downcast()` family of functions were only available on `Bound`. In corner cases (particularly related to `.downcast_into()`) this would require use of `.as_any().downcast()` or `.into_any().downcast_into()` chains. Additionally, `DowncastError` produced Python exception messages which are not very Pythonic due to use of Rust type names in the error messages. + +The `.cast()` family of functions are available on all `Bound` and `Borrowed` smart pointers, whatever the type, and have error messages derived from the actual type at runtime. This produces a nicer experience for both PyO3 module authors and consumers. + +To migrate, replace `.downcast()` with `.cast()` and `DowncastError` with `CastError` (and similar with `.downcast_into()` / `DowncastIntoError` etc). + +`CastError` requires a Python `type` object (or other "classinfo" object compatible with `isinstance()`) as the second object, so in the rare case where `DowncastError` was manually constructed, small adjustments to code may apply. + ## `PyTypeCheck` is now an `unsafe trait` Because `PyTypeCheck` is the trait used to guard the `.cast()` functions to treat Python objects as specific concrete types, the trait is `unsafe` to implement. diff --git a/newsfragments/5468.added.md b/newsfragments/5468.added.md new file mode 100644 index 00000000000..73bb6cba80f --- /dev/null +++ b/newsfragments/5468.added.md @@ -0,0 +1 @@ +Add `CastError` and `CastIntoError`. diff --git a/newsfragments/5468.changed.md b/newsfragments/5468.changed.md new file mode 100644 index 00000000000..5f7cca73682 --- /dev/null +++ b/newsfragments/5468.changed.md @@ -0,0 +1 @@ +Replace `DowncastError` and `DowncastIntoError` with `CastError` and `CastIntoError`. diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index 400538ce14d..8d6f7868524 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -316,7 +316,7 @@ impl FnType { #[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>() + .cast_unchecked::<#pyo3_path::types::PyType>() ) }; Some(quote! { unsafe { #ret }, }) @@ -329,7 +329,7 @@ impl FnType { #[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>() + .cast_unchecked::<#pyo3_path::types::PyModule>() ) }; Some(quote! { unsafe { #ret }, }) @@ -404,7 +404,7 @@ impl SelfType { let pyo3_path = pyo3_path.to_tokens_spanned(*span); error_mode.handle_error( quote_spanned! { *span => - #bound_ref.downcast::<#cls>() + #bound_ref.cast::<#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+) diff --git a/src/conversions/bytes.rs b/src/conversions/bytes.rs index 0aba7409578..fb656579a2d 100644 --- a/src/conversions/bytes.rs +++ b/src/conversions/bytes.rs @@ -68,10 +68,10 @@ use crate::conversion::IntoPyObject; use crate::instance::Bound; use crate::pybacked::PyBackedBytes; use crate::types::PyBytes; -use crate::{Borrowed, DowncastError, FromPyObject, PyAny, PyErr, Python}; +use crate::{Borrowed, CastError, FromPyObject, PyAny, PyErr, Python}; impl<'a, 'py> FromPyObject<'a, 'py> for Bytes { - type Error = DowncastError<'a, 'py>; + type Error = CastError<'a, 'py>; fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result { Ok(Bytes::from_owner(obj.extract::()?)) diff --git a/src/conversions/chrono.rs b/src/conversions/chrono.rs index 0a2a56c3530..e9ca64d0ba7 100644 --- a/src/conversions/chrono.rs +++ b/src/conversions/chrono.rs @@ -692,11 +692,11 @@ mod tests { let none = py.None().into_bound(py); assert_eq!( none.extract::().unwrap_err().to_string(), - "TypeError: 'NoneType' object cannot be converted to 'timedelta'" + "TypeError: 'NoneType' object cannot be cast as 'timedelta'" ); assert_eq!( none.extract::().unwrap_err().to_string(), - "TypeError: 'NoneType' object cannot be converted to 'tzinfo'" + "TypeError: 'NoneType' object cannot be cast as 'tzinfo'" ); assert_eq!( none.extract::().unwrap_err().to_string(), @@ -704,25 +704,25 @@ mod tests { ); assert_eq!( none.extract::().unwrap_err().to_string(), - "TypeError: 'NoneType' object cannot be converted to 'time'" + "TypeError: 'NoneType' object cannot be cast as 'time'" ); assert_eq!( none.extract::().unwrap_err().to_string(), - "TypeError: 'NoneType' object cannot be converted to 'date'" + "TypeError: 'NoneType' object cannot be cast as 'date'" ); assert_eq!( none.extract::().unwrap_err().to_string(), - "TypeError: 'NoneType' object cannot be converted to 'datetime'" + "TypeError: 'NoneType' object cannot be cast as 'datetime'" ); assert_eq!( none.extract::>().unwrap_err().to_string(), - "TypeError: 'NoneType' object cannot be converted to 'datetime'" + "TypeError: 'NoneType' object cannot be cast as 'datetime'" ); assert_eq!( none.extract::>() .unwrap_err() .to_string(), - "TypeError: 'NoneType' object cannot be converted to 'datetime'" + "TypeError: 'NoneType' object cannot be cast as 'datetime'" ); }); } diff --git a/src/conversions/jiff.rs b/src/conversions/jiff.rs index 817b50df79d..9cc9ffd3cee 100644 --- a/src/conversions/jiff.rs +++ b/src/conversions/jiff.rs @@ -581,31 +581,31 @@ mod tests { let none = py.None().into_bound(py); assert_eq!( none.extract::().unwrap_err().to_string(), - "TypeError: 'NoneType' object cannot be converted to 'timedelta'" + "TypeError: 'NoneType' object cannot be cast as 'timedelta'" ); assert_eq!( none.extract::().unwrap_err().to_string(), - "TypeError: 'NoneType' object cannot be converted to 'tzinfo'" + "TypeError: 'NoneType' object cannot be cast as 'tzinfo'" ); assert_eq!( none.extract::().unwrap_err().to_string(), - "TypeError: 'NoneType' object cannot be converted to 'tzinfo'" + "TypeError: 'NoneType' object cannot be cast as 'tzinfo'" ); assert_eq!( none.extract::
- - `__richcmp__(, object, pyo3::basic::CompareOp) -> object` +- `__richcmp__(, object, pyo3::basic::CompareOp) -> object` Implements Python comparison operations (`==`, `!=`, `<`, `<=`, `>`, and `>=`) in a single method. The `CompareOp` argument indicates the comparison operation being performed. You can use @@ -121,8 +126,8 @@ given signatures should be interpreted as follows: signature, the generated code will automatically `return NotImplemented`.
- - `__getattr__(, object) -> object` - - `__getattribute__(, object) -> object` +- `__getattr__(, object) -> object` +- `__getattribute__(, object) -> object`
Differences between `__getattr__` and `__getattribute__` As in Python, `__getattr__` is only called if the attribute is not found @@ -132,24 +137,24 @@ given signatures should be interpreted as follows: infinite recursion, and use `baseclass.__getattribute__()`.
- - `__setattr__(, value: object) -> ()` - - `__delattr__(, object) -> ()` +- `__setattr__(, value: object) -> ()` +- `__delattr__(, object) -> ()` Overrides attribute access. - - `__bool__() -> bool` +- `__bool__() -> bool` Determines the "truthyness" of an object. - - `__call__(, ...) -> object` - here, any argument list can be defined +- `__call__(, ...) -> object` - here, any argument list can be defined as for normal `pymethods` ### Iterable objects Iterators can be defined using these methods: - - `__iter__() -> object` - - `__next__() -> Option or IterNextOutput` ([see details](#returning-a-value-from-iteration)) +- `__iter__() -> object` +- `__next__() -> Option or IterNextOutput` ([see details](#returning-a-value-from-iteration)) Returning `None` from `__next__` indicates that that there are no further items. @@ -235,9 +240,9 @@ with a `PyStopIteration` as the error. ### Awaitable objects - - `__await__() -> object` - - `__aiter__() -> object` - - `__anext__() -> Option` +- `__await__() -> object` +- `__aiter__() -> object` +- `__anext__() -> Option` ### Mapping & Sequence types @@ -248,6 +253,7 @@ The Python C-API which PyO3 is built upon has separate "slots" for sequences and By default PyO3 reproduces the Python behaviour of filling both mapping and sequence slots. This makes sense for the "simple" case which matches Python, and also for sequences, where the mapping slot is used anyway to implement slice indexing. Mapping types usually will not want the sequence slots filled. Having them filled will lead to outcomes which may be unwanted, such as: + - The mapping type will successfully cast to [`PySequence`]. This may lead to consumers of the type handling it incorrectly. - Python provides a default implementation of `__iter__` for sequences, which calls `__getitem__` with consecutive positive integers starting from 0 until an `IndexError` is returned. Unless the mapping only contains consecutive positive integer keys, this `__iter__` implementation will likely not be the intended behavior. @@ -255,11 +261,11 @@ Use the `#[pyclass(mapping)]` annotation to instruct PyO3 to only fill the mappi Use the `#[pyclass(sequence)]` annotation to instruct PyO3 to fill the `sq_length` slot instead of the `mp_length` slot for `__len__`. This will help libraries such as `numpy` recognise the class as a sequence, however will also cause CPython to automatically add the sequence length to any negative indices before passing them to `__getitem__`. (`__getitem__`, `__setitem__` and `__delitem__` mapping slots are still used for sequences, for slice operations.) - - `__len__() -> usize` +- `__len__() -> usize` Implements the built-in function `len()`. - - `__contains__(, object) -> bool` +- `__contains__(, object) -> bool` Implements membership test operators. Should return true if `item` is in `self`, false otherwise. @@ -286,9 +292,10 @@ Use the `#[pyclass(sequence)]` annotation to instruct PyO3 to fill the `sq_lengt const __contains__: Option> = None; } ``` + - - `__getitem__(, object) -> object` +- `__getitem__(, object) -> object` Implements retrieval of the `self[a]` element. @@ -297,39 +304,39 @@ Use the `#[pyclass(sequence)]` annotation to instruct PyO3 to fill the `sq_lengt accessed via `PySequence::get_item`, the underlying C API already adjusts the index to be positive. - - `__setitem__(, object, object) -> ()` +- `__setitem__(, object, object) -> ()` Implements assignment to the `self[a]` element. Should only be implemented if elements can be replaced. Same behavior regarding negative indices as for `__getitem__`. - - `__delitem__(, object) -> ()` +- `__delitem__(, object) -> ()` Implements deletion of the `self[a]` element. Should only be implemented if elements can be deleted. Same behavior regarding negative indices as for `__getitem__`. - * `fn __concat__(&self, other: impl FromPyObject) -> PyResult` +- `fn __concat__(&self, other: impl FromPyObject) -> PyResult` Concatenates two sequences. Used by the `+` operator, after trying the numeric addition via the `__add__` and `__radd__` methods. - * `fn __repeat__(&self, count: isize) -> PyResult` +- `fn __repeat__(&self, count: isize) -> PyResult` Repeats the sequence `count` times. Used by the `*` operator, after trying the numeric multiplication via the `__mul__` and `__rmul__` methods. - * `fn __inplace_concat__(&self, other: impl FromPyObject) -> PyResult` +- `fn __inplace_concat__(&self, other: impl FromPyObject) -> PyResult` Concatenates two sequences. Used by the `+=` operator, after trying the numeric addition via the `__iadd__` method. - * `fn __inplace_repeat__(&self, count: isize) -> PyResult` +- `fn __inplace_repeat__(&self, count: isize) -> PyResult` Concatenates two sequences. Used by the `*=` operator, after trying the numeric multiplication via @@ -337,9 +344,9 @@ Use the `#[pyclass(sequence)]` annotation to instruct PyO3 to fill the `sq_lengt ### Descriptors - - `__get__(, object, object) -> object` - - `__set__(, object, object) -> ()` - - `__delete__(, object) -> ()` +- `__get__(, object, object) -> object` +- `__set__(, object, object) -> ()` +- `__delete__(, object) -> ()` ### Numeric types @@ -349,69 +356,69 @@ Binary arithmetic operations (`+`, `-`, `*`, `@`, `/`, `//`, `%`, `divmod()`, (If the `object` is not of the type specified in the signature, the generated code will automatically `return NotImplemented`.) - - `__add__(, object) -> object` - - `__radd__(, object) -> object` - - `__sub__(, object) -> object` - - `__rsub__(, object) -> object` - - `__mul__(, object) -> object` - - `__rmul__(, object) -> object` - - `__matmul__(, object) -> object` - - `__rmatmul__(, object) -> object` - - `__floordiv__(, object) -> object` - - `__rfloordiv__(, object) -> object` - - `__truediv__(, object) -> object` - - `__rtruediv__(, object) -> object` - - `__divmod__(, object) -> object` - - `__rdivmod__(, object) -> object` - - `__mod__(, object) -> object` - - `__rmod__(, object) -> object` - - `__lshift__(, object) -> object` - - `__rlshift__(, object) -> object` - - `__rshift__(, object) -> object` - - `__rrshift__(, object) -> object` - - `__and__(, object) -> object` - - `__rand__(, object) -> object` - - `__xor__(, object) -> object` - - `__rxor__(, object) -> object` - - `__or__(, object) -> object` - - `__ror__(, object) -> object` - - `__pow__(, object, object) -> object` - - `__rpow__(, object, object) -> object` +- `__add__(, object) -> object` +- `__radd__(, object) -> object` +- `__sub__(, object) -> object` +- `__rsub__(, object) -> object` +- `__mul__(, object) -> object` +- `__rmul__(, object) -> object` +- `__matmul__(, object) -> object` +- `__rmatmul__(, object) -> object` +- `__floordiv__(, object) -> object` +- `__rfloordiv__(, object) -> object` +- `__truediv__(, object) -> object` +- `__rtruediv__(, object) -> object` +- `__divmod__(, object) -> object` +- `__rdivmod__(, object) -> object` +- `__mod__(, object) -> object` +- `__rmod__(, object) -> object` +- `__lshift__(, object) -> object` +- `__rlshift__(, object) -> object` +- `__rshift__(, object) -> object` +- `__rrshift__(, object) -> object` +- `__and__(, object) -> object` +- `__rand__(, object) -> object` +- `__xor__(, object) -> object` +- `__rxor__(, object) -> object` +- `__or__(, object) -> object` +- `__ror__(, object) -> object` +- `__pow__(, object, object) -> object` +- `__rpow__(, object, object) -> object` In-place assignment operations (`+=`, `-=`, `*=`, `@=`, `/=`, `//=`, `%=`, `**=`, `<<=`, `>>=`, `&=`, `^=`, `|=`): - - `__iadd__(, object) -> ()` - - `__isub__(, object) -> ()` - - `__imul__(, object) -> ()` - - `__imatmul__(, object) -> ()` - - `__itruediv__(, object) -> ()` - - `__ifloordiv__(, object) -> ()` - - `__imod__(, object) -> ()` - - `__ipow__(, object, object) -> ()` - - `__ilshift__(, object) -> ()` - - `__irshift__(, object) -> ()` - - `__iand__(, object) -> ()` - - `__ixor__(, object) -> ()` - - `__ior__(, object) -> ()` +- `__iadd__(, object) -> ()` +- `__isub__(, object) -> ()` +- `__imul__(, object) -> ()` +- `__imatmul__(, object) -> ()` +- `__itruediv__(, object) -> ()` +- `__ifloordiv__(, object) -> ()` +- `__imod__(, object) -> ()` +- `__ipow__(, object, object) -> ()` +- `__ilshift__(, object) -> ()` +- `__irshift__(, object) -> ()` +- `__iand__(, object) -> ()` +- `__ixor__(, object) -> ()` +- `__ior__(, object) -> ()` Unary operations (`-`, `+`, `abs()` and `~`): - - `__pos__() -> object` - - `__neg__() -> object` - - `__abs__() -> object` - - `__invert__() -> object` +- `__pos__() -> object` +- `__neg__() -> object` +- `__abs__() -> object` +- `__invert__() -> object` Coercions: - - `__index__() -> object (int)` - - `__int__() -> object (int)` - - `__float__() -> object (float)` +- `__index__() -> object (int)` +- `__int__() -> object (int)` +- `__float__() -> object (float)` ### Buffer objects - - `__getbuffer__(, *mut ffi::Py_buffer, flags) -> ()` - - `__releasebuffer__(, *mut ffi::Py_buffer) -> ()` +- `__getbuffer__(, *mut ffi::Py_buffer, flags) -> ()` +- `__releasebuffer__(, *mut ffi::Py_buffer) -> ()` Errors returned from `__releasebuffer__` will be sent to `sys.unraiseablehook`. It is strongly advised to never return an error from `__releasebuffer__`, and if it really is necessary, to make best effort to perform any required freeing operations before returning. `__releasebuffer__` will not be called a second time; anything not freed will be leaked. ### Garbage Collector Integration @@ -425,8 +432,8 @@ object. `__clear__` must clear out any mutable references to other Python objects (thus breaking reference cycles). Immutable references do not have to be cleared, as every cycle must contain at least one mutable reference. - - `__traverse__(, pyo3::class::gc::PyVisit<'_>) -> Result<(), pyo3::class::gc::PyTraverseError>` - - `__clear__() -> ()` +- `__traverse__(, pyo3::class::gc::PyVisit<'_>) -> Result<(), pyo3::class::gc::PyTraverseError>` +- `__clear__() -> ()` > Note: `__traverse__` does not work with [`#[pyo3(warn(...))]`](../function.md#warn). @@ -463,4 +470,5 @@ i.e. `Python::attach` 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). [`PySequence`]: {{#PYO3_DOCS_URL}}/pyo3/types/struct.PySequence.html + [`CompareOp::matches`]: {{#PYO3_DOCS_URL}}/pyo3/pyclass/enum.CompareOp.html#method.matches diff --git a/guide/src/class/thread-safety.md b/guide/src/class/thread-safety.md index d0cf9e10a7f..c9e8cd6391f 100644 --- a/guide/src/class/thread-safety.md +++ b/guide/src/class/thread-safety.md @@ -1,6 +1,7 @@ # `#[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. diff --git a/guide/src/conversions/tables.md b/guide/src/conversions/tables.md index e120d63cf6d..8e7c0b3dcdf 100644 --- a/guide/src/conversions/tables.md +++ b/guide/src/conversions/tables.md @@ -1,10 +1,10 @@ -## Mapping of Rust types to Python types +# Mapping of Rust types to Python types When writing functions callable from Python (such as a `#[pyfunction]` or in a `#[pymethods]` block), the trait `FromPyObject` is required for function arguments, and `IntoPyObject` is required for function return values. Consult the tables in the following section to find the Rust types provided by PyO3 which implement these traits. -### Argument Types +## Argument Types When accepting a function argument, it is possible to either use Rust library types or PyO3's Python-native types. (See the next section for discussion on when to use each.) @@ -39,7 +39,7 @@ The table below contains the Python type and the corresponding function argument | `decimal.Decimal` | `bigdecimal::BigDecimal`[^9] | - | | `ipaddress.IPv4Address` | `std::net::IpAddr`, `std::net::Ipv4Addr` | - | | `ipaddress.IPv6Address` | `std::net::IpAddr`, `std::net::Ipv6Addr` | - | -| `os.PathLike ` | `PathBuf`, `Path` | `PyString` | +| `os.PathLike` | `PathBuf`, `Path` | `PyString` | | `pathlib.Path` | `PathBuf`, `Path` | `PyString` | | `typing.Optional[T]` | `Option` | - | | `typing.Sequence[T]` | `Vec` | `PySequence` | @@ -59,11 +59,12 @@ It is also worth remembering the following special types: For more detail on accepting `#[pyclass]` values as function arguments, see [the section of this guide on Python Classes](../class.md). -#### Using Rust library types vs Python-native types +### Using Rust library types vs Python-native types Using Rust library types as function arguments will incur a conversion cost compared to using the Python-native types. Using the Python-native types is almost zero-cost (they just require a type check similar to the Python builtin function `isinstance()`). However, once that conversion cost has been paid, the Rust standard library types offer a number of benefits: + - You can write functionality in native-speed Rust code (free of Python's runtime costs). - You get better interoperability with the rest of the Rust ecosystem. - You can use `Python::detach` to detach from the interpreter and let other Python threads make progress while your Rust code is executing. @@ -71,7 +72,7 @@ However, once that conversion cost has been paid, the Rust standard library type For most PyO3 usage the conversion cost is worth paying to get these benefits. As always, if you're not sure it's worth it in your case, benchmark it! -### Returning Rust values to Python +## Returning Rust values to Python When returning values from functions callable from Python, [PyO3's smart pointers](../types.md#pyo3s-smart-pointers) (`Py`, `Bound<'py, T>`, and `Borrowed<'a, 'py, T>`) can be used with zero cost. diff --git a/guide/src/conversions/traits.md b/guide/src/conversions/traits.md index 93ff8daeebf..dbc399e7b2f 100644 --- a/guide/src/conversions/traits.md +++ b/guide/src/conversions/traits.md @@ -1,8 +1,8 @@ -## Conversion traits +# Conversion traits PyO3 provides some handy traits to convert between Python types and Rust types. -### `.extract()` and the `FromPyObject` trait +## `.extract()` and the `FromPyObject` trait The easiest way to convert a Python object to a Rust value is using `.extract()`. It returns a `PyResult` with a type error if the conversion @@ -32,14 +32,14 @@ mutable references, you have to extract the PyO3 reference wrappers [`PyRef`] and [`PyRefMut`]. They work like the reference wrappers of `std::cell::RefCell` and ensure (at runtime) that Rust borrows are allowed. -#### Deriving [`FromPyObject`] +### Deriving [`FromPyObject`] [`FromPyObject`] can be automatically derived for many kinds of structs and enums if the member types themselves implement `FromPyObject`. This even includes members with a generic type `T: FromPyObject`. Derivation for empty enums, enum variants and structs is not supported. -#### Deriving [`FromPyObject`] for structs +### Deriving [`FromPyObject`] for structs The derivation generates code that will attempt to access the attribute `my_string` on the Python object, i.e. `obj.getattr("my_string")`, and call `extract()` on the attribute. @@ -168,7 +168,7 @@ struct RustyStruct { # } ``` -#### Deriving [`FromPyObject`] for tuple structs +### Deriving [`FromPyObject`] for tuple structs Tuple structs are also supported but do not allow customizing the extraction. The input is always assumed to be a Python tuple with the same length as the Rust type, the `n`th field @@ -197,6 +197,7 @@ struct RustyTuple(String, String); Tuple structs with a single field are treated as wrapper types which are described in the following section. To override this behaviour and ensure that the input is in fact a tuple, specify the struct as + ```rust use pyo3::prelude::*; @@ -216,7 +217,7 @@ struct RustyTuple((String,)); # } ``` -#### Deriving [`FromPyObject`] for wrapper types +### Deriving [`FromPyObject`] for wrapper types The `pyo3(transparent)` attribute can be used on structs with exactly one field. This results in extracting directly from the input object, i.e. `obj.extract()`, rather than trying to access @@ -251,7 +252,7 @@ struct RustyTransparentStruct { # } ``` -#### Deriving [`FromPyObject`] for enums +### Deriving [`FromPyObject`] for enums The `FromPyObject` derivation for enums generates code that tries to extract the variants in the order of the fields. As soon as a variant can be extracted successfully, that variant is returned. @@ -466,32 +467,34 @@ enum RustyEnum { If the input is neither a string nor an integer, the error message will be: `"'' cannot be cast as 'str | int'"`. -#### `#[derive(FromPyObject)]` Container Attributes +### `#[derive(FromPyObject)]` Container Attributes + - `pyo3(transparent)` - - extract the field directly from the object as `obj.extract()` instead of `get_item()` or + - extract the field directly from the object as `obj.extract()` instead of `get_item()` or `getattr()` - - Newtype structs and tuple-variants are treated as transparent per default. - - only supported for single-field structs and enum variants + - Newtype structs and tuple-variants are treated as transparent per default. + - only supported for single-field structs and enum variants - `pyo3(annotation = "name")` - - 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 + - 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 + - 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 -#### `#[derive(FromPyObject)]` Field Attributes - `pyo3(attribute)`, `pyo3(attribute("name"))` - - retrieve the field from an attribute, possibly with a custom name specified as an argument - - argument must be a string-literal. + - retrieve the field from an attribute, possibly with a custom name specified as an argument + - argument must be a string-literal. - `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` + - retrieve the field from a mapping, possibly with the custom key specified as an argument. + - can be any literal that implements `ToBorrowedObject` - `pyo3(from_py_with = ...)` - - apply a custom function to convert the field from Python the desired Rust type. - - the argument must be the path to the function. - - the function signature must be `fn(&Bound) -> PyResult` where `T` is the Rust type of the argument. + - apply a custom function to convert the field from Python the desired Rust type. + - 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. - in this case, the argument must be a Rust expression returning a value of the desired Rust type. @@ -535,8 +538,10 @@ struct RustyStruct { # } ``` -#### ⚠ Phase-Out of `FromPyObject` blanket implementation for cloneable PyClasses ⚠ +### ⚠ Phase-Out of `FromPyObject` blanket implementation for cloneable PyClasses ⚠ + Historically PyO3 has provided a blanket implementation for `#[pyclass]` types that also implement `Clone`, to allow extraction of such types by value. Over time this has turned out problematic for a few reasons, the major one being the prevention of custom conversions by downstream crates if their type is `Clone`. Over the next few releases the blanket implementation is gradually phased out, and eventually replaced by an opt-in option. As a first step of this migration a new `skip_from_py_object` option for `#[pyclass]` was introduced, to opt-out of the blanket implementation and allow downstream users to provide their own implementation: + ```rust # #![allow(dead_code)] # use pyo3::prelude::*; @@ -560,21 +565,23 @@ impl<'py> FromPyObject<'_, 'py> for Number { As a second step the `from_py_object` option was introduced. This option also opts-out of the blanket implementation and instead generates a custom `FromPyObject` implementation for the pyclass which is functionally equivalent to the blanket. -### `IntoPyObject` +## `IntoPyObject` + 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. +*without* having a unique python type. -#### derive macro +### 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,no_run # #![allow(dead_code)] # use pyo3::prelude::*; @@ -598,7 +605,6 @@ 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,no_run # #![allow(dead_code)] # use pyo3::prelude::*; @@ -635,13 +641,14 @@ 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 +#### `#[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 identifier - - 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` + - apply a custom function to convert the field from Rust into Python. + - the argument must be the function identifier + - 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,no_run # use pyo3::prelude::*; @@ -662,7 +669,7 @@ Additionally `IntoPyObject` can be derived for a reference to a struct or enum u } ``` -#### manual implementation +### manual implementation If the derive macro is not suitable for your use case, `IntoPyObject` can be implemented manually as demonstrated below. @@ -694,7 +701,7 @@ impl<'a, 'py> IntoPyObject<'py> for &'a MyPyObjectWrapper { } ``` -#### `BoundObject` for conversions that may be `Bound` or `Borrowed` +### `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>`: diff --git a/guide/src/debugging.md b/guide/src/debugging.md index c30a83c7ff9..66c3c23a3b3 100644 --- a/guide/src/debugging.md +++ b/guide/src/debugging.md @@ -34,11 +34,11 @@ 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. @@ -134,76 +134,82 @@ Depending on your OS and your preferences you can use two different debuggers, ` 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) + + - [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"] - } + ```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 + 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). + - 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 + - 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 @@ -335,7 +341,6 @@ To use these functions: 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 diff --git a/guide/src/ecosystem/async-await.md b/guide/src/ecosystem/async-await.md index 9da906edeb9..7ce8da218fd 100644 --- a/guide/src/ecosystem/async-await.md +++ b/guide/src/ecosystem/async-await.md @@ -11,5 +11,6 @@ code runs on the default `asyncio` event loop, so `pyo3-async-runtimes` should w Python libraries. ## Additional Information + - 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/guide/src/ecosystem/tracing.md b/guide/src/ecosystem/tracing.md index c9fd355173a..22b0aaab049 100644 --- a/guide/src/ecosystem/tracing.md +++ b/guide/src/ecosystem/tracing.md @@ -15,6 +15,7 @@ must configure its own `tracing` integration; one extension module will not see [`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 @@ -37,6 +38,7 @@ 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,no_run #[tracing::instrument] #[pyfunction] @@ -51,6 +53,7 @@ pub fn initialize_tracing(py_impl: Bound<'_, PyAny>) { .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] @@ -58,10 +61,12 @@ 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 diff --git a/guide/src/exception.md b/guide/src/exception.md index 74b3dae4e3a..c63974d9624 100644 --- a/guide/src/exception.md +++ b/guide/src/exception.md @@ -10,8 +10,8 @@ use pyo3::create_exception; create_exception!(module, MyError, pyo3::exceptions::PyException); ``` -* `module` is the name of the containing module. -* `MyError` is the name of the new exception type. +- `module` is the name of the containing module. +- `MyError` is the name of the new exception type. For example: @@ -107,7 +107,7 @@ err.is_instance_of::(py); ## Using exceptions defined in Python code It is possible to use an exception defined in Python code as a native Rust type. -The `import_exception!` macro allows importing a specific exception class and defines a Rust type +The [`import_exception!`] macro allows importing a specific exception class and defines a Rust type for that exception. ```rust,no_run @@ -129,15 +129,6 @@ fn tell(file: &Bound<'_, PyAny>) -> PyResult { [`pyo3::exceptions`]({{#PYO3_DOCS_URL}}/pyo3/exceptions/index.html) defines exceptions for several standard library modules. -[`create_exception!`]: {{#PYO3_DOCS_URL}}/pyo3/macro.create_exception.html -[`import_exception!`]: {{#PYO3_DOCS_URL}}/pyo3/macro.import_exception.html - -[`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/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`: @@ -184,3 +175,8 @@ Python::attach(|py| { ``` Note that this is not possible when the ``abi3`` feature is enabled, as that prevents subclassing ``PyException``. + +[`create_exception!`]: {{#PYO3_DOCS_URL}}/pyo3/macro.create_exception.html +[`import_exception!`]: {{#PYO3_DOCS_URL}}/pyo3/macro.import_exception.html +[`PyAny::is_instance`]: {{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.is_instance +[`PyAny::is_instance_of`]: {{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.is_instance_of diff --git a/guide/src/faq.md b/guide/src/faq.md index 3743a7fad72..e5c39d81fe1 100644 --- a/guide/src/faq.md +++ b/guide/src/faq.md @@ -2,7 +2,7 @@ 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 `std::sync::OnceLock`, `std::sync::LazyLock`, `lazy_static`, and `once_cell`! +## I'm experiencing deadlocks using PyO3 with `std::sync::OnceLock`, `std::sync::LazyLock`, `lazy_static`, and `once_cell` `OnceLock`, `LazyLock`, and their thirdparty predecessors use blocking to ensure only one thread ever initializes them. Because the Python interpreter can introduce additional locks (the Python GIL and GC can both require all other threads to pause) this can lead to deadlocks in the following way: @@ -19,41 +19,41 @@ PyO3 provides a struct [`PyOnceLock`] which implements a single-initialization A [`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"! +## 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" Currently, [#340](https://github.com/PyO3/pyo3/issues/340) causes `cargo test` to fail with linking errors when the `extension-module` feature is activated. Linking errors can also happen when building in a cargo workspace where a different crate also uses PyO3 (see [#2521](https://github.com/PyO3/pyo3/issues/2521)). For now, there are three ways we can work around these issues. 1. Make the `extension-module` feature optional. Build with `maturin develop --features "extension-module"` -```toml -[dependencies.pyo3] -{{#PYO3_CRATE_VERSION}} + ```toml + [dependencies.pyo3] + {{#PYO3_CRATE_VERSION}} -[features] -extension-module = ["pyo3/extension-module"] -``` + [features] + extension-module = ["pyo3/extension-module"] + ``` 2. Make the `extension-module` feature optional and default. Run tests with `cargo test --no-default-features`: -```toml -[dependencies.pyo3] -{{#PYO3_CRATE_VERSION}} + ```toml + [dependencies.pyo3] + {{#PYO3_CRATE_VERSION}} -[features] -extension-module = ["pyo3/extension-module"] -default = ["extension-module"] -``` + [features] + extension-module = ["pyo3/extension-module"] + default = ["extension-module"] + ``` 3. If you are using a [`pyproject.toml`](https://maturin.rs/metadata.html) file to control maturin settings, add the following section: -```toml -[tool.maturin] -features = ["pyo3/extension-module"] -# Or for maturin 0.12: -# cargo-extra-args = ["--features", "pyo3/extension-module"] -``` + ```toml + [tool.maturin] + features = ["pyo3/extension-module"] + # Or for maturin 0.12: + # cargo-extra-args = ["--features", "pyo3/extension-module"] + ``` -## I can't run `cargo test`: my crate cannot be found for tests in `tests/` directory! +## I can't run `cargo test`: my crate cannot be found for tests in `tests/` directory The Rust book suggests to [put integration tests inside a `tests/` directory](https://doc.rust-lang.org/book/ch11-03-test-organization.html#integration-tests). @@ -76,13 +76,13 @@ The best solution is to make your crate types include both `rlib` and `cdylib`: crate-type = ["cdylib", "rlib"] ``` -## Ctrl-C doesn't do anything while my Rust code is executing! +## Ctrl-C doesn't do anything while my Rust code is executing This is because Ctrl-C raises a SIGINT signal, which is handled by the calling Python process by simply setting a flag to action upon later. This flag isn't checked while Rust code called from Python is executing, only once control returns to the Python interpreter. You can give the Python interpreter a chance to process the signal properly by calling `Python::check_signals`. It's good practice to call this function regularly if you have a long-running Rust function so that your users can cancel it. -## `#[pyo3(get)]` clones my field! +## `#[pyo3(get)]` clones my field You may have a nested struct similar to this: @@ -126,6 +126,7 @@ b: This can be especially confusing if the field is mutable, as getting the field and then mutating it won't persist - you'll just get a fresh clone of the original on the next access. Unfortunately Python and Rust don't agree about ownership - if PyO3 gave out references to (possibly) temporary Rust objects to Python code, Python code could then keep that reference alive indefinitely. Therefore returning Rust objects requires cloning. If you don't want that cloning to happen, a workaround is to allocate the field on the Python heap and store a reference to that, by using [`Py<...>`]({{#PYO3_DOCS_URL}}/pyo3/struct.Py.html): + ```rust,no_run # use pyo3::prelude::*; #[pyclass] @@ -151,7 +152,9 @@ impl Outer { } } ``` + This time `a` and `b` *are* the same object: + ```python outer = Outer() @@ -166,10 +169,11 @@ print(f"a: {a}\nb: {b}") a: b: ``` + The downside to this approach is that any Rust code working on the `Outer` struct potentially has to attach to the Python interpreter to do anything with the `inner` field. (If `Inner` is `#[pyclass(frozen)]` and implements `Sync`, then `Py::get` may be used to access the `Inner` contents from `Py` without needing to attach to the interpreter.) -## I want to use the `pyo3` crate re-exported from dependency but the proc-macros fail! +## I want to use the `pyo3` crate re-exported from dependency but the proc-macros fail All PyO3 proc-macros (`#[pyclass]`, `#[pyfunction]`, `#[derive(FromPyObject)]` and so on) expect the `pyo3` crate to be available under that name in your crate @@ -190,13 +194,15 @@ done with the `crate` attribute: struct MyClass; ``` -## I'm trying to call Python from Rust but I get `STATUS_DLL_NOT_FOUND` or `STATUS_ENTRYPOINT_NOT_FOUND`! +## I'm trying to call Python from Rust but I get `STATUS_DLL_NOT_FOUND` or `STATUS_ENTRYPOINT_NOT_FOUND` This happens on Windows when linking to the python DLL fails or the wrong one is linked. The Python DLL on Windows will usually be called something like: + - `python3X.dll` for Python 3.X, e.g. `python310.dll` for Python 3.10 - `python3.dll` when using PyO3's `abi3` feature The DLL needs to be locatable using the [Windows DLL search order](https://learn.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-search-order#standard-search-order-for-unpackaged-apps). Some ways to achieve this are: + - Put the Python DLL in the same folder as your build artifacts - Add the directory containing the Python DLL to your `PATH` environment variable, for example `C:\Users\\AppData\Local\Programs\Python\Python310` - If this happens when you are *distributing* your program, consider using [PyOxidizer](https://github.com/indygreg/PyOxidizer) to package it with your binary. diff --git a/guide/src/features.md b/guide/src/features.md index 03b9734d92f..124cd406b69 100644 --- a/guide/src/features.md +++ b/guide/src/features.md @@ -120,14 +120,17 @@ Adds a dependency on [anyhow](https://docs.rs/anyhow). Enables a conversion from Enables Pyo3's `MutexExt` trait for all Mutexes that extend on [`lock_api::Mutex`](https://docs.rs/lock_api/latest/lock_api/struct.Mutex.html) or [`parking_lot::ReentrantMutex`](https://docs.rs/lock_api/latest/lock_api/struct.ReentrantMutex.html) and are wrapped in an [`Arc`](https://doc.rust-lang.org/std/sync/struct.Arc.html) type. Like [`Arc`](https://docs.rs/parking_lot/latest/parking_lot/type.Mutex.html#method.lock_arc) ### `bigdecimal` + Adds a dependency on [bigdecimal](https://docs.rs/bigdecimal) and enables conversions into its [`BigDecimal`](https://docs.rs/bigdecimal/latest/bigdecimal/struct.BigDecimal.html) type. ### `bytes` + Adds a dependency on [bytes](https://docs.rs/bytes/latest/bytes) and enables conversions into its [`Bytes`](https://docs.rs/bytes/latest/bytes/struct.Bytes.html) type. ### `chrono` Adds a dependency on [chrono](https://docs.rs/chrono). Enables a conversion from [chrono](https://docs.rs/chrono)'s types to python: + - [TimeDelta](https://docs.rs/chrono/latest/chrono/struct.TimeDelta.html) -> [`PyDelta`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyDelta.html) - [FixedOffset](https://docs.rs/chrono/latest/chrono/offset/struct.FixedOffset.html) -> [`PyDelta`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyDelta.html) - [Utc](https://docs.rs/chrono/latest/chrono/offset/struct.Utc.html) -> [`PyTzInfo`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyTzInfo.html) @@ -140,6 +143,7 @@ Adds a dependency on [chrono](https://docs.rs/chrono). Enables a conversion from Enables conversion from and to [Local](https://docs.rs/chrono/latest/chrono/struct.Local.html) timezones. The current system timezone as determined by [`iana_time_zone::get_timezone()`](https://docs.rs/iana-time-zone/latest/iana_time_zone/fn.get_timezone.html) will be used for conversions. `chrono::DateTime` will convert from either of: + - `datetime` objects with `tzinfo` equivalent to the current system timezone. - "naive" `datetime` objects (those without a `tzinfo`), as it is a convention that naive datetime objects should be treated as using the system timezone. @@ -169,6 +173,7 @@ Adds a dependency on [indexmap](https://docs.rs/indexmap) and enables conversion ### `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) @@ -198,6 +203,7 @@ Adds a dependency on [num-rational](https://docs.rs/num-rational) and enables co ### `ordered-float` Adds a dependency on [ordered-float](https://docs.rs/ordered-float) and enables conversions between [ordered-float](https://docs.rs/ordered-float)'s types and Python: + - [NotNan](https://docs.rs/ordered-float/latest/ordered_float/struct.NotNan.html) -> [`PyFloat`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyFloat.html) - [OrderedFloat](https://docs.rs/ordered-float/latest/ordered_float/struct.OrderedFloat.html) -> [`PyFloat`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyFloat.html) @@ -212,6 +218,7 @@ Adds a dependency on [rust_decimal](https://docs.rs/rust_decimal) and enables co ### `time` Adds a dependency on [time](https://docs.rs/time) and requires MSRV 1.67.1. Enables conversions between [time](https://docs.rs/time)'s types and Python: + - [Date](https://docs.rs/time/0.3.38/time/struct.Date.html) -> [`PyDate`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyDate.html) - [Time](https://docs.rs/time/0.3.38/time/struct.Time.html) -> [`PyTime`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyTime.html) - [OffsetDateTime](https://docs.rs/time/0.3.38/time/struct.OffsetDateTime.html) -> [`PyDateTime`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyDateTime.html) diff --git a/guide/src/free-threading.md b/guide/src/free-threading.md index 266eca85b4c..e145a273946 100644 --- a/guide/src/free-threading.md +++ b/guide/src/free-threading.md @@ -118,7 +118,6 @@ 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. Many existing extensions @@ -155,13 +154,13 @@ calls into your extension. The free-threaded build triggers global synchronization events in the following situations: -* During garbage collection in order to get a globally consistent view of +- 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 +- 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 +- When either `sys.settrace` or `sys.setprofile` are called in order to instrument running code objects and threads -* During a call to `os.fork()`, to ensure a process-wide consistent state. +- During a call to `os.fork()`, to ensure a process-wide consistent state. This is a non-exhaustive list and there may be other situations in future Python versions that can trigger global synchronization events. @@ -396,7 +395,6 @@ interpreter. [`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#method.get_or_init [`Python::detach`]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.detach [`Python::attach`]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.attach [`Python<'py>`]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html diff --git a/guide/src/function.md b/guide/src/function.md index 2a3d8f84240..60e23781393 100644 --- a/guide/src/function.md +++ b/guide/src/function.md @@ -36,7 +36,7 @@ There are also additional sections on the following topics: The `#[pyo3]` attribute can be used to modify properties of the generated Python function. It can take any combination of the following options: - - `#[pyo3(name = "...")]` +- `#[pyo3(name = "...")]` Overrides the name exposed to Python. @@ -63,15 +63,15 @@ The `#[pyo3]` attribute can be used to modify properties of the generated Python # }); ``` - - `#[pyo3(signature = (...))]` +- `#[pyo3(signature = (...))]` Defines the function signature in Python. See [Function Signatures](./function/signature.md). - - `#[pyo3(text_signature = "...")]` +- `#[pyo3(text_signature = "...")]` Overrides the PyO3-generated function signature visible in Python tooling (such as via [`inspect.signature`]). See the [corresponding topic in the Function Signatures subchapter](./function/signature.md#making-the-function-signature-available-to-python). - - `#[pyo3(pass_module)]` +- `#[pyo3(pass_module)]` Set this option to make PyO3 pass the containing module as the first argument to the function. It is then possible to use the module in the function body. The first argument **must** be of type `&Bound<'_, PyModule>`, `Bound<'_, PyModule>`, or `Py`. @@ -92,7 +92,8 @@ The `#[pyo3]` attribute can be used to modify properties of the generated Python } } ``` - - `#[pyo3(warn(message = "...", category = ...))]` + +- `#[pyo3(warn(message = "...", category = ...))]` This option is used to display a warning when the function is used in Python. It is equivalent to [`warnings.warn(message, category)`](https://docs.python.org/3/library/warnings.html#warnings.warn). The `message` parameter is a string that will be displayed when the function is called, and the `category` parameter is optional and has to be a subclass of [`Warning`](https://docs.python.org/3/library/exceptions.html#Warning). @@ -201,7 +202,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. @@ -248,14 +249,6 @@ The ways to convert a Rust function into a Python object vary depending on the f - use a `#[pyclass]` struct which stores the function as a field and implement `__call__` to call the stored function. - use `PyCFunction::new_closure` to create an object directly from the function. -[`Bound<'_, PyAny>::is_callable`]: {{#PYO3_DOCS_URL}}/pyo3/prelude/trait.PyAnyMethods.html#tymethod.is_callable -[`Bound<'_, PyAny>::call`]: {{#PYO3_DOCS_URL}}/pyo3/prelude/trait.PyAnyMethods.html#tymethod.call -[`Bound<'_, PyAny>::call0`]: {{#PYO3_DOCS_URL}}/pyo3/prelude/trait.PyAnyMethods.html#tymethod.call0 -[`Bound<'_, PyAny>::call1`]: {{#PYO3_DOCS_URL}}/pyo3/prelude/trait.PyAnyMethods.html#tymethod.call1 -[`wrap_pyfunction!`]: {{#PYO3_DOCS_URL}}/pyo3/macro.wrap_pyfunction.html -[`PyFunction`]: {{#PYO3_DOCS_URL}}/pyo3/types/struct.PyFunction.html -[`PyCFunction`]: {{#PYO3_DOCS_URL}}/pyo3/types/struct.PyCFunction.html - ### Accessing the FFI functions In order to make Rust functions callable from Python, PyO3 generates an `extern "C"` @@ -267,4 +260,16 @@ arguments from the input `PyObject`s. The `wrap_pyfunction` macro can be used to directly get a `Bound` given a `#[pyfunction]` and a `Bound`: `wrap_pyfunction!(rust_fun, module)`. + +[`Bound<'_, PyAny>::is_callable`]: {{#PYO3_DOCS_URL}}/pyo3/prelude/trait.PyAnyMethods.html#tymethod.is_callable + +[`Bound<'_, PyAny>::call`]: {{#PYO3_DOCS_URL}}/pyo3/prelude/trait.PyAnyMethods.html#tymethod.call + +[`Bound<'_, PyAny>::call0`]: {{#PYO3_DOCS_URL}}/pyo3/prelude/trait.PyAnyMethods.html#tymethod.call0 + +[`Bound<'_, PyAny>::call1`]: {{#PYO3_DOCS_URL}}/pyo3/prelude/trait.PyAnyMethods.html#tymethod.call1 +[`wrap_pyfunction!`]: {{#PYO3_DOCS_URL}}/pyo3/macro.wrap_pyfunction.html +[`PyFunction`]: {{#PYO3_DOCS_URL}}/pyo3/types/struct.PyFunction.html +[`PyCFunction`]: {{#PYO3_DOCS_URL}}/pyo3/types/struct.PyCFunction.html + [`inspect.signature`]: https://docs.python.org/3/library/inspect.html#inspect.signature diff --git a/guide/src/function/error-handling.md b/guide/src/function/error-handling.md index 0d5d24ca704..cb0eb177d61 100644 --- a/guide/src/function/error-handling.md +++ b/guide/src/function/error-handling.md @@ -13,6 +13,7 @@ Rust code uses the generic [`Result`] enum to propagate errors. The error PyO3 has the [`PyErr`] type which represents a Python exception. If a PyO3 API could result in a Python exception being raised, the return type of that `API` will be [`PyResult`], which is an alias for the type `Result`. In summary: + - When Python exceptions are raised and caught by PyO3, the exception will be stored in the `Err` variant of the `PyResult`. - Passing Python exceptions through Rust code then uses all the "normal" techniques such as the `?` operator, with `PyErr` as the error type. - Finally, when a `PyResult` crosses from Rust back to Python via PyO3, if the result is an `Err` variant the contained exception will be raised. diff --git a/guide/src/function/signature.md b/guide/src/function/signature.md index 0281e163d17..393bf3a779b 100644 --- a/guide/src/function/signature.md +++ b/guide/src/function/signature.md @@ -26,16 +26,17 @@ mod module_with_functions { Just like in Python, the following constructs can be part of the signature:: - * `/`: positional-only arguments separator, each parameter defined before `/` is a positional-only parameter. - * `*`: var arguments separator, each parameter defined after `*` is a keyword-only parameter. - * `*args`: "args" is var args. Type of the `args` parameter has to be `&Bound<'_, PyTuple>`. - * `**kwargs`: "kwargs" receives keyword arguments. The type of the `kwargs` parameter has to be `Option<&Bound<'_, PyDict>>`. - * `arg=Value`: arguments with default value. +- `/`: positional-only arguments separator, each parameter defined before `/` is a positional-only parameter. +- `*`: var arguments separator, each parameter defined after `*` is a keyword-only parameter. +- `*args`: "args" is var args. Type of the `args` parameter has to be `&Bound<'_, PyTuple>`. +- `**kwargs`: "kwargs" receives keyword arguments. The type of the `kwargs` parameter has to be `Option<&Bound<'_, PyDict>>`. +- `arg=Value`: arguments with default value. If the `arg` argument is defined after var arguments, it is treated as a keyword-only argument. Note that `Value` has to be valid rust code, PyO3 just inserts it into the generated code unmodified. Example: + ```rust,no_run # use pyo3::prelude::*; use pyo3::types::{PyDict, PyTuple}; @@ -88,6 +89,7 @@ pub fn simple_python_bound_function(py: Python<'_>, lambda: Py) -> PyResu ``` N.B. the position of the `/` and `*` arguments (if included) control the system of handling positional and keyword arguments. In Python: + ```python import mymodule @@ -96,13 +98,17 @@ print(mc.method(44, False, "World", 666, x=44, y=55)) print(mc.method(num=-1, name="World")) print(mc.make_change(44)) ``` + Produces output: + ```text num=44 (was previously=-1), py_args=(False, 'World', 666), name=Hello, py_kwargs=Some({'x': 44, 'y': 55}) num=-1 (was previously=44), py_args=(), name=World, py_kwargs=None num=44 ``` + + > Note: to use keywords like `struct` as a function argument, use "raw identifier" syntax `r#struct` in both the signature and the function definition: > > ```rust,no_run @@ -115,6 +121,8 @@ num=44 > } > ``` + + ## 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. @@ -251,6 +259,7 @@ Type: builtin_function_or_method ### Type annotations in the signature When the `experimental-inspect` Cargo feature is enabled, the `signature` attribute can also contain type hints: + ```rust # #[cfg(feature = "experimental-inspect")] { use pyo3::prelude::*; @@ -269,10 +278,13 @@ pub mod example { ``` It enables the [work-in-progress capacity of PyO3 to autogenerate type stubs](../type-stub.md) to generate a file with the correct type hints: + ```python def list_of_int_identity(arg: list[int]) -> list[int]: ... ``` + instead of the generic: + ```python import typing diff --git a/guide/src/getting-started.md b/guide/src/getting-started.md index a75b7a75b6e..b7d3df474c5 100644 --- a/guide/src/getting-started.md +++ b/guide/src/getting-started.md @@ -31,29 +31,33 @@ pyenv install 3.12 --keep There are a number of build and Python package management systems such as [`setuptools-rust`](https://github.com/PyO3/setuptools-rust) or [manually](./building-and-distribution.md#manual-builds). We recommend the use of `maturin`, which you can install [here](https://maturin.rs/installation.html). It is developed to work with PyO3 and provides the most "batteries included" experience, especially if you are aiming to publish to PyPI. `maturin` is just a Python package, so you can add it in the same way you already install Python packages. System Python: + ```bash pip install maturin --user ``` pipx: + ```bash pipx install maturin ``` pyenv: + ```bash pyenv activate pyo3 pip install maturin ``` poetry: + ```bash poetry add -G dev maturin ``` After installation, you can run `maturin --version` to check that you have correctly installed it. -# Starting a new project +## Starting a new project First you should create the folder and virtual environment that are going to contain your new project. Here we will use the recommended `pyenv`: @@ -85,7 +89,7 @@ pyenv virtualenv pyo3 pyenv local pyo3 ``` -# Adding to an existing project +## Adding to an existing project Sadly, `maturin` cannot currently be run in existing projects, so if you want to use Python in an existing project you basically have two options: @@ -98,7 +102,6 @@ If you opt for the second option, here are the things you need to pay attention Make sure that the Rust crate you want to be able to access from Python is compiled into a library. You can have a binary output as well, but the code you want to access from Python has to be in the library part. Also, make sure that the crate type is `cdylib` and add PyO3 as a dependency as so: - ```toml # If you already have [package] information in `Cargo.toml`, you can ignore # this section! diff --git a/guide/src/migration.md b/guide/src/migration.md index 099c06bbe29..68543e8654d 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -4,7 +4,9 @@ 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.26.* to 0.27 + ### `FromPyObject` reworked for flexibility and efficiency +
Click to expand @@ -26,6 +28,7 @@ in case of a conversion error. During migration using `PyErr` is a good default, type can be introduced to prevent unneccessary creation of Python exception objects and improved type safety. Before: + ```rust,ignore impl<'py> FromPyObject<'py> for IpAddr { fn extract_bound(obj: &Bound<'py, PyAny>) -> PyResult { @@ -35,6 +38,7 @@ impl<'py> FromPyObject<'py> for IpAddr { ``` After + ```rust,ignore impl<'py> FromPyObject<'_, 'py> for IpAddr { type Error = PyErr; @@ -53,6 +57,7 @@ correct bound depends on how the type is used. For simple wrapper types usually it's possible to just forward the bound. Before: + ```rust,ignore struct MyWrapper(T); @@ -67,6 +72,7 @@ where ``` After: + ```rust # use pyo3::prelude::*; # #[allow(dead_code)] @@ -89,6 +95,7 @@ introduced. It is automatically implemented for any type that implements `FromPy borrow from the input. It is intended to be used as a trait bound in these situations. Before: + ```rust,ignore struct MyVec(Vec); impl<'py, T> FromPyObject<'py> for Vec @@ -106,6 +113,7 @@ where ``` After: + ```rust # use pyo3::prelude::*; # #[allow(dead_code)] @@ -150,7 +158,9 @@ Because `PyTypeCheck` is the trait used to guard the `.cast()` functions to trea This should always have been the case, it was an unfortunate omission from its original implementation which is being corrected in this release. ## from 0.25.* to 0.26 + ### Rename of `Python::with_gil`, `Python::allow_threads`, and `pyo3::prepare_freethreaded_python` +
Click to expand @@ -163,6 +173,7 @@ For this reason, we chose to rename these to more modern terminology introduced
### Deprecation of `PyObject` type alias +
Click to expand @@ -170,6 +181,7 @@ The type alias `PyObject` (aka `Py`) is often confused with the identical
### Replacement of `GILOnceCell` with `PyOnceLock` +
Click to expand @@ -207,9 +219,11 @@ DECIMAL_TYPE.import(py, "decimal", "Decimal")?; # }) # } ``` +
### Deprecation of `GILProtected` +
Click to expand @@ -249,9 +263,11 @@ Python::attach(|py| { # }) # } ``` +
### `PyMemoryError` now maps to `io::ErrorKind::OutOfMemory` when converted to `io::Error` +
Click to expand @@ -262,7 +278,9 @@ This change makes error conversions more precise and matches the semantics of ou
## from 0.24.* to 0.25 + ### `AsPyPointer` removal +
Click to expand The `AsPyPointer` trait is mostly a leftover from the now removed gil-refs API. The last remaining uses were the GC API, namely `PyVisit::call`, and identity comparison (`PyAnyMethods::is` and `Py::is`). @@ -273,28 +291,33 @@ The `AsPyPointer` trait is mostly a leftover from the now removed gil-refs API.
## from 0.23.* to 0.24 +
Click to expand There were no significant changes from 0.23 to 0.24 which required documenting in this guide.
## from 0.22.* to 0.23 +
Click to expand 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. + +- 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. + +- 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 @@ -308,8 +331,9 @@ Aside from the change to `#[pyclass]`, most features of PyO3 work unchanged, as Future PyO3 versions will likely add more traits and data structures to make working with free-threaded Python easier. 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 + +- 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 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. @@ -318,11 +342,13 @@ See [the guide section on free-threaded Python](free-threading.md) for more deta
### New `IntoPyObject` trait unifies to-Python conversions +
Click to expand 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 @@ -359,6 +385,7 @@ The `IntoPyObjectRef` derive macro derives implementations for references (e.g. #### `IntoPyObject` manual implementation Before: + ```rust,ignore # use pyo3::prelude::*; # #[allow(dead_code)] @@ -378,6 +405,7 @@ impl ToPyObject for MyPyObjectWrapper { ``` After: + ```rust,no_run # #![allow(deprecated)] # use pyo3::prelude::*; @@ -405,15 +433,18 @@ impl<'a, 'py> IntoPyObject<'py> for &'a MyPyObjectWrapper { } } ``` +
-### To-Python conversions 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 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` - `[u8; N]` - `SmallVec<[u8; N]>` @@ -438,6 +469,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`: + - `&[T]` - `Cow<[T]>` @@ -446,6 +478,7 @@ This is purely additional and should just extend the possible return types.
### `gil-refs` feature removed +
Click to expand @@ -535,11 +568,13 @@ where } } ``` +
## from 0.21.* to 0.22 ### Deprecation of `gil-refs` feature continues +
Click to expand @@ -549,6 +584,7 @@ See the 0.21 migration entry for help upgrading.
### Deprecation of implicit default for trailing optional arguments +
Click to expand @@ -578,9 +614,11 @@ 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. @@ -593,6 +631,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 @@ -624,9 +663,11 @@ enum SimpleEnum { VariantB = 42, } ``` +
### `PyType::name` reworked to better match Python `__name__` +
Click to expand @@ -680,29 +721,31 @@ Python::with_gil(|py| { }) # } ``` -
- +
## 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. +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 . The "GIL Ref" `&'py PyAny` and similar types such as `&'py PyDict` continue to be available as a deprecated API. Due to the advantages of the new API it is advised that all users make the effort to upgrade as soon as possible. In addition to the major API type overhaul, PyO3 has needed to make a few small breaking adjustments to other APIs to close correctness and soundness gaps. The recommended steps to update to PyO3 0.21 is as follows: - 1. Enable the `gil-refs` feature to silence deprecations related to the API change - 2. Fix all other PyO3 0.21 migration steps - 3. Disable the `gil-refs` feature and migrate off the deprecated APIs + +1. Enable the `gil-refs` feature to silence deprecations related to the API change +2. Fix all other PyO3 0.21 migration steps +3. Disable the `gil-refs` feature and migrate off the deprecated APIs The following sections are laid out in this order.
### Enable the `gil-refs` feature +
Click to expand @@ -727,9 +770,11 @@ After: [dependencies] pyo3 = { version = "0.21", features = ["gil-refs"] } ``` +
### `PyTypeInfo` and `PyTryFrom` have been adjusted +
Click to expand @@ -770,9 +815,11 @@ Python::with_gil(|py| { }) # } ``` +
### `Iter(A)NextOutput` are deprecated +
Click to expand @@ -891,9 +938,11 @@ impl PyClassAsyncIter { } } ``` +
### `PyType::name` has been renamed to `PyType::qualname` +
Click to expand @@ -901,6 +950,7 @@ impl PyClassAsyncIter {
### `PyCell` has been deprecated +
Click to expand @@ -908,6 +958,7 @@ Interactions with Python objects implemented in Rust no longer need to go though
### Migrating from the GIL Refs API to `Bound` +
Click to expand @@ -916,11 +967,13 @@ To minimise breakage of code using the GIL Refs API, the `Bound` smart pointe To identify what to migrate, temporarily switch off the `gil-refs` feature to see deprecation warnings on [almost](#cases-where-pyo3-cannot-emit-gil-ref-deprecation-warnings) all uses of APIs accepting and producing GIL Refs . Over one or more PRs it should be possible to follow the deprecation hints to update code. Depending on your development environment, switching off the `gil-refs` feature may introduce [some very targeted breakages](#deactivating-the-gil-refs-feature), so you may need to fixup those first. For example, the following APIs have gained updated variants: + - `PyList::new`, `PyTuple::new` and similar constructors have replacements `PyList::new_bound`, `PyTuple::new_bound` etc. - `FromPyObject::extract` has a new `FromPyObject::extract_bound` (see the section below) - The `PyTypeInfo` trait has had new `_bound` methods added to accept / return `Bound`. Because the new `Bound` API brings ownership out of the PyO3 framework and into user code, there are a few places where user code is expected to need to adjust while switching to the new API: + - Code will need to add the occasional `&` to borrow the new smart pointer as `&Bound` to pass these types around (or use `.clone()` at the very small cost of increasing the Python reference count) - `Bound` and `Bound` cannot support indexing with `list[0]`, you should use `list.get_item(0)` instead. - `Bound::iter_borrowed` is slightly more efficient than `Bound::iter`. The default iteration of `Bound` cannot return borrowed references because Rust does not (yet) have "lending iterators". Similarly `Bound::get_borrowed_item` is more efficient than `Bound::get_item` for the same reason. @@ -1006,6 +1059,7 @@ Despite a large amount of deprecations warnings produced by PyO3 to aid with the
### Deactivating the `gil-refs` feature +
Click to expand @@ -1073,11 +1127,13 @@ assert_eq!(&*name, "list"); # } # Python::with_gil(example).unwrap(); ``` +
## from 0.19.* to 0.20 ### Drop support for older technologies +
Click to expand @@ -1085,6 +1141,7 @@ PyO3 0.20 has increased minimum Rust version to 1.56. This enables use of newer
### `PyDict::get_item` now returns a `Result` +
Click to expand @@ -1092,7 +1149,6 @@ PyO3 0.20 has increased minimum Rust version to 1.56. This enables use of newer Newer recommendations by the Python core developers advise against using these APIs which suppress exceptions, instead allowing exceptions to bubble upwards. `PyDict::get_item_with_error` already implemented this recommended behavior, so that API has been renamed to `PyDict::get_item`. - Before: ```rust,ignore @@ -1142,9 +1198,11 @@ Python::with_gil(|py| -> PyResult<()> { }); # } ``` +
### Required arguments are no longer accepted after optional arguments +
Click to expand @@ -1171,9 +1229,11 @@ fn x_or_y(x: Option, y: u64) -> u64 { x.unwrap_or(y) } ``` +
### Remove deprecated function forms +
Click to expand @@ -1205,6 +1265,7 @@ fn add(a: u64, b: u64) -> u64 {
### `IntoPyPointer` trait removed +
Click to expand @@ -1212,6 +1273,7 @@ The trait `IntoPyPointer`, which provided the `into_ptr` method on many types, h
### `AsPyPointer` now `unsafe` trait +
Click to expand @@ -1221,6 +1283,7 @@ The trait `AsPyPointer` is now `unsafe trait`, meaning any external implementati ## from 0.18.* to 0.19 ### Access to `Python` inside `__traverse__` implementations are now forbidden +
Click to expand @@ -1242,9 +1305,11 @@ impl SomeClass { } } ``` +
### Smarter `anyhow::Error` / `eyre::Report` conversion when inner error is "simple" `PyErr` +
Click to expand @@ -1287,6 +1352,7 @@ However, if the `anyhow::Error` or `eyre::Report` has a source, then the origina
### The deprecated `Python::acquire_gil` was removed and `Python::with_gil` must be used instead +
Click to expand @@ -1362,6 +1428,7 @@ Furthermore, `Python::acquire_gil` provides ownership of a `GILGuard` which can ## from 0.17.* to 0.18 ### Required arguments after `Option<_>` arguments will no longer be automatically inferred +
Click to expand @@ -1393,9 +1460,11 @@ fn required_argument_after_option_a(x: Option, y: i32) {} #[pyfunction(signature = (x=None, y=0))] fn required_argument_after_option_b(x: Option, y: i32) {} ``` +
### `__text_signature__` is now automatically generated for `#[pyfunction]` and `#[pymethods]` +
Click to expand @@ -1427,11 +1496,13 @@ fn function_with_defaults(a: i32, b: i32, c: i32) {} # }) # } ``` +
## from 0.16.* to 0.17 ### Type checks have been changed for `PyMapping` and `PySequence` types +
Click to expand @@ -1456,6 +1527,7 @@ calling `collections.abc.Mapping.register(MappingPyClass)` or `collections.abc.Sequence.register(SequencePyClass)` from Python. For example, for a mapping class defined in Rust: + ```rust,compile_fail use pyo3::prelude::*; use std::collections::HashMap; @@ -1475,6 +1547,7 @@ impl Mapping { ``` You must register the class with `collections.abc.Mapping` before the downcast will work: + ```rust,compile_fail let m = Py::new(py, Mapping { index }).unwrap(); assert!(m.as_ref(py).downcast::().is_err()); @@ -1486,6 +1559,7 @@ Note that this requirement may go away in the future when a pyclass is able to i
### The `multiple-pymethods` feature now requires Rust 1.62 +
Click to expand @@ -1494,12 +1568,14 @@ requires Rust 1.62. For more information see [dtolnay/inventory#32](https://gith
### Added `impl IntoPy> for &str` +
Click to expand This may cause inference errors. Before: + ```rust,compile_fail # use pyo3::prelude::*; # @@ -1523,9 +1599,11 @@ Python::with_gil(|py| { }); # } ``` +
### The `pyproto` feature is now disabled by default +
Click to expand @@ -1533,6 +1611,7 @@ In preparation for removing the deprecated `#[pyproto]` attribute macro in a fut
### `PyTypeObject` trait has been deprecated +
Click to expand @@ -1564,9 +1643,11 @@ fn get_type_object(py: Python<'_>) -> &PyType { # Python::with_gil(|py| { get_type_object::(py); }); ``` +
### `impl IntoPy for [T; N]` now requires `T: IntoPy` rather than `T: ToPyObject` +
Click to expand @@ -1574,6 +1655,7 @@ If this leads to errors, simply implement `IntoPy`. Because pyclasses already im
### Each `#[pymodule]` can now only be initialized once per process +
Click to expand @@ -1582,7 +1664,12 @@ To make PyO3 modules sound in the presence of Python sub-interpreters, for now i ## from 0.15.* to 0.16 + + ### Drop support for older technologies + + +
Click to expand @@ -1590,6 +1677,7 @@ PyO3 0.16 has increased minimum Rust version to 1.48 and minimum Python version
### `#[pyproto]` has been deprecated +
Click to expand @@ -1644,9 +1732,11 @@ impl MyClass { } } ``` +
### Removed `PartialEq` for object wrappers +
Click to expand @@ -1663,14 +1753,16 @@ method `eq()`.
### Container magic methods now match Python behavior +
Click to expand In PyO3 0.15, `__getitem__`, `__setitem__` and `__delitem__` in `#[pymethods]` would generate only the _mapping_ implementation for a `#[pyclass]`. To match the Python behavior, these methods now generate both the _mapping_ **and** _sequence_ implementations. This means that classes implementing these `#[pymethods]` will now also be treated as sequences, same as a Python `class` would be. Small differences in behavior may result: - - PyO3 will allow instances of these classes to be cast to `PySequence` as well as `PyMapping`. - - Python will provide a default implementation of `__iter__` (if the class did not have one) which repeatedly calls `__getitem__` with integers (starting at 0) until an `IndexError` is raised. + +- PyO3 will allow instances of these classes to be cast to `PySequence` as well as `PyMapping`. +- Python will provide a default implementation of `__iter__` (if the class did not have one) which repeatedly calls `__getitem__` with integers (starting at 0) until an `IndexError` is raised. To explain this in detail, consider the following Python class: @@ -1696,6 +1788,7 @@ The PyO3 behavior in 0.16 has been changed to be closer to this Python behavior
### `wrap_pymodule!` and `wrap_pyfunction!` now respect privacy correctly +
Click to expand @@ -1745,11 +1838,13 @@ fn my_module(_py: Python<'_>, m: &PyModule) -> PyResult<()> { Ok(()) } ``` +
## from 0.14.* to 0.15 ### Changes in sequence indexing +
Click to expand @@ -1763,7 +1858,7 @@ Further, the `get_item` methods now always return a `PyResult` instead of panicking on invalid indices. The `Index` trait has been implemented instead, and provides the same panic behavior as on Rust vectors. -Note that *slice* indices (accepted by `PySequence::get_slice` and other) still +Note that _slice_ indices (accepted by `PySequence::get_slice` and other) still inherit the Python behavior of clamping the indices to the actual length, and not panicking/returning an error on out of range indices. @@ -1780,11 +1875,13 @@ Python::with_gil(|py| { assert_eq!(list[0..2].to_string(), "[1, 2]"); }); ``` +
## from 0.13.* to 0.14 ### `auto-initialize` feature is now opt-in +
Click to expand @@ -1792,6 +1889,7 @@ For projects embedding Python in Rust, PyO3 no longer automatically initializes
### New `multiple-pymethods` feature +
Click to expand @@ -1801,6 +1899,7 @@ The limitation of the new default implementation is that it cannot support multi
### Deprecated `#[pyproto]` methods +
Click to expand @@ -1842,11 +1941,13 @@ impl MyClass { } } ``` +
## from 0.12.* to 0.13 ### Minimum Rust version increased to Rust 1.45 +
Click to expand @@ -1854,6 +1955,7 @@ PyO3 `0.13` makes use of new Rust language features stabilized between Rust 1.40
### Runtime changes to support the CPython limited API +
Click to expand @@ -1869,6 +1971,7 @@ The largest of these is that all types created from PyO3 are what CPython calls ## from 0.11.* to 0.12 ### `PyErr` has been reworked +
Click to expand @@ -1882,6 +1985,7 @@ migrate to the new APIs.
#### `PyErr::new` and `PyErr::from_type` now require `Send + Sync` for their argument +
Click to expand @@ -1893,6 +1997,7 @@ Similarly, any types which implemented `PyErrArguments` will now need to be `Sen
#### `PyErr`'s contents are now private +
Click to expand @@ -1901,6 +2006,7 @@ You should instead now use the new methods `PyErr::ptype`, `PyErr::pvalue` and `
#### `PyErrValue` and `PyErr::from_value` have been removed +
Click to expand @@ -1911,24 +2017,29 @@ Exception types](#exception-types-have-been-reworked)).
#### `Into>` for `PyErr` has been removed +
Click to expand This implementation was redundant. Just construct the `Result::Err` variant directly. Before: + ```rust,compile_fail let result: PyResult<()> = PyErr::new::("error message").into(); ``` After (also using the new reworked exception types; see the following section): + ```rust,no_run # use pyo3::{PyResult, exceptions::PyTypeError}; let result: PyResult<()> = Err(PyTypeError::new_err("error message")); ``` +
### Exception types have been reworked +
Click to expand @@ -1962,9 +2073,11 @@ assert_eq!( # Ok(()) # }).unwrap(); ``` +
### `FromPy` has been removed +
Click to expand @@ -1976,6 +2089,7 @@ Now there is only one way to define the conversion, `IntoPy`, so downstream crat adjust accordingly. Before: + ```rust,compile_fail # use pyo3::prelude::*; struct MyPyObjectWrapper(PyObject); @@ -1988,6 +2102,7 @@ impl FromPy for PyObject { ``` After + ```rust,ignore # use pyo3::prelude::*; # #[allow(dead_code)] @@ -2004,6 +2119,7 @@ impl IntoPy for MyPyObjectWrapper { Similarly, code which was using the `FromPy` trait can be trivially rewritten to use `IntoPy`. Before: + ```rust,compile_fail # use pyo3::prelude::*; # Python::with_gil(|py| { @@ -2012,6 +2128,7 @@ let obj = PyObject::from_py(1.234, py); ``` After: + ```rust,ignore # #![allow(deprecated)] # use pyo3::prelude::*; @@ -2019,9 +2136,11 @@ After: let obj: PyObject = 1.234.into_py(py); # }) ``` +
### `PyObject` is now a type alias of `Py` +
Click to expand @@ -2030,6 +2149,7 @@ This should change very little from a usage perspective. If you implemented trai
### `AsPyRef` has been removed +
Click to expand @@ -2041,6 +2161,7 @@ This should require no code changes except removing `use pyo3::AsPyRef` for code `pyo3::prelude::*`. Before: + ```rust,ignore use pyo3::{AsPyRef, Py, types::PyList}; # pyo3::Python::with_gil(|py| { @@ -2050,6 +2171,7 @@ let list_ref: &PyList = list_py.as_ref(py); ``` After: + ```rust,ignore use pyo3::{Py, types::PyList}; # pyo3::Python::with_gil(|py| { @@ -2057,11 +2179,13 @@ let list_py: Py = PyList::empty(py).into(); let list_ref: &PyList = list_py.as_ref(py); # }) ``` +
## from 0.10.* to 0.11 ### Stable Rust +
Click to expand @@ -2069,6 +2193,7 @@ PyO3 now supports the stable Rust toolchain. The minimum required version is 1.3
### `#[pyclass]` structs must now be `Send` or `unsendable` +
Click to expand @@ -2084,6 +2209,7 @@ There can be two fixes: `RefCell`, and `Box` instead of `Box`. Before: + ```rust,compile_fail use pyo3::prelude::*; use std::rc::Rc; @@ -2097,6 +2223,7 @@ There can be two fixes: ``` After: + ```rust,ignore # #![allow(dead_code)] use pyo3::prelude::*; @@ -2119,6 +2246,7 @@ There can be two fixes: making it thread-safe to expose an unsendable object to the Python interpreter. Before: + ```rust,compile_fail use pyo3::prelude::*; @@ -2129,6 +2257,7 @@ There can be two fixes: ``` After: + ```rust,no_run # #![allow(dead_code)] use pyo3::prelude::*; @@ -2138,9 +2267,11 @@ There can be two fixes: pointers: Vec<*mut std::ffi::c_char>, } ``` +
### All `PyObject` and `Py` methods now take `Python` as an argument +
Click to expand @@ -2149,6 +2280,7 @@ ensure that the Python GIL was held by the current thread). Technically, this wa To migrate, just pass a `py` argument to any calls to these methods. Before: + ```rust,compile_fail # pyo3::Python::attach(|py| { py.None().get_refcnt(); @@ -2156,16 +2288,19 @@ py.None().get_refcnt(); ``` After: + ```rust # pyo3::Python::attach(|py| { py.None().get_refcnt(py); # }) ``` +
## from 0.9.* to 0.10 ### `ObjectProtocol` is removed +
Click to expand @@ -2175,6 +2310,7 @@ all you need to do is remove `ObjectProtocol` from your code. Or if you use `ObjectProtocol` by `use pyo3::prelude::*`, you have to do nothing. Before: + ```rust,compile_fail,ignore use pyo3::ObjectProtocol; @@ -2186,6 +2322,7 @@ assert_eq!(hi.len().unwrap(), 5); ``` After: + ```rust,ignore # pyo3::Python::with_gil(|py| { let obj = py.eval("lambda: 'Hi :)'", None, None).unwrap(); @@ -2193,9 +2330,11 @@ let hi: &pyo3::types::PyString = obj.call0().unwrap().downcast().unwrap(); assert_eq!(hi.len().unwrap(), 5); # }) ``` +
### No `#![feature(specialization)]` in user code +
Click to expand @@ -2206,6 +2345,7 @@ now you don't have to use `#![feature(specialization)]` in your crate. ## from 0.8.* to 0.9 ### `#[new]` interface +
Click to expand @@ -2213,6 +2353,7 @@ now you don't have to use `#![feature(specialization)]` in your crate. is now removed and our syntax for constructors has changed. Before: + ```rust,compile_fail #[pyclass] struct MyClass {} @@ -2227,6 +2368,7 @@ impl MyClass { ``` After: + ```rust,no_run # use pyo3::prelude::*; #[pyclass] @@ -2246,6 +2388,7 @@ For more, see [the constructor section](class.md#constructor) of this guide.
### PyCell +
Click to expand @@ -2259,6 +2402,7 @@ Python exceptions will automatically be raised when your functions are used in a rules of references. Here is an example. + ```rust # use pyo3::prelude::*; @@ -2288,6 +2432,7 @@ impl Names { # "); # }) ``` + `Names` has a `merge` method, which takes `&mut self` and another argument of type `&mut Self`. Given this `#[pyclass]`, calling `names.merge(names)` in Python raises a [`PyBorrowMutError`] exception, since it requires two mutable borrows of `names`. @@ -2295,6 +2440,7 @@ a [`PyBorrowMutError`] exception, since it requires two mutable borrows of `name However, for `#[pyproto]` and some functions, you need to manually fix the code. #### Object creation + 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 @@ -2303,6 +2449,7 @@ If you need [`PyRef`] or [`PyRefMut`], just call `.borrow()` or `.borrow_mut()` on the newly-created `PyCell`. Before: + ```rust,compile_fail # use pyo3::prelude::*; # #[pyclass] @@ -2313,6 +2460,7 @@ let obj_ref = PyRef::new(py, MyClass {}).unwrap(); ``` After: + ```rust,ignore # use pyo3::prelude::*; # #[pyclass] @@ -2324,12 +2472,14 @@ let obj_ref = obj.borrow(); ``` #### Object extraction + For `PyClass` types `T`, `&T` and `&mut T` no longer have [`FromPyObject`] implementations. Instead you should extract `PyRef` or `PyRefMut`, respectively. If `T` implements `Clone`, you can extract `T` itself. In addition, you can also extract `&PyCell`, though you rarely need it. Before: + ```compile_fail let obj: &PyAny = create_obj(); let obj_ref: &MyClass = obj.extract().unwrap(); @@ -2337,6 +2487,7 @@ let obj_ref_mut: &mut MyClass = obj.extract().unwrap(); ``` After: + ```rust,ignore # use pyo3::prelude::*; # use pyo3::types::IntoPyDict; @@ -2357,14 +2508,15 @@ let obj_ref_mut: PyRefMut<'_, MyClass> = obj.extract().unwrap(); # }) ``` - #### `#[pyproto]` + Most of the arguments to methods in `#[pyproto]` impls require a [`FromPyObject`] implementation. So if your protocol methods take `&T` or `&mut T` (where `T: PyClass`), please use [`PyRef`] or [`PyRefMut`] instead. Before: + ```rust,compile_fail # use pyo3::prelude::*; # use pyo3::class::PySequenceProtocol; @@ -2383,6 +2535,7 @@ impl PySequenceProtocol for ByteSequence { ``` After: + ```rust,compile_fail # use pyo3::prelude::*; # use pyo3::class::PySequenceProtocol; @@ -2399,31 +2552,42 @@ impl PySequenceProtocol for ByteSequence { } } ``` +
[`FromPyObject`]: {{#PYO3_DOCS_URL}}/pyo3/conversion/trait.FromPyObject.html diff --git a/guide/src/module.md b/guide/src/module.md index 84ee25e7252..7f022c1960d 100644 --- a/guide/src/module.md +++ b/guide/src/module.md @@ -18,7 +18,7 @@ mod my_extension { #[pymodule_export] use super::double; // The double function is made available from Python, works also with classes - + #[pyfunction] // Inline definition of a pyfunction, also made availlable to Python fn triple(x: usize) -> usize { x * 3 @@ -55,8 +55,9 @@ file. Otherwise, you will get an import error in Python with the following messa `ImportError: dynamic module does not define module export function (PyInit_name_of_your_module)` To import the module, either: - - copy the shared library as described in [Manual builds](building-and-distribution.md#manual-builds), or - - use a tool, e.g. `maturin develop` with [maturin](https://github.com/PyO3/maturin) or + +- copy the shared library as described in [Manual builds](building-and-distribution.md#manual-builds), or +- use a tool, e.g. `maturin develop` with [maturin](https://github.com/PyO3/maturin) or `python setup.py develop` with [setuptools-rust](https://github.com/PyO3/setuptools-rust). ## Documentation @@ -121,6 +122,7 @@ You can provide the `submodule` argument to `#[pymodule()]` for modules that are It is possible to declare functions, classes, sub-modules and constants inline in a module: For example: + ```rust,no_run # mod declarative_module_test { #[pyo3::pymodule] @@ -157,6 +159,7 @@ In the previous example, the `Nested` class will have for `module` `my_extension ## Procedural initialization If the macros provided by PyO3 are not enough, it is possible to run code at the module initialization: + ```rust,no_run # mod procedural_module_test { #[pyo3::pymodule] diff --git a/guide/src/parallelism.md b/guide/src/parallelism.md index 260865eb63e..2d3940a39c2 100644 --- a/guide/src/parallelism.md +++ b/guide/src/parallelism.md @@ -58,6 +58,7 @@ fn search_sequential(contents: &str, needle: &str) -> usize { ``` To enable parallel execution of this function, the [`Python::detach`] method can be used to temporarily release the GIL, thus allowing other Python threads to run. We then have a function exposed to the Python runtime which calls `search_sequential` inside a closure passed to [`Python::detach`] to enable true parallelism: + ```rust,no_run # #![allow(dead_code)] # use pyo3::prelude::*; @@ -82,6 +83,7 @@ fn search_sequential_detached(py: Python<'_>, contents: &str, needle: &str) -> u ``` Now Python threads can use more than one CPU core, resolving the limitation which usually makes multi-threading in Python only good for IO-bound tasks: + ```Python from concurrent.futures import ThreadPoolExecutor from word_count import search_sequential_detached @@ -112,6 +114,7 @@ We are using `pytest-benchmark` to benchmark four word count functions: The benchmark script can be found [here](https://github.com/PyO3/pyo3/blob/main/examples/word-count/tests/test_word_count.py), and we can run `nox` in the `word-count` folder to benchmark these functions. While the results of the benchmark of course depend on your machine, the relative results should be similar to this (mid 2020): + ```text -------------------------------------------------------------------------------------------------- benchmark: 4 tests ------------------------------------------------------------------------------------------------- Name (time in ms) Min Max Mean StdDev Median IQR Outliers OPS Rounds Iterations @@ -133,7 +136,7 @@ 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 +- 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` diff --git a/guide/src/performance.md b/guide/src/performance.md index 85b6c337ffd..8a62212e471 100644 --- a/guide/src/performance.md +++ b/guide/src/performance.md @@ -102,11 +102,12 @@ impl PartialEq for FooBound<'_> { 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. +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 @@ -164,5 +165,3 @@ Python::attach(move |py| { drop(numbers); }); ``` - -[conditional-compilation]: https://doc.rust-lang.org/reference/conditional-compilation.html diff --git a/guide/src/python-from-rust.md b/guide/src/python-from-rust.md index 74d530041d4..80df8066c04 100644 --- a/guide/src/python-from-rust.md +++ b/guide/src/python-from-rust.md @@ -5,10 +5,11 @@ This chapter of the guide documents some ways to interact with Python code from Below is an introduction to the `'py` lifetime and some general remarks about how PyO3's API reasons about Python code. The subchapters also cover the following topics: - - Python object types available in PyO3's API - - How to work with Python exceptions - - How to call Python functions - - How to execute existing Python code + +- Python object types available in PyO3's API +- How to work with Python exceptions +- How to call Python functions +- How to execute existing Python code ## The `'py` lifetime @@ -18,9 +19,9 @@ Its lifetime `'py` is a central part of PyO3's API. The `Python<'py>` token serves three purposes: -* It provides global APIs for the Python interpreter, such as [`py.eval()`][eval] and [`py.import()`][import]. -* It can be passed to functions that require a proof of attachment, such as [`Py::clone_ref`][clone_ref]. -* Its lifetime `'py` is used to bind many of PyO3's types to the Python interpreter, such as [`Bound<'py, T>`][Bound]. +- It provides global APIs for the Python interpreter, such as [`py.eval()`][eval] and [`py.import()`][import]. +- It can be passed to functions that require a proof of attachment, such as [`Py::clone_ref`][clone_ref]. +- Its lifetime `'py` is used to bind many of PyO3's types to the Python interpreter, such as [`Bound<'py, T>`][Bound]. PyO3's types that are bound to the `'py` lifetime, for example `Bound<'py, T>`, all contain a `Python<'py>` token. This means they have full access to the Python interpreter and offer a complete API for interacting with Python objects. @@ -39,6 +40,7 @@ To enable any parallelism on the GIL-enabled build, and best throughput on the f ## Python's memory model Python's memory model differs from Rust's memory model in two key ways: + - There is no concept of ownership; all Python objects are shared and usually implemented via reference counting - There is no concept of exclusive (`&mut`) references; any reference can mutate a Python object @@ -55,3 +57,4 @@ Because of the lack of exclusive `&mut` references, PyO3's APIs for Python objec [import]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.import [clone_ref]: {{#PYO3_DOCS_URL}}/pyo3/prelude/struct.Py.html#method.clone_ref [Bound]: {{#PYO3_DOCS_URL}}/pyo3/struct.Bound.html +[`PyListMethods::append`]: {{#PYO3_DOCS_URL}}/pyo3/types/trait.PyListMethods.html#tymethod.append diff --git a/guide/src/python-from-rust/calling-existing-code.md b/guide/src/python-from-rust/calling-existing-code.md index b516b18663b..d050861ed32 100644 --- a/guide/src/python-from-rust/calling-existing-code.md +++ b/guide/src/python-from-rust/calling-existing-code.md @@ -2,7 +2,7 @@ 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` [`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. @@ -25,7 +25,7 @@ fn main() -> PyResult<()> { [`PyModule::import`]: {{#PYO3_DOCS_URL}}/pyo3/types/struct.PyModule.html#method.import -## Want to run just an expression? Then use `eval`. +## 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) @@ -49,7 +49,7 @@ Python::attach(|py| { # } ``` -## Want to run statements? Then use `run`. +## Want to run statements? Then use `run` [`Python::run`] is a method to execute one or more [Python statements](https://docs.python.org/3/reference/simple_stmts.html). @@ -60,8 +60,6 @@ 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; @@ -99,7 +97,7 @@ 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` [`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 @@ -162,7 +160,7 @@ use pyo3::ffi::c_str; #[pymodule] mod foo { use pyo3::prelude::*; - + #[pyfunction] fn add_one(x: i64) -> i64 { x + 1 @@ -220,6 +218,7 @@ Many Python files can be included and loaded as modules. If one file depends on another you must preserve correct order while declaring `PyModule`. Example directory structure: + ```text . ├── Cargo.lock @@ -233,6 +232,7 @@ Example directory structure: ``` `python_app/app.py`: + ```python from utils.foo import bar @@ -242,17 +242,20 @@ def run(): ``` `python_app/utils/foo.py`: + ```python def bar(): return "baz" ``` The example below shows: -* how to include content of `app.py` and `utils/foo.py` into your rust binary -* how to call function `run()` (declared in `app.py`) that needs function + +- how to include content of `app.py` and `utils/foo.py` into your rust binary +- how to call function `run()` (declared in `app.py`) that needs function imported from `utils/foo.py` `src/main.rs`: + ```rust,ignore use pyo3::prelude::*; use pyo3_ffi::c_str; @@ -277,9 +280,10 @@ fn main() -> PyResult<()> { ``` The example below shows: -* how to load content of `app.py` at runtime so that it sees its dependencies + +- how to load content of `app.py` at runtime so that it sees its dependencies automatically -* how to call function `run()` (declared in `app.py`) that needs function +- how to call function `run()` (declared in `app.py`) that needs function imported from `utils/foo.py` It is recommended to use absolute paths because then your binary can be run @@ -287,6 +291,7 @@ from anywhere as long as your `app.py` is in the expected directory (in this exa that directory is `/usr/share/python_app`). `src/main.rs`: + ```rust,no_run use pyo3::prelude::*; use pyo3::types::PyList; @@ -315,10 +320,6 @@ fn main() -> PyResult<()> { } ``` - -[`Python::run`]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.run -[`py_run!`]: {{#PYO3_DOCS_URL}}/pyo3/macro.py_run.html - ## Need to use a context manager from Rust? Use context managers by directly invoking `__enter__` and `__exit__`. @@ -403,5 +404,6 @@ Python::attach(|py| -> PyResult<()> { # } ``` - +[`py_run!`]: {{#PYO3_DOCS_URL}}/pyo3/macro.py_run.html +[`Python::run`]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.run [`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 e3f346bb777..fa6dae5980f 100644 --- a/guide/src/python-from-rust/function-calls.md +++ b/guide/src/python-from-rust/function-calls.md @@ -4,13 +4,13 @@ The `Bound<'py, T>` smart pointer (such as `Bound<'py, PyAny>`, `Bound<'py, PyLi PyO3 offers two APIs to make function calls: -* [`call`]({{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.call) - call any callable Python object. -* [`call_method`]({{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.call_method) - call a method on the Python object. +- [`call`]({{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.call) - call any callable Python object. +- [`call_method`]({{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.call_method) - call a method on the Python object. Both of these APIs take `args` and `kwargs` arguments (for positional and keyword arguments respectively). There are variants for less complex calls: -* [`call1`]({{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.call1) and [`call_method1`]({{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.call_method1) to call only with positional `args`. -* [`call0`]({{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.call0) and [`call_method0`]({{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.call_method0) to call with no arguments. +- [`call1`]({{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.call1) and [`call_method1`]({{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.call_method1) to call only with positional `args`. +- [`call0`]({{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.call0) and [`call_method0`]({{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.call_method0) to call with no arguments. For convenience the [`Py`](../types.md#pyt) smart pointer also exposes these same six API methods, but needs a `Python` token as an additional first argument to prove the thread is attached to the Python interpreter. diff --git a/guide/src/python-typing-hints.md b/guide/src/python-typing-hints.md index ad0219247de..0e5b881615b 100644 --- a/guide/src/python-typing-hints.md +++ b/guide/src/python-typing-hints.md @@ -61,9 +61,9 @@ It contains a specification for them (highly recommended reading, since it conta [PEP561](https://www.python.org/dev/peps/pep-0561/) recognizes three ways of distributing type information: -* `inline` - the typing is placed directly in source (`py`) files; -* `separate package with stub files` - the typing is placed in `pyi` files distributed in their own, separate package; -* `in-package stub files` - the typing is placed in `pyi` files distributed in the same package as source files. +- `inline` - the typing is placed directly in source (`py`) files; +- `separate package with stub files` - the typing is placed in `pyi` files distributed in their own, separate package; +- `in-package stub files` - the typing is placed in `pyi` files distributed in the same package as source files. The first way is tricky with PyO3 since we do not have `py` files. When it has been investigated and necessary changes are implemented, this document will be updated. diff --git a/guide/src/trait-bounds.md b/guide/src/trait-bounds.md index 3ac56b68035..4c5aab84d52 100644 --- a/guide/src/trait-bounds.md +++ b/guide/src/trait-bounds.md @@ -7,11 +7,13 @@ This tutorial explains how to convert a Rust function that takes a trait as argu Why is this useful? -### Pros +## Pros + - Make your Rust code available to Python users - Code complex algorithms in Rust with the help of the borrow checker ### Cons + - Not as fast as native Rust (type conversion has to be performed and one part of the code runs in Python) - You need to adapt your code to expose it @@ -34,9 +36,12 @@ pub fn solve(model: &mut T) { println!("Magic solver that mutates the model into a resolved state"); } ``` + Let's assume we have the following constraints: + - We cannot change that code as it runs on many Rust models. - We also have many Python models that cannot be solved as this solver is not available in that language. + Rewriting it in Python would be cumbersome and error-prone, as everything is already available in Rust. How could we expose this solver to Python thanks to PyO3 ? @@ -230,8 +235,10 @@ impl UserModel { } } ``` + This wrapper handles the type conversion between the PyO3 requirements and the trait. In order to meet PyO3 requirements, this wrapper must: + - return an object of type `PyResult` - use only values, not references in the method signatures @@ -280,7 +287,6 @@ We will now expose the `solve` function, but before, let's talk about types erro What happens if you have type errors when using Python and how can you improve the error messages? - ### Wrong types in Python function arguments Let's assume in the first case that you will use in your Python file `my_rust_model.set_variables(2.0)` instead of `my_rust_model.set_variables([2.0])`. diff --git a/guide/src/type-stub.md b/guide/src/type-stub.md index 913791fb286..f7641b8f074 100644 --- a/guide/src/type-stub.md +++ b/guide/src/type-stub.md @@ -7,11 +7,13 @@ PyO3 has a work in progress support to generate [type stub files](https://typing.python.org/en/latest/spec/distributing.html#stub-files). It works using: + 1. PyO3 macros (`#[pyclass]`) that generate constant JSON strings that are then included in the built binaries by rustc if the `experimental-inspect` feature is enabled. 2. The `pyo3-introspection` crate that can parse the generated binaries, extract the JSON strings and build stub files from it. -3. [Not done yet] Build tools like `maturin` exposing `pyo3-introspection` features in their CLI API. +3. \[Not done yet\] Build tools like `maturin` exposing `pyo3-introspection` features in their CLI API. For example, the following Rust code + ```rust #[pymodule] pub mod example { @@ -32,13 +34,13 @@ pub mod example { fn new(value: usize) -> Self { Self { value } } - + #[getter] fn value(&self) -> usize { self.value } } - + #[pyfunction] #[pyo3(signature = (arg: "list[int]") -> "list[int]")] fn list_of_int_identity(arg: Bound<'_, PyAny>) -> Bound<'_, PyAny> { @@ -46,7 +48,9 @@ pub mod example { } } ``` + will generate the following stub file: + ```python import typing diff --git a/guide/src/types.md b/guide/src/types.md index 50d98aac41e..1e94aa58482 100644 --- a/guide/src/types.md +++ b/guide/src/types.md @@ -29,8 +29,9 @@ The sections below also explain these smart pointers in a little more detail. Because `Py` is not bound to [the `'py` lifetime](./python-from-rust.md#the-py-lifetime), it is the type to use when storing a Python object inside a Rust `struct` or `enum` which do not want to have a lifetime parameter. In particular, [`#[pyclass]`][pyclass] types are not permitted to have a lifetime, so `Py` is the correct type to store Python objects inside them. The lack of binding to the `'py` lifetime also carries drawbacks: - - Almost all methods on `Py` require a `Python<'py>` token as the first argument - - Other functionality, such as [`Drop`][Drop], needs to check at runtime for attachment to the Python interpreter, at a small performance cost + +- 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 interpreter, 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 better for function arguments. @@ -202,11 +203,13 @@ let obj: Py = borrowed.to_owned().unbind(). In all of `Py`, `Bound<'py, T>`, and `Borrowed<'a, 'py, T>`, the type parameter `T` denotes the type of the Python object referred to by the smart pointer. This parameter `T` can be filled by: - - [`PyAny`][PyAny], which represents any Python object, - - Native Python types such as `PyList`, `PyTuple`, and `PyDict`, and - - [`#[pyclass]`][pyclass] types defined from Rust + +- [`PyAny`][PyAny], which represents any Python object, +- Native Python types such as `PyList`, `PyTuple`, and `PyDict`, and +- [`#[pyclass]`][pyclass] types defined from Rust The following subsections covers some further detail about how to work with these types: + - the APIs that are available for these concrete types, - how to cast `Bound<'py, T>` to a specific concrete type, and - how to get Rust data out of a `Bound<'py, T>`. @@ -216,6 +219,7 @@ The following subsections covers some further detail about how to work with thes Each concrete Python type such as `PyAny`, `PyTuple` and `PyDict` exposes its API on the corresponding bound smart pointer `Bound<'py, PyAny>`, `Bound<'py, PyTuple>` and `Bound<'py, PyDict>`. Each type's API is exposed as a trait: [`PyAnyMethods`], [`PyTupleMethods`], [`PyDictMethods`], and so on for all concrete types. Using traits rather than associated methods on the `Bound` smart pointer is done for a couple of reasons: + - Clarity of documentation: each trait gets its own documentation page in the PyO3 API docs. If all methods were on the `Bound` smart pointer directly, the vast majority of PyO3's API would be on a single, extremely long, documentation page. - Consistency: downstream code implementing Rust APIs for existing Python types can also follow this pattern of using a trait. Downstream code would not be allowed to add new associated methods directly on the `Bound` type. - Future design: it is hoped that a future Rust with [arbitrary self types](https://github.com/rust-lang/rust/issues/44874) will remove the need for these traits in favour of placing the methods directly on `PyAny`, `PyTuple`, `PyDict`, and so on. @@ -322,10 +326,6 @@ for more detail. [pyclass]: class.md [Borrowed]: {{#PYO3_DOCS_URL}}/pyo3/struct.Borrowed.html [Drop]: https://doc.rust-lang.org/std/ops/trait.Drop.html -[eval]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.eval -[clone_ref]: {{#PYO3_DOCS_URL}}/pyo3/struct.Py.html#method.clone_ref -[pyo3::types]: {{#PYO3_DOCS_URL}}/pyo3/types/index.html [PyAny]: {{#PYO3_DOCS_URL}}/pyo3/types/struct.PyAny.html -[PyList_append]: {{#PYO3_DOCS_URL}}/pyo3/types/struct.PyList.html#method.append -[RefCell]: https://doc.rust-lang.org/std/cell/struct.RefCell.html [smart-pointers]: https://doc.rust-lang.org/book/ch15-00-smart-pointers.html +[`FromPyObject`]: {{#PYO3_DOCS_URL}}/pyo3/conversion/trait.FromPyObject.html diff --git a/noxfile.py b/noxfile.py index eca57e27743..df74f8c06b4 100644 --- a/noxfile.py +++ b/noxfile.py @@ -38,7 +38,7 @@ except ImportError: requests = None -nox.options.sessions = ["test", "clippy", "rustfmt", "ruff", "docs"] +nox.options.sessions = ["test", "clippy", "rustfmt", "ruff", "rumdl", "docs"] PYO3_DIR = Path(__file__).parent PYO3_TARGET = Path(os.environ.get("CARGO_TARGET_DIR", PYO3_DIR / "target")).absolute() @@ -202,6 +202,16 @@ def ruff(session: nox.Session): _run(session, "ruff", "check", ".") +@nox.session(name="rumdl") +def rumdl(session: nox.Session): + """Run rumdl to check markdown formatting in the guide. + + Can also run with uv directly, e.g. `uvx rumdl check guide`. + """ + session.install("rumdl") + _run(session, "rumdl", "check", "guide", *session.posargs) + + @nox.session(name="clippy", venv_backend="none") def clippy(session: nox.Session) -> bool: if not _clippy(session) and _clippy_additional_workspaces(session): @@ -686,7 +696,7 @@ def _build_netlify_redirects(preview: bool) -> None: redirects_file.write(f"/ /v{current_version}/ 302\n") -@nox.session(name="check-guide", venv_backend="none") +@nox.session(name="check-guide") def check_guide(session: nox.Session): # reuse other sessions, but with default args posargs = [*session.posargs] @@ -728,6 +738,7 @@ def check_guide(session: nox.Session): *remap_args, "--accept=200,429", *session.posargs, + external=True, ) # check external links in the docs # (intra-doc links are checked by rustdoc) @@ -744,6 +755,7 @@ def check_guide(session: nox.Session): # reduce the concurrency to avoid rate-limit from `pyo3.rs` "--max-concurrency=32", *session.posargs, + external=True, ) diff --git a/pyproject.toml b/pyproject.toml index 5dc682ea044..208918f8465 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,3 +24,26 @@ name = "Removed" [tool.towncrier.fragment.fixed] name = "Fixed" + +[tool.rumdl] + +disable = [ + # TODO: enable in a follow-up, probably with `sentence-per-line` reflow + "MD013", + # TODO: what to do about inline HTML, probably allow? + "MD033", + # TODO: {{#PYO3_DOCS_URL}} placeholder confuses rumdl, change syntax perhaps? + "MD051" +] + +exclude = [ + # cannot figure out how to ignore MD041 in this file + "guide/pyclass-parameters.md", + + # just has an include to the top-level files + "guide/src/changelog.md", + "guide/src/contributing.md" +] + +[tool.rumdl.MD004] +style = "dash" From f9e14fc44f0d98b6dca36d47d7da82e296e9443a Mon Sep 17 00:00:00 2001 From: "Ruben J. Jongejan" Date: Thu, 16 Oct 2025 20:05:33 +0200 Subject: [PATCH 914/936] Use per-file-ignores for MD041 in guide/pyclass-parameters.md (#5526) --- pyproject.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 208918f8465..9160fda11e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,13 +37,13 @@ disable = [ ] exclude = [ - # cannot figure out how to ignore MD041 in this file - "guide/pyclass-parameters.md", - # just has an include to the top-level files "guide/src/changelog.md", "guide/src/contributing.md" ] +[tool.rumdl.per-file-ignores] +"guide/pyclass-parameters.md" = ["MD041"] + [tool.rumdl.MD004] style = "dash" From 012e095ea4fe0b1286bc445cc087ad1e12a01b45 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Thu, 16 Oct 2025 19:52:56 +0100 Subject: [PATCH 915/936] remove `ptr_from_ref` helpers (#5523) --- src/instance.rs | 20 ++++++++++++-------- src/internal_tricks.rs | 12 ------------ src/pycell.rs | 17 ++++++++++------- src/pyclass/create_type_object.rs | 7 ++++--- 4 files changed, 26 insertions(+), 30 deletions(-) diff --git a/src/instance.rs b/src/instance.rs index f335f7d25d6..3fe7ba24de9 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -4,7 +4,6 @@ use crate::call::PyCallArgs; use crate::conversion::IntoPyObject; use crate::err::{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}; use crate::types::{any::PyAnyMethods, string::PyStringMethods, typeobject::PyTypeMethods}; @@ -444,9 +443,10 @@ impl<'py> Bound<'py, PyAny> { _py: Python<'py>, ptr: &'a *mut ffi::PyObject, ) -> &'a Self { + let ptr = NonNull::from(ptr).cast(); // SAFETY: caller has upheld the safety contract, // and `Bound` is layout-compatible with `*mut ffi::PyObject`. - unsafe { &*ptr_from_ref(ptr).cast::>() } + unsafe { ptr.as_ref() } } /// Variant of the above which returns `None` for null pointers. @@ -458,9 +458,10 @@ impl<'py> Bound<'py, PyAny> { _py: Python<'py>, ptr: &'a *mut ffi::PyObject, ) -> &'a Option { + let ptr = NonNull::from(ptr).cast(); // SAFETY: caller has upheld the safety contract, // and `Option>` is layout-compatible with `*mut ffi::PyObject`. - unsafe { &*ptr_from_ref(ptr).cast::>>() } + unsafe { ptr.as_ref() } } /// This slightly strange method is used to obtain `&Bound` from a [`NonNull`] in macro @@ -477,9 +478,10 @@ impl<'py> Bound<'py, PyAny> { _py: Python<'py>, ptr: &'a NonNull, ) -> &'a Self { + let ptr = NonNull::from(ptr).cast(); // SAFETY: caller has upheld the safety contract, // and `Bound` is layout-compatible with `NonNull`. - unsafe { NonNull::from(ptr).cast().as_ref() } + unsafe { ptr.as_ref() } } } @@ -839,9 +841,10 @@ impl<'py, T> Bound<'py, T> { /// Helper to cast to `Bound<'py, PyAny>`. #[inline] pub fn as_any(&self) -> &Bound<'py, PyAny> { + let ptr = NonNull::from(self).cast(); // Safety: all Bound have the same memory layout, and all Bound are valid // Bound, so pointer casting is valid. - unsafe { &*ptr_from_ref(self).cast::>() } + unsafe { ptr.as_ref() } } /// Helper to cast to `Bound<'py, PyAny>`, transferring ownership. @@ -1171,8 +1174,8 @@ impl<'py, T> Deref for Borrowed<'_, 'py, T> { #[inline] fn deref(&self) -> &Bound<'py, T> { - // safety: Bound has the same layout as NonNull - unsafe { &*ptr_from_ref(&self.0).cast() } + // SAFETY: self.0 is a valid object of type T + unsafe { Bound::ref_from_non_null(self.2, &self.0).cast_unchecked() } } } @@ -1483,9 +1486,10 @@ impl Py { /// Helper to cast to `Py`. #[inline] pub fn as_any(&self) -> &Py { + let ptr = NonNull::from(self).cast(); // Safety: all Py have the same memory layout, and all Py are valid // Py, so pointer casting is valid. - unsafe { &*ptr_from_ref(self).cast::>() } + unsafe { ptr.as_ref() } } /// Helper to cast to `Py`, transferring ownership. diff --git a/src/internal_tricks.rs b/src/internal_tricks.rs index e6c6b755ece..1c7b7a802a1 100644 --- a/src/internal_tricks.rs +++ b/src/internal_tricks.rs @@ -19,18 +19,6 @@ pub(crate) fn get_ssize_index(index: usize) -> Py_ssize_t { index.min(PY_SSIZE_T_MAX as usize) as Py_ssize_t } -// 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 -} - -// 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 -} - // 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)] diff --git a/src/pycell.rs b/src/pycell.rs index c1f9606e40c..2a230b05833 100644 --- a/src/pycell.rs +++ b/src/pycell.rs @@ -196,13 +196,13 @@ use crate::conversion::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::{ffi, Borrowed, Bound, PyErr, Python}; use std::convert::Infallible; use std::fmt; use std::mem::ManuallyDrop; use std::ops::{Deref, DerefMut}; +use std::ptr::NonNull; pub(crate) mod impl_; use impl_::{PyClassBorrowChecker, PyClassObjectLayout}; @@ -444,12 +444,13 @@ where /// # }); /// ``` pub fn as_super(&self) -> &PyRef<'p, U> { - let ptr = ptr_from_ref::>(&self.inner) + let ptr = NonNull::from(&self.inner) // `Bound` has the same layout as `Bound` .cast::>() // `Bound` has the same layout as `PyRef` .cast::>(); - unsafe { &*ptr } + // SAFETY: lifetimes are correctly transferred, and `PyRef` and `PyRef` have the same layout + unsafe { ptr.as_ref() } } } @@ -580,8 +581,9 @@ impl<'py, T: PyClass> PyRefMut<'py, T> { } pub(crate) fn downgrade(slf: &Self) -> &PyRef<'py, T> { - // `PyRefMut` and `PyRef` have the same layout - unsafe { &*ptr_from_ref(slf).cast() } + let ptr = NonNull::from(slf).cast(); + // SAFETY: `PyRefMut` and `PyRef` have the same layout + unsafe { ptr.as_ref() } } } @@ -613,13 +615,14 @@ where /// /// See [`PyRef::as_super`] for more. pub fn as_super(&mut self) -> &mut PyRefMut<'p, U> { - let ptr = ptr_from_mut::>(&mut self.inner) + let mut ptr = NonNull::from(&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 } + // SAFETY: lifetimes are correctly transferred, and `PyRefMut` and `PyRefMut` have the same layout + unsafe { ptr.as_mut() } } } diff --git a/src/pyclass/create_type_object.rs b/src/pyclass/create_type_object.rs index 7651497ba86..0da0aa17cdb 100644 --- a/src/pyclass/create_type_object.rs +++ b/src/pyclass/create_type_object.rs @@ -10,7 +10,6 @@ use crate::{ pymethods::{Getter, PyGetterDef, PyMethodDefType, PySetterDef, Setter, _call_clear}, trampoline::trampoline, }, - internal_tricks::ptr_from_ref, types::{typeobject::PyTypeMethods, PyType}, Py, PyClass, PyResult, PyTypeInfo, Python, }; @@ -18,7 +17,7 @@ use std::{ collections::HashMap, ffi::{CStr, CString}, os::raw::{c_char, c_int, c_ulong, c_void}, - ptr, + ptr::{self, NonNull}, }; pub(crate) struct PyClassTypeObject { @@ -700,7 +699,9 @@ impl GetSetDefType { ( Some(getset_getter), Some(getset_setter), - ptr_from_ref::(closure) as *mut _, + NonNull::::from(closure.as_ref()) + .cast() + .as_ptr(), ) } }; From 8f669e7af981df8d4a0c8ae2481f1590a94c947f Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Thu, 16 Oct 2025 21:57:21 +0100 Subject: [PATCH 916/936] attempt to improve unsupported Python version error (#5519) * attempt to improve unsupported Python version error * newsfragment * don't check the internet for latest version * allow some dead code for now --- newsfragments/5519.packaging.md | 1 + pyo3-build-config/src/impl_.rs | 18 +++---- pyo3-build-config/src/lib.rs | 88 ++++++++++++++++++++++++++++++--- pyo3-ffi/build.rs | 61 +++++++++++------------ 4 files changed, 119 insertions(+), 49 deletions(-) create mode 100644 newsfragments/5519.packaging.md diff --git a/newsfragments/5519.packaging.md b/newsfragments/5519.packaging.md new file mode 100644 index 00000000000..9299b9dc326 --- /dev/null +++ b/newsfragments/5519.packaging.md @@ -0,0 +1 @@ +Provide a better error message when building an outdated PyO3 for a too-new Python version. diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 2b3abc70141..ea92cd89d40 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -881,17 +881,10 @@ pub fn is_extension_module() -> bool { || env_var("PYO3_BUILD_EXTENSION_MODULE").is_some() } -/// Checks if we need to link to `libpython` for the current build target. -/// -/// Must be called from a PyO3 crate build script. -pub fn is_linking_libpython() -> bool { - is_linking_libpython_for_target(&target_triple_from_env()) -} - /// Checks if we need to link to `libpython` for the target. /// /// Must be called from a PyO3 crate build script. -fn is_linking_libpython_for_target(target: &Triple) -> bool { +pub 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 @@ -992,6 +985,7 @@ impl CrossCompileConfig { /// /// The conversion can not fail because `PYO3_CROSS_LIB_DIR` variable /// is ensured contain a valid UTF-8 string. + #[allow(dead_code)] fn lib_dir_string(&self) -> Option { self.lib_dir .as_ref() @@ -1118,6 +1112,7 @@ pub fn cross_compiling_from_to( /// /// This must be called from PyO3's build script, because it relies on environment /// variables such as `CARGO_CFG_TARGET_OS` which aren't available at any other time. +#[allow(dead_code)] pub fn cross_compiling_from_cargo_env() -> Result> { let env_vars = CrossCompileEnvVars::from_env(); let host = Triple::host(); @@ -1350,6 +1345,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. +#[allow(dead_code)] fn find_sysconfigdata(cross: &CrossCompileConfig) -> Result> { let mut sysconfig_paths = find_all_sysconfigdata(cross)?; if sysconfig_paths.is_empty() { @@ -1541,6 +1537,7 @@ fn search_lib_dir(path: impl AsRef, cross: &CrossCompileConfig) -> Result< /// [1]: https://github.com/python/cpython/blob/3.8/Lib/sysconfig.py#L348 /// /// Returns `None` when the target Python library directory is not set. +#[allow(dead_code)] fn cross_compile_from_sysconfigdata( cross_compile_config: &CrossCompileConfig, ) -> Result> { @@ -1563,7 +1560,7 @@ fn cross_compile_from_sysconfigdata( /// Windows, macOS and Linux. /// /// Must be called from a PyO3 crate build script. -#[allow(unused_mut)] +#[allow(unused_mut, dead_code)] fn default_cross_compile(cross_compile_config: &CrossCompileConfig) -> Result { let version = cross_compile_config .version @@ -1678,6 +1675,7 @@ fn default_abi3_config(host: &Triple, version: PythonVersion) -> Result Result { @@ -1704,6 +1702,7 @@ const WINDOWS_ABI3_LIB_NAME: &str = "python3"; const WINDOWS_ABI3_DEBUG_LIB_NAME: &str = "python3_d"; /// Generates the default library name for the target platform. +#[allow(dead_code)] fn default_lib_name_for_target( version: PythonVersion, implementation: PythonImplementation, @@ -1915,6 +1914,7 @@ fn get_host_interpreter(abi3_version: Option) -> Result Result> { let interpreter_config = if let Some(cross_config) = cross_compiling_from_cargo_env()? { let mut interpreter_config = load_cross_compile_config(cross_config)?; diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index 3ce21ee1916..fc5ba27e3ef 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -83,10 +83,11 @@ fn _add_extension_module_link_args(triple: &Triple, mut writer: impl std::io::Wr /// All other platforms currently are no-ops. #[cfg(feature = "resolve-config")] pub fn add_python_framework_link_args() { + let target = impl_::target_triple_from_env(); _add_python_framework_link_args( get(), - &impl_::target_triple_from_env(), - impl_::is_linking_libpython(), + &target, + impl_::is_linking_libpython_for_target(&target), std::io::stdout(), ) } @@ -235,21 +236,19 @@ pub fn print_expected_cfgs() { /// /// Please don't use these - they could change at any time. #[doc(hidden)] +#[cfg(feature = "resolve-config")] pub mod pyo3_build_script_impl { - #[cfg(feature = "resolve-config")] use crate::errors::{Context, Result}; - #[cfg(feature = "resolve-config")] use super::*; pub mod errors { pub use crate::errors::*; } pub use crate::impl_::{ - cargo_env_var, env_var, is_linking_libpython, make_cross_compile_config, + cargo_env_var, env_var, is_linking_libpython_for_target, make_cross_compile_config, target_triple_from_env, InterpreterConfig, PythonVersion, }; - pub enum BuildConfigSource { /// Config was provided by `PYO3_CONFIG_FILE`. ConfigFile, @@ -273,7 +272,6 @@ pub mod pyo3_build_script_impl { /// /// Steps 2 and 3 are necessary because `pyo3-ffi`'s build script is the first code run which knows /// the correct target triple. - #[cfg(feature = "resolve-config")] pub fn resolve_build_config(target: &Triple) -> Result { // CONFIG_FILE is generated in build.rs, so it's content can vary #[allow(unknown_lints, clippy::const_is_empty)] @@ -315,6 +313,43 @@ pub mod pyo3_build_script_impl { }) } } + + /// Helper to generate an error message when the configured Python version is newer + /// than PyO3's current supported version. + pub struct MaximumVersionExceeded { + message: String, + } + + impl MaximumVersionExceeded { + pub fn new( + interpreter_config: &InterpreterConfig, + supported_version: PythonVersion, + ) -> Self { + let implementation = match interpreter_config.implementation { + PythonImplementation::CPython => "Python", + PythonImplementation::PyPy => "PyPy", + PythonImplementation::GraalPy => "GraalPy", + }; + let version = &interpreter_config.version; + let message = format!( + "the configured {implementation} version ({version}) is newer than PyO3's maximum supported version ({supported_version})\n\ + = help: this package is being built with PyO3 version {current_version}\n\ + = help: check https://crates.io/crates/pyo3 for the latest PyO3 version available\n\ + = help: updating this package to the latest version of PyO3 may provide compatibility with this {implementation} version", + current_version = env!("CARGO_PKG_VERSION") + ); + Self { message } + } + + pub fn add_help(&mut self, help: &str) { + self.message.push_str("\n= help: "); + self.message.push_str(help); + } + + pub fn finish(self) -> String { + self.message + } + } } fn rustc_minor_version() -> Option { @@ -412,4 +447,43 @@ mod tests { "cargo:rustc-link-arg=-Wl,-rpath,/Applications/Xcode.app/Contents/Developer/Library/Frameworks\n" ); } + + #[test] + #[cfg(feature = "resolve-config")] + fn test_maximum_version_exceeded_formatting() { + 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: None, + }; + let mut error = pyo3_build_script_impl::MaximumVersionExceeded::new( + &interpreter_config, + PythonVersion { + major: 3, + minor: 12, + }, + ); + error.add_help("this is a help message"); + let error = error.finish(); + let expected = concat!("\ + the configured Python version (3.13) is newer than PyO3's maximum supported version (3.12)\n\ + = help: this package is being built with PyO3 version ", env!("CARGO_PKG_VERSION"), "\n\ + = help: check https://crates.io/crates/pyo3 for the latest PyO3 version available\n\ + = help: updating this package to the latest version of PyO3 may provide compatibility with this Python version\n\ + = help: this is a help message" + ); + assert_eq!(error, expected); + } } diff --git a/pyo3-ffi/build.rs b/pyo3-ffi/build.rs index cd9e483bddf..72ef310dc09 100644 --- a/pyo3-ffi/build.rs +++ b/pyo3-ffi/build.rs @@ -1,8 +1,9 @@ use pyo3_build_config::{ bail, ensure, print_feature_cfgs, pyo3_build_script_impl::{ - cargo_env_var, env_var, errors::Result, is_linking_libpython, resolve_build_config, - target_triple_from_env, BuildConfig, BuildConfigSource, InterpreterConfig, PythonVersion, + cargo_env_var, env_var, errors::Result, is_linking_libpython_for_target, + resolve_build_config, target_triple_from_env, BuildConfig, BuildConfigSource, + InterpreterConfig, MaximumVersionExceeded, PythonVersion, }, warn, PythonImplementation, }; @@ -54,20 +55,20 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { versions.min, ); 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").is_some_and(|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(), - ); + let mut error = MaximumVersionExceeded::new(interpreter_config, versions.max); + if interpreter_config.is_free_threaded() { + error.add_help( + "the free-threaded build of CPython does not support the limited API so this check cannot be suppressed.", + ); + return Err(error.finish().into()); + } + + if !env_var("PYO3_USE_ABI3_FORWARD_COMPATIBILITY") + .is_some_and(|os_str| os_str == "1") + { + error.add_help("set PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1 to suppress this check and build anyway using the stable ABI"); + return Err(error.finish().into()); + } } } PythonImplementation::PyPy => { @@ -79,14 +80,10 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { versions.min, ); // PyO3 does not support abi3, so we cannot offer forward compatibility - ensure!( - interpreter_config.version <= versions.max, - "the configured PyPy interpreter version ({}) is newer than PyO3's maximum supported version ({})\n\ - = help: please check if an updated version of PyO3 is available. Current version: {}", - interpreter_config.version, - versions.max, - std::env::var("CARGO_PKG_VERSION").unwrap() - ); + if interpreter_config.version > versions.max { + let error = MaximumVersionExceeded::new(interpreter_config, versions.max); + return Err(error.finish().into()); + } } PythonImplementation::GraalPy => { let versions = SUPPORTED_VERSIONS_GRAALPY; @@ -97,14 +94,10 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { versions.min, ); // GraalPy does not support abi3, so we cannot offer forward compatibility - ensure!( - interpreter_config.version <= versions.max, - "the configured GraalPy interpreter version ({}) is newer than PyO3's maximum supported version ({})\n\ - = help: please check if an updated version of PyO3 is available. Current version: {}", - interpreter_config.version, - versions.max, - std::env::var("CARGO_PKG_VERSION").unwrap() - ); + if interpreter_config.version > versions.max { + let error = MaximumVersionExceeded::new(interpreter_config, versions.max); + return Err(error.finish().into()); + } } } @@ -209,7 +202,9 @@ fn configure_pyo3() -> Result<()> { // Serialize the whole interpreter config into DEP_PYTHON_PYO3_CONFIG env var. interpreter_config.to_cargo_dep_env()?; - if is_linking_libpython() && !interpreter_config.suppress_build_script_link_lines { + if is_linking_libpython_for_target(&target) + && !interpreter_config.suppress_build_script_link_lines + { emit_link_config(&build_config)?; } From 999ee8ab0fed19fff2f02466e93782e42a4caf94 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 17 Oct 2025 11:26:18 +0100 Subject: [PATCH 917/936] ci: enable more tests on 3.14t (#5524) * ci: enable more tests on 3.14t * simplify implementation of `UnraisableCapture` * fix imports on 3.14t * force object cleanup on 3.14t --- tests/test_buffer_protocol.rs | 18 +++---- tests/test_class_attributes.rs | 16 +++--- tests/test_class_basics.rs | 57 ++++++++++++--------- tests/test_exceptions.rs | 31 +++++------- tests/test_utils/mod.rs | 93 +++++++++++++++++++++++++--------- 5 files changed, 132 insertions(+), 83 deletions(-) diff --git a/tests/test_buffer_protocol.rs b/tests/test_buffer_protocol.rs index 5a2baa33d05..9af163f515b 100644 --- a/tests/test_buffer_protocol.rs +++ b/tests/test_buffer_protocol.rs @@ -95,7 +95,7 @@ fn test_buffer_referenced() { } #[test] -#[cfg(all(Py_3_8, not(Py_GIL_DISABLED)))] // sys.unraisablehook not available until Python 3.8 +#[cfg(Py_3_8)] // sys.unraisablehook not available until Python 3.8 fn test_releasebuffer_unraisable_error() { use pyo3::exceptions::PyValueError; use test_utils::UnraisableCapture; @@ -120,20 +120,20 @@ fn test_releasebuffer_unraisable_error() { } Python::attach(|py| { - let capture = UnraisableCapture::install(py); - let instance = Py::new(py, ReleaseBufferError {}).unwrap(); - let env = [("ob", instance.clone_ref(py))].into_py_dict(py).unwrap(); - assert!(capture.borrow(py).capture.is_none()); + let (err, object) = UnraisableCapture::enter(py, |capture| { + let env = [("ob", instance.clone_ref(py))].into_py_dict(py).unwrap(); + + assert!(capture.take_capture().is_none()); - py_assert!(py, *env, "bytes(ob) == b'hello world'"); + py_assert!(py, *env, "bytes(ob) == b'hello world'"); + + capture.take_capture().unwrap() + }); - let (err, object) = capture.borrow_mut(py).capture.take().unwrap(); assert_eq!(err.to_string(), "ValueError: oh dear"); assert!(object.is(&instance)); - - capture.borrow_mut(py).uninstall(py); }); } diff --git a/tests/test_class_attributes.rs b/tests/test_class_attributes.rs index e8d104d9d10..f706b414ff3 100644 --- a/tests/test_class_attributes.rs +++ b/tests/test_class_attributes.rs @@ -151,7 +151,7 @@ fn recursive_class_attributes() { } #[test] -#[cfg(all(Py_3_8, not(Py_GIL_DISABLED)))] // sys.unraisablehook not available until Python 3.8 +#[cfg(Py_3_8)] // sys.unraisablehook not available until Python 3.8 fn test_fallible_class_attribute() { use pyo3::exceptions::PyValueError; use test_utils::UnraisableCapture; @@ -168,29 +168,31 @@ fn test_fallible_class_attribute() { } Python::attach(|py| { - let capture = UnraisableCapture::install(py); - assert!(std::panic::catch_unwind(|| py.get_type::()).is_err()); + let (err, object) = UnraisableCapture::enter(py, |capture| { + // Accessing the type will attempt to initialize the class attributes + assert!(std::panic::catch_unwind(|| py.get_type::()).is_err()); - let (err, object) = capture.borrow_mut(py).capture.take().unwrap(); - assert!(object.is_none(py)); + capture.take_capture().unwrap() + }); + assert!(object.is_none()); assert_eq!( err.to_string(), "RuntimeError: An error occurred while initializing class BrokenClass" ); + let cause = err.cause(py).unwrap(); assert_eq!( cause.to_string(), "RuntimeError: An error occurred while initializing `BrokenClass.fails_to_init`" ); + let cause = cause.cause(py).unwrap(); assert_eq!( cause.to_string(), "ValueError: failed to create class attribute" ); assert!(cause.cause(py).is_none()); - - capture.borrow_mut(py).uninstall(py); }); } diff --git a/tests/test_class_basics.rs b/tests/test_class_basics.rs index 1dda580c2a4..dfe7f1ef0a5 100644 --- a/tests/test_class_basics.rs +++ b/tests/test_class_basics.rs @@ -1,5 +1,7 @@ #![cfg(feature = "macros")] +#[cfg(Py_3_8)] +use pyo3::ffi::c_str; use pyo3::prelude::*; use pyo3::types::PyType; use pyo3::{py_run, PyClass}; @@ -615,7 +617,7 @@ fn access_frozen_class_without_gil() { } #[test] -#[cfg(all(Py_3_8, not(Py_GIL_DISABLED)))] // sys.unraisablehook not available until Python 3.8 +#[cfg(Py_3_8)] #[cfg_attr(target_arch = "wasm32", ignore)] fn drop_unsendable_elsewhere() { use std::sync::{ @@ -637,35 +639,40 @@ fn drop_unsendable_elsewhere() { } Python::attach(|py| { - let capture = UnraisableCapture::install(py); + let (err, object) = UnraisableCapture::enter(py, |capture| { + let dropped = Arc::new(AtomicBool::new(false)); - let dropped = Arc::new(AtomicBool::new(false)); - - let unsendable = Py::new( - py, - Unsendable { - dropped: dropped.clone(), - }, - ) - .unwrap(); - - py.detach(|| { - spawn(move || { - Python::attach(move |_py| { - drop(unsendable); - }); - }) - .join() + let unsendable = Py::new( + py, + Unsendable { + dropped: dropped.clone(), + }, + ) .unwrap(); - }); - assert!(!dropped.load(Ordering::SeqCst)); + py.detach(|| { + spawn(move || { + Python::attach(move |py| { + drop(unsendable); + // On the free-threaded build, dropping an object on its non-origin thread + // will not immediately drop it because the refcounts need to be merged. + // + // Force GC to ensure the drop happens now on the wrong thread. + py.run(c_str!("import gc; gc.collect()"), None, None) + .unwrap(); + }); + }) + .join() + .unwrap(); + }); + + assert!(!dropped.load(Ordering::SeqCst)); + + capture.take_capture().unwrap() + }); - let (err, object) = capture.borrow_mut(py).capture.take().unwrap(); assert_eq!(err.to_string(), "RuntimeError: test_class_basics::drop_unsendable_elsewhere::Unsendable is unsendable, but is being dropped on another thread"); - assert!(object.is_none(py)); - - capture.borrow_mut(py).uninstall(py); + assert!(object.is_none()); }); } diff --git a/tests/test_exceptions.rs b/tests/test_exceptions.rs index 97b5466d205..d09dd0a1dc2 100644 --- a/tests/test_exceptions.rs +++ b/tests/test_exceptions.rs @@ -98,31 +98,28 @@ fn test_exception_nosegfault() { } #[test] -#[cfg(all(Py_3_8, not(Py_GIL_DISABLED)))] +#[cfg(Py_3_8)] fn test_write_unraisable() { - use pyo3::{exceptions::PyRuntimeError, ffi, types::PyNotImplemented}; - use std::ptr; + use pyo3::{exceptions::PyRuntimeError, types::PyNotImplemented}; use test_utils::UnraisableCapture; Python::attach(|py| { - let capture = UnraisableCapture::install(py); + UnraisableCapture::enter(py, |capture| { + let err = PyRuntimeError::new_err("foo"); + err.write_unraisable(py, None); - assert!(capture.borrow(py).capture.is_none()); + let (err, object) = capture.take_capture().unwrap(); - let err = PyRuntimeError::new_err("foo"); - err.write_unraisable(py, None); + assert_eq!(err.to_string(), "RuntimeError: foo"); + assert!(object.is_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(&PyNotImplemented::get(py))); - let err = PyRuntimeError::new_err("bar"); - err.write_unraisable(py, Some(&PyNotImplemented::get(py))); + let (err, object) = capture.take_capture().unwrap(); - let (err, object) = capture.borrow_mut(py).capture.take().unwrap(); - assert_eq!(err.to_string(), "RuntimeError: bar"); - assert!(unsafe { ptr::eq(object.as_ptr(), ffi::Py_NotImplemented()) }); - - capture.borrow_mut(py).uninstall(py); + assert_eq!(err.to_string(), "RuntimeError: bar"); + assert!(object.is(PyNotImplemented::get(py))); + }); }); } diff --git a/tests/test_utils/mod.rs b/tests/test_utils/mod.rs index 235adc2f99d..e154c16372d 100644 --- a/tests/test_utils/mod.rs +++ b/tests/test_utils/mod.rs @@ -18,9 +18,11 @@ mod inner { use pyo3::prelude::*; + #[cfg(any(not(all(Py_GIL_DISABLED, Py_3_14)), all(feature = "macros", Py_3_8)))] use pyo3::sync::MutexExt; use pyo3::types::{IntoPyDict, PyList}; + #[cfg(any(not(all(Py_GIL_DISABLED, Py_3_14)), all(feature = "macros", Py_3_8)))] use std::sync::{Mutex, PoisonError}; use uuid::Uuid; @@ -118,49 +120,88 @@ mod inner { } // sys.unraisablehook not available until Python 3.8 - #[cfg(all(feature = "macros", Py_3_8, not(Py_GIL_DISABLED)))] - #[pyclass(crate = "pyo3")] - pub struct UnraisableCapture { - pub capture: Option<(PyErr, Py)>, - old_hook: Option>, + #[cfg(all(feature = "macros", Py_3_8))] + pub struct UnraisableCapture<'py> { + hook: Bound<'py, UnraisableCaptureHook>, } - #[cfg(all(feature = "macros", Py_3_8, not(Py_GIL_DISABLED)))] + #[cfg(all(feature = "macros", Py_3_8))] + impl<'py> UnraisableCapture<'py> { + /// Runs the closure `f` with a custom sys.unraisablehook installed. + /// + /// `f` + pub fn enter(py: Python<'py>, f: impl FnOnce(&Self) -> R) -> R { + // unraisablehook is a global, so only one thread can be using this struct at a time. + static UNRAISABLE_HOOK_MUTEX: Mutex<()> = Mutex::new(()); + + // NB this is best-effort, other tests could always modify sys.unraisablehook directly. + let mutex_guard = UNRAISABLE_HOOK_MUTEX + .lock_py_attached(py) + .unwrap_or_else(PoisonError::into_inner); + + let guard = Self { + hook: UnraisableCaptureHook::install(py), + }; + + let result = f(&guard); + + drop(guard); + drop(mutex_guard); + + result + } + + /// Takes the captured unraisable error, if any. + pub fn take_capture(&self) -> Option<(PyErr, Bound<'py, PyAny>)> { + let mut guard = self.hook.get().capture.lock().unwrap(); + guard.take().map(|(e, o)| (e, o.into_bound(self.hook.py()))) + } + } + + #[cfg(all(feature = "macros", Py_3_8))] + impl Drop for UnraisableCapture<'_> { + fn drop(&mut self) { + let py = self.hook.py(); + self.hook.get().uninstall(py); + } + } + + #[cfg(all(feature = "macros", Py_3_8))] + #[pyclass(crate = "pyo3", frozen)] + struct UnraisableCaptureHook { + pub capture: Mutex)>>, + old_hook: Py, + } + + #[cfg(all(feature = "macros", Py_3_8))] #[pymethods(crate = "pyo3")] - impl UnraisableCapture { - pub fn hook(&mut self, unraisable: Bound<'_, PyAny>) { + impl UnraisableCaptureHook { + pub fn hook(&self, unraisable: Bound<'_, PyAny>) { let err = PyErr::from_value(unraisable.getattr("exc_value").unwrap()); let instance = unraisable.getattr("object").unwrap(); - self.capture = Some((err, instance.into())); + self.capture.lock().unwrap().replace((err, instance.into())); } } - #[cfg(all(feature = "macros", Py_3_8, not(Py_GIL_DISABLED)))] - impl UnraisableCapture { - pub fn install(py: Python<'_>) -> Py { + #[cfg(all(feature = "macros", Py_3_8))] + impl UnraisableCaptureHook { + fn install(py: Python<'_>) -> Bound<'_, Self> { let sys = py.import("sys").unwrap(); + let old_hook = sys.getattr("unraisablehook").unwrap().into(); + let capture = Mutex::new(None); - let capture = Py::new( - py, - UnraisableCapture { - capture: None, - old_hook: Some(old_hook), - }, - ) - .unwrap(); + let capture = Bound::new(py, UnraisableCaptureHook { capture, old_hook }).unwrap(); - sys.setattr("unraisablehook", capture.getattr(py, "hook").unwrap()) + sys.setattr("unraisablehook", capture.getattr("hook").unwrap()) .unwrap(); capture } - pub fn uninstall(&mut self, py: Python<'_>) { - let old_hook = self.old_hook.take().unwrap(); - + fn uninstall(&self, py: Python<'_>) { let sys = py.import("sys").unwrap(); - sys.setattr("unraisablehook", old_hook).unwrap(); + sys.setattr("unraisablehook", &self.old_hook).unwrap(); } } @@ -170,6 +211,7 @@ mod inner { /// catch_warnings is not thread-safe, so only one thread can be using this struct at /// a time. + #[cfg(not(all(Py_GIL_DISABLED, Py_3_14)))] // Python 3.14t has thread-safe catch_warnings static CATCH_WARNINGS_MUTEX: Mutex<()> = Mutex::new(()); impl<'py> CatchWarnings<'py> { @@ -178,6 +220,7 @@ mod inner { f: impl FnOnce(&Bound<'py, PyList>) -> PyResult, ) -> PyResult { // NB this is best-effort, other tests could always call the warnings API directly. + #[cfg(not(all(Py_GIL_DISABLED, Py_3_14)))] let _mutex_guard = CATCH_WARNINGS_MUTEX .lock_py_attached(py) .unwrap_or_else(PoisonError::into_inner); From 02b54ebd6e005fd70c3a77aa53a114e5169e49d7 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 17 Oct 2025 16:59:14 +0100 Subject: [PATCH 918/936] make warning name distinct in warnings tests (#5532) --- tests/test_pyfunction.rs | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/tests/test_pyfunction.rs b/tests/test_pyfunction.rs index eb4e02dae04..14d74244ecb 100644 --- a/tests/test_pyfunction.rs +++ b/tests/test_pyfunction.rs @@ -669,44 +669,44 @@ impl UserDefinedWarning { #[test] fn test_pyfunction_warn() { #[pyfunction] - #[pyo3(warn(message = "this function raises warning"))] + #[pyo3(warn(message = "TPW: this function raises warning"))] fn function_with_warning() {} py_expect_warning_for_fn!( function_with_warning, f, - [("this function raises warning", PyUserWarning)] + [("TPW: this function raises warning", PyUserWarning)] ); #[pyfunction] - #[pyo3(warn(message = "this function raises warning with category", category = PyFutureWarning))] + #[pyo3(warn(message = "TPW: this function raises warning with category", category = PyFutureWarning))] fn function_with_warning_with_category() {} py_expect_warning_for_fn!( function_with_warning_with_category, f, [( - "this function raises warning with category", + "TPW: this function raises warning with category", PyFutureWarning )] ); #[pyfunction] - #[pyo3(warn(message = "custom deprecated category", category = pyo3::exceptions::PyDeprecationWarning))] + #[pyo3(warn(message = "TPW: custom deprecated category", category = pyo3::exceptions::PyDeprecationWarning))] fn function_with_warning_with_custom_category() {} py_expect_warning_for_fn!( function_with_warning_with_custom_category, f, [( - "custom deprecated category", + "TPW: custom deprecated category", pyo3::exceptions::PyDeprecationWarning )] ); #[cfg(not(Py_LIMITED_API))] #[pyfunction] - #[pyo3(warn(message = "this function raises user-defined warning", category = UserDefinedWarning))] + #[pyo3(warn(message = "TPW: this function raises user-defined warning", category = UserDefinedWarning))] fn function_with_warning_and_user_defined_category() {} #[cfg(not(Py_LIMITED_API))] @@ -714,7 +714,7 @@ fn test_pyfunction_warn() { function_with_warning_and_user_defined_category, f, [( - "this function raises user-defined warning", + "TPW: this function raises user-defined warning", UserDefinedWarning )] ); @@ -723,23 +723,23 @@ fn test_pyfunction_warn() { #[test] fn test_pyfunction_multiple_warnings() { #[pyfunction] - #[pyo3(warn(message = "this function raises warning"))] - #[pyo3(warn(message = "this function raises FutureWarning", category = PyFutureWarning))] + #[pyo3(warn(message = "TPMW: this function raises warning"))] + #[pyo3(warn(message = "TPMW: this function raises FutureWarning", category = PyFutureWarning))] fn function_with_multiple_warnings() {} py_expect_warning_for_fn!( function_with_multiple_warnings, f, [ - ("this function raises warning", PyUserWarning), - ("this function raises FutureWarning", PyFutureWarning) + ("TPMW: this function raises warning", PyUserWarning), + ("TPMW: this function raises FutureWarning", PyFutureWarning) ] ); #[cfg(not(Py_LIMITED_API))] #[pyfunction] - #[pyo3(warn(message = "this function raises FutureWarning", category = PyFutureWarning))] - #[pyo3(warn(message = "this function raises user-defined warning", category = UserDefinedWarning))] + #[pyo3(warn(message = "TPMW: this function raises FutureWarning", category = PyFutureWarning))] + #[pyo3(warn(message = "TPMW: this function raises user-defined warning", category = UserDefinedWarning))] fn function_with_multiple_custom_warnings() {} #[cfg(not(Py_LIMITED_API))] @@ -747,9 +747,9 @@ fn test_pyfunction_multiple_warnings() { function_with_multiple_custom_warnings, f, [ - ("this function raises FutureWarning", PyFutureWarning), + ("TPMW: this function raises FutureWarning", PyFutureWarning), ( - "this function raises user-defined warning", + "TPMW: this function raises user-defined warning", UserDefinedWarning ) ] From f32ed83e32dc822e3dc869d1c4517055ce6de35d Mon Sep 17 00:00:00 2001 From: Paul Stemmet Date: Fri, 17 Oct 2025 14:40:02 -0300 Subject: [PATCH 919/936] fix PyPyModule_ExecDef, PyPyModule_FromDefAndSpec2 definitions (#5529) * fix `PyPyModule_ExecDef` definition * fix PyPy link name for `PyModule_FromDefAndSpec2` * newsfragments: add PR --------- Co-authored-by: David Hewitt --- newsfragments/5529.fixed.md | 1 + pyo3-ffi/src/modsupport.rs | 2 ++ 2 files changed, 3 insertions(+) create mode 100644 newsfragments/5529.fixed.md diff --git a/newsfragments/5529.fixed.md b/newsfragments/5529.fixed.md new file mode 100644 index 00000000000..453e96ae9c8 --- /dev/null +++ b/newsfragments/5529.fixed.md @@ -0,0 +1 @@ +fix `PyPyModule_ExecDef`, `PyPyModule_FromDefAndSpec2` definitions diff --git a/pyo3-ffi/src/modsupport.rs b/pyo3-ffi/src/modsupport.rs index 344e0abb979..8cda23a8c2d 100644 --- a/pyo3-ffi/src/modsupport.rs +++ b/pyo3-ffi/src/modsupport.rs @@ -73,6 +73,7 @@ extern "C" { // skipped PyModule_AddStringMacro pub fn PyModule_SetDocString(arg1: *mut PyObject, arg2: *const c_char) -> c_int; pub fn PyModule_AddFunctions(arg1: *mut PyObject, arg2: *mut PyMethodDef) -> c_int; + #[cfg_attr(PyPy, link_name = "PyPyModule_ExecDef")] pub fn PyModule_ExecDef(module: *mut PyObject, def: *mut PyModuleDef) -> c_int; } @@ -90,6 +91,7 @@ extern "C" { fn PyModule_Create2TraceRefs(module: *mut PyModuleDef, apiver: c_int) -> *mut PyObject; #[cfg(not(py_sys_config = "Py_TRACE_REFS"))] + #[cfg_attr(PyPy, link_name = "PyPyModule_FromDefAndSpec2")] pub fn PyModule_FromDefAndSpec2( def: *mut PyModuleDef, spec: *mut PyObject, From b392013ca34bd1e27bc06f9f57b11583fcb21692 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Fri, 17 Oct 2025 18:40:18 +0100 Subject: [PATCH 920/936] ci: install lychee stable using `install-action` (#5528) --- .github/workflows/netlify-build.yml | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/.github/workflows/netlify-build.yml b/.github/workflows/netlify-build.yml index 9fbb28886a2..6150af3db24 100644 --- a/.github/workflows/netlify-build.yml +++ b/.github/workflows/netlify-build.yml @@ -31,15 +31,7 @@ jobs: - name: Setup mdBook uses: taiki-e/install-action@v2 with: - tool: mdbook, mdbook-linkcheck - - - name: Link Checker - id: lychee - uses: lycheeverse/lychee-action@v2 - with: - # setup lychee but don't run it yet - args: --version - lycheeVersion: nightly + tool: mdbook, mdbook-linkcheck, lychee - name: Prepare tag id: prepare_tag From 752551221a1532350778029ac1094bf18dfb493c Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sun, 19 Oct 2025 09:49:13 +0100 Subject: [PATCH 921/936] release: 0.27.0 (#5520) --- CHANGELOG.md | 76 ++++++++++++++++++- 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/4390.added.md | 2 - newsfragments/4390.changed.md | 2 - newsfragments/4390.removed.md | 1 - newsfragments/5244.changed.md | 1 - newsfragments/5365.added.md | 1 - newsfragments/5368.fixed.md | 1 - newsfragments/5370.added.md | 1 - newsfragments/5384.changed.md | 1 - newsfragments/5387.added.md | 1 - newsfragments/5387.changed.md | 2 - newsfragments/5388.added.md | 1 - newsfragments/5402.added.md | 1 - newsfragments/5403.added.md | 1 - newsfragments/5404.added.md | 1 - newsfragments/5413.changed.md | 1 - newsfragments/5414.changed.md | 1 - newsfragments/5428.packaging.md | 1 - newsfragments/5435.added.md | 1 - newsfragments/5437.added.md | 1 - newsfragments/5442.changed.md | 1 - newsfragments/5444.fixed.md | 1 - newsfragments/5445.added.md | 1 - newsfragments/5450.changed.md | 1 - newsfragments/5454.changed.md | 1 - newsfragments/5456.fixed.md | 1 - newsfragments/5459.fixed.md | 1 - newsfragments/5468.added.md | 1 - newsfragments/5468.changed.md | 1 - newsfragments/5471.changed.md | 1 - newsfragments/5471.packaging.md | 1 - newsfragments/5472.changed.md | 1 - newsfragments/5473.changed.md | 1 - newsfragments/5474.added.md | 1 - newsfragments/5474.changed.md | 3 - newsfragments/5475.added.md | 1 - newsfragments/5478.added.md | 1 - newsfragments/5480.changed.md | 1 - newsfragments/5482.added.md | 1 - newsfragments/5488.added.md | 1 - newsfragments/5489.added.md | 1 - newsfragments/5494.changed.md | 1 - newsfragments/5497.added.md | 1 - newsfragments/5499.fixed.md | 1 - newsfragments/5499.packaging.md | 1 - newsfragments/5503.fixed.md | 1 - newsfragments/5506.added.md | 1 - newsfragments/5507.changed.md | 1 - newsfragments/5516.packaging.md | 1 - newsfragments/5519.packaging.md | 1 - newsfragments/5529.fixed.md | 1 - pyo3-build-config/Cargo.toml | 2 +- pyo3-ffi/Cargo.toml | 4 +- pyo3-ffi/README.md | 4 +- pyo3-introspection/Cargo.toml | 2 +- pyo3-macros-backend/Cargo.toml | 6 +- pyo3-macros/Cargo.toml | 4 +- pyproject.toml | 2 +- tests/ui/reject_generics.stderr | 4 +- 66 files changed, 100 insertions(+), 81 deletions(-) delete mode 100644 newsfragments/4390.added.md delete mode 100644 newsfragments/4390.changed.md delete mode 100644 newsfragments/4390.removed.md delete mode 100644 newsfragments/5244.changed.md delete mode 100644 newsfragments/5365.added.md delete mode 100644 newsfragments/5368.fixed.md delete mode 100644 newsfragments/5370.added.md delete mode 100644 newsfragments/5384.changed.md delete mode 100644 newsfragments/5387.added.md delete mode 100644 newsfragments/5387.changed.md delete mode 100644 newsfragments/5388.added.md delete mode 100644 newsfragments/5402.added.md delete mode 100644 newsfragments/5403.added.md delete mode 100644 newsfragments/5404.added.md delete mode 100644 newsfragments/5413.changed.md delete mode 100644 newsfragments/5414.changed.md delete mode 100644 newsfragments/5428.packaging.md delete mode 100644 newsfragments/5435.added.md delete mode 100644 newsfragments/5437.added.md delete mode 100644 newsfragments/5442.changed.md delete mode 100644 newsfragments/5444.fixed.md delete mode 100644 newsfragments/5445.added.md delete mode 100644 newsfragments/5450.changed.md delete mode 100644 newsfragments/5454.changed.md delete mode 100644 newsfragments/5456.fixed.md delete mode 100644 newsfragments/5459.fixed.md delete mode 100644 newsfragments/5468.added.md delete mode 100644 newsfragments/5468.changed.md delete mode 100644 newsfragments/5471.changed.md delete mode 100644 newsfragments/5471.packaging.md delete mode 100644 newsfragments/5472.changed.md delete mode 100644 newsfragments/5473.changed.md delete mode 100644 newsfragments/5474.added.md delete mode 100644 newsfragments/5474.changed.md delete mode 100644 newsfragments/5475.added.md delete mode 100644 newsfragments/5478.added.md delete mode 100644 newsfragments/5480.changed.md delete mode 100644 newsfragments/5482.added.md delete mode 100644 newsfragments/5488.added.md delete mode 100644 newsfragments/5489.added.md delete mode 100644 newsfragments/5494.changed.md delete mode 100644 newsfragments/5497.added.md delete mode 100644 newsfragments/5499.fixed.md delete mode 100644 newsfragments/5499.packaging.md delete mode 100644 newsfragments/5503.fixed.md delete mode 100644 newsfragments/5506.added.md delete mode 100644 newsfragments/5507.changed.md delete mode 100644 newsfragments/5516.packaging.md delete mode 100644 newsfragments/5519.packaging.md delete mode 100644 newsfragments/5529.fixed.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 764441bd458..2872fac91a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,79 @@ To see unreleased changes, please see the [CHANGELOG on the main branch guide](h +## [0.27.0] - 2025-10-19 + +### Packaging + +- Extend range of supported versions of `hashbrown` optional dependency to include version 0.16. [#5428](https://github.com/PyO3/pyo3/pull/5428) +- Bump optional `num-bigint` dependency minimum version to 0.4.4. [#5471](https://github.com/PyO3/pyo3/pull/5471) +- Test against Python 3.14 final release. [#5499](https://github.com/PyO3/pyo3/pull/5499) +- Drop support for PyPy 3.9 and 3.10. [#5516](https://github.com/PyO3/pyo3/pull/5516) +- Provide a better error message when building an outdated PyO3 for a too-new Python version. [#5519](https://github.com/PyO3/pyo3/pull/5519) + +### Added + +- Add `FromPyObjectOwned` as convenient trait bound for `FromPyObject` when the data is not borrowed from Python. [#4390](https://github.com/PyO3/pyo3/pull/4390) +- Add `Borrowed::extract`, same as `PyAnyMethods::extract`, but does not restrict the lifetime by deref. [#4390](https://github.com/PyO3/pyo3/pull/4390) +- `experimental-inspect`: basic support for `#[derive(IntoPyObject)]` (no struct fields support yet). [#5365](https://github.com/PyO3/pyo3/pull/5365) +- `experimental-inspect`: support `#[pyo3(get, set)]` and `#[pyclass(get_all, set_all)]`. [#5370](https://github.com/PyO3/pyo3/pull/5370) +- Add `PyTypeCheck::classinfo_object` that returns an object that can be used as parameter in `isinstance` or `issubclass`. [#5387](https://github.com/PyO3/pyo3/pull/5387) +- Implement `PyTypeInfo` on `datetime.*` types even when the limited API is enabled. [#5388](https://github.com/PyO3/pyo3/pull/5388) +- Implement `PyTypeInfo` on `PyIterator`, `PyMapping` and `PySequence`. [#5402](https://github.com/PyO3/pyo3/pull/5402) +- Implement `PyTypeInfo` on `PyCode` when using the stable ABI. [#5403](https://github.com/PyO3/pyo3/pull/5403) +- Implement `PyTypeInfo` on `PyWeakrefReference` when using the stable ABI. [#5404](https://github.com/PyO3/pyo3/pull/5404) +- Add `pyo3::sync::RwLockExt` trait, analogous to `pyo3::sync::MutexExt` for readwrite locks. [#5435](https://github.com/PyO3/pyo3/pull/5435) +- Add `PyString::from_bytes`. [#5437](https://github.com/PyO3/pyo3/pull/5437) +- Implement `AsRef<[u8]>` for `PyBytes`. [#5445](https://github.com/PyO3/pyo3/pull/5445) +- Add `CastError` and `CastIntoError`. [#5468](https://github.com/PyO3/pyo3/pull/5468) +- Add `PyCapsuleMethods::pointer_checked` and `PyCapsuleMethods::is_valid_checked`. [#5474](https://github.com/PyO3/pyo3/pull/5474) +- Add `Borrowed::cast`, `Borrowed::cast_exact` and `Borrowed::cast_unchecked`. [#5475](https://github.com/PyO3/pyo3/pull/5475) +- Add conversions for `jiff::civil::ISOWeekDate`. [#5478](https://github.com/PyO3/pyo3/pull/5478) +- Add conversions for `&Cstr`, `Cstring` and `Cow`. [#5482](https://github.com/PyO3/pyo3/pull/5482) +- add `#[pyclass(skip_from_py_object)]` option, to opt-out of the `FromPyObject: PyClass + Clone` blanket impl. [#5488](https://github.com/PyO3/pyo3/pull/5488) +- Add `PyErr::add_note`. [#5489](https://github.com/PyO3/pyo3/pull/5489) +- Add `FromPyObject` impl for `Cow` & `Cow`. [#5497](https://github.com/PyO3/pyo3/pull/5497) +- Add `#[pyclass(from_py_object)]` pyclass option, to opt-in to the extraction of pyclasses by value (requires `Clone`). [#5506](https://github.com/PyO3/pyo3/pull/5506) + +### Changed + +- Rework `FromPyObject` trait for flexibility and performance: [#4390](https://github.com/PyO3/pyo3/pull/4390) + - Add a second lifetime to `FromPyObject`, to allow borrowing data from Python objects (e.g. `&str` from Python `str`). + - Replace `extract_bound` with `extract`, which takes `Borrowed<'a, 'py, PyAny>`. +- Optimize `FromPyObject` implementations for `Vec` and `[u8; N]` from `bytes` and `bytearray`. [#5244](https://github.com/PyO3/pyo3/pull/5244) +- Deprecate `#[pyfn]` attribute. [#5384](https://github.com/PyO3/pyo3/pull/5384) +- Fetch type name dynamically on cast errors instead of using `PyTypeCheck::NAME`. [#5387](https://github.com/PyO3/pyo3/pull/5387) +- Deprecate `PyTypeCheck::NAME` in favour of `PyTypeCheck::classinfo_object` which provides the type information at runtime. [#5387](https://github.com/PyO3/pyo3/pull/5387) +- `PyClassGuard(Mut)` and `PyRef(Mut)` extraction now returns an opaque Rust error [#5413](https://github.com/PyO3/pyo3/pull/5413) +- Fetch type name dynamically when exporting types implementing `PyTypeInfo` with `#[pymodule_use]`. [#5414](https://github.com/PyO3/pyo3/pull/5414) +- Improve `Debug` representation of `PyBuffer`. [#5442](https://github.com/PyO3/pyo3/pull/5442) +- `experimental-inspect`: change the way introspection data is emitted in the binaries to avoid a pointer indirection and simplify parsing. [#5450](https://github.com/PyO3/pyo3/pull/5450) +- Optimize `Py::drop` for the case when attached to the Python interpreter. [#5454](https://github.com/PyO3/pyo3/pull/5454) +- Replace `DowncastError` and `DowncastIntoError` with `CastError` and `CastIntoError`. [#5468](https://github.com/PyO3/pyo3/pull/5468) +- Enable fast-path for 128-bit integer conversions on `GraalPy`. [#5471](https://github.com/PyO3/pyo3/pull/5471) +- Deprecate `PyAnyMethods::downcast` functions in favour of `Bound::cast` functions. [#5472](https://github.com/PyO3/pyo3/pull/5472) +- Make `PyTypeCheck` an `unsafe trait`. [#5473](https://github.com/PyO3/pyo3/pull/5473) +- Deprecate unchecked `PyCapsuleMethods`: `pointer()`, `reference()`, and `is_valid()`. [#5474](https://github.com/PyO3/pyo3/pull/5474) +- Reduce lifetime of return value in `PyCapsuleMethods::reference`. [#5474](https://github.com/PyO3/pyo3/pull/5474) +- `PyCapsuleMethods::name` now returns `CapsuleName` wrapper instead of `&CStr`. [#5474](https://github.com/PyO3/pyo3/pull/5474) +- Deprecate `import_exception_bound` in favour of `import_exception`. [#5480](https://github.com/PyO3/pyo3/pull/5480) +- `PyList::get_item_unchecked`, `PyTuple::get_item_unchecked`, and `PyTuple::get_borrowed_item_unchecked` no longer check for null values at the provided index. [#5494](https://github.com/PyO3/pyo3/pull/5494) +- Allow converting naive datetime into chrono `DateTime`. [#5507](https://github.com/PyO3/pyo3/pull/5507) + +### Removed + +- Removed `FromPyObjectBound` trait. [#4390](https://github.com/PyO3/pyo3/pull/4390) + +### Fixed + +- Fix compilation failure on `wasm32-wasip2`. [#5368](https://github.com/PyO3/pyo3/pull/5368) +- Fix `OsStr` conversion for non-utf8 strings on Windows. [#5444](https://github.com/PyO3/pyo3/pull/5444) +- Fix issue with `cargo vendor` caused by gitignored build artifact `emscripten/pybuilddir.txt`. [#5456](https://github.com/PyO3/pyo3/pull/5456) +- Stop leaking `PyMethodDef` instances inside `#[pyfunction]` macro generated code. [#5459](https://github.com/PyO3/pyo3/pull/5459) +- Don't export definition of FFI struct `PyObjectObFlagsAndRefcnt` on 32-bit Python 3.14 (doesn't exist). [#5499](https://github.com/PyO3/pyo3/pull/5499) +- Fix failure to build for `abi3` interpreters on Windows using maturin's built-in sysconfig in combination with the `generate-import-lib` feature. [#5503](https://github.com/PyO3/pyo3/pull/5503) +- Fix FFI definitions `PyModule_ExecDef` and `PyModule_FromDefAndSpec2` on PyPy. [#5529](https://github.com/PyO3/pyo3/pull/5529) + ## [0.26.0] - 2025-08-29 ### Packaging @@ -2296,7 +2369,8 @@ Yanked - Initial release -[Unreleased]: https://github.com/pyo3/pyo3/compare/v0.26.0...HEAD +[Unreleased]: https://github.com/pyo3/pyo3/compare/v0.27.0...HEAD +[0.27.0]: https://github.com/pyo3/pyo3/compare/v0.26.0...v0.27.0 [0.26.0]: https://github.com/pyo3/pyo3/compare/v0.25.1...v0.26.0 [0.25.1]: https://github.com/pyo3/pyo3/compare/v0.25.0...v0.25.1 [0.25.0]: https://github.com/pyo3/pyo3/compare/v0.24.2...v0.25.0 diff --git a/Cargo.toml b/Cargo.toml index 5199667b7b9..fa49c97ea20 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3" -version = "0.26.0" +version = "0.27.0" description = "Bindings to Python interpreter" authors = ["PyO3 Project and Contributors "] readme = "README.md" @@ -30,10 +30,10 @@ memoffset = "0.9" once_cell = "1.21" # ffi bindings to the python interpreter, split into a separate crate so they can be used independently -pyo3-ffi = { path = "pyo3-ffi", version = "=0.26.0" } +pyo3-ffi = { path = "pyo3-ffi", version = "=0.27.0" } # support crates for macros feature -pyo3-macros = { path = "pyo3-macros", version = "=0.26.0", optional = true } +pyo3-macros = { path = "pyo3-macros", version = "=0.27.0", optional = true } indoc = { version = "2.0.1", optional = true } unindent = { version = "0.2.1", optional = true } @@ -86,7 +86,7 @@ uuid = { version = "1.10.0", features = ["v4"] } parking_lot = { version = "0.12.3", features = ["arc_lock"] } [build-dependencies] -pyo3-build-config = { path = "pyo3-build-config", version = "=0.26.0", features = ["resolve-config"] } +pyo3-build-config = { path = "pyo3-build-config", version = "=0.27.0", features = ["resolve-config"] } [features] default = ["macros"] diff --git a/README.md b/README.md index b2e26b3855c..451e97cf69c 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ name = "string_sum" crate-type = ["cdylib"] [dependencies] -pyo3 = { version = "0.26.0", features = ["extension-module"] } +pyo3 = { version = "0.27.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.26.0" +version = "0.27.0" features = ["auto-initialize"] ``` diff --git a/examples/decorator/.template/pre-script.rhai b/examples/decorator/.template/pre-script.rhai index 3abc923be1f..10287310615 100644 --- a/examples/decorator/.template/pre-script.rhai +++ b/examples/decorator/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.26.0"); +variable::set("PYO3_VERSION", "0.27.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 3abc923be1f..10287310615 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.26.0"); +variable::set("PYO3_VERSION", "0.27.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 27204393c47..bb9d2c4f3b5 100644 --- a/examples/plugin/.template/pre-script.rhai +++ b/examples/plugin/.template/pre-script.rhai @@ -1,4 +1,4 @@ -variable::set("PYO3_VERSION", "0.26.0"); +variable::set("PYO3_VERSION", "0.27.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 9f25f008fe6..f6c8aca02a9 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.26.0"); +variable::set("PYO3_VERSION", "0.27.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 3abc923be1f..10287310615 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.26.0"); +variable::set("PYO3_VERSION", "0.27.0"); file::rename(".template/Cargo.toml", "Cargo.toml"); file::rename(".template/pyproject.toml", "pyproject.toml"); file::delete(".template"); diff --git a/newsfragments/4390.added.md b/newsfragments/4390.added.md deleted file mode 100644 index c234b0b6547..00000000000 --- a/newsfragments/4390.added.md +++ /dev/null @@ -1,2 +0,0 @@ -added `FromPyObjectOwned` as more convenient trait bound -added `Borrowed::extract`, same as `PyAnyMethods::extract`, but does not restrict the lifetime by deref \ No newline at end of file diff --git a/newsfragments/4390.changed.md b/newsfragments/4390.changed.md deleted file mode 100644 index 690bdaf17e5..00000000000 --- a/newsfragments/4390.changed.md +++ /dev/null @@ -1,2 +0,0 @@ -added second lifetime to `FromPyObject` -reintroduced `extract` method \ No newline at end of file diff --git a/newsfragments/4390.removed.md b/newsfragments/4390.removed.md deleted file mode 100644 index a40ee2db268..00000000000 --- a/newsfragments/4390.removed.md +++ /dev/null @@ -1 +0,0 @@ -removed `FromPyObjectBound` \ No newline at end of file diff --git a/newsfragments/5244.changed.md b/newsfragments/5244.changed.md deleted file mode 100644 index dcbf400c20d..00000000000 --- a/newsfragments/5244.changed.md +++ /dev/null @@ -1 +0,0 @@ -Optimize `FromPyObject` implementations for `Vec` and `[u8; N]` from `bytes` and `bytearray` diff --git a/newsfragments/5365.added.md b/newsfragments/5365.added.md deleted file mode 100644 index c272ba1bfe9..00000000000 --- a/newsfragments/5365.added.md +++ /dev/null @@ -1 +0,0 @@ -Basic introspection of `#[derive(IntoPyObject)]` (no struct fields support yet) \ No newline at end of file diff --git a/newsfragments/5368.fixed.md b/newsfragments/5368.fixed.md deleted file mode 100644 index 837a7398e62..00000000000 --- a/newsfragments/5368.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fix compilation on `wasm32-wasip2`. diff --git a/newsfragments/5370.added.md b/newsfragments/5370.added.md deleted file mode 100644 index 20b8543bebc..00000000000 --- a/newsfragments/5370.added.md +++ /dev/null @@ -1 +0,0 @@ -Introspection: support `#[pyo3(get, set)]` and `#[pyclass(get_all, set_all)]` \ No newline at end of file diff --git a/newsfragments/5384.changed.md b/newsfragments/5384.changed.md deleted file mode 100644 index fe7377d7de8..00000000000 --- a/newsfragments/5384.changed.md +++ /dev/null @@ -1 +0,0 @@ -Deprecate `pyfn` attribute. diff --git a/newsfragments/5387.added.md b/newsfragments/5387.added.md deleted file mode 100644 index 9b96ac46353..00000000000 --- a/newsfragments/5387.added.md +++ /dev/null @@ -1 +0,0 @@ -Add `PyTypeCheck::classinfo_object` that returns an object that can be used as parameter in `isinstance` or `issubclass` \ No newline at end of file diff --git a/newsfragments/5387.changed.md b/newsfragments/5387.changed.md deleted file mode 100644 index 8c737793ef0..00000000000 --- a/newsfragments/5387.changed.md +++ /dev/null @@ -1,2 +0,0 @@ -- Fetch type name dynamically on cast errors instead of using `PyTypeCheck::NAME` -- Deprecate `PyTypeCheck::NAME`, please `TypeTypeCheck::classinfo_object` to get the expected type and format it at runtime. \ No newline at end of file diff --git a/newsfragments/5388.added.md b/newsfragments/5388.added.md deleted file mode 100644 index b62dfc6b9fc..00000000000 --- a/newsfragments/5388.added.md +++ /dev/null @@ -1 +0,0 @@ -Implement `PyTypeInfo` on `datetime.*` types even when the limited API is enabled \ No newline at end of file diff --git a/newsfragments/5402.added.md b/newsfragments/5402.added.md deleted file mode 100644 index 88ff4a9c589..00000000000 --- a/newsfragments/5402.added.md +++ /dev/null @@ -1 +0,0 @@ -Implement `PyTypeInfo` on `PyIterator`, `PyMapping` and `PySequence` \ No newline at end of file diff --git a/newsfragments/5403.added.md b/newsfragments/5403.added.md deleted file mode 100644 index e2828417a12..00000000000 --- a/newsfragments/5403.added.md +++ /dev/null @@ -1 +0,0 @@ -Implement `PyTypeInfo` on `PyCode` when using the stable ABI \ No newline at end of file diff --git a/newsfragments/5404.added.md b/newsfragments/5404.added.md deleted file mode 100644 index 0d6d8492826..00000000000 --- a/newsfragments/5404.added.md +++ /dev/null @@ -1 +0,0 @@ -Implement `PyTypeInfo` on `PyWeakrefReference` when using the stable ABI \ No newline at end of file diff --git a/newsfragments/5413.changed.md b/newsfragments/5413.changed.md deleted file mode 100644 index 74cec2205c8..00000000000 --- a/newsfragments/5413.changed.md +++ /dev/null @@ -1 +0,0 @@ -`PyClassGuard(Mut)` and `PyRef(Mut)` extraction now returns an opaque Rust error \ No newline at end of file diff --git a/newsfragments/5414.changed.md b/newsfragments/5414.changed.md deleted file mode 100644 index 91e3bee5c55..00000000000 --- a/newsfragments/5414.changed.md +++ /dev/null @@ -1 +0,0 @@ -`AddTypeToModule`: fetch name dynamically \ No newline at end of file diff --git a/newsfragments/5428.packaging.md b/newsfragments/5428.packaging.md deleted file mode 100644 index a6d0c1e6183..00000000000 --- a/newsfragments/5428.packaging.md +++ /dev/null @@ -1 +0,0 @@ -Extend range of supported versions of `hashbrown` optional dependency to include version 0.16. diff --git a/newsfragments/5435.added.md b/newsfragments/5435.added.md deleted file mode 100644 index b86073a5525..00000000000 --- a/newsfragments/5435.added.md +++ /dev/null @@ -1 +0,0 @@ -Added the `pyo3::sync::RwLockExt` trait to allow detaching from the Python interpreter while blocking to acquire a rwlock. The trait is implemented for `std::sync::RwLock` as well as `parking_lot`'s / `lock_api`'s `RwLock`. \ No newline at end of file diff --git a/newsfragments/5437.added.md b/newsfragments/5437.added.md deleted file mode 100644 index f828546a05d..00000000000 --- a/newsfragments/5437.added.md +++ /dev/null @@ -1 +0,0 @@ -Add PyString::from_bytes. This saves a redundant UTF-8 validation check because Python internally validates the bytes again. diff --git a/newsfragments/5442.changed.md b/newsfragments/5442.changed.md deleted file mode 100644 index 4355627ea0a..00000000000 --- a/newsfragments/5442.changed.md +++ /dev/null @@ -1 +0,0 @@ -Improve `Debug` representation of `PyBuffer`. diff --git a/newsfragments/5444.fixed.md b/newsfragments/5444.fixed.md deleted file mode 100644 index 86055f8f1be..00000000000 --- a/newsfragments/5444.fixed.md +++ /dev/null @@ -1 +0,0 @@ -fix `OsStr` conversion for non-utf8 strings on windows diff --git a/newsfragments/5445.added.md b/newsfragments/5445.added.md deleted file mode 100644 index af00d098f85..00000000000 --- a/newsfragments/5445.added.md +++ /dev/null @@ -1 +0,0 @@ -Implement `AsRef<[u8]>` for `PyBytes` diff --git a/newsfragments/5450.changed.md b/newsfragments/5450.changed.md deleted file mode 100644 index deb249d35fd..00000000000 --- a/newsfragments/5450.changed.md +++ /dev/null @@ -1 +0,0 @@ -Introspection: change the way introspection data is emitted in the binaries to avoid a pointer indirection and simplify parsing. \ No newline at end of file diff --git a/newsfragments/5454.changed.md b/newsfragments/5454.changed.md deleted file mode 100644 index e65a6e2ae47..00000000000 --- a/newsfragments/5454.changed.md +++ /dev/null @@ -1 +0,0 @@ -Optimize `Py::drop` for the case when attached to the Python interpreter. diff --git a/newsfragments/5456.fixed.md b/newsfragments/5456.fixed.md deleted file mode 100644 index 51be2e4200b..00000000000 --- a/newsfragments/5456.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fix issue with `cargo vendor` caused by gitignored build artifact `emscripten/pybuilddir.txt`. diff --git a/newsfragments/5459.fixed.md b/newsfragments/5459.fixed.md deleted file mode 100644 index 5061eeff98d..00000000000 --- a/newsfragments/5459.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Stop leaking `PyMethodDef` instances inside `#[pyfunction]` macro generated code. diff --git a/newsfragments/5468.added.md b/newsfragments/5468.added.md deleted file mode 100644 index 73bb6cba80f..00000000000 --- a/newsfragments/5468.added.md +++ /dev/null @@ -1 +0,0 @@ -Add `CastError` and `CastIntoError`. diff --git a/newsfragments/5468.changed.md b/newsfragments/5468.changed.md deleted file mode 100644 index 5f7cca73682..00000000000 --- a/newsfragments/5468.changed.md +++ /dev/null @@ -1 +0,0 @@ -Replace `DowncastError` and `DowncastIntoError` with `CastError` and `CastIntoError`. diff --git a/newsfragments/5471.changed.md b/newsfragments/5471.changed.md deleted file mode 100644 index 56f1d369156..00000000000 --- a/newsfragments/5471.changed.md +++ /dev/null @@ -1 +0,0 @@ -Enable fast-path for 128-bit integer conversions on `GraalPy`. diff --git a/newsfragments/5471.packaging.md b/newsfragments/5471.packaging.md deleted file mode 100644 index 0b4bba9e3fa..00000000000 --- a/newsfragments/5471.packaging.md +++ /dev/null @@ -1 +0,0 @@ -Bump optional `num-bigint` dependency minimum version to 0.4.4. diff --git a/newsfragments/5472.changed.md b/newsfragments/5472.changed.md deleted file mode 100644 index 4634c5afea2..00000000000 --- a/newsfragments/5472.changed.md +++ /dev/null @@ -1 +0,0 @@ -Deprecate `PyAnyMethods::downcast` functions in favour of `Bound::cast` functions diff --git a/newsfragments/5473.changed.md b/newsfragments/5473.changed.md deleted file mode 100644 index 71c54298f55..00000000000 --- a/newsfragments/5473.changed.md +++ /dev/null @@ -1 +0,0 @@ -Make `PyTypeCheck` an `unsafe trait`. diff --git a/newsfragments/5474.added.md b/newsfragments/5474.added.md deleted file mode 100644 index e67123448d8..00000000000 --- a/newsfragments/5474.added.md +++ /dev/null @@ -1 +0,0 @@ -added checked methods into `PyCapsuleMethods`: `pointer_checked()`, `is_valid_checked()` diff --git a/newsfragments/5474.changed.md b/newsfragments/5474.changed.md deleted file mode 100644 index 7618d23c8d8..00000000000 --- a/newsfragments/5474.changed.md +++ /dev/null @@ -1,3 +0,0 @@ -Deprecate unchecked `PyCapsuleMethods`: `pointer()`, `reference()`, `is_valid()` -Reduce lifetime of return value in `PyCapsuleMethods::reference` -`PyCapsuleMethods::name` now returns `CapsuleName` wrapper instead of `&CStr`. diff --git a/newsfragments/5475.added.md b/newsfragments/5475.added.md deleted file mode 100644 index 29e37a413f7..00000000000 --- a/newsfragments/5475.added.md +++ /dev/null @@ -1 +0,0 @@ -added `Borrowed::cast`, `Borrowed::cast_exact` and `Borrowed::cast_unchecked` \ No newline at end of file diff --git a/newsfragments/5478.added.md b/newsfragments/5478.added.md deleted file mode 100644 index 87c59d425e2..00000000000 --- a/newsfragments/5478.added.md +++ /dev/null @@ -1 +0,0 @@ -Add conversions for `jiff::civil::ISOWeekDate` diff --git a/newsfragments/5480.changed.md b/newsfragments/5480.changed.md deleted file mode 100644 index 5a0792946cf..00000000000 --- a/newsfragments/5480.changed.md +++ /dev/null @@ -1 +0,0 @@ -Deprecate `import_exception_bound` in favour of `import_exception`. diff --git a/newsfragments/5482.added.md b/newsfragments/5482.added.md deleted file mode 100644 index b856a03a2ae..00000000000 --- a/newsfragments/5482.added.md +++ /dev/null @@ -1 +0,0 @@ -Add conversion for `&Cstr`, `Cstring` and `Cow` diff --git a/newsfragments/5488.added.md b/newsfragments/5488.added.md deleted file mode 100644 index 06b6a46bb7b..00000000000 --- a/newsfragments/5488.added.md +++ /dev/null @@ -1 +0,0 @@ -add `skip_from_py_object` pyclass option, to opt-out of the `FromPyObject: PyClass + Clone` blanket impl diff --git a/newsfragments/5489.added.md b/newsfragments/5489.added.md deleted file mode 100644 index dd8c76d8d99..00000000000 --- a/newsfragments/5489.added.md +++ /dev/null @@ -1 +0,0 @@ -Added `PyErr::add_note` diff --git a/newsfragments/5494.changed.md b/newsfragments/5494.changed.md deleted file mode 100644 index a66b4aadd1d..00000000000 --- a/newsfragments/5494.changed.md +++ /dev/null @@ -1 +0,0 @@ -`PyList::get_item_unchecked`, `PyTuple::get_item_unchecked`, and `PyTuple::get_borrowed_item_unchecked` no longer check for null values at the provided index. diff --git a/newsfragments/5497.added.md b/newsfragments/5497.added.md deleted file mode 100644 index 6fea77cdcec..00000000000 --- a/newsfragments/5497.added.md +++ /dev/null @@ -1 +0,0 @@ -Add `FromPyObject` impl for `Cow` & `Cow` diff --git a/newsfragments/5499.fixed.md b/newsfragments/5499.fixed.md deleted file mode 100644 index 0b50b0caf59..00000000000 --- a/newsfragments/5499.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Don't export definition of FFI struct PyObjectObFlagsAndRefcnt on 32-bit Python (doesn't exist). diff --git a/newsfragments/5499.packaging.md b/newsfragments/5499.packaging.md deleted file mode 100644 index fdd0e5d13ee..00000000000 --- a/newsfragments/5499.packaging.md +++ /dev/null @@ -1 +0,0 @@ -Test against Python 3.14 final release. diff --git a/newsfragments/5503.fixed.md b/newsfragments/5503.fixed.md deleted file mode 100644 index 9fd8eab6274..00000000000 --- a/newsfragments/5503.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fix failure to build for `abi3` interpreters on Windows using maturin's built-in sysconfig in combination with the `generate-import-lib` feature. diff --git a/newsfragments/5506.added.md b/newsfragments/5506.added.md deleted file mode 100644 index 439b0a5852d..00000000000 --- a/newsfragments/5506.added.md +++ /dev/null @@ -1 +0,0 @@ -add `from_py_object` pyclass option, to opt-in to the extraction of pyclasses by value (requires `Clone`) diff --git a/newsfragments/5507.changed.md b/newsfragments/5507.changed.md deleted file mode 100644 index 7b050ccda81..00000000000 --- a/newsfragments/5507.changed.md +++ /dev/null @@ -1 +0,0 @@ -Allow converting naive datetime into chrono `DateTime`. diff --git a/newsfragments/5516.packaging.md b/newsfragments/5516.packaging.md deleted file mode 100644 index 52036b8b1f6..00000000000 --- a/newsfragments/5516.packaging.md +++ /dev/null @@ -1 +0,0 @@ -Drop support for PyPy 3.9 and 3.10. diff --git a/newsfragments/5519.packaging.md b/newsfragments/5519.packaging.md deleted file mode 100644 index 9299b9dc326..00000000000 --- a/newsfragments/5519.packaging.md +++ /dev/null @@ -1 +0,0 @@ -Provide a better error message when building an outdated PyO3 for a too-new Python version. diff --git a/newsfragments/5529.fixed.md b/newsfragments/5529.fixed.md deleted file mode 100644 index 453e96ae9c8..00000000000 --- a/newsfragments/5529.fixed.md +++ /dev/null @@ -1 +0,0 @@ -fix `PyPyModule_ExecDef`, `PyPyModule_FromDefAndSpec2` definitions diff --git a/pyo3-build-config/Cargo.toml b/pyo3-build-config/Cargo.toml index 79d7e9b597c..cd7cd52b73f 100644 --- a/pyo3-build-config/Cargo.toml +++ b/pyo3-build-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-build-config" -version = "0.26.0" +version = "0.27.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 d4a20849020..03de6584643 100644 --- a/pyo3-ffi/Cargo.toml +++ b/pyo3-ffi/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-ffi" -version = "0.26.0" +version = "0.27.0" description = "Python-API bindings for the PyO3 ecosystem" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -44,7 +44,7 @@ generate-import-lib = ["pyo3-build-config/generate-import-lib"] paste = "1" [build-dependencies] -pyo3-build-config = { path = "../pyo3-build-config", version = "=0.26.0", features = ["resolve-config"] } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.27.0", features = ["resolve-config"] } [lints] workspace = true diff --git a/pyo3-ffi/README.md b/pyo3-ffi/README.md index be7428b712e..fe18cd0e2bb 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.26.0" +version = "0.27.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.26.0" +pyo3_build_config = "0.27.0" ``` If you need to use conditional compilation based on Python version or how diff --git a/pyo3-introspection/Cargo.toml b/pyo3-introspection/Cargo.toml index 0aa76535cc6..8b79b97622f 100644 --- a/pyo3-introspection/Cargo.toml +++ b/pyo3-introspection/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-introspection" -version = "0.26.0" +version = "0.27.0" description = "Introspect dynamic libraries built with PyO3 to get metadata about the exported Python types" authors = ["PyO3 Project and Contributors "] homepage = "https://github.com/pyo3/pyo3" diff --git a/pyo3-macros-backend/Cargo.toml b/pyo3-macros-backend/Cargo.toml index 02a2c39f238..8c6671772d4 100644 --- a/pyo3-macros-backend/Cargo.toml +++ b/pyo3-macros-backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-macros-backend" -version = "0.26.0" +version = "0.27.0" description = "Code generation for PyO3 package" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -17,7 +17,7 @@ rust-version.workspace = true [dependencies] heck = "0.5" proc-macro2 = { version = "1.0.60", default-features = false } -pyo3-build-config = { path = "../pyo3-build-config", version = "=0.26.0", features = ["resolve-config"] } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.27.0", features = ["resolve-config"] } quote = { version = "1", default-features = false } [dependencies.syn] @@ -27,7 +27,7 @@ default-features = false features = ["derive", "parsing", "printing", "clone-impls", "full", "extra-traits", "visit-mut"] [build-dependencies] -pyo3-build-config = { path = "../pyo3-build-config", version = "=0.26.0" } +pyo3-build-config = { path = "../pyo3-build-config", version = "=0.27.0" } [lints] workspace = true diff --git a/pyo3-macros/Cargo.toml b/pyo3-macros/Cargo.toml index 8b7f19634b0..604a5bbaac7 100644 --- a/pyo3-macros/Cargo.toml +++ b/pyo3-macros/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pyo3-macros" -version = "0.26.0" +version = "0.27.0" description = "Proc macros for PyO3 package" authors = ["PyO3 Project and Contributors "] keywords = ["pyo3", "python", "cpython", "ffi"] @@ -23,7 +23,7 @@ experimental-inspect = ["pyo3-macros-backend/experimental-inspect"] 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.26.0" } +pyo3-macros-backend = { path = "../pyo3-macros-backend", version = "=0.27.0" } [lints] workspace = true diff --git a/pyproject.toml b/pyproject.toml index 9160fda11e1..ccb4c50a280 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ [tool.towncrier] filename = "CHANGELOG.md" -version = "0.26.0" +version = "0.27.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 5c759ac8a20..159d1407f27 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.26.0/class.html#no-generic-parameters +error: #[pyclass] cannot have generic parameters. For an explanation, see https://pyo3.rs/v0.27.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.26.0/class.html#no-lifetime-parameters +error: #[pyclass] cannot have lifetime parameters. For an explanation, see https://pyo3.rs/v0.27.0/class.html#no-lifetime-parameters --> tests/ui/reject_generics.rs:9:27 | 9 | struct ClassWithLifetimes<'a> { From 167416f0aeb9c946eb1500fb8ecbec3f3f379490 Mon Sep 17 00:00:00 2001 From: MusicalNinjaDad <102677655+MusicalNinjaDad@users.noreply.github.com> Date: Sun, 19 Oct 2025 11:15:00 +0200 Subject: [PATCH 922/936] docs: Further updates to Contributing.md re testing, linting etc. (#4115) * add nox details to contributing.md * recommend basic clippy not clippy-all * consistent wording * add details on rust-test skip-full * add quick-CI * segregate PR CI and dev environment descriptions of nox etc. * add check-docs / lychee - this just bit me on another PR * make ci opt-in * remove the quick ci --------- Co-authored-by: David Hewitt --- Contributing.md | 72 ++++++++++++++++++++++++++----------------------- 1 file changed, 39 insertions(+), 33 deletions(-) diff --git a/Contributing.md b/Contributing.md index 2257829a06e..97dda500998 100644 --- a/Contributing.md +++ b/Contributing.md @@ -18,11 +18,43 @@ The following sections also contain specific ideas on where to start contributin ## Setting up a development environment To work and develop PyO3, you need Python & Rust installed on your system. + * We encourage the use of [rustup](https://rustup.rs/) to be able to select and choose specific toolchains based on the project. * [Pyenv](https://github.com/pyenv/pyenv) is also highly recommended for being able to choose a specific Python version. * [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. +### Testing, linting, etc. with nox + +[`Nox`][nox] is used to automate many of our CI tasks and can be used locally to handle verfication tasks as you code. We recommend running these actions via nox to make use of our prefered configuration options. You can install nox into your global python with pip: `pip install nox` or (recommended) with [`pipx`][pipx] `pip install pipx`, `pipx install nox` + +The main nox commands we have implemented are: + +* `nox -s test` will run the full suite of recommended rust and python tests (>10 minutes) +* `nox -s test-rust -- skip-full` will run a short suite of rust tests (2-3 minutes) +* `nox -s ruff` will check python linting and apply standard formatting rules +* `nox -s rustfmt` will check basic rust linting and apply standard formatting rules +* `nox -s rumdl` will check the markdown in the guide +* `nox -s clippy` will run clippy to make recommendations on rust style +* `nox -s bench` will benchmark your rust code +* `nox -s codspeed` will run our suite of rust and python performance tests +* `nox -s coverage` will analyse test coverage and output `coverage.json` (alternatively: `nox -s coverage lcov` outputs `lcov.info`) +* `nox -s check-guide` will use [`lychee`][lychee] to check all the links in the guide and doc comments. + +Use `nox -l` to list the full set of subcommands you can run. + +#### UI Tests + +PyO3 uses [`trybuild`][trybuild] to develop UI tests to capture error messages from the Rust compiler for some of the macro functionality. + +Because there are several feature combinations for these UI tests, when updating them all (e.g. for a new Rust compiler version) it may be helpful to use the `update-ui-tests` nox session: + +```bash +nox -s update-ui-tests +``` + +## Ways to help + ### 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. @@ -88,44 +120,16 @@ Here are a few things to note when you are writing PRs. ### 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"`). +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 (the pipeline will abort early if formatting fails to save resources). +In addition, all warnings in Rust code are disallowed (using `RUSTFLAGS="-D warnings"`). Tests run with all supported Python versions with the latest stable Rust compiler, as well as for Python 3.9 with the minimum supported Rust version. If you are adding a new feature, you should add it to the `full` feature in our *Cargo.toml** so that it is tested in CI. -You can run these checks 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` - -#### Linting Markdown documentation -`nox -s rumdl` - -#### Semver checks -`cargo semver-checks check-release` - -#### Clippy -`nox -s clippy-all` - -#### Tests -`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` - -#### UI Tests - -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: - -```bash -nox -s update-ui-tests -``` +You can run the CI pipeline components yourself with `nox`, see [the testing section above](#testing-linting-etc-with-nox). ### Documenting changes @@ -255,3 +259,5 @@ In the meanwhile, some of our maintainers have personal GitHub sponsorship pages [mdbook-tabs]: https://mdbook-plugins.rustforweb.org/tabs.html [lychee]: https://github.com/lycheeverse/lychee [nox]: https://github.com/theacodes/nox +[pipx]: https://pipx.pypa.io/stable/ +[trybuild]: https://github.com/dtolnay/trybuild From ef67ba813a64d804723bdc1cf72370a9b38df1e0 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Sun, 19 Oct 2025 10:29:39 +0100 Subject: [PATCH 923/936] ci: fix clippy boolean condition (#5533) --- noxfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index df74f8c06b4..cb71e6896dc 100644 --- a/noxfile.py +++ b/noxfile.py @@ -214,7 +214,7 @@ def rumdl(session: nox.Session): @nox.session(name="clippy", venv_backend="none") def clippy(session: nox.Session) -> bool: - if not _clippy(session) and _clippy_additional_workspaces(session): + if not (_clippy(session) and _clippy_additional_workspaces(session)): session.error("one or more jobs failed") From 10df97e7e6c82e789d537e7580b75f87e6ce1c82 Mon Sep 17 00:00:00 2001 From: Alex Gaynor Date: Sun, 19 Oct 2025 12:30:52 -0400 Subject: [PATCH 924/936] fixes #5537 -- silence a clippy warning on rust 1.83 (#5538) --- newsfragments/5538.changed.md | 1 + pyo3-macros-backend/src/pyfunction.rs | 2 ++ 2 files changed, 3 insertions(+) create mode 100644 newsfragments/5538.changed.md diff --git a/newsfragments/5538.changed.md b/newsfragments/5538.changed.md new file mode 100644 index 00000000000..56493d0862f --- /dev/null +++ b/newsfragments/5538.changed.md @@ -0,0 +1 @@ +silence a clippy warning on rust 1.83 diff --git a/pyo3-macros-backend/src/pyfunction.rs b/pyo3-macros-backend/src/pyfunction.rs index a91f064a161..cd088028796 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -443,6 +443,8 @@ pub fn impl_wrap_pyfunction( // inside a function body) #[allow(unknown_lints, non_local_definitions)] impl #name::MakeDef { + // We're using this to initialize a static, so it's fine. + #[allow(clippy::declare_interior_mutable_const)] const _PYO3_DEF: #pyo3_path::impl_::pyfunction::PyFunctionDef = #pyo3_path::impl_::pyfunction::PyFunctionDef::from_method_def(#methoddef); } From 70defdbdebcbb67c5802afc9f0deda794d613525 Mon Sep 17 00:00:00 2001 From: Paul Kehrer Date: Sun, 19 Oct 2025 12:38:04 -0700 Subject: [PATCH 925/936] update MSRV shields.io badge (#5540) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Not sure this badge really has value, but it should be accurate 😄 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 451e97cf69c..c6e6d1cd771 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![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) +[![minimum rustc 1.74](https://img.shields.io/badge/rustc-1.74+-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) From 67ac7ab53bd04906ebece78116814c6b31a8c4c0 Mon Sep 17 00:00:00 2001 From: Peter Holloway Date: Mon, 20 Oct 2025 11:10:01 +0100 Subject: [PATCH 926/936] Expose types::iterator::PySendResult in types module (#5539) * Expose types::iterator::PySendResult in types module PyIterator::send is a public method but the returned values are not usable while `PySendResult` is not public. Making it public allows callers to match on the result and access the wrapped values. * Gate export of PySendResult behind Py_3_10 * Document PySendResult and add newsfragment --- newsfragments/5539.changed.md | 1 + src/types/iterator.rs | 3 +++ src/types/mod.rs | 2 ++ 3 files changed, 6 insertions(+) create mode 100644 newsfragments/5539.changed.md diff --git a/newsfragments/5539.changed.md b/newsfragments/5539.changed.md new file mode 100644 index 00000000000..2ea40282f2d --- /dev/null +++ b/newsfragments/5539.changed.md @@ -0,0 +1 @@ +Expose types::iterator::PySendResult in public API diff --git a/src/types/iterator.rs b/src/types/iterator.rs index 7ee15531a2b..c41d2fb85eb 100644 --- a/src/types/iterator.rs +++ b/src/types/iterator.rs @@ -58,10 +58,13 @@ impl PyIterator { } } +/// Outcomes from sending a value into a python generator #[derive(Debug)] #[cfg(all(not(PyPy), Py_3_10))] pub enum PySendResult<'py> { + /// The generator yielded a new value Next(Bound<'py, PyAny>), + /// The generator completed, returning a (possibly None) final value Return(Bound<'py, PyAny>), } diff --git a/src/types/mod.rs b/src/types/mod.rs index 3dc8d9de6d9..8f99455dd33 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -27,6 +27,8 @@ pub use self::function::PyFunction; #[cfg(Py_3_9)] pub use self::genericalias::PyGenericAlias; pub use self::iterator::PyIterator; +#[cfg(all(not(PyPy), Py_3_10))] +pub use self::iterator::PySendResult; pub use self::list::{PyList, PyListMethods}; pub use self::mapping::{PyMapping, PyMappingMethods}; pub use self::mappingproxy::PyMappingProxy; From 6b8b1c1f3b304808884e146cc20a06ce07b8774e Mon Sep 17 00:00:00 2001 From: Paul Stemmet Date: Mon, 20 Oct 2025 16:35:21 -0300 Subject: [PATCH 927/936] bump supported graalpy to v25.0 (#5542) * github/workflows: bump CI graalpy to v25.0 * docs: update README to reflect graalpy v25 support * newsfragments: add PR * pyo3-build-config: bump MINIMUM_SUPPORTED_VERSION_GRAALPY to (25, 0) --- .github/workflows/ci.yml | 2 +- README.md | 2 +- newsfragments/5542.packaging.md | 1 + pyo3-build-config/src/impl_.rs | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 newsfragments/5542.packaging.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5487052412d..c1cd610e991 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -235,7 +235,7 @@ jobs: "3.14", "3.14t", "pypy3.11", - "graalpy24.2", + "graalpy25.0", ] platform: [ diff --git a/README.md b/README.md index c6e6d1cd771..011032e4cab 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Requires Rust 1.74 or greater. PyO3 supports the following Python distributions: - CPython 3.7 or greater - PyPy 7.3 (Python 3.11+) - - GraalPy 24.2 or greater (Python 3.11+) + - GraalPy 25.0 or greater (Python 3.12+) 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/newsfragments/5542.packaging.md b/newsfragments/5542.packaging.md new file mode 100644 index 00000000000..dd381c6f005 --- /dev/null +++ b/newsfragments/5542.packaging.md @@ -0,0 +1 @@ +Bump supported GraalPy version to 25.0. diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index ea92cd89d40..78a8b36acac 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -35,7 +35,7 @@ pub(crate) const MINIMUM_SUPPORTED_VERSION: PythonVersion = PythonVersion { majo /// GraalPy may implement the same CPython version over multiple releases. const MINIMUM_SUPPORTED_VERSION_GRAALPY: PythonVersion = PythonVersion { - major: 24, + major: 25, minor: 0, }; From e3e24cc9e1e86a4ebf1e9d659b09de4ebf1c0fd3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Oct 2025 07:30:34 +0100 Subject: [PATCH 928/936] build(deps): bump actions/setup-node from 5 to 6 (#5544) Bumps [actions/setup-node](https://github.com/actions/setup-node) from 5 to 6. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/setup-node dependency-version: '6' 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/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c1cd610e991..2270eb1432f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -462,7 +462,7 @@ jobs: with: targets: wasm32-unknown-emscripten components: rust-src - - uses: actions/setup-node@v5 + - uses: actions/setup-node@v6 with: node-version: 18 - run: python -m pip install --upgrade pip && pip install nox[uv] From da5777a7b81a60ff0e3374a32a660efe4cbcb719 Mon Sep 17 00:00:00 2001 From: Thomas Tanon Date: Tue, 21 Oct 2025 17:00:12 +0200 Subject: [PATCH 929/936] Use assert_eq! and assert_ne! where relevant (#5547) --- pyo3-build-config/src/impl_.rs | 2 +- src/conversions/hashbrown.rs | 2 +- src/conversions/indexmap.rs | 2 +- src/conversions/std/array.rs | 6 +++--- src/conversions/std/map.rs | 8 ++++---- src/conversions/std/vec.rs | 6 +++--- src/internal/state.rs | 2 +- src/sync.rs | 4 ++-- src/types/list.rs | 2 +- src/types/mutex.rs | 2 +- src/version.rs | 10 +++++----- tests/test_class_new.rs | 12 ++++++------ tests/test_datetime.rs | 8 ++++---- 13 files changed, 33 insertions(+), 33 deletions(-) diff --git a/pyo3-build-config/src/impl_.rs b/pyo3-build-config/src/impl_.rs index 78a8b36acac..cca9393d873 100644 --- a/pyo3-build-config/src/impl_.rs +++ b/pyo3-build-config/src/impl_.rs @@ -1998,7 +1998,7 @@ fn escape(bytes: &[u8]) -> String { } fn unescape(escaped: &str) -> Vec { - assert!(escaped.len() % 2 == 0, "invalid hex encoding"); + assert_eq!(escaped.len() % 2, 0, "invalid hex encoding"); let mut bytes = Vec::with_capacity(escaped.len() / 2); diff --git a/src/conversions/hashbrown.rs b/src/conversions/hashbrown.rs index 85bc350999b..48735a9734c 100644 --- a/src/conversions/hashbrown.rs +++ b/src/conversions/hashbrown.rs @@ -158,7 +158,7 @@ mod tests { let py_map = (&map).into_pyobject(py).unwrap(); - assert!(py_map.len() == 1); + assert_eq!(py_map.len(), 1); assert!( py_map .get_item(1) diff --git a/src/conversions/indexmap.rs b/src/conversions/indexmap.rs index 89ef8172961..cccd3c7a49a 100644 --- a/src/conversions/indexmap.rs +++ b/src/conversions/indexmap.rs @@ -165,7 +165,7 @@ mod test_indexmap { let py_map = (&map).into_pyobject(py).unwrap(); - assert!(py_map.len() == 1); + assert_eq!(py_map.len(), 1); assert!( py_map .get_item(1) diff --git a/src/conversions/std/array.rs b/src/conversions/std/array.rs index b66968cf305..5df09664df8 100644 --- a/src/conversions/std/array.rs +++ b/src/conversions/std/array.rs @@ -183,7 +183,7 @@ mod tests { .unwrap() .extract() .unwrap(); - assert!(&v == b"abcabcabcabcabcabcabcabcabcabcabc"); + assert_eq!(&v, b"abcabcabcabcabcabcabcabcabcabcabc"); }) } @@ -213,7 +213,7 @@ mod tests { .unwrap() .extract() .unwrap(); - assert!(&v == b"abcabcabcabcabcabcabcabcabcabcabc"); + assert_eq!(&v, b"abcabcabcabcabcabcabcabcabcabcabc"); }) } @@ -225,7 +225,7 @@ mod tests { .unwrap() .extract() .unwrap(); - assert!(&v == b"abc"); + assert_eq!(&v, b"abc"); }); } #[test] diff --git a/src/conversions/std/map.rs b/src/conversions/std/map.rs index 676351abf4a..ce6c50adb73 100644 --- a/src/conversions/std/map.rs +++ b/src/conversions/std/map.rs @@ -171,7 +171,7 @@ mod tests { let py_map = (&map).into_pyobject(py).unwrap(); - assert!(py_map.len() == 1); + assert_eq!(py_map.len(), 1); assert!( py_map .get_item(1) @@ -193,7 +193,7 @@ mod tests { let py_map = (&map).into_pyobject(py).unwrap(); - assert!(py_map.len() == 1); + assert_eq!(py_map.len(), 1); assert!( py_map .get_item(1) @@ -215,7 +215,7 @@ mod tests { let py_map = map.into_pyobject(py).unwrap(); - assert!(py_map.len() == 1); + assert_eq!(py_map.len(), 1); assert!( py_map .get_item(1) @@ -236,7 +236,7 @@ mod tests { let py_map = map.into_pyobject(py).unwrap(); - assert!(py_map.len() == 1); + assert_eq!(py_map.len(), 1); assert!( py_map .get_item(1) diff --git a/src/conversions/std/vec.rs b/src/conversions/std/vec.rs index f17a512290e..bd3a0e65714 100644 --- a/src/conversions/std/vec.rs +++ b/src/conversions/std/vec.rs @@ -165,7 +165,7 @@ mod tests { .unwrap() .extract() .unwrap(); - assert!(v == [1, 2]); + assert_eq!(v, [1, 2]); }); } @@ -177,7 +177,7 @@ mod tests { .unwrap() .extract() .unwrap(); - assert!(v == [1, 2, 3, 4]); + assert_eq!(v, [1, 2, 3, 4]); }); } @@ -189,7 +189,7 @@ mod tests { .unwrap() .extract() .unwrap(); - assert!(v == b"abc"); + assert_eq!(v, b"abc"); }); } } diff --git a/src/internal/state.rs b/src/internal/state.rs index 899c37bd13e..ba92e512800 100644 --- a/src/internal/state.rs +++ b/src/internal/state.rs @@ -504,7 +504,7 @@ mod tests { Python::attach(|py| { // Make a simple object with 1 reference let obj = get_object(py); - assert!(obj.get_refcnt(py) == 1); + assert_eq!(obj.get_refcnt(py), 1); // Cloning the object when detached should panic py.detach(|| obj.clone()); }); diff --git a/src/sync.rs b/src/sync.rs index a3c527aa62f..40d77d712a9 100644 --- a/src/sync.rs +++ b/src/sync.rs @@ -1467,7 +1467,7 @@ mod tests { Err(poisoned) => poisoned.into_inner(), } }); - assert!(*guard == 42); + assert_eq!(*guard, 42); } #[cfg(feature = "macros")] @@ -1699,7 +1699,7 @@ mod tests { Python::attach(|py| { // recover from the poisoning let guard = rwlock.write_py_attached(py).unwrap_err().into_inner(); - assert!(*guard == 42); + assert_eq!(*guard, 42); }); } } diff --git a/src/types/list.rs b/src/types/list.rs index ad3e0741c88..55aa50d39e4 100644 --- a/src/types/list.rs +++ b/src/types/list.rs @@ -1222,7 +1222,7 @@ mod tests { -5 }); assert_eq!(sum, -5); - assert!(list.len() == 0); + assert_eq!(list.len(), 0); }); } diff --git a/src/types/mutex.rs b/src/types/mutex.rs index d6b49be5038..fe9f9e436f3 100644 --- a/src/types/mutex.rs +++ b/src/types/mutex.rs @@ -452,7 +452,7 @@ mod tests { .join() }); mutex.clear_poison(); - assert!(*mutex.lock().unwrap() == 0); + assert_eq!(*mutex.lock().unwrap(), 0); } #[test] diff --git a/src/version.rs b/src/version.rs index 6760bd8d337..48d8b5d36a8 100644 --- a/src/version.rs +++ b/src/version.rs @@ -128,12 +128,12 @@ mod test { fn test_python_version_info_parse() { assert!(PythonVersionInfo::from_str("3.5.0a1").unwrap() >= (3, 5, 0)); assert!(PythonVersionInfo::from_str("3.5+").unwrap() >= (3, 5, 0)); - assert!(PythonVersionInfo::from_str("3.5+").unwrap() == (3, 5, 0)); - assert!(PythonVersionInfo::from_str("3.5+").unwrap() != (3, 5, 1)); + assert_eq!(PythonVersionInfo::from_str("3.5+").unwrap(), (3, 5, 0)); + assert_ne!(PythonVersionInfo::from_str("3.5+").unwrap(), (3, 5, 1)); assert!(PythonVersionInfo::from_str("3.5.2a1+").unwrap() < (3, 5, 3)); - assert!(PythonVersionInfo::from_str("3.5.2a1+").unwrap() == (3, 5, 2)); - assert!(PythonVersionInfo::from_str("3.5.2a1+").unwrap() == (3, 5)); - assert!(PythonVersionInfo::from_str("3.5+").unwrap() == (3, 5)); + assert_eq!(PythonVersionInfo::from_str("3.5.2a1+").unwrap(), (3, 5, 2)); + assert_eq!(PythonVersionInfo::from_str("3.5.2a1+").unwrap(), (3, 5)); + assert_eq!(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)); diff --git a/tests/test_class_new.rs b/tests/test_class_new.rs index 7f56ebabc23..6a35e681500 100644 --- a/tests/test_class_new.rs +++ b/tests/test_class_new.rs @@ -244,12 +244,12 @@ fn test_new_existing() { let obj5 = typeobj.call1((2,)).unwrap(); let obj6 = typeobj.call1((2,)).unwrap(); - assert!(obj1.getattr("num").unwrap().extract::().unwrap() == 0); - assert!(obj2.getattr("num").unwrap().extract::().unwrap() == 0); - assert!(obj3.getattr("num").unwrap().extract::().unwrap() == 1); - assert!(obj4.getattr("num").unwrap().extract::().unwrap() == 1); - assert!(obj5.getattr("num").unwrap().extract::().unwrap() == 2); - assert!(obj6.getattr("num").unwrap().extract::().unwrap() == 2); + assert_eq!(obj1.getattr("num").unwrap().extract::().unwrap(), 0); + assert_eq!(obj2.getattr("num").unwrap().extract::().unwrap(), 0); + assert_eq!(obj3.getattr("num").unwrap().extract::().unwrap(), 1); + assert_eq!(obj4.getattr("num").unwrap().extract::().unwrap(), 1); + assert_eq!(obj5.getattr("num").unwrap().extract::().unwrap(), 2); + assert_eq!(obj6.getattr("num").unwrap().extract::().unwrap(), 2); assert!(obj1.is(&obj2)); assert!(obj3.is(&obj4)); diff --git a/tests/test_datetime.rs b/tests/test_datetime.rs index b24b94397d6..d045d43dfeb 100644 --- a/tests/test_datetime.rs +++ b/tests/test_datetime.rs @@ -52,8 +52,8 @@ macro_rules! assert_check_exact { ($check_func:ident, $check_func_exact:ident, $obj: expr) => { unsafe { use pyo3::ffi::*; - assert!($check_func(($obj).as_ptr()) != 0); - assert!($check_func_exact(($obj).as_ptr()) != 0); + assert_ne!($check_func(($obj).as_ptr()), 0); + assert_ne!($check_func_exact(($obj).as_ptr()), 0); } }; } @@ -62,8 +62,8 @@ macro_rules! assert_check_only { ($check_func:ident, $check_func_exact:ident, $obj: expr) => { unsafe { use pyo3::ffi::*; - assert!($check_func(($obj).as_ptr()) != 0); - assert!($check_func_exact(($obj).as_ptr()) == 0); + assert_ne!($check_func(($obj).as_ptr()), 0); + assert_eq!($check_func_exact(($obj).as_ptr()), 0); } }; } From 114a45e36cb30f7b60cfdc2ae7aeb99fe945fa9e Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Tue, 21 Oct 2025 17:39:31 +0100 Subject: [PATCH 930/936] bump MSRV to 1.83 (#5531) * bump MSRV to 1.83 * drop `return_position_impl_trait_in_traits` conditional code * drop `c_str_lit` cfg * drop `diagnostic_namespace` cfg * drop `io_error_more` cfg * drop `mut_ref_in_const_fn` cfg * newsfragment * clippy * update msrv badge * bump `quote` to 1.0.37 --- Cargo.toml | 2 +- README.md | 4 +- examples/Cargo.toml | 2 +- examples/decorator/Cargo.toml | 2 +- examples/getitem/Cargo.toml | 2 +- examples/maturin-starter/Cargo.toml | 2 +- examples/plugin/Cargo.toml | 2 +- examples/setuptools-rust-starter/Cargo.toml | 2 +- examples/word-count/Cargo.toml | 2 +- guide/src/features.md | 4 +- guide/src/getting-started.md | 2 +- newsfragments/5531.packaging.md | 2 + pyo3-build-config/src/lib.rs | 10 +---- pyo3-ffi/build.rs | 3 +- pyo3-ffi/examples/sequential/Cargo.toml | 2 +- pyo3-ffi/examples/string-sum/Cargo.toml | 2 +- pyo3-ffi/src/lib.rs | 1 + pyo3-macros-backend/Cargo.toml | 2 +- pyo3-macros-backend/src/konst.rs | 6 +-- pyo3-macros-backend/src/method.rs | 11 +++--- pyo3-macros-backend/src/module.rs | 9 +++-- pyo3-macros-backend/src/pyclass.rs | 12 +++--- pyo3-macros-backend/src/pyfunction.rs | 6 +-- pyo3-macros-backend/src/pyimpl.rs | 2 +- pyo3-macros-backend/src/pymethod.rs | 15 ++++---- pyo3-macros-backend/src/utils.rs | 42 +-------------------- pytests/Cargo.toml | 2 +- src/buffer.rs | 6 +-- src/call.rs | 13 +++---- src/conversion.rs | 33 +++------------- src/conversions/smallvec.rs | 1 - src/conversions/std/array.rs | 18 +-------- src/conversions/std/map.rs | 2 - src/conversions/std/num.rs | 22 ----------- src/conversions/std/set.rs | 1 - src/conversions/std/slice.rs | 1 - src/conversions/std/vec.rs | 5 +-- src/err/impls.rs | 18 ++------- src/impl_/concat.rs | 22 ----------- src/impl_/extract_argument.rs | 11 ++---- src/impl_/pyclass.rs | 15 +++----- src/impl_/pyclass/assertions.rs | 13 +++---- src/pyclass/create_type_object.rs | 7 +--- src/pyclass/gc.rs | 25 ++---------- src/types/mod.rs | 7 +--- src/types/tuple.rs | 4 +- 46 files changed, 97 insertions(+), 280 deletions(-) create mode 100644 newsfragments/5531.packaging.md diff --git a/Cargo.toml b/Cargo.toml index fa49c97ea20..d278cd1b57e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -188,7 +188,7 @@ members = [ "pytests", "examples", ] -package.rust-version = "1.74" +package.rust-version = "1.83" [package.metadata.docs.rs] no-default-features = true diff --git a/README.md b/README.md index 011032e4cab..9704b67d8c7 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![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.74](https://img.shields.io/badge/rustc-1.74+-blue?logo=rust)](https://rust-lang.github.io/rfcs/2495-min-rust-version.html) +[![minimum rustc 1.83](https://img.shields.io/badge/rustc-1.83+-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) @@ -16,7 +16,7 @@ ## Usage -Requires Rust 1.74 or greater. +Requires Rust 1.83 or greater. PyO3 supports the following Python distributions: - CPython 3.7 or greater diff --git a/examples/Cargo.toml b/examples/Cargo.toml index b17fd4887ad..68aa8be7a76 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -3,7 +3,7 @@ name = "pyo3-examples" version = "0.0.0" publish = false edition = "2021" -rust-version = "1.74" +rust-version = "1.83" [dev-dependencies] pyo3 = { path = "..", features = ["auto-initialize", "extension-module"] } diff --git a/examples/decorator/Cargo.toml b/examples/decorator/Cargo.toml index 1c3586b884b..ef7e5dc2e1c 100644 --- a/examples/decorator/Cargo.toml +++ b/examples/decorator/Cargo.toml @@ -2,7 +2,7 @@ name = "decorator" version = "0.1.0" edition = "2021" -rust-version = "1.74" +rust-version = "1.83" [lib] name = "decorator" diff --git a/examples/getitem/Cargo.toml b/examples/getitem/Cargo.toml index fa35cf10ffc..c31047eba24 100644 --- a/examples/getitem/Cargo.toml +++ b/examples/getitem/Cargo.toml @@ -2,7 +2,7 @@ name = "getitem" version = "0.1.0" edition = "2021" -rust-version = "1.74" +rust-version = "1.83" [lib] name = "getitem" diff --git a/examples/maturin-starter/Cargo.toml b/examples/maturin-starter/Cargo.toml index f65b0ed3ba7..53e464bbffe 100644 --- a/examples/maturin-starter/Cargo.toml +++ b/examples/maturin-starter/Cargo.toml @@ -2,7 +2,7 @@ name = "maturin-starter" version = "0.1.0" edition = "2021" -rust-version = "1.74" +rust-version = "1.83" [lib] name = "maturin_starter" diff --git a/examples/plugin/Cargo.toml b/examples/plugin/Cargo.toml index 4cc6b003ed5..ab342767f2d 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.74" +rust-version = "1.83" [dependencies] pyo3 = { path = "../../", features = ["macros"] } diff --git a/examples/setuptools-rust-starter/Cargo.toml b/examples/setuptools-rust-starter/Cargo.toml index 6132cf878e2..e7ae56a7612 100644 --- a/examples/setuptools-rust-starter/Cargo.toml +++ b/examples/setuptools-rust-starter/Cargo.toml @@ -2,7 +2,7 @@ name = "setuptools-rust-starter" version = "0.1.0" edition = "2021" -rust-version = "1.74" +rust-version = "1.83" [lib] name = "setuptools_rust_starter" diff --git a/examples/word-count/Cargo.toml b/examples/word-count/Cargo.toml index 514bcfe06e2..816e3dd9a34 100644 --- a/examples/word-count/Cargo.toml +++ b/examples/word-count/Cargo.toml @@ -2,7 +2,7 @@ name = "word-count" version = "0.1.0" edition = "2021" -rust-version = "1.74" +rust-version = "1.83" [lib] name = "word_count" diff --git a/guide/src/features.md b/guide/src/features.md index 124cd406b69..3d9314a954d 100644 --- a/guide/src/features.md +++ b/guide/src/features.md @@ -172,7 +172,7 @@ Adds a dependency on [indexmap](https://docs.rs/indexmap) and enables conversion ### `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: +Adds a dependency on [jiff@0.2](https://docs.rs/jiff/0.2). 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) @@ -217,7 +217,7 @@ Adds a dependency on [rust_decimal](https://docs.rs/rust_decimal) and enables co ### `time` -Adds a dependency on [time](https://docs.rs/time) and requires MSRV 1.67.1. Enables conversions between [time](https://docs.rs/time)'s types and Python: +Adds a dependency on [time](https://docs.rs/time). Enables conversions between [time](https://docs.rs/time)'s types and Python: - [Date](https://docs.rs/time/0.3.38/time/struct.Date.html) -> [`PyDate`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyDate.html) - [Time](https://docs.rs/time/0.3.38/time/struct.Time.html) -> [`PyTime`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyTime.html) diff --git a/guide/src/getting-started.md b/guide/src/getting-started.md index b7d3df474c5..f3a548eba3a 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.74. +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.83. If you can run `rustc --version` and the version is new enough you're good to go! diff --git a/newsfragments/5531.packaging.md b/newsfragments/5531.packaging.md new file mode 100644 index 00000000000..8bf82fe545b --- /dev/null +++ b/newsfragments/5531.packaging.md @@ -0,0 +1,2 @@ +Bump MSRV to Rust 1.83. +Bump minimum supported `quote` version to 1.0.37. diff --git a/pyo3-build-config/src/lib.rs b/pyo3-build-config/src/lib.rs index fc5ba27e3ef..267d3eb6b8c 100644 --- a/pyo3-build-config/src/lib.rs +++ b/pyo3-build-config/src/lib.rs @@ -142,8 +142,7 @@ fn config_from_pyo3_config_file_env() -> Option { const CONFIG_FILE: &str = include_str!(concat!(env!("OUT_DIR"), "/pyo3-build-config-file.txt")); // CONFIG_FILE is generated in build.rs, so its content can vary - // TODO: `unknown_lints` allow not needed on MSRV 1.79+ - #[allow(unknown_lints, clippy::const_is_empty)] + #[allow(clippy::const_is_empty)] if !CONFIG_FILE.is_empty() { let config = InterpreterConfig::from_reader(Cursor::new(CONFIG_FILE)) .expect("contents of CONFIG_FILE should always be valid (generated by pyo3-build-config's build.rs)"); @@ -195,13 +194,6 @@ fn print_feature_cfg(minor_version_required: u32, cfg: &str) { /// so this function is unstable. #[doc(hidden)] pub fn print_feature_cfgs() { - print_feature_cfg(75, "return_position_impl_trait_in_traits"); - 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 - print_feature_cfg(79, "diagnostic_namespace"); - print_feature_cfg(83, "io_error_more"); - print_feature_cfg(83, "mut_ref_in_const_fn"); print_feature_cfg(85, "fn_ptr_eq"); print_feature_cfg(86, "from_bytes_with_nul_error"); } diff --git a/pyo3-ffi/build.rs b/pyo3-ffi/build.rs index 72ef310dc09..3e2354f2d38 100644 --- a/pyo3-ffi/build.rs +++ b/pyo3-ffi/build.rs @@ -63,8 +63,7 @@ fn ensure_python_version(interpreter_config: &InterpreterConfig) -> Result<()> { return Err(error.finish().into()); } - if !env_var("PYO3_USE_ABI3_FORWARD_COMPATIBILITY") - .is_some_and(|os_str| os_str == "1") + if env_var("PYO3_USE_ABI3_FORWARD_COMPATIBILITY").is_none_or(|os_str| os_str != "1") { error.add_help("set PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1 to suppress this check and build anyway using the stable ABI"); return Err(error.finish().into()); diff --git a/pyo3-ffi/examples/sequential/Cargo.toml b/pyo3-ffi/examples/sequential/Cargo.toml index 950322ec0b7..21e87c09496 100644 --- a/pyo3-ffi/examples/sequential/Cargo.toml +++ b/pyo3-ffi/examples/sequential/Cargo.toml @@ -2,7 +2,7 @@ name = "sequential" version = "0.1.0" edition = "2021" -rust-version = "1.74" +rust-version = "1.83" [lib] name = "sequential" diff --git a/pyo3-ffi/examples/string-sum/Cargo.toml b/pyo3-ffi/examples/string-sum/Cargo.toml index 29724e14c66..173521eaab5 100644 --- a/pyo3-ffi/examples/string-sum/Cargo.toml +++ b/pyo3-ffi/examples/string-sum/Cargo.toml @@ -2,7 +2,7 @@ name = "string_sum" version = "0.1.0" edition = "2021" -rust-version = "1.74" +rust-version = "1.83" [lib] name = "string_sum" diff --git a/pyo3-ffi/src/lib.rs b/pyo3-ffi/src/lib.rs index 8adadb1c0c5..2383873e2a1 100644 --- a/pyo3-ffi/src/lib.rs +++ b/pyo3-ffi/src/lib.rs @@ -366,6 +366,7 @@ macro_rules! opaque_struct { /// ``` #[macro_export] macro_rules! c_str { + // TODO: deprecate this now MSRV is above 1.77 ($s:expr) => { $crate::_cstr_from_utf8_with_nul_checked(concat!($s, "\0")) }; diff --git a/pyo3-macros-backend/Cargo.toml b/pyo3-macros-backend/Cargo.toml index 8c6671772d4..eadee73d0c6 100644 --- a/pyo3-macros-backend/Cargo.toml +++ b/pyo3-macros-backend/Cargo.toml @@ -18,7 +18,7 @@ rust-version.workspace = true heck = "0.5" proc-macro2 = { version = "1.0.60", default-features = false } pyo3-build-config = { path = "../pyo3-build-config", version = "=0.27.0", features = ["resolve-config"] } -quote = { version = "1", default-features = false } +quote = { version = "1.0.37", default-features = false } [dependencies.syn] # 2.0.59 for `LitCStr` diff --git a/pyo3-macros-backend/src/konst.rs b/pyo3-macros-backend/src/konst.rs index ae88f785249..e56afd0516d 100644 --- a/pyo3-macros-backend/src/konst.rs +++ b/pyo3-macros-backend/src/konst.rs @@ -2,8 +2,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 proc_macro2::{Ident, Span}; +use syn::LitCStr; use syn::{ ext::IdentExt, parse::{Parse, ParseStream}, @@ -26,9 +26,9 @@ impl ConstSpec { } /// Null-terminated Python name - pub fn null_terminated_python_name(&self, ctx: &Ctx) -> LitCStr { + pub fn null_terminated_python_name(&self) -> LitCStr { let name = self.python_name().to_string(); - LitCStr::new(CString::new(name).unwrap(), Span::call_site(), ctx) + LitCStr::new(&CString::new(name).unwrap(), Span::call_site()) } } diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index 8d6f7868524..5484b8d5d0c 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -4,11 +4,12 @@ use std::fmt::Display; use proc_macro2::{Span, TokenStream}; use quote::{format_ident, quote, quote_spanned, ToTokens}; +use syn::LitCStr; use syn::{ext::IdentExt, spanned::Spanned, Ident, Result}; use crate::pyfunction::{PyFunctionWarning, WarningFactory}; use crate::pyversions::is_abi3_before; -use crate::utils::{expr_to_python, Ctx, LitCStr}; +use crate::utils::{expr_to_python, Ctx}; use crate::{ attributes::{FromPyWithAttribute, TextSignatureAttribute, TextSignatureAttributeValue}, params::{impl_arg_params, Holders}, @@ -407,7 +408,7 @@ impl SelfType { #bound_ref.cast::<#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+) + #[allow(clippy::unnecessary_fallible_conversions)] // In case slf is Py |bound| ::std::convert::TryFrom::try_from(bound).map_err(::std::convert::Into::into) ) @@ -550,10 +551,10 @@ impl<'a> FnSpec<'a> { }) } - pub fn null_terminated_python_name(&self, ctx: &Ctx) -> LitCStr { + pub fn null_terminated_python_name(&self) -> LitCStr { let name = self.python_name.to_string(); let name = CString::new(name).unwrap(); - LitCStr::new(name, self.python_name.span(), ctx) + LitCStr::new(&name, self.python_name.span()) } fn parse_fn_type( @@ -901,7 +902,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(ctx); + let python_name = self.null_terminated_python_name(); match self.convention { CallingConvention::Noargs => quote! { #pyo3_path::impl_::pymethods::PyMethodDef::noargs( diff --git a/pyo3-macros-backend/src/module.rs b/pyo3-macros-backend/src/module.rs index dc504949e2f..831bd677b92 100644 --- a/pyo3-macros-backend/src/module.rs +++ b/pyo3-macros-backend/src/module.rs @@ -15,11 +15,12 @@ use crate::{ get_doc, pyclass::PyClassPyO3Option, pyfunction::{impl_wrap_pyfunction, PyFunctionOptions}, - utils::{has_attribute, has_attribute_with_namespace, Ctx, IdentOrStr, LitCStr}, + utils::{has_attribute, has_attribute_with_namespace, Ctx, IdentOrStr}, }; use proc_macro2::{Span, TokenStream}; use quote::quote; use std::ffi::CString; +use syn::LitCStr; use syn::{ ext::IdentExt, parse::{Parse, ParseStream}, @@ -404,7 +405,7 @@ pub fn pymodule_module_impl( ctx, module_def, options.submodule.is_some(), - options.gil_used.map_or(true, |op| op.value.value), + options.gil_used.is_none_or(|op| op.value.value), ); let module_consts_names = module_consts.iter().map(|i| i.unraw().to_string()); @@ -460,7 +461,7 @@ pub fn pymodule_function_impl( ctx, quote! { MakeDef::make_def() }, false, - options.gil_used.map_or(true, |op| op.value.value), + options.gil_used.is_none_or(|op| op.value.value), ); #[cfg(feature = "experimental-inspect")] @@ -523,7 +524,7 @@ fn module_initialization( 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); + let pyo3_name = LitCStr::new(&CString::new(name).unwrap(), Span::call_site()); let mut result = quote! { #[doc(hidden)] diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 78527b147eb..d64cbd67726 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -32,7 +32,7 @@ use crate::pymethod::{ __RICHCMP__, __STR__, }; use crate::pyversions::{is_abi3_before, is_py_before}; -use crate::utils::{self, apply_renaming_rule, Ctx, LitCStr, PythonDoc}; +use crate::utils::{self, apply_renaming_rule, Ctx, PythonDoc}; use crate::PyFunctionOptions; /// If the class is derived from a Rust `struct` or `enum`. @@ -1661,7 +1661,7 @@ pub fn gen_complex_enum_variant_attr( let Ctx { pyo3_path, .. } = ctx; let member = &spec.rust_ident; let wrapper_ident = format_ident!("__pymethod_variant_cls_{}__", member); - let python_name = spec.null_terminated_python_name(ctx); + let python_name = spec.null_terminated_python_name(); let variant_cls = format_ident!("{}_{}", cls, member); let associated_method = quote! { @@ -2376,10 +2376,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( - LitCStr::empty(ctx).to_token_stream(), - PythonDoc::to_token_stream, - ); + let doc = self + .doc + .as_ref() + .map_or(c"".to_token_stream(), 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 cd088028796..1333e6d2bcd 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -2,7 +2,7 @@ use crate::attributes::KeywordAttribute; use crate::combine_errors::CombineErrors; #[cfg(feature = "experimental-inspect")] use crate::introspection::{function_introspection_code, introspection_id_const}; -use crate::utils::{Ctx, LitCStr}; +use crate::utils::Ctx; use crate::{ attributes::{ self, get_pyo3_options, take_attributes, take_pyo3_options, CrateAttribute, @@ -17,6 +17,7 @@ use std::cmp::PartialEq; use std::ffi::CString; use syn::parse::{Parse, ParseStream}; use syn::punctuated::Punctuated; +use syn::LitCStr; use syn::{ext::IdentExt, spanned::Spanned, LitStr, Path, Result, Token}; mod signature; @@ -133,9 +134,8 @@ impl WarningFactory for PyFunctionWarning { fn build_py_warning(&self, ctx: &Ctx) -> TokenStream { let message = &self.message.value(); let c_message = LitCStr::new( - CString::new(message.clone()).unwrap(), + &CString::new(message.clone()).unwrap(), Spanned::span(&message), - ctx, ); let pyo3_path = &ctx.pyo3_path; let category = match &self.category { diff --git a/pyo3-macros-backend/src/pyimpl.rs b/pyo3-macros-backend/src/pyimpl.rs index 282a6f35d7b..a7db6f3a88e 100644 --- a/pyo3-macros-backend/src/pyimpl.rs +++ b/pyo3-macros-backend/src/pyimpl.rs @@ -232,7 +232,7 @@ pub fn impl_methods( 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 python_name = spec.null_terminated_python_name(ctx); + let python_name = spec.null_terminated_python_name(); 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 628dd19bf25..cfd7d4e083b 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -7,8 +7,8 @@ use crate::introspection::unique_element_id; use crate::method::{CallingConvention, ExtractErrorMode, PyArg}; use crate::params::{impl_regular_arg_param, Holders}; use crate::pyfunction::WarningFactory; +use crate::utils::Ctx; use crate::utils::PythonDoc; -use crate::utils::{Ctx, LitCStr}; use crate::{ method::{FnArg, FnSpec, FnType, SelfType}, pyfunction::PyFunctionOptions, @@ -16,6 +16,7 @@ use crate::{ use crate::{quotes, utils}; use proc_macro2::{Span, TokenStream}; use quote::{format_ident, quote, quote_spanned, ToTokens}; +use syn::LitCStr; use syn::{ext::IdentExt, spanned::Spanned, Field, Ident, Result}; /// Generated code for a single pymethod item. @@ -587,7 +588,7 @@ pub(crate) fn impl_py_class_attribute( }; let wrapper_ident = format_ident!("__pymethod_{}__", name); - let python_name = spec.null_terminated_python_name(ctx); + let python_name = spec.null_terminated_python_name(); let body = quotes::ok_wrap(fncall, ctx); let associated_method = quote! { @@ -651,7 +652,7 @@ pub fn impl_py_setter_def( ctx: &Ctx, ) -> Result { let Ctx { pyo3_path, .. } = ctx; - let python_name = property_type.null_terminated_python_name(ctx)?; + let python_name = property_type.null_terminated_python_name()?; let doc = property_type.doc(ctx)?; let mut holders = Holders::new(); let setter_impl = match property_type { @@ -835,7 +836,7 @@ pub fn impl_py_getter_def( ctx: &Ctx, ) -> Result { let Ctx { pyo3_path, .. } = ctx; - let python_name = property_type.null_terminated_python_name(ctx)?; + let python_name = property_type.null_terminated_python_name()?; let doc = property_type.doc(ctx)?; let mut cfg_attrs = TokenStream::new(); @@ -971,7 +972,7 @@ pub enum PropertyType<'a> { } impl PropertyType<'_> { - fn null_terminated_python_name(&self, ctx: &Ctx) -> Result { + fn null_terminated_python_name(&self) -> Result { match self { PropertyType::Descriptor { field, @@ -981,9 +982,9 @@ impl PropertyType<'_> { } => { let name = field_python_name(field, *python_name, *renaming_rule)?; let name = CString::new(name).unwrap(); - Ok(LitCStr::new(name, field.span(), ctx)) + Ok(LitCStr::new(&name, field.span())) } - PropertyType::Function { spec, .. } => Ok(spec.null_terminated_python_name(ctx)), + PropertyType::Function { spec, .. } => Ok(spec.null_terminated_python_name()), } } diff --git a/pyo3-macros-backend/src/utils.rs b/pyo3-macros-backend/src/utils.rs index 3e553acfdbd..81228cd8b69 100644 --- a/pyo3-macros-backend/src/utils.rs +++ b/pyo3-macros-backend/src/utils.rs @@ -3,6 +3,7 @@ use proc_macro2::{Span, TokenStream}; use quote::{quote, quote_spanned, ToTokens}; use std::ffi::CString; use syn::spanned::Spanned; +use syn::LitCStr; use syn::{punctuated::Punctuated, Token}; /// Macro inspired by `anyhow::anyhow!` to create a compiler error with the given span. @@ -68,44 +69,6 @@ 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 @@ -208,9 +171,8 @@ pub fn get_doc( ) })?; Ok(PythonDoc(PythonDocKind::LitCStr(LitCStr::new( - docs, + &docs, current_part_span.unwrap_or(Span::call_site()), - ctx, )))) } } diff --git a/pytests/Cargo.toml b/pytests/Cargo.toml index 0b8dfe5d5ba..0d9c74d21b7 100644 --- a/pytests/Cargo.toml +++ b/pytests/Cargo.toml @@ -5,7 +5,7 @@ version = "0.1.0" description = "Python-based tests for PyO3" edition = "2021" publish = false -rust-version = "1.74" +rust-version = "1.83" [dependencies] pyo3 = { path = "../", features = ["experimental-inspect"] } diff --git a/src/buffer.rs b/src/buffer.rs index 78a92f712b7..f83f9649e13 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -207,8 +207,7 @@ 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 our MSRV is 1.82 - let mut buf = Box::new(mem::MaybeUninit::::uninit()); + let mut buf = Box::::new_uninit(); let buf: Box = { err::error_on_minusone(obj.py(), unsafe { ffi::PyObject_GetBuffer( @@ -219,8 +218,7 @@ impl PyBuffer { ) })?; // Safety: buf is initialized by PyObject_GetBuffer. - // TODO: use nightly API Box::assume_init() once our MSRV is 1.82 - unsafe { mem::transmute(buf) } + unsafe { buf.assume_init() } }; // Create PyBuffer immediately so that if validation checks fail, the PyBuffer::drop code // will call PyBuffer_Release (thus avoiding any leaks). diff --git a/src/call.rs b/src/call.rs index 5e36d1bd4a9..51e67246ef6 100644 --- a/src/call.rs +++ b/src/call.rs @@ -42,14 +42,11 @@ pub(crate) mod private { /// [`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, `(,)`" - ) +#[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)] diff --git a/src/conversion.rs b/src/conversion.rs index 588a9c75e9f..7ff34b5fb5f 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -10,7 +10,6 @@ use crate::{ Borrowed, Bound, BoundObject, Py, PyAny, PyClass, PyClassGuard, PyErr, PyRef, PyRefMut, Python, }; use std::convert::Infallible; -#[cfg(return_position_impl_trait_in_traits)] use std::marker::PhantomData; /// Defines a conversion from a Rust type to a Python object, which may fail. @@ -32,14 +31,11 @@ use std::marker::PhantomData; /// /// - 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( - 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::*`" - ) +#[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 @@ -417,7 +413,6 @@ pub trait FromPyObject<'a, 'py>: Sized { /// iteration. #[doc(hidden)] #[inline(always)] - #[cfg(return_position_impl_trait_in_traits)] fn sequence_extractor( _obj: Borrowed<'_, 'py, PyAny>, _: private::Token, @@ -439,17 +434,6 @@ pub trait FromPyObject<'a, 'py>: Sized { Option::>::None } - /// Equivalent to the above for MSRV < 1.75, which pays an additional allocation cost. - #[doc(hidden)] - #[inline(always)] - #[cfg(not(return_position_impl_trait_in_traits))] - fn sequence_extractor<'b>( - _obj: Borrowed<'b, 'b, PyAny>, - _: private::Token, - ) -> Option + 'b>> { - None - } - /// Helper used to make a specialized path in extracting `DateTime` where `Tz` is /// `chrono::Local`, which will accept "naive" datetime objects as being in the local timezone. #[cfg(feature = "chrono-local")] @@ -469,14 +453,7 @@ mod from_py_object_sequence { fn to_vec(&self) -> Vec; - #[cfg(return_position_impl_trait_in_traits)] fn to_array(&self) -> PyResult<[Self::Target; N]>; - - /// Fills an uninit slice with values from the object. - /// - /// on success, `out` is fully initialized, on failure, `out` should be considered uninitialized. - #[cfg(not(return_position_impl_trait_in_traits))] - fn fill_slice(&self, out: &mut [std::mem::MaybeUninit]) -> PyResult<()>; } } diff --git a/src/conversions/smallvec.rs b/src/conversions/smallvec.rs index a1f2fed210e..8f199bded78 100644 --- a/src/conversions/smallvec.rs +++ b/src/conversions/smallvec.rs @@ -54,7 +54,6 @@ 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>; diff --git a/src/conversions/std/array.rs b/src/conversions/std/array.rs index 5df09664df8..f5f43eb6dd1 100644 --- a/src/conversions/std/array.rs +++ b/src/conversions/std/array.rs @@ -1,4 +1,4 @@ -use crate::conversion::{FromPyObjectOwned, IntoPyObject}; +use crate::conversion::{FromPyObjectOwned, FromPyObjectSequence, IntoPyObject}; use crate::types::any::PyAnyMethods; use crate::types::PySequence; use crate::{err::CastError, ffi, FromPyObject, PyAny, PyResult, PyTypeInfo, Python}; @@ -44,21 +44,7 @@ where fn extract(obj: Borrowed<'_, 'py, PyAny>) -> PyResult { if let Some(extractor) = T::sequence_extractor(obj, crate::conversion::private::Token) { - #[cfg(return_position_impl_trait_in_traits)] - { - use crate::conversion::FromPyObjectSequence; - return extractor.to_array(); - } - - #[cfg(not(return_position_impl_trait_in_traits))] - { - use std::array; - - let mut out = array::from_fn(|_| std::mem::MaybeUninit::uninit()); - extractor.fill_slice(&mut out)?; - // Safety: `out` is fully initialized by successful `fill_slice` - return Ok(out.map(|x| unsafe { x.assume_init() })); - } + return extractor.to_array(); } create_array_from_obj(obj) diff --git a/src/conversions/std/map.rs b/src/conversions/std/map.rs index ce6c50adb73..ad9282bc720 100644 --- a/src/conversions/std/map.rs +++ b/src/conversions/std/map.rs @@ -37,8 +37,6 @@ 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; diff --git a/src/conversions/std/num.rs b/src/conversions/std/num.rs index a2ccf64f818..71823ae9557 100644 --- a/src/conversions/std/num.rs +++ b/src/conversions/std/num.rs @@ -296,7 +296,6 @@ impl<'py> FromPyObject<'_, 'py> for u8 { Self::type_output() } - #[cfg(return_position_impl_trait_in_traits)] #[inline] fn sequence_extractor( obj: Borrowed<'_, 'py, PyAny>, @@ -310,21 +309,6 @@ impl<'py> FromPyObject<'_, 'py> for u8 { None } } - - #[cfg(not(return_position_impl_trait_in_traits))] - #[inline] - fn sequence_extractor<'b>( - obj: Borrowed<'b, 'b, PyAny>, - _: crate::conversion::private::Token, - ) -> Option + 'b>> { - if let Ok(bytes) = obj.cast::() { - Some(Box::new(BytesSequenceExtractor::Bytes(bytes))) - } else if let Ok(byte_array) = obj.cast::() { - Some(Box::new(BytesSequenceExtractor::ByteArray(byte_array))) - } else { - None - } - } } pub(crate) enum BytesSequenceExtractor<'a, 'py> { @@ -365,7 +349,6 @@ impl FromPyObjectSequence for BytesSequenceExtractor<'_, '_> { } } - #[cfg(return_position_impl_trait_in_traits)] fn to_array(&self) -> PyResult<[u8; N]> { let mut out: MaybeUninit<[u8; N]> = MaybeUninit::uninit(); @@ -379,11 +362,6 @@ impl FromPyObjectSequence for BytesSequenceExtractor<'_, '_> { // Safety: `out` is fully initialized Ok(unsafe { out.assume_init() }) } - - #[cfg(not(return_position_impl_trait_in_traits))] - fn fill_slice(&self, out: &mut [MaybeUninit]) -> PyResult<()> { - self.fill_slice(out) - } } int_fits_c_long!(i8); diff --git a/src/conversions/std/set.rs b/src/conversions/std/set.rs index 58c38f6ca1c..f741f02f8d0 100644 --- a/src/conversions/std/set.rs +++ b/src/conversions/std/set.rs @@ -35,7 +35,6 @@ where 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; diff --git a/src/conversions/std/slice.rs b/src/conversions/std/slice.rs index a2047deb15b..ad26188a9d6 100644 --- a/src/conversions/std/slice.rs +++ b/src/conversions/std/slice.rs @@ -11,7 +11,6 @@ use crate::{ 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>; diff --git a/src/conversions/std/vec.rs b/src/conversions/std/vec.rs index bd3a0e65714..7831683088b 100644 --- a/src/conversions/std/vec.rs +++ b/src/conversions/std/vec.rs @@ -1,7 +1,7 @@ #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; use crate::{ - conversion::{FromPyObject, FromPyObjectOwned, IntoPyObject}, + conversion::{FromPyObject, FromPyObjectOwned, FromPyObjectSequence, IntoPyObject}, exceptions::PyTypeError, ffi, types::{PyAnyMethods, PySequence, PyString}, @@ -35,7 +35,6 @@ where 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>; @@ -63,8 +62,6 @@ where fn extract(obj: Borrowed<'_, 'py, PyAny>) -> PyResult { if let Some(extractor) = T::sequence_extractor(obj, crate::conversion::private::Token) { - #[cfg(return_position_impl_trait_in_traits)] - use crate::conversion::FromPyObjectSequence; return Ok(extractor.to_vec()); } diff --git a/src/err/impls.rs b/src/err/impls.rs index 6de043aabe0..c49c7a56f82 100644 --- a/src/err/impls.rs +++ b/src/err/impls.rs @@ -28,17 +28,11 @@ impl From for io::Error { io::ErrorKind::TimedOut } else if err.is_instance_of::(py) { io::ErrorKind::OutOfMemory + } else if err.is_instance_of::(py) { + io::ErrorKind::IsADirectory + } else if err.is_instance_of::(py) { + io::ErrorKind::NotADirectory } else { - #[cfg(io_error_more)] - #[allow(clippy::incompatible_msrv)] // gated by `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 } }); @@ -67,9 +61,7 @@ impl From for PyErr { io::ErrorKind::WouldBlock => exceptions::PyBlockingIOError::new_err(err), io::ErrorKind::TimedOut => exceptions::PyTimeoutError::new_err(err), io::ErrorKind::OutOfMemory => exceptions::PyMemoryError::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), } @@ -184,9 +176,7 @@ 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"); } } diff --git a/src/impl_/concat.rs b/src/impl_/concat.rs index 245a8b63aeb..9f2c0e33c17 100644 --- a/src/impl_/concat.rs +++ b/src/impl_/concat.rs @@ -15,8 +15,6 @@ pub const fn combined_len(pieces: &[&[u8]]) -> usize { /// Combines all bytes pieces into a single byte array. /// /// `out` should be a buffer at the correct size of `combined_len(pieces)`, else this will panic. -#[cfg(mut_ref_in_const_fn)] // requires MSRV 1.83 -#[allow(clippy::incompatible_msrv)] // `split_at_mut` also requires MSRV 1.83 const fn combine(pieces: &[&[u8]], mut out: &mut [u8]) { let mut pieces_idx = 0; while pieces_idx < pieces.len() { @@ -32,33 +30,13 @@ const fn combine(pieces: &[&[u8]], mut out: &mut [u8]) { /// Wrapper around `combine` which has a const generic parameter, this is going to be more codegen /// at compile time (?) -/// -/// Unfortunately the `&mut [u8]` buffer needs MSRV 1.83 pub const fn combine_to_array(pieces: &[&[u8]]) -> [u8; LEN] { let mut out: [u8; LEN] = [0u8; LEN]; - #[cfg(mut_ref_in_const_fn)] combine(pieces, &mut out); - #[cfg(not(mut_ref_in_const_fn))] // inlined here for higher code - { - let mut out_idx = 0; - let mut pieces_idx = 0; - while pieces_idx < pieces.len() { - let piece = pieces[pieces_idx]; - let mut piece_idx = 0; - while piece_idx < piece.len() { - out[out_idx] = piece[piece_idx]; - out_idx += 1; - piece_idx += 1; - } - pieces_idx += 1; - } - assert!(out_idx == out.len(), "output buffer too large"); - } out } /// Replacement for `slice::copy_from_slice`, which is const from 1.87 -#[cfg(mut_ref_in_const_fn)] // requires MSRV 1.83 const fn slice_copy_from_slice(out: &mut [u8], src: &[u8]) { let mut i = 0; while i < src.len() { diff --git a/src/impl_/extract_argument.rs b/src/impl_/extract_argument.rs index daadc2c5235..a0ea13e1ca6 100644 --- a/src/impl_/extract_argument.rs +++ b/src/impl_/extract_argument.rs @@ -44,13 +44,10 @@ mod function_argument { /// some additional types which don't implement `FromPyObject`, such as `&T` for `#[pyclass]` types. /// All types should only implement this trait once; either by the `FromPyObject` blanket or one /// of the specialized implementations which needs a `Holder`. -#[cfg_attr( - diagnostic_namespace, - diagnostic::on_unimplemented( - message = "`{Self}` cannot be used as a Python function argument", - note = "implement `FromPyObject` to enable using `{Self}` as a function argument", - note = "`Python<'py>` is also a valid argument type to pass the Python token into `#[pyfunction]`s and `#[pymethods]`" - ) +#[diagnostic::on_unimplemented( + message = "`{Self}` cannot be used as a Python function argument", + note = "implement `FromPyObject` to enable using `{Self}` as a function argument", + note = "`Python<'py>` is also a valid argument type to pass the Python token into `#[pyfunction]`s and `#[pymethods]`" )] pub trait PyFunctionArgument<'a, 'holder, 'py, const IMPLEMENTS_FROMPYOBJECT: bool>: Sized + function_argument::Sealed diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index 7666a883af7..a96ee94991e 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -1119,7 +1119,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, Py_LIMITED_API), + Py_LIMITED_API, diagnostic::on_unimplemented( message = "pyclass `{Self}` cannot be subclassed", label = "required for `#[pyclass(extends={Self})]`", @@ -1128,7 +1128,7 @@ impl PyClassThreadChecker for ThreadCheckerImpl { ) )] #[cfg_attr( - all(diagnostic_namespace, not(Py_LIMITED_API)), + not(Py_LIMITED_API), diagnostic::on_unimplemented( message = "pyclass `{Self}` cannot be subclassed", label = "required for `#[pyclass(extends={Self})]`", @@ -1308,13 +1308,10 @@ where } } -#[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" - ) +#[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> + Clone {} impl<'py, T> PyO3GetField<'py> for T where T: IntoPyObject<'py> + Clone {} diff --git a/src/impl_/pyclass/assertions.rs b/src/impl_/pyclass/assertions.rs index daf2ed5310b..4365e2d52c3 100644 --- a/src/impl_/pyclass/assertions.rs +++ b/src/impl_/pyclass/assertions.rs @@ -13,14 +13,11 @@ where { } -#[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", - ) +#[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 {} diff --git a/src/pyclass/create_type_object.rs b/src/pyclass/create_type_object.rs index 0da0aa17cdb..c4229e08c53 100644 --- a/src/pyclass/create_type_object.rs +++ b/src/pyclass/create_type_object.rs @@ -250,11 +250,8 @@ impl PyTypeBuilder { 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>(); + let dict_ptr = + object.byte_offset(dict_offset).cast::<*mut ffi::PyObject>(); if (*dict_ptr).is_null() { std::ptr::write(dict_ptr, ffi::PyDict_New()); } diff --git a/src/pyclass/gc.rs b/src/pyclass/gc.rs index 295e7e05894..6b9da449c3e 100644 --- a/src/pyclass/gc.rs +++ b/src/pyclass/gc.rs @@ -1,5 +1,6 @@ use std::{ marker::PhantomData, + num::NonZero, os::raw::{c_int, c_void}, }; @@ -7,7 +8,7 @@ use crate::{ffi, Py}; /// Error returned by a `__traverse__` visitor implementation. #[repr(transparent)] -pub struct PyTraverseError(NonZeroCInt); +pub struct PyTraverseError(NonZero); impl PyTraverseError { /// Returns the error code. @@ -38,7 +39,7 @@ impl PyVisit<'_> { { let ptr = obj.into().map_or_else(std::ptr::null_mut, Py::as_ptr); if !ptr.is_null() { - match NonZeroCInt::new(unsafe { (self.visit)(ptr, self.arg) }) { + match NonZero::new(unsafe { (self.visit)(ptr, self.arg) }) { None => Ok(()), Some(r) => Err(PyTraverseError(r)), } @@ -48,26 +49,6 @@ impl PyVisit<'_> { } } -/// 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; diff --git a/src/types/mod.rs b/src/types/mod.rs index 8f99455dd33..6c9097aefeb 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -136,12 +136,7 @@ macro_rules! pyobject_native_type_named ( #[macro_export] macro_rules! pyobject_native_static_type_object( ($typeobject:expr) => { - |_py| { - // TODO: remove `unsafe` and `allow` on MSRV 1.82+ - #[allow(unused_unsafe)] // https://github.com/rust-lang/rust/pull/125834 - // SAFETY: `typeobject` is a known `static mut PyTypeObject` - unsafe { ::std::ptr::addr_of_mut!($typeobject) } - } + |_py| ::std::ptr::addr_of_mut!($typeobject) }; ); diff --git a/src/types/tuple.rs b/src/types/tuple.rs index b216451b431..719cca50c40 100644 --- a/src/types/tuple.rs +++ b/src/types/tuple.rs @@ -624,7 +624,6 @@ 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>,)+ - $($T: 'a,)+ // MSRV { type Target = PyTuple; type Output = Bound<'py, Self::Target>; @@ -769,11 +768,10 @@ macro_rules! tuple_conversion ({$length:expr,$(($refN:ident, $n:tt, $T:ident)),+ } } - 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::private::Sealed for &'a ($($T,)+) where $(&'a $T: IntoPyObject<'py>,)+ {} 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( From 67da00d07da5a5755344d101663e69e9209554bb Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Tue, 21 Oct 2025 21:46:36 +0100 Subject: [PATCH 931/936] docs: improve convention for handling `static mut` slots (#5490) Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> --- pyo3-ffi/README.md | 4 ++-- pyo3-ffi/examples/sequential/src/id.rs | 4 ++-- pyo3-ffi/examples/sequential/src/module.rs | 5 +++-- pyo3-ffi/examples/string-sum/src/lib.rs | 4 ++-- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/pyo3-ffi/README.md b/pyo3-ffi/README.md index fe18cd0e2bb..625344566d1 100644 --- a/pyo3-ffi/README.md +++ b/pyo3-ffi/README.md @@ -76,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 *const [PyMethodDef] as *mut PyMethodDef }, + m_methods: std::ptr::addr_of_mut!(METHODS).cast(), m_slots: unsafe { SLOTS as *const [PyModuleDef_Slot] as *mut PyModuleDef_Slot }, m_traverse: None, m_clear: None, m_free: None, }; -static mut METHODS: &[PyMethodDef] = &[ +static mut METHODS: [PyMethodDef; 2] = [ PyMethodDef { ml_name: c_str!("sum_as_string").as_ptr(), ml_meth: PyMethodDefPointer { diff --git a/pyo3-ffi/examples/sequential/src/id.rs b/pyo3-ffi/examples/sequential/src/id.rs index a20cc54b3b2..30f92d3188c 100644 --- a/pyo3-ffi/examples/sequential/src/id.rs +++ b/pyo3-ffi/examples/sequential/src/id.rs @@ -99,7 +99,7 @@ unsafe extern "C" fn id_richcompare( } } -static mut SLOTS: &[PyType_Slot] = &[ +static mut SLOTS: [PyType_Slot; 6] = [ PyType_Slot { slot: Py_tp_new, pfunc: id_new as *mut c_void, @@ -132,5 +132,5 @@ pub static mut ID_SPEC: PyType_Spec = PyType_Spec { basicsize: mem::size_of::() as c_int, itemsize: 0, flags: (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_IMMUTABLETYPE) as c_uint, - slots: unsafe { SLOTS as *const [PyType_Slot] as *mut PyType_Slot }, + slots: ptr::addr_of_mut!(SLOTS).cast(), }; diff --git a/pyo3-ffi/examples/sequential/src/module.rs b/pyo3-ffi/examples/sequential/src/module.rs index 8895b32a734..7c7eb12531b 100644 --- a/pyo3-ffi/examples/sequential/src/module.rs +++ b/pyo3-ffi/examples/sequential/src/module.rs @@ -8,13 +8,14 @@ pub static mut MODULE_DEF: PyModuleDef = PyModuleDef { 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 }, + m_slots: std::ptr::addr_of_mut!(SEQUENTIAL_SLOTS).cast(), m_traverse: Some(sequential_traverse), m_clear: Some(sequential_clear), m_free: Some(sequential_free), }; -static mut SEQUENTIAL_SLOTS: &[PyModuleDef_Slot] = &[ +const SEQUENTIAL_SLOTS_LEN: usize = 3 + if cfg!(Py_GIL_DISABLED) { 1 } else { 0 }; +static mut SEQUENTIAL_SLOTS: [PyModuleDef_Slot; SEQUENTIAL_SLOTS_LEN] = [ PyModuleDef_Slot { slot: Py_mod_exec, value: sequential_exec as *mut c_void, diff --git a/pyo3-ffi/examples/string-sum/src/lib.rs b/pyo3-ffi/examples/string-sum/src/lib.rs index 2c086a0889b..f2c9d550b8b 100644 --- a/pyo3-ffi/examples/string-sum/src/lib.rs +++ b/pyo3-ffi/examples/string-sum/src/lib.rs @@ -8,14 +8,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 *const [PyMethodDef] as *mut PyMethodDef }, + m_methods: std::ptr::addr_of_mut!(METHODS).cast(), m_slots: unsafe { SLOTS as *const [PyModuleDef_Slot] as *mut PyModuleDef_Slot }, m_traverse: None, m_clear: None, m_free: None, }; -static mut METHODS: &[PyMethodDef] = &[ +static mut METHODS: [PyMethodDef; 2] = [ PyMethodDef { ml_name: c_str!("sum_as_string").as_ptr(), ml_meth: PyMethodDefPointer { From b9326352b670c5b839f05626aa696a9fe1614ef4 Mon Sep 17 00:00:00 2001 From: Thomas Tanon Date: Wed, 22 Oct 2025 09:03:40 +0200 Subject: [PATCH 932/936] Use C-string literals where possible (#5548) * Use C-string literals where possible * Update pyo3-ffi/examples/sequential/tests/test.rs Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> --------- Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> --- pyo3-benches/benches/bench_call.rs | 2 +- pyo3-ffi/examples/sequential/src/id.rs | 7 +- pyo3-ffi/examples/sequential/src/module.rs | 11 ++-- pyo3-ffi/examples/sequential/tests/test.rs | 13 ++-- pyo3-ffi/examples/string-sum/src/lib.rs | 15 ++--- pyo3-ffi/src/abstract_.rs | 2 +- pyo3-ffi/src/compat/py_3_10.rs | 2 +- pyo3-ffi/src/compat/py_3_13.rs | 2 +- pyo3-ffi/src/datetime.rs | 38 +++++------ pyo3-ffi/src/lib.rs | 12 ++-- pyo3-ffi/src/pyerrors.rs | 2 +- src/buffer.rs | 76 +++++++++++----------- src/conversions/anyhow.rs | 10 +-- src/conversions/bigdecimal.rs | 5 +- src/conversions/chrono.rs | 7 +- src/conversions/eyre.rs | 10 +-- src/conversions/jiff.rs | 3 +- src/conversions/num_bigint.rs | 6 +- src/conversions/num_complex.rs | 33 ++++------ src/conversions/num_rational.rs | 14 ++-- src/conversions/ordered_float.rs | 3 +- src/conversions/rust_decimal.rs | 7 +- src/conversions/std/array.rs | 20 ++---- src/conversions/std/num.rs | 12 ++-- src/conversions/std/slice.rs | 11 ++-- src/conversions/std/vec.rs | 12 ++-- src/err/err_state.rs | 2 +- src/err/mod.rs | 22 +++---- src/exceptions.rs | 58 +++++++---------- src/ffi/tests.rs | 10 +-- src/impl_/pyclass.rs | 6 +- src/impl_/pyclass/doc.rs | 5 +- src/impl_/pymethods.rs | 4 +- src/impl_/pymodule.rs | 9 ++- src/instance.rs | 25 +++---- src/internal/state.rs | 8 +-- src/interpreter_lifecycle.rs | 2 +- src/lib.rs | 2 +- src/macros.rs | 2 +- src/marker.rs | 37 ++++------- src/pyclass/create_type_object.rs | 10 ++- src/types/any.rs | 63 ++++++++---------- src/types/bytearray.rs | 5 +- src/types/capsule.rs | 14 ++-- src/types/code.rs | 2 +- src/types/dict.rs | 2 +- src/types/function.rs | 6 +- src/types/genericalias.rs | 17 ++--- src/types/iterator.rs | 36 ++++------ src/types/list.rs | 8 +-- src/types/mod.rs | 2 +- src/types/module.rs | 12 ++-- src/types/sequence.rs | 4 +- src/types/set.rs | 7 +- src/types/string.rs | 47 +++++-------- src/types/traceback.rs | 17 ++--- src/types/typeobject.rs | 12 ++-- src/types/weakref/anyref.rs | 6 +- src/types/weakref/proxy.rs | 24 ++++--- src/types/weakref/reference.rs | 16 ++--- tests/test_anyhow.rs | 13 +--- tests/test_append_to_inittab.rs | 14 ++-- tests/test_buffer.rs | 4 +- tests/test_class_basics.rs | 5 +- tests/test_datetime.rs | 10 +-- tests/test_frompyobject.rs | 12 ++-- tests/test_gc.rs | 6 +- tests/test_inheritance.rs | 10 ++- tests/test_module.rs | 5 +- tests/test_proto_methods.rs | 14 ++-- tests/test_pyerr_debug_unformattable.rs | 13 ++-- tests/test_pyfunction.rs | 20 ++---- 72 files changed, 384 insertions(+), 579 deletions(-) diff --git a/pyo3-benches/benches/bench_call.rs b/pyo3-benches/benches/bench_call.rs index 92587b67437..909e5761149 100644 --- a/pyo3-benches/benches/bench_call.rs +++ b/pyo3-benches/benches/bench_call.rs @@ -8,7 +8,7 @@ use pyo3::types::IntoPyDict; macro_rules! test_module { ($py:ident, $code:literal) => { - PyModule::from_code($py, c_str!($code), c_str!(file!()), c_str!("test_module")) + PyModule::from_code($py, c_str!($code), c_str!(file!()), c"test_module") .expect("module creation failed") }; } diff --git a/pyo3-ffi/examples/sequential/src/id.rs b/pyo3-ffi/examples/sequential/src/id.rs index 30f92d3188c..21c667242de 100644 --- a/pyo3-ffi/examples/sequential/src/id.rs +++ b/pyo3-ffi/examples/sequential/src/id.rs @@ -31,7 +31,7 @@ unsafe extern "C" fn id_new( // 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()); + PyErr_SetString(PyExc_TypeError, c"Id() takes no arguments".as_ptr()); return ptr::null_mut(); } @@ -106,8 +106,7 @@ static mut SLOTS: [PyType_Slot; 6] = [ }, PyType_Slot { slot: Py_tp_doc, - pfunc: c_str!("An id that is increased every time an instance is created").as_ptr() - as *mut c_void, + pfunc: c"An id that is increased every time an instance is created".as_ptr() as *mut c_void, }, PyType_Slot { slot: Py_tp_repr, @@ -128,7 +127,7 @@ static mut SLOTS: [PyType_Slot; 6] = [ ]; pub static mut ID_SPEC: PyType_Spec = PyType_Spec { - name: c_str!("sequential.Id").as_ptr(), + name: c"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/pyo3-ffi/examples/sequential/src/module.rs b/pyo3-ffi/examples/sequential/src/module.rs index 7c7eb12531b..c8d506c5568 100644 --- a/pyo3-ffi/examples/sequential/src/module.rs +++ b/pyo3-ffi/examples/sequential/src/module.rs @@ -4,8 +4,8 @@ use std::ffi::{c_int, c_void}; pub static mut MODULE_DEF: PyModuleDef = PyModuleDef { m_base: PyModuleDef_HEAD_INIT, - m_name: c_str!("sequential").as_ptr(), - m_doc: c_str!("A library for generating sequential ids, written in Rust.").as_ptr(), + m_name: c"sequential".as_ptr(), + m_doc: c"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: std::ptr::addr_of_mut!(SEQUENTIAL_SLOTS).cast(), @@ -45,15 +45,12 @@ unsafe extern "C" fn sequential_exec(module: *mut PyObject) -> c_int { ptr::null_mut(), ); if id_type.is_null() { - PyErr_SetString( - PyExc_SystemError, - c_str!("cannot locate type object").as_ptr(), - ); + PyErr_SetString(PyExc_SystemError, c"cannot locate type object".as_ptr()); return -1; } (*state).id_type = id_type.cast::(); - PyModule_AddObjectRef(module, c_str!("Id").as_ptr(), id_type) + PyModule_AddObjectRef(module, c"Id".as_ptr(), id_type) } unsafe extern "C" fn sequential_traverse( diff --git a/pyo3-ffi/examples/sequential/tests/test.rs b/pyo3-ffi/examples/sequential/tests/test.rs index f2a08433cea..f4b68092e1d 100644 --- a/pyo3-ffi/examples/sequential/tests/test.rs +++ b/pyo3-ffi/examples/sequential/tests/test.rs @@ -5,13 +5,11 @@ use std::thread; use pyo3_ffi::*; use sequential::PyInit_sequential; -static COMMAND: &'static str = c_str!( - " +static COMMAND: &'static CStr= c" from sequential import Id s = sum(int(Id()) for _ in range(12)) -" -); +"; // Newtype to be able to pass it to another thread. struct State(*mut PyThreadState); @@ -21,7 +19,7 @@ unsafe impl Send for State {} #[test] fn lets_go_fast() -> Result<(), String> { unsafe { - let ret = PyImport_AppendInittab(c_str!("sequential").as_ptr(), Some(PyInit_sequential)); + let ret = PyImport_AppendInittab(c"sequential".as_ptr(), Some(PyInit_sequential)); if ret == -1 { return Err("could not add module to inittab".into()); } @@ -121,8 +119,7 @@ unsafe fn fetch() -> String { fn run_code() -> Result { unsafe { - let code_obj = - Py_CompileString(COMMAND.as_ptr(), c_str!("program").as_ptr(), Py_file_input); + let code_obj = Py_CompileString(COMMAND.as_ptr(), c"program".as_ptr(), Py_file_input); if code_obj.is_null() { return Err(fetch()); } @@ -134,7 +131,7 @@ fn run_code() -> Result { } else { Py_DECREF(res_ptr); } - let sum = PyDict_GetItemString(globals, c_str!("s").as_ptr()); /* borrowed reference */ + let sum = PyDict_GetItemString(globals, c"s".as_ptr()); /* borrowed reference */ if sum.is_null() { Py_DECREF(globals); return Err("globals did not have `s`".into()); diff --git a/pyo3-ffi/examples/string-sum/src/lib.rs b/pyo3-ffi/examples/string-sum/src/lib.rs index f2c9d550b8b..a0e53765481 100644 --- a/pyo3-ffi/examples/string-sum/src/lib.rs +++ b/pyo3-ffi/examples/string-sum/src/lib.rs @@ -5,8 +5,8 @@ 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_name: c"string_sum".as_ptr(), + m_doc: c"A Python module written in Rust.".as_ptr(), m_size: 0, m_methods: std::ptr::addr_of_mut!(METHODS).cast(), m_slots: unsafe { SLOTS as *const [PyModuleDef_Slot] as *mut PyModuleDef_Slot }, @@ -17,12 +17,12 @@ static mut MODULE_DEF: PyModuleDef = PyModuleDef { static mut METHODS: [PyMethodDef; 2] = [ PyMethodDef { - ml_name: c_str!("sum_as_string").as_ptr(), + ml_name: c"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(), + ml_doc: c"returns the sum of two integers as a string".as_ptr(), }, // A zeroed PyMethodDef to mark the end of the array. PyMethodDef::zeroed(), @@ -107,7 +107,7 @@ pub unsafe extern "C" fn sum_as_string( if nargs != 2 { PyErr_SetString( PyExc_TypeError, - c_str!("sum_as_string expected 2 positional arguments").as_ptr(), + c"sum_as_string expected 2 positional arguments".as_ptr(), ); return std::ptr::null_mut(); } @@ -129,10 +129,7 @@ pub unsafe extern "C" fn sum_as_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(), - ); + PyErr_SetString(PyExc_OverflowError, c"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 bf80ce2e56b..712a0739b43 100644 --- a/pyo3-ffi/src/abstract_.rs +++ b/pyo3-ffi/src/abstract_.rs @@ -137,7 +137,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(), c_str!("__next__").as_ptr()) + crate::PyObject_HasAttrString(crate::Py_TYPE(o).cast(), c"__next__".as_ptr()) } extern "C" { diff --git a/pyo3-ffi/src/compat/py_3_10.rs b/pyo3-ffi/src/compat/py_3_10.rs index b59eb8ee8e4..2d4daa194c6 100644 --- a/pyo3-ffi/src/compat/py_3_10.rs +++ b/pyo3-ffi/src/compat/py_3_10.rs @@ -30,7 +30,7 @@ compat_function!( if value.is_null() && crate::PyErr_Occurred().is_null() { crate::PyErr_SetString( crate::PyExc_SystemError, - c_str!("PyModule_AddObjectRef() must be called with an exception raised if value is NULL").as_ptr(), + c"PyModule_AddObjectRef() must be called with an exception raised if value is NULL".as_ptr(), ); return -1; } diff --git a/pyo3-ffi/src/compat/py_3_13.rs b/pyo3-ffi/src/compat/py_3_13.rs index f158c843606..a433f7ac334 100644 --- a/pyo3-ffi/src/compat/py_3_13.rs +++ b/pyo3-ffi/src/compat/py_3_13.rs @@ -66,7 +66,7 @@ compat_function!( if !reference.is_null() && PyWeakref_Check(reference) == 0 { *pobj = std::ptr::null_mut(); - PyErr_SetString(PyExc_TypeError, c_str!("expected a weakref").as_ptr()); + PyErr_SetString(PyExc_TypeError, c"expected a weakref".as_ptr()); return -1; } let obj = PyWeakref_GetObject(reference); diff --git a/pyo3-ffi/src/datetime.rs b/pyo3-ffi/src/datetime.rs index 143de98e676..dc3a8b32dec 100644 --- a/pyo3-ffi/src/datetime.rs +++ b/pyo3-ffi/src/datetime.rs @@ -358,55 +358,55 @@ pub unsafe fn _get_attr(obj: *mut PyObject, field: &std::ffi::CStr) -> c_int { #[inline] #[cfg(GraalPy)] pub unsafe fn PyDateTime_GET_YEAR(o: *mut PyObject) -> c_int { - _get_attr(o, c_str!("year")) + _get_attr(o, c"year") } #[inline] #[cfg(GraalPy)] pub unsafe fn PyDateTime_GET_MONTH(o: *mut PyObject) -> c_int { - _get_attr(o, c_str!("month")) + _get_attr(o, c"month") } #[inline] #[cfg(GraalPy)] pub unsafe fn PyDateTime_GET_DAY(o: *mut PyObject) -> c_int { - _get_attr(o, c_str!("day")) + _get_attr(o, c"day") } #[inline] #[cfg(GraalPy)] pub unsafe fn PyDateTime_DATE_GET_HOUR(o: *mut PyObject) -> c_int { - _get_attr(o, c_str!("hour")) + _get_attr(o, c"hour") } #[inline] #[cfg(GraalPy)] pub unsafe fn PyDateTime_DATE_GET_MINUTE(o: *mut PyObject) -> c_int { - _get_attr(o, c_str!("minute")) + _get_attr(o, c"minute") } #[inline] #[cfg(GraalPy)] pub unsafe fn PyDateTime_DATE_GET_SECOND(o: *mut PyObject) -> c_int { - _get_attr(o, c_str!("second")) + _get_attr(o, c"second") } #[inline] #[cfg(GraalPy)] pub unsafe fn PyDateTime_DATE_GET_MICROSECOND(o: *mut PyObject) -> c_int { - _get_attr(o, c_str!("microsecond")) + _get_attr(o, c"microsecond") } #[inline] #[cfg(GraalPy)] pub unsafe fn PyDateTime_DATE_GET_FOLD(o: *mut PyObject) -> c_int { - _get_attr(o, c_str!("fold")) + _get_attr(o, c"fold") } #[inline] #[cfg(GraalPy)] pub unsafe fn PyDateTime_DATE_GET_TZINFO(o: *mut PyObject) -> *mut PyObject { - let res = PyObject_GetAttrString(o, c_str!("tzinfo").as_ptr().cast()); + let res = PyObject_GetAttrString(o, c"tzinfo".as_ptr().cast()); Py_DecRef(res); // the original macros are borrowing res } @@ -414,37 +414,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, c_str!("hour")) + _get_attr(o, c"hour") } #[inline] #[cfg(GraalPy)] pub unsafe fn PyDateTime_TIME_GET_MINUTE(o: *mut PyObject) -> c_int { - _get_attr(o, c_str!("minute")) + _get_attr(o, c"minute") } #[inline] #[cfg(GraalPy)] pub unsafe fn PyDateTime_TIME_GET_SECOND(o: *mut PyObject) -> c_int { - _get_attr(o, c_str!("second")) + _get_attr(o, c"second") } #[inline] #[cfg(GraalPy)] pub unsafe fn PyDateTime_TIME_GET_MICROSECOND(o: *mut PyObject) -> c_int { - _get_attr(o, c_str!("microsecond")) + _get_attr(o, c"microsecond") } #[inline] #[cfg(GraalPy)] pub unsafe fn PyDateTime_TIME_GET_FOLD(o: *mut PyObject) -> c_int { - _get_attr(o, c_str!("fold")) + _get_attr(o, c"fold") } #[inline] #[cfg(GraalPy)] pub unsafe fn PyDateTime_TIME_GET_TZINFO(o: *mut PyObject) -> *mut PyObject { - let res = PyObject_GetAttrString(o, c_str!("tzinfo").as_ptr().cast()); + let res = PyObject_GetAttrString(o, c"tzinfo".as_ptr().cast()); Py_DecRef(res); // the original macros are borrowing res } @@ -452,19 +452,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, c_str!("days")) + _get_attr(o, c"days") } #[inline] #[cfg(GraalPy)] pub unsafe fn PyDateTime_DELTA_GET_SECONDS(o: *mut PyObject) -> c_int { - _get_attr(o, c_str!("seconds")) + _get_attr(o, c"seconds") } #[inline] #[cfg(GraalPy)] pub unsafe fn PyDateTime_DELTA_GET_MICROSECONDS(o: *mut PyObject) -> c_int { - _get_attr(o, c_str!("microseconds")) + _get_attr(o, c"microseconds") } #[cfg(PyPy)] @@ -590,7 +590,7 @@ 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"); +pub const PyDateTime_CAPSULE_NAME: &CStr = c"datetime.datetime_CAPI"; /// Returns a pointer to a `PyDateTime_CAPI` instance /// diff --git a/pyo3-ffi/src/lib.rs b/pyo3-ffi/src/lib.rs index 2383873e2a1..3faf4d4c2c3 100644 --- a/pyo3-ffi/src/lib.rs +++ b/pyo3-ffi/src/lib.rs @@ -137,8 +137,8 @@ //! //! 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_name: c"string_sum".as_ptr(), +//! m_doc: c"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(), @@ -149,12 +149,12 @@ //! //! static mut METHODS: &[PyMethodDef] = &[ //! PyMethodDef { -//! ml_name: c_str!("sum_as_string").as_ptr(), +//! ml_name: c"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(), +//! ml_doc: c"returns the sum of two integers as a string".as_ptr(), //! }, //! // A zeroed PyMethodDef to mark the end of the array. //! PyMethodDef::zeroed(), @@ -233,7 +233,7 @@ //! if nargs != 2 { //! PyErr_SetString( //! PyExc_TypeError, -//! c_str!("sum_as_string expected 2 positional arguments").as_ptr(), +//! c"sum_as_string expected 2 positional arguments".as_ptr(), //! ); //! return std::ptr::null_mut(); //! } @@ -257,7 +257,7 @@ //! None => { //! PyErr_SetString( //! PyExc_OverflowError, -//! c_str!("arguments too large to add").as_ptr(), +//! c"arguments too large to add".as_ptr(), //! ); //! std::ptr::null_mut() //! } diff --git a/pyo3-ffi/src/pyerrors.rs b/pyo3-ffi/src/pyerrors.rs index 836491bb6e6..557f314c7cb 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, - c_str!("sy#nns").as_ptr(), + c"sy#nns".as_ptr(), encoding, object, length, diff --git a/src/buffer.rs b/src/buffer.rs index f83f9649e13..081f520e795 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -367,7 +367,7 @@ impl PyBuffer { #[inline] pub fn format(&self) -> &CStr { if self.raw().format.is_null() { - ffi::c_str!("B") + c"B" } else { unsafe { CStr::from_ptr(self.raw().format) } } @@ -774,114 +774,114 @@ mod tests { for (cstr, expected) in [ // @ prefix goes to native_element_type_from_type_char ( - ffi::c_str!("@b"), + c"@b", SignedInteger { bytes: size_of::(), }, ), ( - ffi::c_str!("@c"), + c"@c", UnsignedInteger { bytes: size_of::(), }, ), ( - ffi::c_str!("@b"), + c"@b", SignedInteger { bytes: size_of::(), }, ), ( - ffi::c_str!("@B"), + c"@B", UnsignedInteger { bytes: size_of::(), }, ), - (ffi::c_str!("@?"), Bool), + (c"@?", Bool), ( - ffi::c_str!("@h"), + c"@h", SignedInteger { bytes: size_of::(), }, ), ( - ffi::c_str!("@H"), + c"@H", UnsignedInteger { bytes: size_of::(), }, ), ( - ffi::c_str!("@i"), + c"@i", SignedInteger { bytes: size_of::(), }, ), ( - ffi::c_str!("@I"), + c"@I", UnsignedInteger { bytes: size_of::(), }, ), ( - ffi::c_str!("@l"), + c"@l", SignedInteger { bytes: size_of::(), }, ), ( - ffi::c_str!("@L"), + c"@L", UnsignedInteger { bytes: size_of::(), }, ), ( - ffi::c_str!("@q"), + c"@q", SignedInteger { bytes: size_of::(), }, ), ( - ffi::c_str!("@Q"), + c"@Q", UnsignedInteger { bytes: size_of::(), }, ), ( - ffi::c_str!("@n"), + c"@n", SignedInteger { bytes: size_of::(), }, ), ( - ffi::c_str!("@N"), + c"@N", UnsignedInteger { bytes: size_of::(), }, ), - (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), + (c"@e", Float { bytes: 2 }), + (c"@f", Float { bytes: 4 }), + (c"@d", Float { bytes: 8 }), + (c"@z", Unknown), // = prefix goes to standard_element_type_from_type_char - (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), + (c"=b", SignedInteger { bytes: 1 }), + (c"=c", UnsignedInteger { bytes: 1 }), + (c"=B", UnsignedInteger { bytes: 1 }), + (c"=?", Bool), + (c"=h", SignedInteger { bytes: 2 }), + (c"=H", UnsignedInteger { bytes: 2 }), + (c"=l", SignedInteger { bytes: 4 }), + (c"=l", SignedInteger { bytes: 4 }), + (c"=I", UnsignedInteger { bytes: 4 }), + (c"=L", UnsignedInteger { bytes: 4 }), + (c"=q", SignedInteger { bytes: 8 }), + (c"=Q", UnsignedInteger { bytes: 8 }), + (c"=e", Float { bytes: 2 }), + (c"=f", Float { bytes: 4 }), + (c"=d", Float { bytes: 8 }), + (c"=z", Unknown), + (c"=0", Unknown), // unknown prefix -> Unknown - (ffi::c_str!(":b"), Unknown), + (c":b", Unknown), ] { assert_eq!( ElementType::from_format(cstr), diff --git a/src/conversions/anyhow.rs b/src/conversions/anyhow.rs index 5cae107b5b3..af6bf515fee 100644 --- a/src/conversions/anyhow.rs +++ b/src/conversions/anyhow.rs @@ -120,8 +120,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}; @@ -146,9 +146,7 @@ mod test_anyhow { Python::attach(|py| { let locals = [("err", pyerr)].into_py_dict(py).unwrap(); - let pyerr = py - .run(ffi::c_str!("raise err"), None, Some(&locals)) - .unwrap_err(); + let pyerr = py.run(c"raise err", None, Some(&locals)).unwrap_err(); assert_eq!(pyerr.value(py).to_string(), expected_contents); }) } @@ -165,9 +163,7 @@ mod test_anyhow { Python::attach(|py| { let locals = [("err", pyerr)].into_py_dict(py).unwrap(); - let pyerr = py - .run(ffi::c_str!("raise err"), None, Some(&locals)) - .unwrap_err(); + let pyerr = py.run(c"raise err", None, Some(&locals)).unwrap_err(); assert_eq!(pyerr.value(py).to_string(), expected_contents); }) } diff --git a/src/conversions/bigdecimal.rs b/src/conversions/bigdecimal.rs index 991c5567cce..70783d0b2b1 100644 --- a/src/conversions/bigdecimal.rs +++ b/src/conversions/bigdecimal.rs @@ -113,7 +113,6 @@ mod test_bigdecimal { use crate::types::PyDict; use std::ffi::CString; - use crate::ffi; use bigdecimal::{One, Zero}; #[cfg(not(target_arch = "wasm32"))] use proptest::prelude::*; @@ -204,7 +203,7 @@ mod test_bigdecimal { Python::attach(|py| { let locals = PyDict::new(py); py.run( - ffi::c_str!("import decimal\npy_dec = decimal.Decimal(\"NaN\")"), + c"import decimal\npy_dec = decimal.Decimal(\"NaN\")", None, Some(&locals), ) @@ -220,7 +219,7 @@ mod test_bigdecimal { Python::attach(|py| { let locals = PyDict::new(py); py.run( - ffi::c_str!("import decimal\npy_dec = decimal.Decimal(\"Infinity\")"), + c"import decimal\npy_dec = decimal.Decimal(\"Infinity\")", None, Some(&locals), ) diff --git a/src/conversions/chrono.rs b/src/conversions/chrono.rs index 61f50530d35..7d67f504ad5 100644 --- a/src/conversions/chrono.rs +++ b/src/conversions/chrono.rs @@ -56,7 +56,7 @@ use crate::{ types::{PyString, PyStringMethods}, Py, }; -use crate::{ffi, Borrowed, Bound, FromPyObject, IntoPyObjectExt, PyAny, PyErr, PyResult, Python}; +use crate::{Borrowed, Bound, FromPyObject, IntoPyObjectExt, PyAny, PyErr, PyResult, Python}; use chrono::offset::{FixedOffset, Utc}; #[cfg(feature = "chrono-local")] use chrono::Local; @@ -547,7 +547,7 @@ fn warn_truncated_leap_second(obj: &Bound<'_, PyAny>) { if let Err(e) = PyErr::warn( py, &py.get_type::(), - ffi::c_str!("ignored leap-second, `datetime` does not support leap-seconds"), + c"ignored leap-second, `datetime` does not support leap-seconds", 0, ) { e.write_unraisable(py, Some(obj)) @@ -644,14 +644,13 @@ 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::attach(|py| { let locals = crate::types::PyDict::new(py); py.run( - ffi::c_str!("import zoneinfo; zi = zoneinfo.ZoneInfo('Europe/London')"), + c"import zoneinfo; zi = zoneinfo.ZoneInfo('Europe/London')", None, Some(&locals), ) diff --git a/src/conversions/eyre.rs b/src/conversions/eyre.rs index 1ba8ec55152..5923854a188 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,9 +152,7 @@ mod tests { Python::attach(|py| { let locals = [("err", pyerr)].into_py_dict(py).unwrap(); - let pyerr = py - .run(ffi::c_str!("raise err"), None, Some(&locals)) - .unwrap_err(); + let pyerr = py.run(c"raise err", None, Some(&locals)).unwrap_err(); assert_eq!(pyerr.value(py).to_string(), expected_contents); }) } @@ -171,9 +169,7 @@ mod tests { Python::attach(|py| { let locals = [("err", pyerr)].into_py_dict(py).unwrap(); - let pyerr = py - .run(ffi::c_str!("raise err"), None, Some(&locals)) - .unwrap_err(); + let pyerr = py.run(c"raise err", None, Some(&locals)).unwrap_err(); assert_eq!(pyerr.value(py).to_string(), expected_contents); }) } diff --git a/src/conversions/jiff.rs b/src/conversions/jiff.rs index 9cc9ffd3cee..db1a591b968 100644 --- a/src/conversions/jiff.rs +++ b/src/conversions/jiff.rs @@ -524,14 +524,13 @@ 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::attach(|py| { let locals = crate::types::PyDict::new(py); py.run( - ffi::c_str!("import zoneinfo; zi = zoneinfo.ZoneInfo('Europe/London')"), + c"import zoneinfo; zi = zoneinfo.ZoneInfo('Europe/London')", None, Some(&locals), ) diff --git a/src/conversions/num_bigint.rs b/src/conversions/num_bigint.rs index 3344870dc7f..1be763070dd 100644 --- a/src/conversions/num_bigint.rs +++ b/src/conversions/num_bigint.rs @@ -399,7 +399,7 @@ mod tests { PyModule::from_code( py, index_code, - c_str!("index.py"), + c"index.py", &generate_unique_module_name("index"), ) .unwrap() @@ -411,9 +411,7 @@ mod tests { let index = python_index_class(py); let locals = PyDict::new(py); locals.set_item("index", index).unwrap(); - let ob = py - .eval(ffi::c_str!("index.C(10)"), None, Some(&locals)) - .unwrap(); + let ob = py.eval(c"index.C(10)", None, Some(&locals)).unwrap(); let _: BigInt = ob.extract().unwrap(); }); } diff --git a/src/conversions/num_complex.rs b/src/conversions/num_complex.rs index 778680153fe..4464e1addc9 100644 --- a/src/conversions/num_complex.rs +++ b/src/conversions/num_complex.rs @@ -202,7 +202,6 @@ mod tests { use crate::types::PyAnyMethods as _; use crate::types::{complex::PyComplexMethods, PyModule}; use crate::IntoPyObject; - use pyo3_ffi::c_str; #[test] fn from_complex() { @@ -233,17 +232,15 @@ mod tests { Python::attach(|py| { let module = PyModule::from_code( py, - c_str!( - r#" + cr#" 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 - "# - ), - c_str!("test.py"), + "#, + c"test.py", &generate_unique_module_name("test"), ) .unwrap(); @@ -273,8 +270,7 @@ class C: Python::attach(|py| { let module = PyModule::from_code( py, - c_str!( - r#" + cr#" class First: pass class ComplexMixin: def __complex__(self): return 3.0+1.2j @@ -285,9 +281,8 @@ class IndexMixin: class A(First, ComplexMixin): pass class B(First, FloatMixin): pass class C(First, IndexMixin): pass - "# - ), - c_str!("test.py"), + "#, + c"test.py", &generate_unique_module_name("test"), ) .unwrap(); @@ -319,15 +314,13 @@ class C(First, IndexMixin): pass Python::attach(|py| { let module = PyModule::from_code( py, - c_str!( - r#" + cr#" class A: @property def __complex__(self): return lambda: 3.0+1.2j - "# - ), - c_str!("test.py"), + "#, + c"test.py", &generate_unique_module_name("test"), ) .unwrap(); @@ -344,15 +337,13 @@ class A: Python::attach(|py| { let module = PyModule::from_code( py, - c_str!( - r#" + cr#" class MyComplex: def __call__(self): return 3.0+1.2j class A: __complex__ = MyComplex() - "# - ), - c_str!("test.py"), + "#, + c"test.py", &generate_unique_module_name("test"), ) .unwrap(); diff --git a/src/conversions/num_rational.rs b/src/conversions/num_rational.rs index c64a48323a2..c15b81b6174 100644 --- a/src/conversions/num_rational.rs +++ b/src/conversions/num_rational.rs @@ -126,7 +126,7 @@ mod tests { Python::attach(|py| { let locals = PyDict::new(py); py.run( - ffi::c_str!("import fractions\npy_frac = fractions.Fraction(-0.125)"), + c"import fractions\npy_frac = fractions.Fraction(-0.125)", None, Some(&locals), ) @@ -142,7 +142,7 @@ mod tests { Python::attach(|py| { let locals = PyDict::new(py); py.run( - ffi::c_str!("not_fraction = \"contains_incorrect_atts\""), + c"not_fraction = \"contains_incorrect_atts\"", None, Some(&locals), ) @@ -157,9 +157,7 @@ mod tests { Python::attach(|py| { let locals = PyDict::new(py); py.run( - ffi::c_str!( - "import fractions\npy_frac = fractions.Fraction(fractions.Fraction(10))" - ), + c"import fractions\npy_frac = fractions.Fraction(fractions.Fraction(10))", None, Some(&locals), ) @@ -176,7 +174,7 @@ mod tests { Python::attach(|py| { let locals = PyDict::new(py); py.run( - ffi::c_str!("import fractions\n\nfrom decimal import Decimal\npy_frac = fractions.Fraction(Decimal(\"1.1\"))"), + c"import fractions\n\nfrom decimal import Decimal\npy_frac = fractions.Fraction(Decimal(\"1.1\"))", None, Some(&locals), ) @@ -193,7 +191,7 @@ mod tests { Python::attach(|py| { let locals = PyDict::new(py); py.run( - ffi::c_str!("import fractions\npy_frac = fractions.Fraction(10,5)"), + c"import fractions\npy_frac = fractions.Fraction(10,5)", None, Some(&locals), ) @@ -258,7 +256,7 @@ mod tests { Python::attach(|py| { let locals = PyDict::new(py); let py_bound = py.run( - ffi::c_str!("import fractions\npy_frac = fractions.Fraction(\"Infinity\")"), + c"import fractions\npy_frac = fractions.Fraction(\"Infinity\")", None, Some(&locals), ); diff --git a/src/conversions/ordered_float.rs b/src/conversions/ordered_float.rs index 71bcd29bca7..1ddc057120a 100644 --- a/src/conversions/ordered_float.rs +++ b/src/conversions/ordered_float.rs @@ -101,7 +101,6 @@ float_conversions!(NotNan, f64, |val| NotNan::new(val) #[cfg(test)] mod test_ordered_float { use super::*; - use crate::ffi::c_str; use crate::py_run; use crate::types::PyAnyMethods; @@ -295,7 +294,7 @@ mod test_ordered_float { #[test] fn $test_name() { Python::attach(|py| { - let nan_py = py.eval(c_str!("float('nan')"), None, None).unwrap(); + let nan_py = py.eval(c"float('nan')", None, None).unwrap(); let nan_rs: Result, _> = nan_py.extract(); diff --git a/src/conversions/rust_decimal.rs b/src/conversions/rust_decimal.rs index d67a076869b..0abe6e58987 100644 --- a/src/conversions/rust_decimal.rs +++ b/src/conversions/rust_decimal.rs @@ -113,7 +113,6 @@ mod test_rust_decimal { use crate::types::PyDict; use std::ffi::CString; - use crate::ffi; #[cfg(not(target_arch = "wasm32"))] use proptest::prelude::*; @@ -194,7 +193,7 @@ mod test_rust_decimal { Python::attach(|py| { let locals = PyDict::new(py); py.run( - ffi::c_str!("import decimal\npy_dec = decimal.Decimal(\"NaN\")"), + c"import decimal\npy_dec = decimal.Decimal(\"NaN\")", None, Some(&locals), ) @@ -210,7 +209,7 @@ mod test_rust_decimal { Python::attach(|py| { let locals = PyDict::new(py); py.run( - ffi::c_str!("import decimal\npy_dec = decimal.Decimal(\"1e3\")"), + c"import decimal\npy_dec = decimal.Decimal(\"1e3\")", None, Some(&locals), ) @@ -227,7 +226,7 @@ mod test_rust_decimal { Python::attach(|py| { let locals = PyDict::new(py); py.run( - ffi::c_str!("import decimal\npy_dec = decimal.Decimal(\"Infinity\")"), + c"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 f5f43eb6dd1..4b312f769b8 100644 --- a/src/conversions/std/array.rs +++ b/src/conversions/std/array.rs @@ -131,7 +131,6 @@ mod tests { use crate::{ conversion::IntoPyObject, - ffi, types::{any::PyAnyMethods, PyBytes, PyBytesMethods}, }; use crate::{types::PyList, PyResult, Python}; @@ -161,11 +160,7 @@ mod tests { fn test_extract_bytes_to_array() { Python::attach(|py| { let v: [u8; 33] = py - .eval( - ffi::c_str!("b'abcabcabcabcabcabcabcabcabcabcabc'"), - None, - None, - ) + .eval(c"b'abcabcabcabcabcabcabcabcabcabcabc'", None, None) .unwrap() .extract() .unwrap(); @@ -176,10 +171,7 @@ mod tests { #[test] fn test_extract_bytes_wrong_length() { Python::attach(|py| { - let v: PyResult<[u8; 3]> = py - .eval(ffi::c_str!("b'abcdefg'"), None, None) - .unwrap() - .extract(); + let v: PyResult<[u8; 3]> = py.eval(c"b'abcdefg'", None, None).unwrap().extract(); assert_eq!( v.unwrap_err().to_string(), "ValueError: expected a sequence of length 3 (got 7)" @@ -192,7 +184,7 @@ mod tests { Python::attach(|py| { let v: [u8; 33] = py .eval( - ffi::c_str!("bytearray(b'abcabcabcabcabcabcabcabcabcabcabc')"), + c"bytearray(b'abcabcabcabcabcabcabcabcabcabcabc')", None, None, ) @@ -207,7 +199,7 @@ mod tests { fn test_extract_small_bytearray_to_array() { Python::attach(|py| { let v: [u8; 3] = py - .eval(ffi::c_str!("bytearray(b'abc')"), None, None) + .eval(c"bytearray(b'abc')", None, None) .unwrap() .extract() .unwrap(); @@ -231,7 +223,7 @@ mod tests { fn test_extract_invalid_sequence_length() { Python::attach(|py| { let v: PyResult<[u8; 3]> = py - .eval(ffi::c_str!("bytearray(b'abcdefg')"), None, None) + .eval(c"bytearray(b'abcdefg')", None, None) .unwrap() .extract(); assert_eq!( @@ -276,7 +268,7 @@ mod tests { #[test] fn test_extract_non_iterable_to_array() { Python::attach(|py| { - let v = py.eval(ffi::c_str!("42"), None, None).unwrap(); + let v = py.eval(c"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 71823ae9557..b27d56da343 100644 --- a/src/conversions/std/num.rs +++ b/src/conversions/std/num.rs @@ -837,7 +837,7 @@ mod test_128bit_integers { #[test] fn test_i128_overflow() { Python::attach(|py| { - let obj = py.eval(ffi::c_str!("(1 << 130) * -1"), None, None).unwrap(); + let obj = py.eval(c"(1 << 130) * -1", None, None).unwrap(); let err = obj.extract::().unwrap_err(); assert!(err.is_instance_of::(py)); }) @@ -846,7 +846,7 @@ mod test_128bit_integers { #[test] fn test_u128_overflow() { Python::attach(|py| { - let obj = py.eval(ffi::c_str!("1 << 130"), None, None).unwrap(); + let obj = py.eval(c"1 << 130", None, None).unwrap(); let err = obj.extract::().unwrap_err(); assert!(err.is_instance_of::(py)); }) @@ -890,7 +890,7 @@ mod test_128bit_integers { #[test] fn test_nonzero_i128_overflow() { Python::attach(|py| { - let obj = py.eval(ffi::c_str!("(1 << 130) * -1"), None, None).unwrap(); + let obj = py.eval(c"(1 << 130) * -1", None, None).unwrap(); let err = obj.extract::().unwrap_err(); assert!(err.is_instance_of::(py)); }) @@ -899,7 +899,7 @@ mod test_128bit_integers { #[test] fn test_nonzero_u128_overflow() { Python::attach(|py| { - let obj = py.eval(ffi::c_str!("1 << 130"), None, None).unwrap(); + let obj = py.eval(c"1 << 130", None, None).unwrap(); let err = obj.extract::().unwrap_err(); assert!(err.is_instance_of::(py)); }) @@ -908,7 +908,7 @@ mod test_128bit_integers { #[test] fn test_nonzero_i128_zero_value() { Python::attach(|py| { - let obj = py.eval(ffi::c_str!("0"), None, None).unwrap(); + let obj = py.eval(c"0", None, None).unwrap(); let err = obj.extract::().unwrap_err(); assert!(err.is_instance_of::(py)); }) @@ -917,7 +917,7 @@ mod test_128bit_integers { #[test] fn test_nonzero_u128_zero_value() { Python::attach(|py| { - let obj = py.eval(ffi::c_str!("0"), None, None).unwrap(); + let obj = py.eval(c"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 ad26188a9d6..c9598d1fd31 100644 --- a/src/conversions/std/slice.rs +++ b/src/conversions/std/slice.rs @@ -95,7 +95,6 @@ mod tests { use crate::{ conversion::IntoPyObject, - ffi, types::{any::PyAnyMethods, PyBytes, PyBytesMethods, PyList}, Python, }; @@ -103,7 +102,7 @@ mod tests { #[test] fn test_extract_bytes() { Python::attach(|py| { - let py_bytes = py.eval(ffi::c_str!("b'Hello Python'"), None, None).unwrap(); + let py_bytes = py.eval(c"b'Hello Python'", None, None).unwrap(); let bytes: &[u8] = py_bytes.extract().unwrap(); assert_eq!(bytes, b"Hello Python"); }); @@ -112,17 +111,15 @@ mod tests { #[test] fn test_cow_impl() { Python::attach(|py| { - let bytes = py.eval(ffi::c_str!(r#"b"foobar""#), None, None).unwrap(); + let bytes = py.eval(cr#"b"foobar""#, None, None).unwrap(); let cow = bytes.extract::>().unwrap(); assert_eq!(cow, Cow::<[u8]>::Borrowed(b"foobar")); - let byte_array = py - .eval(ffi::c_str!(r#"bytearray(b"foobar")"#), None, None) - .unwrap(); + let byte_array = py.eval(cr#"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(ffi::c_str!("42"), None, None).unwrap(); + let something_else_entirely = py.eval(c"42", None, None).unwrap(); something_else_entirely .extract::>() .unwrap_err(); diff --git a/src/conversions/std/vec.rs b/src/conversions/std/vec.rs index 7831683088b..c62aa7f1d56 100644 --- a/src/conversions/std/vec.rs +++ b/src/conversions/std/vec.rs @@ -103,7 +103,7 @@ where mod tests { use crate::conversion::IntoPyObject; use crate::types::{PyAnyMethods, PyBytes, PyBytesMethods, PyList}; - use crate::{ffi, Python}; + use crate::Python; #[test] fn test_vec_intopyobject_impl() { @@ -157,11 +157,7 @@ mod tests { #[test] fn test_extract_tuple_to_vec() { Python::attach(|py| { - let v: Vec = py - .eval(ffi::c_str!("(1, 2)"), None, None) - .unwrap() - .extract() - .unwrap(); + let v: Vec = py.eval(c"(1, 2)", None, None).unwrap().extract().unwrap(); assert_eq!(v, [1, 2]); }); } @@ -170,7 +166,7 @@ mod tests { fn test_extract_range_to_vec() { Python::attach(|py| { let v: Vec = py - .eval(ffi::c_str!("range(1, 5)"), None, None) + .eval(c"range(1, 5)", None, None) .unwrap() .extract() .unwrap(); @@ -182,7 +178,7 @@ mod tests { fn test_extract_bytearray_to_vec() { Python::attach(|py| { let v: Vec = py - .eval(ffi::c_str!("bytearray(b'abc')"), None, None) + .eval(c"bytearray(b'abc')", None, None) .unwrap() .extract() .unwrap(); diff --git a/src/err/err_state.rs b/src/err/err_state.rs index e2583a863da..44e83edc4e3 100644 --- a/src/err/err_state.rs +++ b/src/err/err_state.rs @@ -356,7 +356,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(), - ffi::c_str!("exceptions must derive from BaseException").as_ptr(), + c"exceptions must derive from BaseException".as_ptr(), ) } else { ffi::PyErr_SetObject(ptype.as_ptr(), pvalue.as_ptr()) diff --git a/src/err/mod.rs b/src/err/mod.rs index 680c759f69a..8c69f82a761 100644 --- a/src/err/mod.rs +++ b/src/err/mod.rs @@ -490,7 +490,7 @@ impl PyErr { /// # fn main() -> PyResult<()> { /// Python::attach(|py| { /// let user_warning = py.get_type::(); - /// PyErr::warn(py, &user_warning, c_str!("I am warning you"), 0)?; + /// PyErr::warn(py, &user_warning, c"I am warning you", 0)?; /// Ok(()) /// }) /// # } @@ -745,7 +745,7 @@ mod tests { use crate::exceptions::{self, PyTypeError, PyValueError}; use crate::impl_::pyclass::{value_of, IsSend, IsSync}; use crate::test_utils::assert_warnings; - use crate::{ffi, PyErr, PyTypeInfo, Python}; + use crate::{PyErr, PyTypeInfo, Python}; #[test] fn no_error() { @@ -836,7 +836,7 @@ mod tests { Python::attach(|py| { let err = py - .run(ffi::c_str!("raise Exception('banana')"), None, None) + .run(c"raise Exception('banana')", None, None) .expect_err("raising should have given us an error"); let debug_str = format!("{err:?}"); @@ -862,7 +862,7 @@ mod tests { fn err_display() { Python::attach(|py| { let err = py - .run(ffi::c_str!("raise Exception('banana')"), None, None) + .run(c"raise Exception('banana')", None, None) .expect_err("raising should have given us an error"); assert_eq!(err.to_string(), "Exception: banana"); }); @@ -902,13 +902,13 @@ mod tests { fn test_pyerr_cause() { Python::attach(|py| { let err = py - .run(ffi::c_str!("raise Exception('banana')"), None, None) + .run(c"raise Exception('banana')", None, None) .expect_err("raising should have given us an error"); assert!(err.cause(py).is_none()); let err = py .run( - ffi::c_str!("raise Exception('banana') from Exception('apple')"), + c"raise Exception('banana') from Exception('apple')", None, None, ) @@ -946,7 +946,7 @@ mod tests { // First, test the warning is emitted assert_warnings!( py, - { PyErr::warn(py, &cls, ffi::c_str!("I am warning you"), 0).unwrap() }, + { PyErr::warn(py, &cls, c"I am warning you", 0).unwrap() }, [(exceptions::PyUserWarning, "I am warning you")] ); @@ -954,7 +954,7 @@ mod tests { warnings .call_method1("simplefilter", ("error", &cls)) .unwrap(); - PyErr::warn(py, &cls, ffi::c_str!("I am warning you"), 0).unwrap_err(); + PyErr::warn(py, &cls, c"I am warning you", 0).unwrap_err(); // Test with error for an explicit module warnings.call_method0("resetwarnings").unwrap(); @@ -965,15 +965,15 @@ mod tests { // This has the wrong module and will not raise, just be emitted assert_warnings!( py, - { PyErr::warn(py, &cls, ffi::c_str!("I am warning you"), 0).unwrap() }, + { PyErr::warn(py, &cls, c"I am warning you", 0).unwrap() }, [(exceptions::PyUserWarning, "I am warning you")] ); let err = PyErr::warn_explicit( py, &cls, - ffi::c_str!("I am warning you"), - ffi::c_str!("pyo3test.py"), + c"I am warning you", + c"pyo3test.py", 427, None, None, diff --git a/src/exceptions.rs b/src/exceptions.rs index a0936eedfbf..56e6d09261d 100644 --- a/src/exceptions.rs +++ b/src/exceptions.rs @@ -142,12 +142,12 @@ macro_rules! import_exception_bound { /// # locals.set_item("MyError", py.get_type::())?; /// # locals.set_item("raise_myerror", fun)?; /// # -/// # py.run(pyo3::ffi::c_str!( -/// # "try: +/// # py.run( +/// # c"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), /// # )?; @@ -640,13 +640,7 @@ impl PyUnicodeDecodeError { err: std::str::Utf8Error, ) -> PyResult> { let pos = err.valid_up_to(); - PyUnicodeDecodeError::new( - py, - ffi::c_str!("utf-8"), - input, - pos..(pos + 1), - ffi::c_str!("invalid utf-8"), - ) + PyUnicodeDecodeError::new(py, c"utf-8", input, pos..(pos + 1), c"invalid utf-8") } } @@ -802,13 +796,9 @@ mod tests { .map_err(|e| e.display(py)) .expect("could not setitem"); - py.run( - ffi::c_str!("assert isinstance(exc, socket.gaierror)"), - None, - Some(&d), - ) - .map_err(|e| e.display(py)) - .expect("assertion failed"); + py.run(c"assert isinstance(exc, socket.gaierror)", None, Some(&d)) + .map_err(|e| e.display(py)) + .expect("assertion failed"); }); } @@ -830,7 +820,7 @@ mod tests { .expect("could not setitem"); py.run( - ffi::c_str!("assert isinstance(exc, email.errors.MessageError)"), + c"assert isinstance(exc, email.errors.MessageError)", None, Some(&d), ) @@ -847,23 +837,19 @@ mod tests { let error_type = py.get_type::(); let ctx = [("CustomError", error_type)].into_py_dict(py).unwrap(); let type_description: String = py - .eval(ffi::c_str!("str(CustomError)"), None, Some(&ctx)) + .eval(c"str(CustomError)", None, Some(&ctx)) .unwrap() .extract() .unwrap(); assert_eq!(type_description, ""); py.run( - ffi::c_str!("assert CustomError('oops').args == ('oops',)"), - None, - Some(&ctx), - ) - .unwrap(); - py.run( - ffi::c_str!("assert CustomError.__doc__ is None"), + c"assert CustomError('oops').args == ('oops',)", None, Some(&ctx), ) .unwrap(); + py.run(c"assert CustomError.__doc__ is None", None, Some(&ctx)) + .unwrap(); }); } @@ -874,7 +860,7 @@ mod tests { let error_type = py.get_type::(); let ctx = [("CustomError", error_type)].into_py_dict(py).unwrap(); let type_description: String = py - .eval(ffi::c_str!("str(CustomError)"), None, Some(&ctx)) + .eval(c"str(CustomError)", None, Some(&ctx)) .unwrap() .extract() .unwrap(); @@ -893,19 +879,19 @@ mod tests { let error_type = py.get_type::(); let ctx = [("CustomError", error_type)].into_py_dict(py).unwrap(); let type_description: String = py - .eval(ffi::c_str!("str(CustomError)"), None, Some(&ctx)) + .eval(c"str(CustomError)", None, Some(&ctx)) .unwrap() .extract() .unwrap(); assert_eq!(type_description, ""); py.run( - ffi::c_str!("assert CustomError('oops').args == ('oops',)"), + c"assert CustomError('oops').args == ('oops',)", None, Some(&ctx), ) .unwrap(); py.run( - ffi::c_str!("assert CustomError.__doc__ == 'Some docs'"), + c"assert CustomError.__doc__ == 'Some docs'", None, Some(&ctx), ) @@ -926,19 +912,19 @@ mod tests { let error_type = py.get_type::(); let ctx = [("CustomError", error_type)].into_py_dict(py).unwrap(); let type_description: String = py - .eval(ffi::c_str!("str(CustomError)"), None, Some(&ctx)) + .eval(c"str(CustomError)", None, Some(&ctx)) .unwrap() .extract() .unwrap(); assert_eq!(type_description, ""); py.run( - ffi::c_str!("assert CustomError('oops').args == ('oops',)"), + c"assert CustomError('oops').args == ('oops',)", None, Some(&ctx), ) .unwrap(); py.run( - ffi::c_str!("assert CustomError.__doc__ == 'Some more docs'"), + c"assert CustomError.__doc__ == 'Some more docs'", None, Some(&ctx), ) @@ -950,7 +936,7 @@ mod tests { fn native_exception_debug() { Python::attach(|py| { let exc = py - .run(ffi::c_str!("raise Exception('banana')"), None, None) + .run(c"raise Exception('banana')", None, None) .expect_err("raising should have given us an error") .into_value(py) .into_bound(py); @@ -965,7 +951,7 @@ mod tests { fn native_exception_display() { Python::attach(|py| { let exc = py - .run(ffi::c_str!("raise Exception('banana')"), None, None) + .run(c"raise Exception('banana')", None, None) .expect_err("raising should have given us an error") .into_value(py) .into_bound(py); @@ -1045,7 +1031,7 @@ mod tests { ) }); test_exception!(PyUnicodeEncodeError, |py| py - .eval(ffi::c_str!("chr(40960).encode('ascii')"), None, None) + .eval(c"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 667be67c772..de6bf0d86bf 100644 --- a/src/ffi/tests.rs +++ b/src/ffi/tests.rs @@ -1,4 +1,4 @@ -use crate::ffi::{self, *}; +use crate::ffi::*; use crate::types::any::PyAnyMethods; use crate::Python; @@ -24,7 +24,7 @@ fn test_datetime_fromtimestamp() { let locals = PyDict::new(py); locals.set_item("dt", dt).unwrap(); py.run( - ffi::c_str!("import datetime; assert dt == datetime.datetime.fromtimestamp(100)"), + c"import datetime; assert dt == datetime.datetime.fromtimestamp(100)", None, Some(&locals), ) @@ -46,7 +46,7 @@ fn test_date_fromtimestamp() { let locals = PyDict::new(py); locals.set_item("dt", dt).unwrap(); py.run( - ffi::c_str!("import datetime; assert dt == datetime.date.fromtimestamp(100)"), + c"import datetime; assert dt == datetime.date.fromtimestamp(100)", None, Some(&locals), ) @@ -66,7 +66,7 @@ fn test_utc_timezone() { let locals = PyDict::new(py); locals.set_item("utc_timezone", utc_timezone).unwrap(); py.run( - ffi::c_str!("import datetime; assert utc_timezone is datetime.timezone.utc"), + c"import datetime; assert utc_timezone is datetime.timezone.utc", None, Some(&locals), ) @@ -302,7 +302,7 @@ fn test_get_tzinfo() { #[test] fn test_inc_dec_ref() { Python::attach(|py| { - let obj = py.eval(ffi::c_str!("object()"), None, None).unwrap(); + let obj = py.eval(c"object()", None, None).unwrap(); let ref_count = obj.get_refcnt(); let ptr = obj.as_ptr(); diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index a96ee94991e..d25e78d3a1e 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -1454,7 +1454,7 @@ mod tests { match methods.first() { Some(PyMethodDefType::StructMember(member)) => { - assert_eq!(unsafe { CStr::from_ptr(member.name) }, ffi::c_str!("value")); + assert_eq!(unsafe { CStr::from_ptr(member.name) }, c"value"); assert_eq!(member.type_code, ffi::Py_T_OBJECT_EX); assert_eq!( member.offset, @@ -1492,8 +1492,8 @@ mod tests { match methods.first() { Some(PyMethodDefType::Getter(getter)) => { - assert_eq!(getter.name, ffi::c_str!("value")); - assert_eq!(getter.doc, ffi::c_str!("")); + assert_eq!(getter.name, c"value"); + assert_eq!(getter.doc, c""); // tests for the function pointer are in test_getter_setter.py } _ => panic!("Expected a StructMember"), diff --git a/src/impl_/pyclass/doc.rs b/src/impl_/pyclass/doc.rs index 616800e3fdd..4e7a1ee2060 100644 --- a/src/impl_/pyclass/doc.rs +++ b/src/impl_/pyclass/doc.rs @@ -60,9 +60,6 @@ pub const fn doc_bytes_as_cstr(bytes: &'static [u8]) -> &'static ::std::ffi::CSt #[cfg(test)] mod tests { - - use crate::ffi; - use super::*; #[test] @@ -104,7 +101,7 @@ mod tests { #[test] fn test_doc_bytes_as_cstr() { let cstr = doc_bytes_as_cstr(b"MyClass\0"); - assert_eq!(cstr, ffi::c_str!("MyClass")); + assert_eq!(cstr, c"MyClass"); } #[test] diff --git a/src/impl_/pymethods.rs b/src/impl_/pymethods.rs index 980619bb81d..521914ea392 100644 --- a/src/impl_/pymethods.rs +++ b/src/impl_/pymethods.rs @@ -731,9 +731,9 @@ mod tests { Python::attach(|py| { let def = PyFunctionDef::from_method_def(PyMethodDef::fastcall_cfunction_with_keywords( - ffi::c_str!("test"), + c"test", accepts_no_arguments, - ffi::c_str!("doc"), + c"doc", )); // leak to make it 'static // deliberately done at runtime to have coverage of `PyFunctionDef::from_method_def` diff --git a/src/impl_/pymodule.rs b/src/impl_/pymodule.rs index c743a5354ff..7151ad3bad3 100644 --- a/src/impl_/pymodule.rs +++ b/src/impl_/pymodule.rs @@ -229,7 +229,6 @@ mod tests { }; use crate::{ - ffi, types::{any::PyAnyMethods, module::PyModuleMethods, PyModule}, Bound, PyResult, Python, }; @@ -240,8 +239,8 @@ mod tests { fn module_init() { static MODULE_DEF: ModuleDef = unsafe { ModuleDef::new( - ffi::c_str!("test_module"), - ffi::c_str!("some doc"), + c"test_module", + c"some doc", ModuleInitializer(|m| { m.add("SOME_CONSTANT", 42)?; Ok(()) @@ -281,8 +280,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: &CStr = ffi::c_str!("test_module"); - static DOC: &CStr = ffi::c_str!("some doc"); + static NAME: &CStr = c"test_module"; + static DOC: &CStr = c"some doc"; static INIT_CALLED: AtomicBool = AtomicBool::new(false); diff --git a/src/instance.rs b/src/instance.rs index 3fe7ba24de9..09f0987cfbc 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -2398,7 +2398,6 @@ mod tests { use crate::test_utils::generate_unique_module_name; use crate::types::{dict::IntoPyDict, PyAnyMethods, PyCapsule, PyDict, PyString}; use crate::{ffi, Borrowed, IntoPyObjectExt, PyAny, PyResult, Python}; - use pyo3_ffi::c_str; use std::ffi::CStr; #[test] @@ -2507,15 +2506,12 @@ mod tests { use crate::types::PyModule; Python::attach(|py| { - const CODE: &CStr = c_str!( - r#" + const CODE: &CStr = cr#" class A: pass a = A() - "# - ); - let module = - PyModule::from_code(py, CODE, c_str!(""), &generate_unique_module_name(""))?; + "#; + let module = PyModule::from_code(py, CODE, c"", &generate_unique_module_name(""))?; let instance: Py = module.getattr("a")?.into(); instance.getattr(py, "foo").unwrap_err(); @@ -2537,15 +2533,12 @@ a = A() use crate::types::PyModule; Python::attach(|py| { - const CODE: &CStr = c_str!( - r#" + const CODE: &CStr = cr#" class A: pass a = A() - "# - ); - let module = - PyModule::from_code(py, CODE, c_str!(""), &generate_unique_module_name(""))?; + "#; + let module = PyModule::from_code(py, CODE, c"", &generate_unique_module_name(""))?; let instance: Py = module.getattr("a")?.into(); let foo = crate::intern!(py, "foo"); @@ -2561,7 +2554,7 @@ a = A() #[test] fn invalid_attr() -> PyResult<()> { Python::attach(|py| { - let instance: Py = py.eval(ffi::c_str!("object()"), None, None)?.into(); + let instance: Py = py.eval(c"object()", None, None)?.into(); instance.getattr(py, "foo").unwrap_err(); @@ -2574,7 +2567,7 @@ a = A() #[test] fn test_py2_from_py_object() { Python::attach(|py| { - let instance = py.eval(ffi::c_str!("object()"), None, None).unwrap(); + let instance = py.eval(c"object()", None, None).unwrap(); let ptr = instance.as_ptr(); let instance: Bound<'_, PyAny> = instance.extract().unwrap(); assert_eq!(instance.as_ptr(), ptr); @@ -2584,7 +2577,7 @@ a = A() #[test] fn test_py2_into_py_object() { Python::attach(|py| { - let instance = py.eval(ffi::c_str!("object()"), None, None).unwrap(); + let instance = py.eval(c"object()", None, None).unwrap(); let ptr = instance.as_ptr(); let instance: Py = instance.clone().unbind(); assert_eq!(instance.as_ptr(), ptr); diff --git a/src/internal/state.rs b/src/internal/state.rs index ba92e512800..9d9194f3e7d 100644 --- a/src/internal/state.rs +++ b/src/internal/state.rs @@ -362,12 +362,10 @@ fn decrement_attach_count() { mod tests { use super::*; - use crate::{ffi, types::PyAnyMethods, Py, PyAny, Python}; + use crate::{types::PyAnyMethods, Py, PyAny, Python}; fn get_object(py: Python<'_>) -> Py { - py.eval(ffi::c_str!("object()"), None, None) - .unwrap() - .unbind() + py.eval(c"object()", None, None).unwrap().unbind() } #[cfg(not(pyo3_disable_reference_pool))] @@ -513,7 +511,7 @@ mod tests { #[test] fn recursive_attach_ok() { Python::attach(|py| { - let obj = Python::attach(|_| py.eval(ffi::c_str!("object()"), None, None).unwrap()); + let obj = Python::attach(|_| py.eval(c"object()", None, None).unwrap()); assert_eq!(obj.get_refcnt(), 1); }) } diff --git a/src/interpreter_lifecycle.rs b/src/interpreter_lifecycle.rs index d672f887e02..0ffbff7f0d4 100644 --- a/src/interpreter_lifecycle.rs +++ b/src/interpreter_lifecycle.rs @@ -51,7 +51,7 @@ pub fn prepare_freethreaded_python() { /// ```rust /// unsafe { /// pyo3::with_embedded_python_interpreter(|py| { -/// if let Err(e) = py.run(pyo3::ffi::c_str!("print('Hello World')"), None, None) { +/// if let Err(e) = py.run(c"print('Hello World')", None, None) { /// // We must make sure to not return a `PyErr`! /// e.print(py); /// } diff --git a/src/lib.rs b/src/lib.rs index ce70e11439a..ab2fb3de5d5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -260,7 +260,7 @@ //! let version: String = sys.getattr("version")?.extract()?; //! //! let locals = [("os", py.import("os")?)].into_py_dict(py)?; -//! let code = c_str!("os.getenv('USER') or os.getenv('USERNAME') or 'Unknown'"); +//! let code = c"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); diff --git a/src/macros.rs b/src/macros.rs index e5e45097fd8..4b3d158c141 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -117,7 +117,7 @@ macro_rules! py_run_impl { // 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($crate::ffi::c_str!("import sys; sys.stderr.flush()"), None, None) + $py.run(c"import sys; sys.stderr.flush()", None, None) .unwrap(); ::std::panic!("{}", $code) } diff --git a/src/marker.rs b/src/marker.rs index 91371f06467..4c4cc7d7a82 100644 --- a/src/marker.rs +++ b/src/marker.rs @@ -407,7 +407,7 @@ impl Python<'_> { /// /// # fn main() -> PyResult<()> { /// Python::attach(|py| -> PyResult<()> { - /// let x: i32 = py.eval(c_str!("5"), None, None)?.extract()?; + /// let x: i32 = py.eval(c"5", None, None)?.extract()?; /// assert_eq!(x, 5); /// Ok(()) /// }) @@ -475,7 +475,7 @@ impl Python<'_> { /// /// # fn main() -> PyResult<()> { /// Python::initialize(); - /// Python::attach(|py| py.run(pyo3::ffi::c_str!("print('Hello World')"), None, None)) + /// Python::attach(|py| py.run(c"print('Hello World')", None, None)) /// # } /// ``` #[cfg(not(any(PyPy, GraalPy)))] @@ -618,7 +618,7 @@ impl<'py> Python<'py> { /// # use pyo3::prelude::*; /// # use pyo3::ffi::c_str; /// # Python::attach(|py| { - /// let result = py.eval(c_str!("[i * 10 for i in range(5)]"), None, None).unwrap(); + /// let result = py.eval(c"[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]) /// # }); @@ -629,12 +629,7 @@ impl<'py> Python<'py> { globals: Option<&Bound<'py, PyDict>>, locals: Option<&Bound<'py, PyDict>>, ) -> PyResult> { - let code = PyCode::compile( - self, - code, - ffi::c_str!(""), - crate::types::PyCodeInput::Eval, - )?; + let code = PyCode::compile(self, code, c"", crate::types::PyCodeInput::Eval)?; code.run(globals, locals) } @@ -655,12 +650,11 @@ impl<'py> Python<'py> { /// }; /// Python::attach(|py| { /// let locals = PyDict::new(py); - /// py.run(c_str!( - /// r#" + /// py.run(cr#" /// import base64 /// s = 'Hello Rust!' /// ret = base64.b64encode(s.encode('utf-8')) - /// "#), + /// "#, /// None, /// Some(&locals), /// ) @@ -679,12 +673,7 @@ impl<'py> Python<'py> { globals: Option<&Bound<'py, PyDict>>, locals: Option<&Bound<'py, PyDict>>, ) -> PyResult<()> { - let code = PyCode::compile( - self, - code, - ffi::c_str!(""), - crate::types::PyCodeInput::File, - )?; + let code = PyCode::compile(self, code, c"", crate::types::PyCodeInput::File)?; code.run(globals, locals).map(|obj| { debug_assert!(obj.is_none()); }) @@ -861,7 +850,7 @@ mod tests { Python::attach(|py| { // Make sure builtin names are accessible let v: i32 = py - .eval(ffi::c_str!("min(1, 2)"), None, None) + .eval(c"min(1, 2)", None, None) .map_err(|e| e.display(py)) .unwrap() .extract() @@ -872,7 +861,7 @@ mod tests { // Inject our own global namespace let v: i32 = py - .eval(ffi::c_str!("foo + 29"), Some(&d), None) + .eval(c"foo + 29", Some(&d), None) .unwrap() .extract() .unwrap(); @@ -880,7 +869,7 @@ mod tests { // Inject our own local namespace let v: i32 = py - .eval(ffi::c_str!("foo + 29"), None, Some(&d)) + .eval(c"foo + 29", None, Some(&d)) .unwrap() .extract() .unwrap(); @@ -888,7 +877,7 @@ mod tests { // Make sure builtin names are still accessible when using a local namespace let v: i32 = py - .eval(ffi::c_str!("min(foo, 2)"), None, Some(&d)) + .eval(c"min(foo, 2)", None, Some(&d)) .unwrap() .extract() .unwrap(); @@ -985,7 +974,7 @@ mod tests { assert_eq!(py.Ellipsis().to_string(), "Ellipsis"); let v = py - .eval(ffi::c_str!("..."), None, None) + .eval(c"...", None, None) .map_err(|e| e.display(py)) .unwrap(); @@ -1000,7 +989,7 @@ mod tests { Python::attach(|py| { let namespace = PyDict::new(py); py.run( - ffi::c_str!("class Foo: pass\na = int(3)"), + c"class Foo: pass\na = int(3)", Some(&namespace), Some(&namespace), ) diff --git a/src/pyclass/create_type_object.rs b/src/pyclass/create_type_object.rs index c4229e08c53..26252ae9836 100644 --- a/src/pyclass/create_type_object.rs +++ b/src/pyclass/create_type_object.rs @@ -265,7 +265,7 @@ impl PyTypeBuilder { } property_defs.push(ffi::PyGetSetDef { - name: ffi::c_str!("__dict__").as_ptr(), + name: c"__dict__".as_ptr(), get: Some(get_dict), set: Some(ffi::PyObject_GenericSetDict), doc: ptr::null(), @@ -382,15 +382,13 @@ impl PyTypeBuilder { // __dict__ support if let Some(dict_offset) = dict_offset { self.member_defs - .push(offset_def(ffi::c_str!("__dictoffset__"), dict_offset)); + .push(offset_def(c"__dictoffset__", dict_offset)); } // weakref support if let Some(weaklist_offset) = weaklist_offset { - self.member_defs.push(offset_def( - ffi::c_str!("__weaklistoffset__"), - weaklist_offset, - )); + self.member_defs + .push(offset_def(c"__weaklistoffset__", weaklist_offset)); } } diff --git a/src/types/any.rs b/src/types/any.rs index cf08f22a9cf..90034686a85 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -449,16 +449,16 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// use pyo3_ffi::c_str; /// use std::ffi::CStr; /// - /// const CODE: &CStr = c_str!(r#" + /// const CODE: &CStr = cr#" /// def function(*args, **kwargs): /// assert args == ("hello",) /// assert kwargs == {"cruel": "world"} /// return "called with args and kwargs" - /// "#); + /// "#; /// /// # fn main() -> PyResult<()> { /// Python::attach(|py| { - /// let module = PyModule::from_code(py, CODE, c_str!("func.py"), c_str!(""))?; + /// let module = PyModule::from_code(py, CODE, c"func.py", c"")?; /// let fun = module.getattr("function")?; /// let args = ("hello",); /// let kwargs = PyDict::new(py); @@ -506,16 +506,16 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// use pyo3_ffi::c_str; /// use std::ffi::CStr; /// - /// const CODE: &CStr = c_str!(r#" + /// const CODE: &CStr = cr#" /// def function(*args, **kwargs): /// assert args == ("hello",) /// assert kwargs == {} /// return "called with args" - /// "#); + /// "#; /// /// # fn main() -> PyResult<()> { /// Python::attach(|py| { - /// let module = PyModule::from_code(py, CODE, c_str!("func.py"), c_str!(""))?; + /// let module = PyModule::from_code(py, CODE, c"func.py", c"")?; /// let fun = module.getattr("function")?; /// let args = ("hello",); /// let result = fun.call1(args)?; @@ -543,18 +543,18 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// use pyo3_ffi::c_str; /// use std::ffi::CStr; /// - /// const CODE: &CStr = c_str!(r#" + /// const CODE: &CStr = cr#" /// 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::attach(|py| { - /// let module = PyModule::from_code(py, CODE, c_str!("a.py"), c_str!(""))?; + /// let module = PyModule::from_code(py, CODE, c"a.py", c"")?; /// let instance = module.getattr("a")?; /// let args = ("hello",); /// let kwargs = PyDict::new(py); @@ -589,18 +589,18 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// use pyo3_ffi::c_str; /// use std::ffi::CStr; /// - /// const CODE: &CStr = c_str!(r#" + /// const CODE: &CStr = cr#" /// class A: /// def method(self, *args, **kwargs): /// assert args == () /// assert kwargs == {} /// return "called with no arguments" /// a = A() - /// "#); + /// "#; /// /// # fn main() -> PyResult<()> { /// Python::attach(|py| { - /// let module = PyModule::from_code(py, CODE, c_str!("a.py"), c_str!(""))?; + /// let module = PyModule::from_code(py, CODE, c"a.py", c"")?; /// let instance = module.getattr("a")?; /// let result = instance.call_method0("method")?; /// assert_eq!(result.extract::()?, "called with no arguments"); @@ -626,18 +626,18 @@ pub trait PyAnyMethods<'py>: crate::sealed::Sealed { /// use pyo3_ffi::c_str; /// use std::ffi::CStr; /// - /// const CODE: &CStr = c_str!(r#" + /// const CODE: &CStr = cr#" /// class A: /// def method(self, *args, **kwargs): /// assert args == ("hello",) /// assert kwargs == {} /// return "called with args" /// a = A() - /// "#); + /// "#; /// /// # fn main() -> PyResult<()> { /// Python::attach(|py| { - /// let module = PyModule::from_code(py, CODE, c_str!("a.py"), c_str!(""))?; + /// let module = PyModule::from_code(py, CODE, c"a.py", c"")?; /// let instance = module.getattr("a")?; /// let args = ("hello",); /// let result = instance.call_method1("method", args)?; @@ -1656,7 +1656,6 @@ impl<'py> Bound<'py, PyAny> { mod tests { use crate::{ basic::CompareOp, - ffi, test_utils::generate_unique_module_name, types::{IntoPyDict, PyAny, PyAnyMethods, PyBool, PyInt, PyList, PyModule, PyTypeMethods}, Bound, BoundObject, IntoPyObject, PyTypeInfo, Python, @@ -1669,8 +1668,7 @@ mod tests { Python::attach(|py| { let module = PyModule::from_code( py, - c_str!( - r#" + cr#" class CustomCallable: def __call__(self): return 1 @@ -1700,9 +1698,8 @@ class ErrorInDescriptorInt: class NonHeapNonDescriptorInt: # A static-typed callable that doesn't implement `__get__`. These are pretty hard to come by. __int__ = int - "# - ), - c_str!("test.py"), + "#, + c"test.py", &generate_unique_module_name("test"), ) .unwrap(); @@ -1743,17 +1740,15 @@ class NonHeapNonDescriptorInt: Python::attach(|py| { let module = PyModule::from_code( py, - c_str!( - r#" + cr#" class Test: class_str_attribute = "class_string" @property def error(self): raise ValueError("This is an intentional error") - "# - ), - c_str!("test.py"), + "#, + c"test.py", &generate_unique_module_name("test"), ) .unwrap(); @@ -1786,7 +1781,7 @@ class Test: #[test] fn test_call_for_non_existing_method() { Python::attach(|py| { - let a = py.eval(ffi::c_str!("42"), None, None).unwrap(); + let a = py.eval(c"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()); @@ -1809,13 +1804,11 @@ class Test: Python::attach(|py| { let module = PyModule::from_code( py, - c_str!( - r#" + cr#" class SimpleClass: def foo(self): return 42 -"# - ), +"#, c_str!(file!()), &generate_unique_module_name("test_module"), ) @@ -1836,7 +1829,7 @@ class SimpleClass: #[test] fn test_type() { Python::attach(|py| { - let obj = py.eval(ffi::c_str!("42"), None, None).unwrap(); + let obj = py.eval(c"42", None, None).unwrap(); assert_eq!(obj.get_type().as_type_ptr(), obj.get_type_ptr()); }); } @@ -1844,9 +1837,9 @@ class SimpleClass: #[test] fn test_dir() { Python::attach(|py| { - let obj = py.eval(ffi::c_str!("42"), None, None).unwrap(); + let obj = py.eval(c"42", None, None).unwrap(); let dir = py - .eval(ffi::c_str!("dir(42)"), None, None) + .eval(c"dir(42)", None, None) .unwrap() .cast_into::() .unwrap(); @@ -1902,7 +1895,7 @@ class SimpleClass: #[test] fn test_nan_eq() { Python::attach(|py| { - let nan = py.eval(ffi::c_str!("float('nan')"), None, None).unwrap(); + let nan = py.eval(c"float('nan')", None, None).unwrap(); assert!(nan.compare(&nan).is_err()); }); } diff --git a/src/types/bytearray.rs b/src/types/bytearray.rs index b1f1370f87c..4ff3e4bbb71 100644 --- a/src/types/bytearray.rs +++ b/src/types/bytearray.rs @@ -161,14 +161,13 @@ pub trait PyByteArrayMethods<'py>: crate::sealed::Sealed { /// # let locals = pyo3::types::PyDict::new(py); /// # locals.set_item("a_valid_function", fun)?; /// # - /// # py.run(pyo3::ffi::c_str!( - /// # r#"b = bytearray(b"hello world") + /// # py.run(cr#"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/capsule.rs b/src/types/capsule.rs index 1acf90e3f96..96fc174c5fc 100644 --- a/src/types/capsule.rs +++ b/src/types/capsule.rs @@ -34,12 +34,12 @@ use std::ptr::{self, NonNull}; /// /// let r = Python::attach(|py| -> PyResult<()> { /// let foo = Foo { val: 123 }; -/// let capsule = PyCapsule::new(py, foo, Some(c_str!("builtins.capsule").to_owned()))?; +/// let capsule = PyCapsule::new(py, foo, Some(c"builtins.capsule".to_owned()))?; /// /// let module = PyModule::import(py, "builtins")?; /// module.add("capsule", capsule)?; /// -/// let cap: &Foo = unsafe { PyCapsule::import(py, c_str!("builtins.capsule"))? }; +/// let cap: &Foo = unsafe { PyCapsule::import(py, c"builtins.capsule")? }; /// assert_eq!(cap.val, 123); /// Ok(()) /// }); @@ -66,7 +66,7 @@ impl PyCapsule { /// use std::ptr::NonNull; /// /// // this can be c"foo" on Rust 1.77+ - /// const NAME: &CStr = c_str!("foo"); + /// const NAME: &CStr = c"foo"; /// /// Python::attach(|py| { /// let capsule = PyCapsule::new(py, 123_u32, Some(NAME.to_owned())).unwrap(); @@ -491,7 +491,6 @@ fn name_ptr(name: Option<&CStr>) -> *const c_char { #[cfg(test)] mod tests { - use crate::ffi; use crate::prelude::PyModule; use crate::types::capsule::PyCapsuleMethods; use crate::types::module::PyModuleMethods; @@ -499,7 +498,7 @@ mod tests { use std::ffi::{c_void, CStr}; use std::sync::mpsc::{channel, Sender}; - const NAME: &CStr = ffi::c_str!("foo"); + const NAME: &CStr = c"foo"; #[test] fn test_pycapsule_struct() { @@ -584,7 +583,7 @@ mod tests { Python::attach(|py| { let foo = Foo { val: 123 }; - let name = ffi::c_str!("builtins.capsule"); + let name = c"builtins.capsule"; let capsule = PyCapsule::new(py, foo, Some(name.to_owned())).unwrap(); @@ -593,8 +592,7 @@ mod tests { // check error when wrong named passed for capsule. // SAFETY: this function will fail so the cast is never done - let result: PyResult<&Foo> = - unsafe { PyCapsule::import(py, ffi::c_str!("builtins.non_existant")) }; + let result: PyResult<&Foo> = unsafe { PyCapsule::import(py, c"builtins.non_existant") }; assert!(result.is_err()); // correct name is okay. diff --git a/src/types/code.rs b/src/types/code.rs index 7cbfa5262bc..1a2068e6ab7 100644 --- a/src/types/code.rs +++ b/src/types/code.rs @@ -87,7 +87,7 @@ impl<'py> PyCodeMethods<'py> for Bound<'py, PyCode> { locals: Option<&Bound<'py, PyDict>>, ) -> PyResult> { let mptr = unsafe { - ffi::compat::PyImport_AddModuleRef(ffi::c_str!("__main__").as_ptr()) + ffi::compat::PyImport_AddModuleRef(c"__main__".as_ptr()) .assume_owned_or_err(self.py())? }; let attr = mptr.getattr(crate::intern!(self.py(), "__dict__"))?; diff --git a/src/types/dict.rs b/src/types/dict.rs index 926354cc684..34add02c608 100644 --- a/src/types/dict.rs +++ b/src/types/dict.rs @@ -1009,7 +1009,7 @@ mod tests { fn test_set_item_refcnt() { Python::attach(|py| { let cnt; - let obj = py.eval(ffi::c_str!("object()"), None, None).unwrap(); + let obj = py.eval(c"object()", None, None).unwrap(); { cnt = obj.get_refcnt(); let _dict = [(10, &obj)].into_py_dict(py); diff --git a/src/types/function.rs b/src/types/function.rs index aa61535d4c2..af41b6a2a4c 100644 --- a/src/types/function.rs +++ b/src/types/function.rs @@ -85,8 +85,8 @@ impl PyCFunction { F: Fn(&Bound<'_, PyTuple>, Option<&Bound<'_, PyDict>>) -> R + Send + 'static, 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!("")); + let name = name.unwrap_or(c"pyo3-closure"); + let doc = doc.unwrap_or(c""); let method_def = pymethods::PyMethodDef::cfunction_with_keywords(name, run_closure::, doc); let def = method_def.into_raw(); @@ -117,7 +117,7 @@ impl PyCFunction { } } -static CLOSURE_CAPSULE_NAME: &CStr = ffi::c_str!("pyo3-closure"); +static CLOSURE_CAPSULE_NAME: &CStr = c"pyo3-closure"; unsafe extern "C" fn run_closure( capsule_ptr: *mut ffi::PyObject, diff --git a/src/types/genericalias.rs b/src/types/genericalias.rs index 04e0407784e..3efafb065aa 100644 --- a/src/types/genericalias.rs +++ b/src/types/genericalias.rs @@ -42,7 +42,7 @@ impl PyGenericAlias { mod tests { use crate::instance::BoundObject; use crate::types::any::PyAnyMethods; - use crate::{ffi, Python}; + use crate::Python; use super::PyGenericAlias; @@ -51,19 +51,10 @@ mod tests { #[test] fn equivalency_test() { Python::attach(|py| { - let list_int = py - .eval(ffi::c_str!("list[int]"), None, None) - .unwrap() - .into_bound(); + let list_int = py.eval(c"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 cls = py.eval(c"list", None, None).unwrap().into_bound(); + let key = py.eval(c"(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/iterator.rs b/src/types/iterator.rs index c41d2fb85eb..7105e1058c6 100644 --- a/src/types/iterator.rs +++ b/src/types/iterator.rs @@ -18,7 +18,7 @@ use crate::{ffi, Bound, Py, PyAny, PyErr, PyResult}; /// /// # fn main() -> PyResult<()> { /// Python::attach(|py| -> PyResult<()> { -/// let list = py.eval(c_str!("iter([1, 2, 3, 4])"), None, None)?; +/// let list = py.eval(c"iter([1, 2, 3, 4])", None, None)?; /// let numbers: PyResult> = list /// .try_iter()? /// .map(|i| i.and_then(|i|i.extract::())) @@ -142,7 +142,7 @@ mod tests { #[cfg(all(not(PyPy), Py_3_10))] use crate::types::PyNone; use crate::types::{PyAnyMethods, PyDict, PyList, PyListMethods}; - use crate::{ffi, IntoPyObject, PyTypeInfo, Python}; + use crate::{IntoPyObject, PyTypeInfo, Python}; #[test] fn vec_iter() { @@ -188,7 +188,7 @@ mod tests { fn iter_item_refcnt() { Python::attach(|py| { let count; - let obj = py.eval(ffi::c_str!("object()"), None, None).unwrap(); + let obj = py.eval(c"object()", None, None).unwrap(); let list = { let list = PyList::empty(py); list.append(10).unwrap(); @@ -210,24 +210,20 @@ mod tests { #[test] fn fibonacci_generator() { - let fibonacci_generator = ffi::c_str!( - r#" + let fibonacci_generator = cr#" def fibonacci(target): a = 1 b = 1 for _ in range(target): yield a a, b = b, a + b -"# - ); +"#; Python::attach(|py| { let context = PyDict::new(py); py.run(fibonacci_generator, None, Some(&context)).unwrap(); - let generator = py - .eval(ffi::c_str!("fibonacci(5)"), None, Some(&context)) - .unwrap(); + let generator = py.eval(c"fibonacci(5)", None, Some(&context)).unwrap(); for (actual, expected) in generator.try_iter().unwrap().zip(&[1, 1, 2, 3, 5]) { let actual = actual.unwrap().extract::().unwrap(); assert_eq!(actual, *expected) @@ -238,22 +234,20 @@ def fibonacci(target): #[test] #[cfg(all(not(PyPy), Py_3_10))] fn send_generator() { - let generator = ffi::c_str!( - r#" + let generator = cr#" def gen(): value = None while(True): value = yield value if value is None: return -"# - ); +"#; Python::attach(|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 generator = py.eval(c"gen()", None, Some(&context)).unwrap(); let one = 1i32.into_pyobject(py).unwrap(); assert!(matches!( @@ -276,23 +270,21 @@ def gen(): use crate::types::any::PyAnyMethods; use crate::Bound; - let fibonacci_generator = ffi::c_str!( - r#" + let fibonacci_generator = cr#" def fibonacci(target): a = 1 b = 1 for _ in range(target): yield a a, b = b, a + b -"# - ); +"#; Python::attach(|py| { let context = PyDict::new(py); py.run(fibonacci_generator, None, Some(&context)).unwrap(); let generator: Bound<'_, PyIterator> = py - .eval(ffi::c_str!("fibonacci(5)"), None, Some(&context)) + .eval(c"fibonacci(5)", None, Some(&context)) .unwrap() .cast_into() .unwrap(); @@ -391,7 +383,7 @@ def fibonacci(target): #[cfg(not(Py_LIMITED_API))] fn length_hint_becomes_size_hint_lower_bound() { Python::attach(|py| { - let list = py.eval(ffi::c_str!("[1, 2, 3]"), None, None).unwrap(); + let list = py.eval(c"[1, 2, 3]", None, None).unwrap(); let iter = list.try_iter().unwrap(); let hint = iter.size_hint(); assert_eq!(hint, (3, None)); @@ -402,7 +394,7 @@ def fibonacci(target): fn test_type_object() { Python::attach(|py| { let abc = PyIterator::type_object(py); - let iter = py.eval(ffi::c_str!("iter(())"), None, None).unwrap(); + let iter = py.eval(c"iter(())", None, None).unwrap(); assert!(iter.is_instance(&abc).unwrap()); }) } diff --git a/src/types/list.rs b/src/types/list.rs index 55aa50d39e4..53b686e8e18 100644 --- a/src/types/list.rs +++ b/src/types/list.rs @@ -945,7 +945,7 @@ mod tests { use crate::types::list::PyListMethods; use crate::types::sequence::PySequenceMethods; use crate::types::{PyList, PyTuple}; - use crate::{ffi, IntoPyObject, PyResult, Python}; + use crate::{IntoPyObject, PyResult, Python}; #[cfg(feature = "nightly")] use std::num::NonZero; @@ -1006,7 +1006,7 @@ mod tests { #[test] fn test_set_item_refcnt() { Python::attach(|py| { - let obj = py.eval(ffi::c_str!("object()"), None, None).unwrap(); + let obj = py.eval(c"object()", None, None).unwrap(); let cnt; { let v = vec![2]; @@ -1041,7 +1041,7 @@ mod tests { fn test_insert_refcnt() { Python::attach(|py| { let cnt; - let obj = py.eval(ffi::c_str!("object()"), None, None).unwrap(); + let obj = py.eval(c"object()", None, None).unwrap(); { let list = PyList::empty(py); cnt = obj.get_refcnt(); @@ -1066,7 +1066,7 @@ mod tests { fn test_append_refcnt() { Python::attach(|py| { let cnt; - let obj = py.eval(ffi::c_str!("object()"), None, None).unwrap(); + let obj = py.eval(c"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 6c9097aefeb..beff336d4db 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -68,7 +68,7 @@ pub use self::weakref::{PyWeakref, PyWeakrefMethods, PyWeakrefProxy, PyWeakrefRe /// /// # pub fn main() -> PyResult<()> { /// Python::attach(|py| { -/// let dict = py.eval(c_str!("{'a':'b', 'c':'d'}"), None, None)?.cast_into::()?; +/// let dict = py.eval(c"{'a':'b', 'c':'d'}", None, None)?.cast_into::()?; /// /// for (key, value) in &dict { /// println!("key: {}, value: {}", key, value); diff --git a/src/types/module.rs b/src/types/module.rs index 72ba2d341d9..82f3f176180 100644 --- a/src/types/module.rs +++ b/src/types/module.rs @@ -1,5 +1,3 @@ -use pyo3_ffi::c_str; - use crate::err::{PyErr, PyResult}; use crate::ffi_ptr_ext::FfiPtrExt; use crate::impl_::callback::IntoPyCallbackOutput; @@ -125,7 +123,7 @@ impl PyModule { /// let code = c_str!(include_str!("../../assets/script.py")); /// /// Python::attach(|py| -> PyResult<()> { - /// PyModule::from_code(py, code, c_str!("example.py"), c_str!("example"))?; + /// PyModule::from_code(py, code, c"example.py", c"example")?; /// Ok(()) /// })?; /// # Ok(()) @@ -148,7 +146,7 @@ impl PyModule { /// let code = std::fs::read_to_string("assets/script.py")?; /// /// Python::attach(|py| -> PyResult<()> { - /// PyModule::from_code(py, CString::new(code)?.as_c_str(), c_str!("example.py"), c_str!("example"))?; + /// PyModule::from_code(py, CString::new(code)?.as_c_str(), c"example.py", c"example")?; /// Ok(()) /// })?; /// # } @@ -162,7 +160,7 @@ impl PyModule { module_name: &CStr, ) -> PyResult> { let file_name = if file_name.is_empty() { - c_str!("") + c"" } else { file_name }; @@ -551,8 +549,6 @@ fn __name__(py: Python<'_>) -> &Bound<'_, PyString> { #[cfg(test)] mod tests { - use pyo3_ffi::c_str; - use crate::{ types::{module::PyModuleMethods, PyModule}, Python, @@ -583,7 +579,7 @@ mod tests { #[test] fn module_from_code_empty_file() { Python::attach(|py| { - let builtins = PyModule::from_code(py, c_str!(""), c_str!(""), c_str!("")).unwrap(); + let builtins = PyModule::from_code(py, c"", c"", c"").unwrap(); assert_eq!(builtins.filename().unwrap(), ""); }) } diff --git a/src/types/sequence.rs b/src/types/sequence.rs index 6b9e864ef59..6cb6a921495 100644 --- a/src/types/sequence.rs +++ b/src/types/sequence.rs @@ -357,13 +357,13 @@ impl<'py> PySequenceMethods<'py> for Bound<'py, PySequence> { #[cfg(test)] mod tests { use crate::types::{PyAnyMethods, PyList, PySequence, PySequenceMethods, PyTuple}; - use crate::{ffi, IntoPyObject, Py, PyAny, PyTypeInfo, Python}; + use crate::{IntoPyObject, Py, PyAny, PyTypeInfo, Python}; use std::ptr; fn get_object() -> Py { // Convenience function for getting a single unique object Python::attach(|py| { - let obj = py.eval(ffi::c_str!("object()"), None, None).unwrap(); + let obj = py.eval(c"object()", None, None).unwrap(); obj.into_pyobject(py).unwrap().unbind() }) diff --git a/src/types/set.rs b/src/types/set.rs index 6b89be40c85..2f22b2f9ba2 100644 --- a/src/types/set.rs +++ b/src/types/set.rs @@ -292,7 +292,6 @@ mod tests { use super::PySet; use crate::{ conversion::IntoPyObject, - ffi, types::{PyAnyMethods, PySetMethods}, Python, }; @@ -383,11 +382,7 @@ mod tests { let val2 = set.pop(); assert!(val2.is_none()); assert!(py - .eval( - ffi::c_str!("print('Exception state should not be set.')"), - None, - None - ) + .eval(c"print('Exception state should not be set.')", None, None) .is_ok()); }); } diff --git a/src/types/string.rs b/src/types/string.rs index e61d37d541d..ff1875a6c62 100644 --- a/src/types/string.rs +++ b/src/types/string.rs @@ -75,7 +75,7 @@ impl<'a> PyStringData<'a> { Err(PyUnicodeDecodeError::new( py, - ffi::c_str!("utf-16"), + c"utf-16", self.as_bytes(), 0..self.as_bytes().len(), CStr::from_bytes_with_nul(&message).unwrap(), @@ -87,10 +87,10 @@ impl<'a> PyStringData<'a> { Some(s) => Ok(Cow::Owned(s)), None => Err(PyUnicodeDecodeError::new( py, - ffi::c_str!("utf-32"), + c"utf-32", self.as_bytes(), 0..self.as_bytes().len(), - ffi::c_str!("error converting utf-32"), + c"error converting utf-32", )? .into()), }, @@ -371,13 +371,9 @@ impl<'a> Borrowed<'a, '_, PyString> { } let bytes = unsafe { - ffi::PyUnicode_AsEncodedString( - ptr, - ffi::c_str!("utf-8").as_ptr(), - ffi::c_str!("surrogatepass").as_ptr(), - ) - .assume_owned(py) - .cast_into_unchecked::() + ffi::PyUnicode_AsEncodedString(ptr, c"utf-8".as_ptr(), c"surrogatepass".as_ptr()) + .assume_owned(py) + .cast_into_unchecked::() }; Cow::Owned(String::from_utf8_lossy(bytes.as_bytes()).into_owned()) } @@ -573,8 +569,6 @@ impl PartialEq> for &'_ str { #[cfg(test)] mod tests { - use pyo3_ffi::c_str; - use super::*; use crate::{exceptions::PyLookupError, types::PyAnyMethods as _, IntoPyObject}; @@ -591,7 +585,7 @@ mod tests { fn test_to_cow_surrogate() { Python::attach(|py| { let py_string = py - .eval(ffi::c_str!(r"'\ud800'"), None, None) + .eval(cr"'\ud800'", None, None) .unwrap() .cast_into::() .unwrap(); @@ -620,10 +614,7 @@ mod tests { #[test] fn test_encode_utf8_surrogate() { Python::attach(|py| { - let obj: Py = py - .eval(ffi::c_str!(r"'\ud800'"), None, None) - .unwrap() - .into(); + let obj: Py = py.eval(cr"'\ud800'", None, None).unwrap().into(); assert!(obj .bind(py) .cast::() @@ -637,7 +628,7 @@ mod tests { fn test_to_string_lossy() { Python::attach(|py| { let py_string = py - .eval(ffi::c_str!(r"'🐈 Hello \ud800World'"), None, None) + .eval(cr"'🐈 Hello \ud800World'", None, None) .unwrap() .cast_into::() .unwrap(); @@ -675,7 +666,7 @@ mod tests { // with `ignore` error handler, the invalid byte is dropped let py_string = - PyString::from_encoded_object(&py_bytes, None, Some(c_str!("ignore"))).unwrap(); + PyString::from_encoded_object(&py_bytes, None, Some(c"ignore")).unwrap(); let result = py_string.to_cow().unwrap(); assert_eq!(result, "abcd"); @@ -694,18 +685,14 @@ mod tests { let py_bytes = PyBytes::new(py, b"abcd"); // invalid encoding - let err = - PyString::from_encoded_object(&py_bytes, Some(c_str!("wat")), None).unwrap_err(); + let err = PyString::from_encoded_object(&py_bytes, Some(c"wat"), None).unwrap_err(); assert!(err.is_instance(py, &py.get_type::())); assert_eq!(err.to_string(), "LookupError: unknown encoding: wat"); // invalid error handler - let err = PyString::from_encoded_object( - &PyBytes::new(py, b"ab\xFFcd"), - None, - Some(c_str!("wat")), - ) - .unwrap_err(); + let err = + PyString::from_encoded_object(&PyBytes::new(py, b"ab\xFFcd"), None, Some(c"wat")) + .unwrap_err(); assert!(err.is_instance(py, &py.get_type::())); assert_eq!( err.to_string(), @@ -765,7 +752,7 @@ mod tests { #[cfg(not(any(Py_LIMITED_API, PyPy, GraalPy)))] fn test_string_data_ucs2() { Python::attach(|py| { - let s = py.eval(ffi::c_str!("'foo\\ud800'"), None, None).unwrap(); + let s = py.eval(c"'foo\\ud800'", None, None).unwrap(); let py_string = s.cast::().unwrap(); let data = unsafe { py_string.data().unwrap() }; @@ -891,7 +878,7 @@ mod tests { fn test_py_to_str_surrogate() { Python::attach(|py| { let py_string: Py = py - .eval(ffi::c_str!(r"'\ud800'"), None, None) + .eval(cr"'\ud800'", None, None) .unwrap() .extract() .unwrap(); @@ -907,7 +894,7 @@ mod tests { fn test_py_to_string_lossy() { Python::attach(|py| { let py_string: Py = py - .eval(ffi::c_str!(r"'🐈 Hello \ud800World'"), None, None) + .eval(cr"'🐈 Hello \ud800World'", None, None) .unwrap() .extract() .unwrap(); diff --git a/src/types/traceback.rs b/src/types/traceback.rs index 1d6f88d1afe..2dfdc3121ba 100644 --- a/src/types/traceback.rs +++ b/src/types/traceback.rs @@ -39,7 +39,7 @@ pub trait PyTracebackMethods<'py>: crate::sealed::Sealed { /// # let result: PyResult<()> = /// Python::attach(|py| { /// let err = py - /// .run(c_str!("raise Exception('banana')"), None, None) + /// .run(c"raise Exception('banana')", None, None) /// .expect_err("raise will create a Python error"); /// /// let traceback = err.traceback(py).expect("raised exception will have a traceback"); @@ -82,7 +82,6 @@ impl<'py> PyTracebackMethods<'py> for Bound<'py, PyTraceback> { mod tests { use crate::IntoPyObject; use crate::{ - ffi, types::{any::PyAnyMethods, dict::PyDictMethods, traceback::PyTracebackMethods, PyDict}, PyErr, Python, }; @@ -91,7 +90,7 @@ mod tests { fn format_traceback() { Python::attach(|py| { let err = py - .run(ffi::c_str!("raise Exception('banana')"), None, None) + .run(c"raise Exception('banana')", None, None) .expect_err("raising should have given us an error"); assert_eq!( @@ -107,14 +106,12 @@ mod tests { let locals = PyDict::new(py); // Produce an error from python so that it has a traceback py.run( - ffi::c_str!( - r" + cr" try: raise ValueError('raised exception') except Exception as e: err = e -" - ), +", None, Some(&locals), ) @@ -131,12 +128,10 @@ except Exception as e: let locals = PyDict::new(py); // Produce an error from python so that it has a traceback py.run( - ffi::c_str!( - r" + cr" def f(): raise ValueError('raised exception') -" - ), +", None, Some(&locals), ) diff --git a/src/types/typeobject.rs b/src/types/typeobject.rs index 953a78bc6aa..f04cc2ada7f 100644 --- a/src/types/typeobject.rs +++ b/src/types/typeobject.rs @@ -313,12 +313,10 @@ mod tests { let module_name = generate_unique_module_name("test_module"); let module = PyModule::from_code( py, - c_str!( - r#" + cr#" class MyClass: pass -"# - ), +"#, c_str!(file!()), &module_name, ) @@ -355,13 +353,11 @@ class MyClass: let module_name = generate_unique_module_name("test_module"); let module = PyModule::from_code( py, - c_str!( - r#" + cr#" class OuterClass: class InnerClass: pass -"# - ), +"#, c_str!(file!()), &module_name, ) diff --git a/src/types/weakref/anyref.rs b/src/types/weakref/anyref.rs index 8567922419a..7e08abcb87a 100644 --- a/src/types/weakref/anyref.rs +++ b/src/types/weakref/anyref.rs @@ -378,13 +378,13 @@ mod tests { use super::*; #[cfg(Py_3_10)] use crate::types::PyInt; - use crate::{ffi, PyTypeCheck}; + use crate::PyTypeCheck; use crate::{py_result_ext::PyResultExt, types::PyType}; use std::ptr; 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).cast_into::() + py.run(c"class A:\n pass\n", None, None)?; + py.eval(c"A", None, None).cast_into::() } #[test] diff --git a/src/types/weakref/proxy.rs b/src/types/weakref/proxy.rs index 90f682ab1ad..1dc30620181 100644 --- a/src/types/weakref/proxy.rs +++ b/src/types/weakref/proxy.rs @@ -112,13 +112,13 @@ impl PyWeakrefProxy { /// fn callback(wref: Bound<'_, PyWeakrefProxy>) -> PyResult<()> { /// let py = wref.py(); /// assert!(wref.upgrade_as::()?.is_none()); - /// py.run(c_str!("counter = 1"), None, None) + /// py.run(c"counter = 1", None, None) /// } /// /// # fn main() -> PyResult<()> { /// Python::attach(|py| { - /// py.run(c_str!("counter = 0"), None, None)?; - /// assert_eq!(py.eval(c_str!("counter"), None, None)?.extract::()?, 0); + /// py.run(c"counter = 0", None, None)?; + /// assert_eq!(py.eval(c"counter", None, None)?.extract::()?, 0); /// let foo = Bound::new(py, Foo{})?; /// /// // This is fine. @@ -128,7 +128,7 @@ impl PyWeakrefProxy { /// // In normal situations where a direct `Bound<'py, Foo>` is required use `upgrade::` /// weakref.upgrade().is_some_and(|obj| obj.is(&foo)) /// ); - /// assert_eq!(py.eval(c_str!("counter"), None, None)?.extract::()?, 0); + /// assert_eq!(py.eval(c"counter", None, None)?.extract::()?, 0); /// /// let weakref2 = PyWeakrefProxy::new_with(&foo, wrap_pyfunction!(callback, py)?)?; /// assert!(!weakref.is(&weakref2)); // Not the same weakref @@ -137,7 +137,7 @@ impl PyWeakrefProxy { /// drop(foo); /// /// assert!(weakref.upgrade_as::()?.is_none()); - /// assert_eq!(py.eval(c_str!("counter"), None, None)?.extract::()?, 1); + /// assert_eq!(py.eval(c"counter", None, None)?.extract::()?, 1); /// Ok(()) /// }) /// # } @@ -250,15 +250,14 @@ mod tests { use super::*; #[cfg(Py_3_10)] use crate::types::PyInt; - use crate::{ffi, PyTypeCheck}; + use crate::PyTypeCheck; use crate::{py_result_ext::PyResultExt, types::PyDict, types::PyType}; use std::ptr; fn get_type(py: Python<'_>) -> PyResult> { 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) - .cast_into::() + py.run(c"class A:\n pass\n", Some(&globals), None)?; + py.eval(c"A", Some(&globals), None).cast_into::() } #[test] @@ -609,19 +608,18 @@ mod tests { mod python_class { use super::*; - use crate::{ffi, PyTypeCheck}; + use crate::PyTypeCheck; use crate::{py_result_ext::PyResultExt, types::PyDict, types::PyType}; use std::ptr; 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"), + c"class A:\n def __call__(self):\n return 'This class is callable!'\n", Some(&globals), None, )?; - py.eval(ffi::c_str!("A"), Some(&globals), None) - .cast_into::() + py.eval(c"A", Some(&globals), None).cast_into::() } #[test] diff --git a/src/types/weakref/reference.rs b/src/types/weakref/reference.rs index 492da7888b4..74369a6ba5a 100644 --- a/src/types/weakref/reference.rs +++ b/src/types/weakref/reference.rs @@ -120,13 +120,13 @@ impl PyWeakrefReference { /// fn callback(wref: Bound<'_, PyWeakrefReference>) -> PyResult<()> { /// let py = wref.py(); /// assert!(wref.upgrade_as::()?.is_none()); - /// py.run(c_str!("counter = 1"), None, None) + /// py.run(c"counter = 1", None, None) /// } /// /// # fn main() -> PyResult<()> { /// Python::attach(|py| { - /// py.run(c_str!("counter = 0"), None, None)?; - /// assert_eq!(py.eval(c_str!("counter"), None, None)?.extract::()?, 0); + /// py.run(c"counter = 0", None, None)?; + /// assert_eq!(py.eval(c"counter", None, None)?.extract::()?, 0); /// let foo = Bound::new(py, Foo{})?; /// /// // This is fine. @@ -136,7 +136,7 @@ impl PyWeakrefReference { /// // In normal situations where a direct `Bound<'py, Foo>` is required use `upgrade::` /// weakref.upgrade().is_some_and(|obj| obj.is(&foo)) /// ); - /// assert_eq!(py.eval(c_str!("counter"), None, None)?.extract::()?, 0); + /// assert_eq!(py.eval(c"counter", None, None)?.extract::()?, 0); /// /// let weakref2 = PyWeakrefReference::new_with(&foo, wrap_pyfunction!(callback, py)?)?; /// assert!(!weakref.is(&weakref2)); // Not the same weakref @@ -145,7 +145,7 @@ impl PyWeakrefReference { /// drop(foo); /// /// assert!(weakref.upgrade_as::()?.is_none()); - /// assert_eq!(py.eval(c_str!("counter"), None, None)?.extract::()?, 1); + /// assert_eq!(py.eval(c"counter", None, None)?.extract::()?, 1); /// Ok(()) /// }) /// # } @@ -242,13 +242,13 @@ mod tests { mod python_class { use super::*; - use crate::{ffi, PyTypeInfo}; + use crate::PyTypeInfo; use crate::{py_result_ext::PyResultExt, types::PyType}; use std::ptr; 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).cast_into::() + py.run(c"class A:\n pass\n", None, None)?; + py.eval(c"A", None, None).cast_into::() } #[test] diff --git a/tests/test_anyhow.rs b/tests/test_anyhow.rs index 82c586d778d..ff09e9d296f 100644 --- a/tests/test_anyhow.rs +++ b/tests/test_anyhow.rs @@ -1,6 +1,6 @@ #![cfg(feature = "anyhow")] -use pyo3::{ffi, wrap_pyfunction}; +use pyo3::wrap_pyfunction; #[test] fn test_anyhow_py_function_ok_result() { @@ -40,15 +40,6 @@ fn test_anyhow_py_function_err_result() { let locals = PyDict::new(py); locals.set_item("func", func).unwrap(); - py.run( - ffi::c_str!( - r#" - func() - "# - ), - None, - Some(&locals), - ) - .unwrap_err(); + py.run(c"func()", None, Some(&locals)).unwrap_err(); }); } diff --git a/tests/test_append_to_inittab.rs b/tests/test_append_to_inittab.rs index 541c63870d6..2da4f29efb2 100644 --- a/tests/test_append_to_inittab.rs +++ b/tests/test_append_to_inittab.rs @@ -22,7 +22,7 @@ mod module_mod_with_functions { #[cfg(not(any(PyPy, GraalPy)))] #[test] fn test_module_append_to_inittab() { - use pyo3::{append_to_inittab, ffi}; + use pyo3::append_to_inittab; append_to_inittab!(module_fn_with_functions); @@ -30,12 +30,10 @@ fn test_module_append_to_inittab() { Python::attach(|py| { py.run( - ffi::c_str!( - r#" + cr#" import module_fn_with_functions assert module_fn_with_functions.foo() == 123 -"# - ), +"#, None, None, ) @@ -45,12 +43,10 @@ assert module_fn_with_functions.foo() == 123 Python::attach(|py| { py.run( - ffi::c_str!( - r#" + cr#" import module_mod_with_functions assert module_mod_with_functions.foo() == 123 -"# - ), +"#, None, None, ) diff --git a/tests/test_buffer.rs b/tests/test_buffer.rs index 7ffa3e58864..5e66e67ab3a 100644 --- a/tests/test_buffer.rs +++ b/tests/test_buffer.rs @@ -48,7 +48,7 @@ impl TestBufferErrors { (*view).readonly = 1; (*view).itemsize = std::mem::size_of::() as isize; - let msg = ffi::c_str!("I"); + let msg = c"I"; (*view).format = msg.as_ptr() as *mut _; (*view).ndim = 1; @@ -72,7 +72,7 @@ impl TestBufferErrors { (*view).itemsize += 1; } IncorrectFormat => { - (*view).format = ffi::c_str!("B").as_ptr() as _; + (*view).format = c"B".as_ptr() as _; } IncorrectAlignment => (*view).buf = (*view).buf.add(1), } diff --git a/tests/test_class_basics.rs b/tests/test_class_basics.rs index dfe7f1ef0a5..a4fd02584fe 100644 --- a/tests/test_class_basics.rs +++ b/tests/test_class_basics.rs @@ -1,7 +1,5 @@ #![cfg(feature = "macros")] -#[cfg(Py_3_8)] -use pyo3::ffi::c_str; use pyo3::prelude::*; use pyo3::types::PyType; use pyo3::{py_run, PyClass}; @@ -658,8 +656,7 @@ fn drop_unsendable_elsewhere() { // will not immediately drop it because the refcounts need to be merged. // // Force GC to ensure the drop happens now on the wrong thread. - py.run(c_str!("import gc; gc.collect()"), None, None) - .unwrap(); + py.run(c"import gc; gc.collect()", None, None).unwrap(); }); }) .join() diff --git a/tests/test_datetime.rs b/tests/test_datetime.rs index d045d43dfeb..d50c992288d 100644 --- a/tests/test_datetime.rs +++ b/tests/test_datetime.rs @@ -1,7 +1,7 @@ #![cfg(not(Py_LIMITED_API))] +use pyo3::prelude::*; use pyo3::types::{IntoPyDict, PyDate, PyDateTime, PyTime, PyTzInfo}; -use pyo3::{ffi, prelude::*}; use pyo3_ffi::PyDateTime_IMPORT; use std::ffi::CString; @@ -19,7 +19,7 @@ fn _get_subclasses<'py>( let make_subclass_py = CString::new(format!("class Subklass({py_type}):\n pass"))?; - let make_sub_subclass_py = ffi::c_str!("class SubSubklass(Subklass):\n pass"); + let make_sub_subclass_py = c"class SubSubklass(Subklass):\n pass"; py.run(&make_subclass_py, None, Some(&locals))?; py.run(make_sub_subclass_py, None, Some(&locals))?; @@ -140,11 +140,7 @@ fn test_datetime_utc() { let locals = [("dt", dt)].into_py_dict(py).unwrap(); let offset: f32 = py - .eval( - ffi::c_str!("dt.utcoffset().total_seconds()"), - None, - Some(&locals), - ) + .eval(c"dt.utcoffset().total_seconds()", None, Some(&locals)) .unwrap() .extract() .unwrap(); diff --git a/tests/test_frompyobject.rs b/tests/test_frompyobject.rs index e0353221db9..ecec5bcac06 100644 --- a/tests/test_frompyobject.rs +++ b/tests/test_frompyobject.rs @@ -633,7 +633,7 @@ fn test_from_py_with() { Python::attach(|py| { let py_zap = py .eval( - pyo3_ffi::c_str!(r#"{"name": "whatever", "my_object": [1, 2, 3]}"#), + cr#"{"name": "whatever", "my_object": [1, 2, 3]}"#, None, None, ) @@ -656,7 +656,7 @@ pub struct ZapTuple( fn test_from_py_with_tuple_struct() { Python::attach(|py| { let py_zap = py - .eval(pyo3_ffi::c_str!(r#"("whatever", [1, 2, 3])"#), None, None) + .eval(cr#"("whatever", [1, 2, 3])"#, None, None) .expect("failed to create tuple"); let zap = py_zap.extract::().unwrap(); @@ -670,11 +670,7 @@ fn test_from_py_with_tuple_struct() { fn test_from_py_with_tuple_struct_error() { Python::attach(|py| { let py_zap = py - .eval( - pyo3_ffi::c_str!(r#"("whatever", [1, 2, 3], "third")"#), - None, - None, - ) + .eval(cr#"("whatever", [1, 2, 3], "third")"#, None, None) .expect("failed to create tuple"); let f = py_zap.extract::(); @@ -700,7 +696,7 @@ pub enum ZapEnum { fn test_from_py_with_enum() { Python::attach(|py| { let py_zap = py - .eval(pyo3_ffi::c_str!(r#"("whatever", [1, 2, 3])"#), None, None) + .eval(cr#"("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 f97868fec9f..9a702a2c546 100644 --- a/tests/test_gc.rs +++ b/tests/test_gc.rs @@ -103,8 +103,7 @@ impl DropCheck { } Python::attach(|py| { - py.run(ffi::c_str!("import gc; gc.collect()"), None, None) - .unwrap(); + py.run(c"import gc; gc.collect()", None, None).unwrap(); }); #[cfg(Py_GIL_DISABLED)] { @@ -230,8 +229,7 @@ fn gc_null_traversal() { obj.borrow_mut(py).cycle = Some(obj.clone_ref(py)); // the object doesn't have to be cleaned up, it just needs to be traversed. - py.run(ffi::c_str!("import gc; gc.collect()"), None, None) - .unwrap(); + py.run(c"import gc; gc.collect()", None, None).unwrap(); }); } diff --git a/tests/test_inheritance.rs b/tests/test_inheritance.rs index 99c26ae1041..37c6b3ea198 100644 --- a/tests/test_inheritance.rs +++ b/tests/test_inheritance.rs @@ -2,8 +2,6 @@ use pyo3::prelude::*; use pyo3::py_run; - -use pyo3::ffi; use pyo3::types::IntoPyDict; mod test_utils; @@ -25,7 +23,7 @@ fn subclass() { .unwrap(); py.run( - ffi::c_str!("class A(SubclassAble): pass\nassert issubclass(A, SubclassAble)"), + c"class A(SubclassAble): pass\nassert issubclass(A, SubclassAble)", None, Some(&d), ) @@ -102,7 +100,7 @@ fn mutation_fails() { 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))"), + c"obj.base_set(lambda: obj.sub_set_and_ret(1))", Some(&global), None, ) @@ -244,7 +242,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(ffi::c_str!("object()"), None, None).unwrap(); + let item = &py.eval(c"object()", None, None).unwrap(); assert_eq!(item.get_refcnt(), 1); dict_sub.bind(py).set_item("foo", item).unwrap(); @@ -277,7 +275,7 @@ mod inheriting_native_type { let cls = py.get_type::(); 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"), + c"e = cls('hello'); assert str(e) == 'hello'; assert e.context == 'Hello :)'; raise e", None, Some(&dict) ); diff --git a/tests/test_module.rs b/tests/test_module.rs index 20174f9baeb..5ba238cd91e 100644 --- a/tests/test_module.rs +++ b/tests/test_module.rs @@ -5,7 +5,6 @@ use pyo3::prelude::*; use pyo3::py_run; use pyo3::types::PyString; use pyo3::types::{IntoPyDict, PyDict, PyTuple}; -use pyo3_ffi::c_str; mod test_utils; @@ -248,8 +247,8 @@ fn test_module_from_code_bound() { Python::attach(|py| { let adder_mod = PyModule::from_code( py, - c_str!("def add(a,b):\n\treturn a+b"), - c_str!("adder_mod.py"), + c"def add(a,b):\n\treturn a+b", + c"adder_mod.py", &test_utils::generate_unique_module_name("adder_mod"), ) .expect("Module code should be loaded"); diff --git a/tests/test_proto_methods.rs b/tests/test_proto_methods.rs index 94a8ddd368a..900d874eb66 100644 --- a/tests/test_proto_methods.rs +++ b/tests/test_proto_methods.rs @@ -669,8 +669,7 @@ impl OnceFuture { fn test_await() { Python::attach(|py| { let once = py.get_type::(); - let source = pyo3_ffi::c_str!( - r#" + let source = cr#" import asyncio import sys @@ -683,8 +682,7 @@ 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(source, Some(&globals), None) @@ -721,8 +719,7 @@ impl AsyncIterator { fn test_anext_aiter() { Python::attach(|py| { let once = py.get_type::(); - let source = pyo3_ffi::c_str!( - r#" + let source = cr#" import asyncio import sys @@ -739,8 +736,7 @@ 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 @@ -788,7 +784,7 @@ impl DescrCounter { fn descr_getset() { Python::attach(|py| { let counter = py.get_type::(); - let source = pyo3_ffi::c_str!(pyo3::indoc::indoc!( + let source = pyo3_ffi::c_str!(indoc::indoc!( r#" class Class: counter = Counter() diff --git a/tests/test_pyerr_debug_unformattable.rs b/tests/test_pyerr_debug_unformattable.rs index 9bb481ffec8..0ae234061b8 100644 --- a/tests/test_pyerr_debug_unformattable.rs +++ b/tests/test_pyerr_debug_unformattable.rs @@ -1,4 +1,3 @@ -use pyo3::ffi; use pyo3::prelude::*; // This test mucks around with sys.modules, so run it separately to prevent it @@ -20,13 +19,11 @@ fn err_debug_unformattable() { // TypeError: 'Mock' object cannot be cast as 'str' let err = py .run( - ffi::c_str!( - r#" + cr#" import io, sys, unittest.mock sys.modules['orig_io'] = sys.modules['io'] sys.modules['io'] = unittest.mock.Mock() -raise Exception('banana')"# - ), +raise Exception('banana')"#, None, None, ) @@ -51,13 +48,11 @@ raise Exception('banana')"# assert!(fields.next().is_none()); py.run( - ffi::c_str!( - r#" + cr#" import io, sys, unittest.mock sys.modules['io'] = sys.modules['orig_io'] del sys.modules['orig_io'] -"# - ), +"#, None, None, ) diff --git a/tests/test_pyfunction.rs b/tests/test_pyfunction.rs index 14d74244ecb..dc601dd653a 100644 --- a/tests/test_pyfunction.rs +++ b/tests/test_pyfunction.rs @@ -8,7 +8,6 @@ use pyo3::buffer::PyBuffer; #[cfg(not(Py_LIMITED_API))] use pyo3::exceptions::PyWarning; use pyo3::exceptions::{PyFutureWarning, PyUserWarning}; -use pyo3::ffi::c_str; use pyo3::prelude::*; #[cfg(not(Py_LIMITED_API))] use pyo3::types::PyDateTime; @@ -385,8 +384,8 @@ fn test_pycfunction_new() { let py_fn = PyCFunction::new( py, c_fn, - c_str!("py_fn"), - c_str!("py_fn for test (this is the docstring)"), + c"py_fn", + c"py_fn for test (this is the docstring)", None, ) .unwrap(); @@ -423,17 +422,13 @@ fn test_pycfunction_new_with_keywords() { let mut args_names = [foo_name.into_raw(), kw_bar_name.into_raw(), ptr::null_mut()]; #[cfg(Py_3_13)] - let args_names = [ - c_str!("foo").as_ptr(), - c_str!("kw_bar").as_ptr(), - ptr::null_mut(), - ]; + let args_names = [c"foo".as_ptr(), c"kw_bar".as_ptr(), ptr::null_mut()]; unsafe { ffi::PyArg_ParseTupleAndKeywords( args, kwds, - c_str!("l|l").as_ptr(), + c"l|l".as_ptr(), #[cfg(Py_3_13)] args_names.as_ptr(), #[cfg(not(Py_3_13))] @@ -454,8 +449,8 @@ fn test_pycfunction_new_with_keywords() { let py_fn = PyCFunction::new_with_keywords( py, c_fn, - c_str!("py_fn"), - c_str!("py_fn for test (this is the docstring)"), + c"py_fn", + c"py_fn for test (this is the docstring)", None, ) .unwrap(); @@ -496,8 +491,7 @@ fn test_closure() { }) }; let closure_py = - PyCFunction::new_closure(py, Some(c_str!("test_fn")), Some(c_str!("test_fn doc")), f) - .unwrap(); + PyCFunction::new_closure(py, Some(c"test_fn"), Some(c"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 52c198a012aab8607314658d3f620e371f603862 Mon Sep 17 00:00:00 2001 From: Thomas Tanon Date: Wed, 22 Oct 2025 11:07:53 +0200 Subject: [PATCH 933/936] Doc: Use C-string literals where possible (#5549) Missed from previous MR --- README.md | 3 +- guide/src/class.md | 4 +- guide/src/class/numeric.md | 4 +- guide/src/conversions/traits.md | 37 +++++++++---------- guide/src/module.md | 3 +- .../python-from-rust/calling-existing-code.md | 36 ++++++++---------- guide/src/python-from-rust/function-calls.md | 18 ++++----- pyo3-ffi/README.md | 12 +++--- 8 files changed, 52 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index 9704b67d8c7..5b190c921fc 100644 --- a/README.md +++ b/README.md @@ -146,7 +146,6 @@ 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::attach(|py| { @@ -154,7 +153,7 @@ fn main() -> PyResult<()> { let version: String = sys.getattr("version")?.extract()?; let locals = [("os", py.import("os")?)].into_py_dict(py)?; - let code = c_str!("os.getenv('USER') or os.getenv('USERNAME') or 'Unknown'"); + let code = c"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); diff --git a/guide/src/class.md b/guide/src/class.md index a2f9b0de3fc..26e6880d42b 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -1426,8 +1426,8 @@ impl pyo3::impl_::pyclass::PyClassImpl for MyClass { type WeakRef = pyo3::impl_::pyclass::PyClassDummySlot; type BaseNativeType = pyo3::PyAny; - const RAW_DOC: &'static std::ffi::CStr = pyo3::ffi::c_str!("..."); - const DOC: &'static std::ffi::CStr = pyo3::ffi::c_str!("..."); + const RAW_DOC: &'static std::ffi::CStr = c"..."; + const DOC: &'static std::ffi::CStr = c"..."; fn items_iter() -> pyo3::impl_::pyclass::PyClassItemsIter { use pyo3::impl_::pyclass::*; diff --git a/guide/src/class/numeric.md b/guide/src/class/numeric.md index 769275d04a7..f6d58e6a216 100644 --- a/guide/src/class/numeric.md +++ b/guide/src/class/numeric.md @@ -335,7 +335,7 @@ mod my_module { #[pymodule_export] use super::Number; } -# const SCRIPT: &'static std::ffi::CStr = pyo3::ffi::c_str!(r#" +# const SCRIPT: &'static std::ffi::CStr = cr#" # def hash_djb2(s: str): # n = Number(0) # five = Number(5) @@ -384,7 +384,7 @@ mod my_module { # pass # assert Number(1337).__str__() == '1337' # assert Number(1337).__repr__() == 'Number(1337)' -"#); +"#; # # use pyo3::PyTypeInfo; diff --git a/guide/src/conversions/traits.md b/guide/src/conversions/traits.md index dbc399e7b2f..3aecc9d5cc2 100644 --- a/guide/src/conversions/traits.md +++ b/guide/src/conversions/traits.md @@ -46,7 +46,6 @@ 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 { @@ -57,11 +56,11 @@ struct RustyStruct { # Python::attach(|py| -> PyResult<()> { # let module = PyModule::from_code( # py, -# c_str!("class Foo: +# c"class Foo: # def __init__(self): -# self.my_string = 'test'"), -# c_str!(""), -# c_str!(""), +# self.my_string = 'test'", +# c"", +# c"", # )?; # # let class = module.getattr("Foo")?; @@ -101,7 +100,6 @@ 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 { @@ -115,12 +113,12 @@ struct RustyStruct { # Python::attach(|py| -> PyResult<()> { # let module = PyModule::from_code( # py, -# c_str!("class Foo(dict): +# c"class Foo(dict): # def __init__(self): # self.name = 'test' -# self['key'] = 'test2'"), -# c_str!(""), -# c_str!(""), +# self['key'] = 'test2'", +# c"", +# c"", # )?; # # let class = module.getattr("Foo")?; @@ -157,7 +155,7 @@ struct RustyStruct { # # fn main() -> PyResult<()> { # Python::attach(|py| -> PyResult<()> { -# let py_dict = py.eval(pyo3::ffi::c_str!("{'foo': 'foo', 'bar': 'bar', 'foobar': 'foobar'}"), None, None)?; +# let py_dict = py.eval(c"{'foo': 'foo', 'bar': 'bar', 'foobar': 'foobar'}", None, None)?; # let rustystruct: RustyStruct = py_dict.extract()?; # assert_eq!(rustystruct.foo, "foo"); # assert_eq!(rustystruct.bar, "bar"); @@ -265,7 +263,6 @@ attribute can be applied to single-field-variants. ```rust use pyo3::prelude::*; -use pyo3_ffi::c_str; #[derive(FromPyObject)] # #[derive(Debug)] @@ -345,13 +342,13 @@ enum RustyEnum<'py> { # { # let module = PyModule::from_code( # py, -# c_str!("class Foo(dict): +# c"class Foo(dict): # def __init__(self): # self.x = 0 # self.y = 1 -# self.z = 2"), -# c_str!(""), -# c_str!(""), +# self.z = 2", +# c"", +# c"", # )?; # # let class = module.getattr("Foo")?; @@ -370,12 +367,12 @@ enum RustyEnum<'py> { # { # let module = PyModule::from_code( # py, -# c_str!("class Foo(dict): +# c"class Foo(dict): # def __init__(self): # self.x = 3 -# self.y = 4"), -# c_str!(""), -# c_str!(""), +# self.y = 4", +# c"", +# c"", # )?; # # let class = module.getattr("Foo")?; diff --git a/guide/src/module.md b/guide/src/module.md index 7f022c1960d..d1fd8deeaee 100644 --- a/guide/src/module.md +++ b/guide/src/module.md @@ -101,11 +101,10 @@ fn func() -> String { # Python::attach(|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).unwrap(); # -# py.run(c_str!("assert parent_module.child_module.func() == 'func'"), None, Some(&ctx)).unwrap(); +# py.run(c"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 d050861ed32..8ecef32ec5a 100644 --- a/guide/src/python-from-rust/calling-existing-code.md +++ b/guide/src/python-from-rust/calling-existing-code.md @@ -33,12 +33,11 @@ and return the evaluated value as a `Bound<'py, PyAny>` object. ```rust use pyo3::prelude::*; -use pyo3::ffi::c_str; # fn main() -> Result<(), ()> { Python::attach(|py| { let result = py - .eval(c_str!("[i * 10 for i in range(5)]"), None, None) + .eval(c"[i * 10 for i in range(5)]", None, None) .map_err(|e| { e.print_and_set_sys_last_vars(py); })?; @@ -108,22 +107,21 @@ to this function! ```rust use pyo3::{prelude::*, types::IntoPyDict}; -use pyo3_ffi::c_str; # fn main() -> PyResult<()> { Python::attach(|py| { let activators = PyModule::from_code( py, - c_str!(r#" + cr#" 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 - "#), - c_str!("activators.py"), - c_str!("activators"), + "#, + c"activators.py", + c"activators", )?; let relu_result: f64 = activators.getattr("relu")?.call1((-1.0,))?.extract()?; @@ -155,7 +153,6 @@ As an example, the below adds the module `foo` to the embedded interpreter: ```rust use pyo3::prelude::*; -use pyo3::ffi::c_str; #[pymodule] mod foo { @@ -169,7 +166,7 @@ mod foo { fn main() -> PyResult<()> { pyo3::append_to_inittab!(foo); - Python::attach(|py| Python::run(py, c_str!("import foo; foo.add_one(6)"), None, None)) + Python::attach(|py| Python::run(py, c"import foo; foo.add_one(6)", None, None)) } ``` @@ -180,7 +177,6 @@ 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 { @@ -201,7 +197,7 @@ fn main() -> PyResult<()> { py_modules.set_item("foo", foo_module)?; // Now we can import + run our python code - Python::run(py, c_str!("import foo; foo.add_one(6)"), None, None) + Python::run(py, c"import foo; foo.add_one(6)", None, None) }) } ``` @@ -267,8 +263,8 @@ fn main() -> PyResult<()> { ))); let py_app = c_str!(include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/python_app/app.py"))); let from_python = Python::attach(|py| -> PyResult> { - PyModule::from_code(py, py_foo, c_str!("foo.py"), c_str!("utils.foo"))?; - let app: Py = PyModule::from_code(py, py_app, c_str!("app.py"), c_str!(""))? + PyModule::from_code(py, py_foo, c"foo.py", c"utils.foo")?; + let app: Py = PyModule::from_code(py, py_app, c"app.py", c"")? .getattr("run")? .into(); app.call0(py) @@ -295,7 +291,6 @@ 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; @@ -309,7 +304,7 @@ fn main() -> PyResult<()> { .getattr("path")? .cast_into::()?; syspath.insert(0, path)?; - let app: Py = PyModule::from_code(py, py_app.as_c_str(), c_str!("app.py"), c_str!(""))? + let app: Py = PyModule::from_code(py, py_app.as_c_str(), c"app.py", c"")? .getattr("run")? .into(); app.call0(py) @@ -326,13 +321,12 @@ Use context managers by directly invoking `__enter__` and `__exit__`. ```rust use pyo3::prelude::*; -use pyo3::ffi::c_str; fn main() { Python::attach(|py| { let custom_manager = PyModule::from_code( py, - c_str!(r#" + cr#" class House(object): def __init__(self, address): self.address = address @@ -344,9 +338,9 @@ class House(object): else: print(f"Thank you for visiting {self.address}, come again soon!") - "#), - c_str!("house.py"), - c_str!("house"), + "#, + c"house.py", + c"house", ) .unwrap(); @@ -355,7 +349,7 @@ class House(object): house.call_method0("__enter__").unwrap(); - let result = py.eval(c_str!("undefined_variable + 1"), None, None); + let result = py.eval(c"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/guide/src/python-from-rust/function-calls.md b/guide/src/python-from-rust/function-calls.md index fa6dae5980f..692c524ec6d 100644 --- a/guide/src/python-from-rust/function-calls.md +++ b/guide/src/python-from-rust/function-calls.md @@ -19,7 +19,6 @@ The example below calls a Python function behind a `Py` reference: ```rust use pyo3::prelude::*; use pyo3::types::PyTuple; -use pyo3_ffi::c_str; fn main() -> PyResult<()> { let arg1 = "arg1"; @@ -29,15 +28,15 @@ fn main() -> PyResult<()> { Python::attach(|py| { let fun: Py = PyModule::from_code( py, - c_str!("def example(*args, **kwargs): + c"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')"), - c_str!("example.py"), - c_str!(""), + print('called with no arguments')", + c"example.py", + c"", )? .getattr("example")? .into(); @@ -65,7 +64,6 @@ For the `call` and `call_method` APIs, `kwargs` are `Option<&Bound<'py, PyDict>> use pyo3::prelude::*; use pyo3::types::{PyDict, IntoPyDict}; use std::collections::HashMap; -use pyo3::ffi::c_str; fn main() -> PyResult<()> { let key1 = "key1"; @@ -76,15 +74,15 @@ fn main() -> PyResult<()> { Python::attach(|py| { let fun: Py = PyModule::from_code( py, - c_str!("def example(*args, **kwargs): + c"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')"), - c_str!("example.py"), - c_str!(""), + print('called with no arguments')", + c"example.py", + c"", )? .getattr("example")? .into(); diff --git a/pyo3-ffi/README.md b/pyo3-ffi/README.md index 625344566d1..41b9546ac50 100644 --- a/pyo3-ffi/README.md +++ b/pyo3-ffi/README.md @@ -73,8 +73,8 @@ 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_name: c"string_sum".as_ptr(), + m_doc: c"A Python module written in Rust.".as_ptr(), m_size: 0, m_methods: std::ptr::addr_of_mut!(METHODS).cast(), m_slots: unsafe { SLOTS as *const [PyModuleDef_Slot] as *mut PyModuleDef_Slot }, @@ -85,12 +85,12 @@ static mut MODULE_DEF: PyModuleDef = PyModuleDef { static mut METHODS: [PyMethodDef; 2] = [ PyMethodDef { - ml_name: c_str!("sum_as_string").as_ptr(), + ml_name: c"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(), + ml_doc: c"returns the sum of two integers as a string".as_ptr(), }, // A zeroed PyMethodDef to mark the end of the array. PyMethodDef::zeroed(), @@ -178,7 +178,7 @@ pub unsafe extern "C" fn sum_as_string( if nargs != 2 { PyErr_SetString( PyExc_TypeError, - c_str!("sum_as_string expected 2 positional arguments").as_ptr(), + c"sum_as_string expected 2 positional arguments".as_ptr(), ); return std::ptr::null_mut(); } @@ -202,7 +202,7 @@ pub unsafe extern "C" fn sum_as_string( None => { PyErr_SetString( PyExc_OverflowError, - c_str!("arguments too large to add").as_ptr(), + c"arguments too large to add".as_ptr(), ); std::ptr::null_mut() } From 85e6507f261e8a288fc699cb25bd6fd6986d2101 Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Wed, 22 Oct 2025 15:39:47 +0100 Subject: [PATCH 934/936] drop `memoffset` dependency (#5545) * drop `memoffset` dependency * drop "Runtime" building of `PyMethodDefType` * Update src/impl_/pyclass.rs Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> * coverage * newsfragment --------- Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> --- Cargo.toml | 1 - newsfragments/5545.packaging.md | 1 + pyo3-macros-backend/src/pyclass.rs | 14 +- pyo3-macros-backend/src/pyimpl.rs | 14 +- pyo3-macros-backend/src/pymethod.rs | 60 ++---- src/impl_/pyclass.rs | 282 +++++++++++++++++--------- src/impl_/pyclass/lazy_type_object.rs | 12 +- src/pycell/impl_.rs | 6 +- src/pyclass/create_type_object.rs | 10 +- src/types/capsule.rs | 3 +- 10 files changed, 224 insertions(+), 179 deletions(-) create mode 100644 newsfragments/5545.packaging.md diff --git a/Cargo.toml b/Cargo.toml index d278cd1b57e..3c2e5372fbb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,6 @@ rust-version.workspace = true [dependencies] libc = "0.2.62" -memoffset = "0.9" once_cell = "1.21" # ffi bindings to the python interpreter, split into a separate crate so they can be used independently diff --git a/newsfragments/5545.packaging.md b/newsfragments/5545.packaging.md new file mode 100644 index 00000000000..2591f9d8798 --- /dev/null +++ b/newsfragments/5545.packaging.md @@ -0,0 +1 @@ +Drop `memoffset` dependency. diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index d64cbd67726..0f735d5d90b 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -1671,14 +1671,12 @@ pub fn gen_complex_enum_variant_attr( }; let method_def = quote! { - #pyo3_path::impl_::pyclass::MaybeRuntimePyMethodDef::Static( - #pyo3_path::impl_::pymethods::PyMethodDefType::ClassAttribute({ - #pyo3_path::impl_::pymethods::PyClassAttributeDef::new( - #python_name, - #cls_type::#wrapper_ident - ) - }) - ) + #pyo3_path::impl_::pymethods::PyMethodDefType::ClassAttribute({ + #pyo3_path::impl_::pymethods::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 a7db6f3a88e..36d413d21b9 100644 --- a/pyo3-macros-backend/src/pyimpl.rs +++ b/pyo3-macros-backend/src/pyimpl.rs @@ -242,14 +242,12 @@ 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::impl_::pymethods::PyMethodDefType::ClassAttribute({ - #pyo3_path::impl_::pymethods::PyClassAttributeDef::new( - #python_name, - #cls::#wrapper_ident - ) - }) - ) + #pyo3_path::impl_::pymethods::PyMethodDefType::ClassAttribute({ + #pyo3_path::impl_::pymethods::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 cfd7d4e083b..755bd367f27 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -365,9 +365,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::impl_::pymethods::PyMethodDefType::#methoddef_type(#methoddef #add_flags) - ) + #pyo3_path::impl_::pymethods::PyMethodDefType::#methoddef_type(#methoddef #add_flags) }; Ok(MethodAndMethodDef { associated_method, @@ -600,14 +598,12 @@ pub(crate) fn impl_py_class_attribute( }; let method_def = quote! { - #pyo3_path::impl_::pyclass::MaybeRuntimePyMethodDef::Static( - #pyo3_path::impl_::pymethods::PyMethodDefType::ClassAttribute({ - #pyo3_path::impl_::pymethods::PyClassAttributeDef::new( - #python_name, - #cls::#wrapper_ident - ) - }) - ) + #pyo3_path::impl_::pymethods::PyMethodDefType::ClassAttribute({ + #pyo3_path::impl_::pymethods::PyClassAttributeDef::new( + #python_name, + #cls::#wrapper_ident + ) + }) }; Ok(MethodAndMethodDef { @@ -788,13 +784,11 @@ pub fn impl_py_setter_def( let method_def = quote! { #cfg_attrs - #pyo3_path::impl_::pyclass::MaybeRuntimePyMethodDef::Static( - #pyo3_path::impl_::pymethods::PyMethodDefType::Setter( - #pyo3_path::impl_::pymethods::PySetterDef::new( - #python_name, - #cls::#wrapper_ident, - #doc - ) + #pyo3_path::impl_::pymethods::PyMethodDefType::Setter( + #pyo3_path::impl_::pymethods::PySetterDef::new( + #python_name, + #cls::#wrapper_ident, + #doc ) ) }; @@ -862,14 +856,10 @@ pub fn impl_py_getter_def( syn::Index::from(field_index).to_token_stream() }; - // 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 generator = quote_spanned! { ty.span() => - #pyo3_path::impl_::pyclass::MaybeRuntimePyMethodDef::Runtime( - || GENERATOR.generate(#python_name, #doc) - ) + GENERATOR.generate(#python_name, #doc) }; - // This is separate so that the unsafe below does not inherit the span and thus does not + // This is separate from `generator` 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 @@ -877,18 +867,10 @@ pub fn impl_py_getter_def( #[allow(unused_imports)] // might not be used if all probes are positive use #pyo3_path::impl_::pyclass::Probe as _; - 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, + { ::std::mem::offset_of!(#cls, #field) }, { #pyo3_path::impl_::pyclass::IsPyT::<#ty>::VALUE }, { #pyo3_path::impl_::pyclass::IsIntoPyObjectRef::<#ty>::VALUE }, { #pyo3_path::impl_::pyclass::IsIntoPyObject::<#ty>::VALUE }, @@ -930,13 +912,11 @@ pub fn impl_py_getter_def( let method_def = quote! { #cfg_attrs - #pyo3_path::impl_::pyclass::MaybeRuntimePyMethodDef::Static( - #pyo3_path::impl_::pymethods::PyMethodDefType::Getter( - #pyo3_path::impl_::pymethods::PyGetterDef::new( - #python_name, - #cls::#wrapper_ident, - #doc - ) + #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 d25e78d3a1e..772979136bb 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -9,15 +9,15 @@ use crate::{ }, pycell::PyBorrowError, types::{any::PyAnyMethods, PyBool}, - Borrowed, BoundObject, IntoPyObject, IntoPyObjectExt, Py, PyAny, PyClass, PyClassGuard, PyErr, - PyResult, PyTypeInfo, Python, + Borrowed, IntoPyObject, IntoPyObjectExt, Py, PyAny, PyClass, PyClassGuard, PyErr, PyResult, + PyTypeInfo, Python, }; use std::{ ffi::CStr, marker::PhantomData, + mem::offset_of, os::raw::{c_int, c_void}, - ptr, - ptr::NonNull, + ptr::{self, NonNull}, sync::Mutex, thread, }; @@ -145,15 +145,8 @@ 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 [MaybeRuntimePyMethodDef], + pub methods: &'static [PyMethodDefType], pub slots: &'static [ffi::PyType_Slot], } @@ -1189,46 +1182,25 @@ pub(crate) unsafe extern "C" fn assign_sequence_item_from_mapping( } } -/// 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 + // structural information about the field: class type, field type, offset of the field within + // the class struct ClassT: PyClass, FieldT, - Offset: OffsetCalculator, // on Rust 1.77+ this could be a const OFFSET: usize + 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_INTOPYOBJECT_REF: bool, const IMPLEMENTS_INTOPYOBJECT: bool, ->(PhantomData<(ClassT, FieldT, Offset)>); +>(PhantomData<(ClassT, FieldT)>); impl< ClassT: PyClass, FieldT, - Offset: OffsetCalculator, + const OFFSET: usize, const IS_PY_T: bool, const IMPLEMENTS_INTOPYOBJECT_REF: bool, const IMPLEMENTS_INTOPYOBJECT: bool, @@ -1236,7 +1208,7 @@ impl< PyClassGetterGenerator< ClassT, FieldT, - Offset, + OFFSET, IS_PY_T, IMPLEMENTS_INTOPYOBJECT_REF, IMPLEMENTS_INTOPYOBJECT, @@ -1252,14 +1224,14 @@ impl< impl< ClassT: PyClass, U, - Offset: OffsetCalculator>, + const OFFSET: usize, const IMPLEMENTS_INTOPYOBJECT_REF: bool, const IMPLEMENTS_INTOPYOBJECT: bool, > PyClassGetterGenerator< ClassT, Py, - Offset, + OFFSET, true, IMPLEMENTS_INTOPYOBJECT_REF, IMPLEMENTS_INTOPYOBJECT, @@ -1270,20 +1242,20 @@ impl< /// /// 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 { + pub const 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, + offset: (offset_of!(PyClassObject, contents) + OFFSET) as ffi::Py_ssize_t, flags: ffi::Py_READONLY, doc: doc.as_ptr(), }) } else { PyMethodDefType::Getter(PyGetterDef { name, - meth: pyo3_get_value_into_pyobject_ref::, Offset>, + meth: pyo3_get_value_into_pyobject_ref::, OFFSET>, doc, }) } @@ -1292,17 +1264,16 @@ impl< /// Field is not `Py`; try to use `IntoPyObject` for `&T` (preferred over `ToPyObject`) to avoid /// potentially expensive clones of containers like `Vec` -impl - PyClassGetterGenerator +impl + PyClassGetterGenerator where ClassT: PyClass, for<'a, 'py> &'a FieldT: IntoPyObject<'py>, - Offset: OffsetCalculator, { pub const fn generate(&self, name: &'static CStr, doc: &'static CStr) -> PyMethodDefType { PyMethodDefType::Getter(PyGetterDef { name, - meth: pyo3_get_value_into_pyobject_ref::, + meth: pyo3_get_value_into_pyobject_ref::, doc, }) } @@ -1317,12 +1288,8 @@ 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< - ClassT: PyClass, - FieldT, - Offset: OffsetCalculator, - const IMPLEMENTS_INTOPYOBJECT: bool, - > PyClassGetterGenerator +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 @@ -1332,7 +1299,7 @@ impl< { PyMethodDefType::Getter(PyGetterDef { name, - meth: pyo3_get_value_into_pyobject::, + meth: pyo3_get_value_into_pyobject::, doc, }) } @@ -1347,55 +1314,91 @@ unsafe fn ensure_no_mutable_alias<'a, ClassT: PyClass>( unsafe { PyClassGuard::try_borrow(NonNull::from(obj).cast::>().as_ref()) } } -/// 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_into_pyobject_ref( +/// Gets a field value from a pyclass and produces a python value using `IntoPyObject` for `&FieldT` +/// +/// # Safety +/// - `obj` must be a valid pointer to an instance of `ClassT` +/// - there must be a value of type `FieldT` at the calculated offset within `ClassT` +unsafe 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, { - let _holder = unsafe { ensure_no_mutable_alias::(py, &obj)? }; - let value = field_from_object::(obj); + /// Inner function to convert the field value at the given offset + /// + /// # Safety + /// - mutable aliasing is prevented by the caller + /// - value of type `FieldT` must exist at the given offset within obj + unsafe fn inner( + py: Python<'_>, + obj: *mut ffi::PyObject, + offset: usize, + ) -> PyResult<*mut ffi::PyObject> + where + for<'a, 'py> &'a FieldT: IntoPyObject<'py>, + { + // SAFETY: caller upholds safety invariants + let value = unsafe { &*obj.byte_add(offset).cast::() }; + value.into_py_any(py).map(Py::into_ptr) + } - // SAFETY: Offset is known to describe the location of the value, and - // _holder is preventing mutable aliasing - Ok((unsafe { &*value }) - .into_pyobject(py) - .map_err(Into::into)? - .into_ptr()) + // SAFETY: `obj` is a valid pointer to `ClassT` + let _holder = unsafe { ensure_no_mutable_alias::(py, &obj)? }; + // SAFETY: _holder prevents mutable aliasing, caller upholds other safety invariants + unsafe { + inner::( + py, + obj, + offset_of!(PyClassObject, contents) + OFFSET, + ) + } } -fn pyo3_get_value_into_pyobject( +/// Gets a field value from a pyclass and produces a python value using `IntoPyObject` for `FieldT`, +/// after cloning the value. +/// +/// # Safety +/// - `obj` must be a valid pointer to an instance of `ClassT` +/// - there must be a value of type `FieldT` at the calculated offset within `ClassT` +unsafe 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, { - let _holder = unsafe { ensure_no_mutable_alias::(py, &obj)? }; - let value = field_from_object::(obj); + /// Inner function to convert the field value at the given offset + /// + /// # Safety + /// - mutable aliasing is prevented by the caller + /// - value of type `FieldT` must exist at the given offset within obj + unsafe fn inner( + py: Python<'_>, + obj: *mut ffi::PyObject, + offset: usize, + ) -> PyResult<*mut ffi::PyObject> + where + for<'py> FieldT: IntoPyObject<'py> + Clone, + { + // SAFETY: caller upholds safety invariants + let value = unsafe { &*obj.byte_add(offset).cast::() }; + value.clone().into_py_any(py).map(Py::into_ptr) + } - // 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()) + // SAFETY: `obj` is a valid pointer to `ClassT` + let _holder = unsafe { ensure_no_mutable_alias::(py, &obj)? }; + // SAFETY: _holder prevents mutable aliasing, caller upholds other safety invariants + unsafe { + inner::( + py, + obj, + offset_of!(PyClassObject, contents) + OFFSET, + ) + } } pub struct ConvertField< @@ -1442,10 +1445,7 @@ mod tests { let mut slots = Vec::new(); for items in FrozenClass::items_iter() { - methods.extend(items.methods.iter().map(|m| match m { - MaybeRuntimePyMethodDef::Static(m) => *m, - MaybeRuntimePyMethodDef::Runtime(r) => r(), - })); + methods.extend_from_slice(items.methods); slots.extend_from_slice(items.slots); } @@ -1458,9 +1458,8 @@ mod tests { 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 + (offset_of!(PyClassObject, contents) + + offset_of!(FrozenClass, value)) as ffi::Py_ssize_t ); assert_eq!(member.flags, ffi::Py_READONLY); } @@ -1480,10 +1479,7 @@ mod tests { let mut slots = Vec::new(); for items in FrozenClass::items_iter() { - methods.extend(items.methods.iter().map(|m| match m { - MaybeRuntimePyMethodDef::Static(m) => *m, - MaybeRuntimePyMethodDef::Runtime(r) => r(), - })); + methods.extend_from_slice(items.methods); slots.extend_from_slice(items.slots); } @@ -1499,4 +1495,96 @@ mod tests { _ => panic!("Expected a StructMember"), } } + + #[test] + fn test_field_getter_generator() { + #[crate::pyclass(crate = "crate")] + struct MyClass { + my_field: i32, + } + + const FIELD_OFFSET: usize = offset_of!(MyClass, my_field); + + // generate for a non-py field using IntoPyObject for &i32 + // SAFETY: offset is correct + let generator = unsafe { + PyClassGetterGenerator::::new() + }; + let PyMethodDefType::Getter(def) = generator.generate(c"my_field", c"My field doc") else { + panic!("Expected a Getter"); + }; + + assert_eq!(def.name, c"my_field"); + assert_eq!(def.doc, c"My field doc"); + assert_eq!( + def.meth as usize, + pyo3_get_value_into_pyobject_ref:: as usize + ); + + // generate for a field via `IntoPyObject` + `Clone` + // SAFETY: offset is correct + let generator = unsafe { + PyClassGetterGenerator::::new() + }; + let PyMethodDefType::Getter(def) = generator.generate(c"my_field", c"My field doc") else { + panic!("Expected a Getter"); + }; + assert_eq!(def.name, c"my_field"); + assert_eq!(def.doc, c"My field doc"); + assert_eq!( + def.meth as usize, + pyo3_get_value_into_pyobject:: as usize + ); + } + + #[test] + fn test_field_getter_generator_py_field_frozen() { + #[crate::pyclass(crate = "crate", frozen)] + struct MyClass { + my_field: Py, + } + + const FIELD_OFFSET: usize = offset_of!(MyClass, my_field); + // SAFETY: offset is correct + let generator = unsafe { + PyClassGetterGenerator::, FIELD_OFFSET, true, true, true>::new() + }; + let PyMethodDefType::StructMember(def) = generator.generate(c"my_field", c"My field doc") + else { + panic!("Expected a StructMember"); + }; + // SAFETY: def.name originated from a CStr + assert_eq!(unsafe { CStr::from_ptr(def.name) }, c"my_field"); + // SAFETY: def.doc originated from a CStr + assert_eq!(unsafe { CStr::from_ptr(def.doc) }, c"My field doc"); + assert_eq!(def.type_code, ffi::Py_T_OBJECT_EX); + assert_eq!( + def.offset, + (offset_of!(PyClassObject, contents) + FIELD_OFFSET) as ffi::Py_ssize_t + ); + assert_eq!(def.flags, ffi::Py_READONLY); + } + + #[test] + fn test_field_getter_generator_py_field_non_frozen() { + #[crate::pyclass(crate = "crate")] + struct MyClass { + my_field: Py, + } + + const FIELD_OFFSET: usize = offset_of!(MyClass, my_field); + // SAFETY: offset is correct + let generator = unsafe { + PyClassGetterGenerator::, FIELD_OFFSET, true, true, true>::new() + }; + let PyMethodDefType::Getter(def) = generator.generate(c"my_field", c"My field doc") else { + panic!("Expected a Getter"); + }; + assert_eq!(def.name, c"my_field"); + assert_eq!(def.doc, c"My field doc"); + assert_eq!( + def.meth as usize, + pyo3_get_value_into_pyobject_ref::, FIELD_OFFSET> as usize + ); + } } diff --git a/src/impl_/pyclass/lazy_type_object.rs b/src/impl_/pyclass/lazy_type_object.rs index 408d00ad6d7..e6e07174296 100644 --- a/src/impl_/pyclass/lazy_type_object.rs +++ b/src/impl_/pyclass/lazy_type_object.rs @@ -13,7 +13,7 @@ use crate::types::PyTypeMethods; use crate::{ exceptions::PyRuntimeError, ffi, - impl_::{pyclass::MaybeRuntimePyMethodDef, pymethods::PyMethodDefType}, + impl_::pymethods::PyMethodDefType, pyclass::{create_type_object, PyClassTypeObject}, types::PyType, Bound, Py, PyAny, PyClass, PyErr, PyResult, Python, @@ -162,15 +162,7 @@ impl LazyTypeObjectInner { // meantime: at worst, we'll just make a useless computation. let mut items = vec![]; for class_items in items_iter { - for def in class_items.methods { - let built_method; - let method = match def { - MaybeRuntimePyMethodDef::Runtime(builder) => { - built_method = builder(); - &built_method - } - MaybeRuntimePyMethodDef::Static(method) => method, - }; + for method in class_items.methods { if let PyMethodDefType::ClassAttribute(attr) = method { match (attr.meth)(py) { Ok(val) => items.push((attr.name, val)), diff --git a/src/pycell/impl_.rs b/src/pycell/impl_.rs index 3401ed8a8b3..32d345fcfb6 100644 --- a/src/pycell/impl_.rs +++ b/src/pycell/impl_.rs @@ -3,7 +3,7 @@ use std::cell::UnsafeCell; use std::marker::PhantomData; -use std::mem::ManuallyDrop; +use std::mem::{offset_of, ManuallyDrop}; use std::sync::atomic::{AtomicUsize, Ordering}; use crate::impl_::pyclass::{ @@ -299,8 +299,6 @@ impl PyClassObject { /// Gets the offset of the dictionary from the start of the struct in bytes. pub(crate) fn dict_offset() -> ffi::Py_ssize_t { - use memoffset::offset_of; - let offset = offset_of!(PyClassObject, contents) + offset_of!(PyClassObjectContents, dict); @@ -311,8 +309,6 @@ impl PyClassObject { /// Gets the offset of the weakref list from the start of the struct in bytes. pub(crate) fn weaklist_offset() -> ffi::Py_ssize_t { - use memoffset::offset_of; - let offset = offset_of!(PyClassObject, contents) + offset_of!(PyClassObjectContents, weakref); diff --git a/src/pyclass/create_type_object.rs b/src/pyclass/create_type_object.rs index 26252ae9836..7fd3ed2b09e 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, MaybeRuntimePyMethodDef, PyClassItemsIter, + tp_dealloc_with_gc, PyClassItemsIter, }, pymethods::{Getter, PyGetterDef, PyMethodDefType, PySetterDef, Setter, _call_clear}, trampoline::trampoline, @@ -322,14 +322,6 @@ impl PyTypeBuilder { unsafe { 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); } } diff --git a/src/types/capsule.rs b/src/types/capsule.rs index 96fc174c5fc..8f109884c6e 100644 --- a/src/types/capsule.rs +++ b/src/types/capsule.rs @@ -7,6 +7,7 @@ use crate::{Bound, Python}; use crate::{PyErr, PyResult}; use std::ffi::{c_char, c_int, c_void}; use std::ffi::{CStr, CString}; +use std::mem::offset_of; use std::ptr::{self, NonNull}; /// Represents a Python Capsule @@ -112,7 +113,7 @@ impl PyCapsule { AssertNotZeroSized::assert_not_zero_sized(&value); // Sanity check for capsule layout - debug_assert_eq!(memoffset::offset_of!(CapsuleContents::, value), 0); + debug_assert_eq!(offset_of!(CapsuleContents::, value), 0); let name_ptr = name.as_ref().map_or(std::ptr::null(), |name| name.as_ptr()); let val = Box::into_raw(Box::new(CapsuleContents { From d15a14ad73b827b5c402577c21c5c92388aa114e Mon Sep 17 00:00:00 2001 From: Thomas Tanon Date: Wed, 22 Oct 2025 22:49:23 +0200 Subject: [PATCH 935/936] Introspection: Introduce TypeHint struct (#5438) * Introduce TypeHint struct inspect::TypeHint is composed of an "annotation" string and a list of "imports" ("from X import Y" kind) The type is expected to be built using the macros `type_hint!(module, name)`, `type_hint_union!(*args)` and `type_hint_subscript(main, *args)` that take care of maintaining the import list Introspection data generation is done using the hidden type_hint_json macro to avoid that the proc macros generate too much code Sadly, outside `type_hint` these macros can't be converted into const functions because they need to do some concatenation. I introduced `type_hint!` for consistency, happy to convert it to a const function. Miscellaneous changes: - Rename PyType{Info,Check}::TYPE_INFO into TYPE_HINT - Drop redundant PyClassImpl::TYPE_NAME * Makes TypeHint an enum * Code review feedbacks * Handle nested modules imports * MSRV is now 1.83 * Code review feedback --- newsfragments/5438.changed.md | 2 + noxfile.py | 10 +- pyo3-introspection/Cargo.toml | 1 - pyo3-introspection/src/introspection.rs | 124 +++++- pyo3-introspection/src/model.rs | 33 +- pyo3-introspection/src/stubs.rs | 539 +++++++++++++++++------ pyo3-macros-backend/src/frompyobject.rs | 64 ++- pyo3-macros-backend/src/intopyobject.rs | 72 ++- pyo3-macros-backend/src/introspection.rs | 71 ++- pyo3-macros-backend/src/pyclass.rs | 50 +-- pytests/Cargo.toml | 5 +- pytests/src/pyfunctions.rs | 6 +- pytests/stubs/__init__.pyi | 4 +- pytests/stubs/consts.pyi | 9 +- pytests/stubs/pyclasses.pyi | 10 +- pytests/stubs/pyfunctions.pyi | 60 ++- src/conversion.rs | 14 +- src/conversions/std/cell.rs | 8 +- src/conversions/std/num.rs | 44 +- src/conversions/std/string.rs | 29 +- src/impl_/extract_argument.rs | 19 +- src/impl_/introspection.rs | 7 +- src/impl_/pyclass.rs | 3 - src/inspect/mod.rs | 267 +++++++++++ src/instance.rs | 6 +- src/pycell.rs | 10 +- src/pyclass/guard.rs | 4 +- src/type_object.rs | 12 +- src/types/boolobject.rs | 15 +- src/types/float.rs | 14 +- src/types/weakref/anyref.rs | 8 +- src/types/weakref/proxy.rs | 7 +- 32 files changed, 1108 insertions(+), 419 deletions(-) create mode 100644 newsfragments/5438.changed.md diff --git a/newsfragments/5438.changed.md b/newsfragments/5438.changed.md new file mode 100644 index 00000000000..bcd7885673d --- /dev/null +++ b/newsfragments/5438.changed.md @@ -0,0 +1,2 @@ +Introspection: introduce `TypeHint` and make use of it to encode type hint annotations. +Rename `PyType{Info,Check}::TYPE_INFO` into `PyType{Info,Check}::TYPE_HINT` diff --git a/noxfile.py b/noxfile.py index cb71e6896dc..fc1d2b50c9b 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1109,7 +1109,15 @@ def test_introspection(session: nox.Session): profile = os.environ.get("CARGO_BUILD_PROFILE") if profile == "release": options.append("--release") - session.run_always("maturin", "develop", "-m", "./pytests/Cargo.toml", *options) + session.run_always( + "maturin", + "develop", + "-m", + "./pytests/Cargo.toml", + "--features", + "experimental-inspect", + *options, + ) # We look for the built library lib_file = None for file in Path(session.virtualenv.location).rglob("pyo3_pytests.*"): diff --git a/pyo3-introspection/Cargo.toml b/pyo3-introspection/Cargo.toml index 8b79b97622f..ae78c7d5030 100644 --- a/pyo3-introspection/Cargo.toml +++ b/pyo3-introspection/Cargo.toml @@ -13,7 +13,6 @@ anyhow = "1" goblin = ">=0.9, <0.11" serde = { version = "1", features = ["derive"] } serde_json = "1" -unicode-ident = "1" [dev-dependencies] tempfile = "3.12.0" diff --git a/pyo3-introspection/src/introspection.rs b/pyo3-introspection/src/introspection.rs index 2a5b94931f9..ea57fdaf814 100644 --- a/pyo3-introspection/src/introspection.rs +++ b/pyo3-introspection/src/introspection.rs @@ -1,5 +1,6 @@ use crate::model::{ - Argument, Arguments, Attribute, Class, Function, Module, VariableLengthArgument, + Argument, Arguments, Attribute, Class, Function, Module, TypeHint, TypeHintExpr, + VariableLengthArgument, }; use anyhow::{anyhow, bail, ensure, Context, Result}; use goblin::elf::section_header::SHN_XINDEX; @@ -9,11 +10,13 @@ use goblin::mach::symbols::{NO_SECT, N_SECT}; use goblin::mach::{Mach, MachO, SingleArch}; use goblin::pe::PE; use goblin::Object; -use serde::Deserialize; +use serde::de::value::MapAccessDeserializer; +use serde::de::{Error, MapAccess, Visitor}; +use serde::{Deserialize, Deserializer}; use std::cmp::Ordering; use std::collections::HashMap; use std::path::Path; -use std::{fs, str}; +use std::{fmt, fs, str}; /// Introspect a cdylib built with PyO3 and returns the definition of a Python module. /// @@ -192,7 +195,7 @@ fn convert_function( name: &str, arguments: &ChunkArguments, decorators: &[String], - returns: &Option, + returns: &Option, ) -> Function { Function { name: name.into(), @@ -210,7 +213,7 @@ fn convert_function( .as_ref() .map(convert_variable_length_argument), }, - returns: returns.clone(), + returns: returns.as_ref().map(convert_type_hint), } } @@ -218,22 +221,50 @@ fn convert_argument(arg: &ChunkArgument) -> Argument { Argument { name: arg.name.clone(), default_value: arg.default.clone(), - annotation: arg.annotation.clone(), + annotation: arg.annotation.as_ref().map(convert_type_hint), } } fn convert_variable_length_argument(arg: &ChunkArgument) -> VariableLengthArgument { VariableLengthArgument { name: arg.name.clone(), - annotation: arg.annotation.clone(), + annotation: arg.annotation.as_ref().map(convert_type_hint), } } -fn convert_attribute(name: &str, value: &Option, annotation: &Option) -> Attribute { +fn convert_attribute( + name: &str, + value: &Option, + annotation: &Option, +) -> Attribute { Attribute { name: name.into(), value: value.clone(), - annotation: annotation.clone(), + annotation: annotation.as_ref().map(convert_type_hint), + } +} + +fn convert_type_hint(arg: &ChunkTypeHint) -> TypeHint { + match arg { + ChunkTypeHint::Ast(expr) => TypeHint::Ast(convert_type_hint_expr(expr)), + ChunkTypeHint::Plain(t) => TypeHint::Plain(t.clone()), + } +} + +fn convert_type_hint_expr(expr: &ChunkTypeHintExpr) -> TypeHintExpr { + match expr { + ChunkTypeHintExpr::Builtin { id } => TypeHintExpr::Builtin { id: id.clone() }, + ChunkTypeHintExpr::Attribute { module, attr } => TypeHintExpr::Attribute { + module: module.clone(), + attr: attr.clone(), + }, + ChunkTypeHintExpr::Union { elts } => TypeHintExpr::Union { + elts: elts.iter().map(convert_type_hint_expr).collect(), + }, + ChunkTypeHintExpr::Subscript { value, slice } => TypeHintExpr::Subscript { + value: Box::new(convert_type_hint_expr(value)), + slice: slice.iter().map(convert_type_hint_expr).collect(), + }, } } @@ -388,8 +419,8 @@ enum Chunk { parent: Option, #[serde(default)] decorators: Vec, - #[serde(default)] - returns: Option, + #[serde(default, deserialize_with = "deserialize_type_hint")] + returns: Option, }, Attribute { #[serde(default)] @@ -399,8 +430,8 @@ enum Chunk { name: String, #[serde(default)] value: Option, - #[serde(default)] - annotation: Option, + #[serde(default, deserialize_with = "deserialize_type_hint")] + annotation: Option, }, } @@ -423,6 +454,69 @@ struct ChunkArgument { name: String, #[serde(default)] default: Option, - #[serde(default)] - annotation: Option, + #[serde(default, deserialize_with = "deserialize_type_hint")] + annotation: Option, +} + +/// Variant of [`TypeHint`] that implements deserialization. +/// +/// We keep separated type to allow them to evolve independently (this type will need to handle backward compatibility). +enum ChunkTypeHint { + Ast(ChunkTypeHintExpr), + Plain(String), +} + +#[derive(Deserialize)] +#[serde(tag = "type", rename_all = "lowercase")] +enum ChunkTypeHintExpr { + Builtin { + id: String, + }, + Attribute { + module: String, + attr: String, + }, + Union { + elts: Vec, + }, + Subscript { + value: Box, + slice: Vec, + }, +} + +fn deserialize_type_hint<'de, D: Deserializer<'de>>( + deserializer: D, +) -> Result, D::Error> { + struct AnnotationVisitor; + + impl<'de> Visitor<'de> for AnnotationVisitor { + type Value = ChunkTypeHint; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("annotation") + } + + fn visit_str(self, v: &str) -> Result + where + E: Error, + { + self.visit_string(v.into()) + } + + fn visit_string(self, v: String) -> Result + where + E: Error, + { + Ok(ChunkTypeHint::Plain(v)) + } + + fn visit_map>(self, map: M) -> Result { + Ok(ChunkTypeHint::Ast(Deserialize::deserialize( + MapAccessDeserializer::new(map), + )?)) + } + } + + Ok(Some(deserializer.deserialize_any(AnnotationVisitor)?)) } diff --git a/pyo3-introspection/src/model.rs b/pyo3-introspection/src/model.rs index 9f86bb7e303..5d13d15c51b 100644 --- a/pyo3-introspection/src/model.rs +++ b/pyo3-introspection/src/model.rs @@ -22,7 +22,7 @@ pub struct Function { pub decorators: Vec, pub arguments: Arguments, /// return type - pub returns: Option, + pub returns: Option, } #[derive(Debug, Eq, PartialEq, Clone, Hash)] @@ -31,7 +31,7 @@ pub struct Attribute { /// Value as a Python expression if easily expressible pub value: Option, /// Type annotation as a Python expression - pub annotation: Option, + pub annotation: Option, } #[derive(Debug, Eq, PartialEq, Clone, Hash)] @@ -54,7 +54,7 @@ pub struct Argument { /// Default value as a Python expression pub default_value: Option, /// Type annotation as a Python expression - pub annotation: Option, + pub annotation: Option, } /// A variable length argument ie. *vararg or **kwarg @@ -62,5 +62,30 @@ pub struct Argument { pub struct VariableLengthArgument { pub name: String, /// Type annotation as a Python expression - pub annotation: Option, + pub annotation: Option, +} + +/// A type hint annotation +/// +/// Might be a plain string or an AST fragment +#[derive(Debug, Eq, PartialEq, Clone, Hash)] +pub enum TypeHint { + Ast(TypeHintExpr), + Plain(String), +} + +/// A type hint annotation as an AST fragment +#[derive(Debug, Eq, PartialEq, Clone, Hash)] +pub enum TypeHintExpr { + /// A Python builtin like `int` + Builtin { id: String }, + /// The attribute of a python object like `{value}.{attr}` + Attribute { module: String, attr: String }, + /// A union `{left} | {right}` + Union { elts: Vec }, + /// A subscript `{value}[*slice]` + Subscript { + value: Box, + slice: Vec, + }, } diff --git a/pyo3-introspection/src/stubs.rs b/pyo3-introspection/src/stubs.rs index baad91dd6e2..d66d899ce00 100644 --- a/pyo3-introspection/src/stubs.rs +++ b/pyo3-introspection/src/stubs.rs @@ -1,9 +1,10 @@ use crate::model::{ - Argument, Arguments, Attribute, Class, Function, Module, VariableLengthArgument, + Argument, Arguments, Attribute, Class, Function, Module, TypeHint, TypeHintExpr, + VariableLengthArgument, }; -use std::collections::{BTreeSet, HashMap}; -use std::path::{Path, PathBuf}; -use unicode_ident::{is_xid_continue, is_xid_start}; +use std::collections::{BTreeMap, BTreeSet, HashMap}; +use std::path::PathBuf; +use std::str::FromStr; /// Generates the [type stubs](https://typing.readthedocs.io/en/latest/source/stubs.html) of a given module. /// It returns a map between the file name and the file content. @@ -11,40 +12,49 @@ use unicode_ident::{is_xid_continue, is_xid_start}; /// in files with a relevant name. pub fn module_stub_files(module: &Module) -> HashMap { let mut output_files = HashMap::new(); - add_module_stub_files(module, Path::new(""), &mut output_files); + add_module_stub_files(module, &[], &mut output_files); output_files } fn add_module_stub_files( module: &Module, - module_path: &Path, + module_path: &[&str], output_files: &mut HashMap, ) { - output_files.insert(module_path.join("__init__.pyi"), module_stubs(module)); + let mut file_path = PathBuf::new(); + for e in module_path { + file_path = file_path.join(e); + } + output_files.insert( + file_path.join("__init__.pyi"), + module_stubs(module, module_path), + ); + let mut module_path = module_path.to_vec(); + module_path.push(&module.name); for submodule in &module.modules { if submodule.modules.is_empty() { output_files.insert( - module_path.join(format!("{}.pyi", submodule.name)), - module_stubs(submodule), + file_path.join(format!("{}.pyi", submodule.name)), + module_stubs(submodule, &module_path), ); } else { - add_module_stub_files(submodule, &module_path.join(&submodule.name), output_files); + add_module_stub_files(submodule, &module_path, output_files); } } } /// Generates the module stubs to a String, not including submodules -fn module_stubs(module: &Module) -> String { - let mut modules_to_import = BTreeSet::new(); +fn module_stubs(module: &Module, parents: &[&str]) -> String { + let imports = Imports::create(module, parents); let mut elements = Vec::new(); for attribute in &module.attributes { - elements.push(attribute_stubs(attribute, &mut modules_to_import)); + elements.push(attribute_stubs(attribute, &imports)); } for class in &module.classes { - elements.push(class_stubs(class, &mut modules_to_import)); + elements.push(class_stubs(class, &imports)); } for function in &module.functions { - elements.push(function_stubs(function, &mut modules_to_import)); + elements.push(function_stubs(function, &imports)); } // We generate a __getattr__ method to tag incomplete stubs @@ -59,22 +69,22 @@ fn module_stubs(module: &Module) -> String { arguments: vec![Argument { name: "name".to_string(), default_value: None, - annotation: Some("str".into()), + annotation: Some(TypeHint::Ast(TypeHintExpr::Builtin { id: "str".into() })), }], vararg: None, keyword_only_arguments: Vec::new(), kwarg: None, }, - returns: Some("_typeshed.Incomplete".into()), + returns: Some(TypeHint::Ast(TypeHintExpr::Attribute { + module: "_typeshed".into(), + attr: "Incomplete".into(), + })), }, - &mut modules_to_import, + &imports, )); } - let mut final_elements = Vec::new(); - for module_to_import in &modules_to_import { - final_elements.push(format!("import {module_to_import}")); - } + let mut final_elements = imports.imports; final_elements.extend(elements); let mut output = String::new(); @@ -99,7 +109,7 @@ fn module_stubs(module: &Module) -> String { output } -fn class_stubs(class: &Class, modules_to_import: &mut BTreeSet) -> String { +fn class_stubs(class: &Class, imports: &Imports) -> String { let mut buffer = format!("class {}:", class.name); if class.methods.is_empty() && class.attributes.is_empty() { buffer.push_str(" ..."); @@ -108,43 +118,43 @@ fn class_stubs(class: &Class, modules_to_import: &mut BTreeSet) -> Strin for attribute in &class.attributes { // We do the indentation buffer.push_str("\n "); - buffer.push_str(&attribute_stubs(attribute, modules_to_import).replace('\n', "\n ")); + buffer.push_str(&attribute_stubs(attribute, imports).replace('\n', "\n ")); } for method in &class.methods { // We do the indentation buffer.push_str("\n "); - buffer.push_str(&function_stubs(method, modules_to_import).replace('\n', "\n ")); + buffer.push_str(&function_stubs(method, imports).replace('\n', "\n ")); } buffer } -fn function_stubs(function: &Function, modules_to_import: &mut BTreeSet) -> String { +fn function_stubs(function: &Function, imports: &Imports) -> String { // Signature let mut parameters = Vec::new(); for argument in &function.arguments.positional_only_arguments { - parameters.push(argument_stub(argument, modules_to_import)); + parameters.push(argument_stub(argument, imports)); } if !function.arguments.positional_only_arguments.is_empty() { parameters.push("/".into()); } for argument in &function.arguments.arguments { - parameters.push(argument_stub(argument, modules_to_import)); + parameters.push(argument_stub(argument, imports)); } if let Some(argument) = &function.arguments.vararg { parameters.push(format!( "*{}", - variable_length_argument_stub(argument, modules_to_import) + variable_length_argument_stub(argument, imports) )); } else if !function.arguments.keyword_only_arguments.is_empty() { parameters.push("*".into()); } for argument in &function.arguments.keyword_only_arguments { - parameters.push(argument_stub(argument, modules_to_import)); + parameters.push(argument_stub(argument, imports)); } if let Some(argument) = &function.arguments.kwarg { parameters.push(format!( "**{}", - variable_length_argument_stub(argument, modules_to_import) + variable_length_argument_stub(argument, imports) )); } let mut buffer = String::new(); @@ -160,150 +170,327 @@ fn function_stubs(function: &Function, modules_to_import: &mut BTreeSet) buffer.push(')'); if let Some(returns) = &function.returns { buffer.push_str(" -> "); - buffer.push_str(annotation_stub(returns, modules_to_import)); + type_hint_stub(returns, imports, &mut buffer); } buffer.push_str(": ..."); buffer } -fn attribute_stubs(attribute: &Attribute, modules_to_import: &mut BTreeSet) -> String { - let mut output = attribute.name.clone(); +fn attribute_stubs(attribute: &Attribute, imports: &Imports) -> String { + let mut buffer = attribute.name.clone(); if let Some(annotation) = &attribute.annotation { - output.push_str(": "); - output.push_str(annotation_stub(annotation, modules_to_import)); + buffer.push_str(": "); + type_hint_stub(annotation, imports, &mut buffer); } if let Some(value) = &attribute.value { - output.push_str(" = "); - output.push_str(value); + buffer.push_str(" = "); + buffer.push_str(value); } - output + buffer } -fn argument_stub(argument: &Argument, modules_to_import: &mut BTreeSet) -> String { - let mut output = argument.name.clone(); +fn argument_stub(argument: &Argument, imports: &Imports) -> String { + let mut buffer = argument.name.clone(); if let Some(annotation) = &argument.annotation { - output.push_str(": "); - output.push_str(annotation_stub(annotation, modules_to_import)); + buffer.push_str(": "); + type_hint_stub(annotation, imports, &mut buffer); } if let Some(default_value) = &argument.default_value { - output.push_str(if argument.annotation.is_some() { + buffer.push_str(if argument.annotation.is_some() { " = " } else { "=" }); - output.push_str(default_value); + buffer.push_str(default_value); } - output + buffer } -fn variable_length_argument_stub( - argument: &VariableLengthArgument, - modules_to_import: &mut BTreeSet, -) -> String { - let mut output = argument.name.clone(); +fn variable_length_argument_stub(argument: &VariableLengthArgument, imports: &Imports) -> String { + let mut buffer = argument.name.clone(); if let Some(annotation) = &argument.annotation { - output.push_str(": "); - output.push_str(annotation_stub(annotation, modules_to_import)); + buffer.push_str(": "); + type_hint_stub(annotation, imports, &mut buffer); } - output + buffer } -fn annotation_stub<'a>(annotation: &'a str, modules_to_import: &mut BTreeSet) -> &'a str { - // We iterate on the annotation string - // If it starts with a Python path like foo.bar, we add the module name (here foo) to the import list - // and we skip after it - let mut i = 0; - while i < annotation.len() { - if let Some(path) = path_prefix(&annotation[i..]) { - // We found a path! - i += path.len(); - if let Some((module, _)) = path.rsplit_once('.') { - modules_to_import.insert(module.into()); - } - } - i += 1; +fn type_hint_stub(type_hint: &TypeHint, imports: &Imports, buffer: &mut String) { + match type_hint { + TypeHint::Ast(t) => imports.serialize_type_hint(t, buffer), + TypeHint::Plain(t) => buffer.push_str(t), } - annotation } -// If the input starts with a path like foo.bar, returns it -fn path_prefix(input: &str) -> Option<&str> { - let mut length = identifier_prefix(input)?.len(); - loop { - // We try to add another identifier to the path - let Some(remaining) = input[length..].strip_prefix('.') else { - break; - }; - let Some(id) = identifier_prefix(remaining) else { - break; - }; - length += id.len() + 1; - } - Some(&input[..length]) +/// Datastructure to deduplicate, validate and generate imports +#[derive(Default)] +struct Imports { + /// Import lines ready to use + imports: Vec, + /// Renaming map: from module name and member name return the name to use in type hints + renaming: BTreeMap<(String, String), String>, } -// If the input starts with an identifier like foo, returns it -fn identifier_prefix(input: &str) -> Option<&str> { - // We get the first char and validate it - let mut iter = input.chars(); - let first_char = iter.next()?; - if first_char != '_' && !is_xid_start(first_char) { - return None; - } - let mut length = first_char.len_utf8(); - // We add extra chars as much as we can - for c in iter { - if is_xid_continue(c) { - length += c.len_utf8(); - } else { - break; +impl Imports { + /// This generates a map from the builtin or module name to the actual alias used in the file + /// + /// For Python builtins and elements declared by the module the alias is always the actual name. + /// + /// For other elements, we can alias them using the `from X import Y as Z` syntax. + /// So, we first list all builtins and local elements, then iterate on imports + /// and create the aliases when needed. + fn create(module: &Module, module_parents: &[&str]) -> Self { + let mut elements_used_in_annotations = ElementsUsedInAnnotations::new(); + elements_used_in_annotations.walk_module(module); + + let mut imports = Vec::new(); + let mut renaming = BTreeMap::new(); + let mut local_name_to_module_and_attribute = BTreeMap::new(); + + // We first process local and built-ins elements, they are never aliased or imported + for name in module + .classes + .iter() + .map(|c| c.name.clone()) + .chain(module.functions.iter().map(|f| f.name.clone())) + .chain(module.attributes.iter().map(|a| a.name.clone())) + .chain(elements_used_in_annotations.builtins) + { + local_name_to_module_and_attribute.insert(name.clone(), (None, name.clone())); } + + // We compute the set of ways the current module can be named + let mut possible_current_module_names = vec![module.name.clone()]; + let mut current_module_name = Some(module.name.clone()); + for parent in module_parents.iter().rev() { + let path = if let Some(current) = current_module_name { + format!("{parent}.{current}") + } else { + parent.to_string() + }; + possible_current_module_names.push(path.clone()); + current_module_name = Some(path); + } + + // We process then imports, normalizing local imports + for (module, attrs) in elements_used_in_annotations.module_members { + let normalized_module = if possible_current_module_names.contains(&module) { + None + } else { + Some(module.clone()) + }; + let mut import_for_module = Vec::new(); + for attr in attrs { + // We split nested classes A.B in "A" (the part that must be imported and can have naming conflicts) and ".B" + let (root_attr, attr_path) = attr + .split_once('.') + .map_or((attr.as_str(), None), |(root, path)| (root, Some(path))); + let mut local_name = root_attr.to_owned(); + let mut already_imported = false; + while let Some((possible_conflict_module, possible_conflict_attr)) = + local_name_to_module_and_attribute.get(&local_name) + { + if *possible_conflict_module == normalized_module + && *possible_conflict_attr == root_attr + { + // It's the same + already_imported = true; + break; + } + // We generate a new local name + // TODO: we use currently a format like Foo2. It might be nicer to use something like ModFoo + let number_of_digits_at_the_end = local_name + .bytes() + .rev() + .take_while(|b| b.is_ascii_digit()) + .count(); + let (local_name_prefix, local_name_number) = + local_name.split_at(local_name.len() - number_of_digits_at_the_end); + local_name = format!( + "{local_name_prefix}{}", + u64::from_str(local_name_number).unwrap_or(1) + 1 + ); + } + renaming.insert( + (module.clone(), attr.clone()), + if let Some(attr_path) = attr_path { + format!("{local_name}.{attr_path}") + } else { + local_name.clone() + }, + ); + if !already_imported { + local_name_to_module_and_attribute.insert( + local_name.clone(), + (normalized_module.clone(), root_attr.to_owned()), + ); + import_for_module.push(if local_name == root_attr { + local_name + } else { + format!("{root_attr} as {local_name}") + }); + } + } + if let Some(module) = normalized_module { + imports.push(format!( + "from {module} import {}", + import_for_module.join(", ") + )); + } + } + + Self { imports, renaming } } - Some(&input[0..length]) -} -#[cfg(test)] -mod tests { - use super::*; - use crate::model::Arguments; + fn serialize_type_hint(&self, expr: &TypeHintExpr, buffer: &mut String) { + match expr { + TypeHintExpr::Builtin { id } => buffer.push_str(id), + TypeHintExpr::Attribute { module, attr } => { + let alias = self + .renaming + .get(&(module.clone(), attr.clone())) + .expect("All type hint attributes should have been visited"); + buffer.push_str(alias) + } + TypeHintExpr::Union { elts } => { + for (i, elt) in elts.iter().enumerate() { + if i > 0 { + buffer.push_str(" | "); + } + self.serialize_type_hint(elt, buffer); + } + } + TypeHintExpr::Subscript { value, slice } => { + self.serialize_type_hint(value, buffer); + buffer.push('['); + for (i, elt) in slice.iter().enumerate() { + if i > 0 { + buffer.push_str(", "); + } + self.serialize_type_hint(elt, buffer); + } + buffer.push(']'); + } + } + } +} - #[test] - fn annotation_stub_proper_imports() { - let mut modules_to_import = BTreeSet::new(); +/// Lists all the elements used in annotations +struct ElementsUsedInAnnotations { + /// module -> name + module_members: BTreeMap>, + builtins: BTreeSet, +} - // Basic int - annotation_stub("int", &mut modules_to_import); - assert!(modules_to_import.is_empty()); +impl ElementsUsedInAnnotations { + fn new() -> Self { + Self { + module_members: BTreeMap::new(), + builtins: BTreeSet::new(), + } + } - // Simple path - annotation_stub("collections.abc.Iterable", &mut modules_to_import); - assert!(modules_to_import.contains("collections.abc")); + fn walk_module(&mut self, module: &Module) { + for attr in &module.attributes { + self.walk_attribute(attr); + } + for class in &module.classes { + self.walk_class(class); + } + for function in &module.functions { + self.walk_function(function); + } + if module.incomplete { + self.builtins.insert("str".into()); + self.module_members + .entry("_typeshed".into()) + .or_default() + .insert("Incomplete".into()); + } + } - // With underscore - annotation_stub("_foo._bar_baz", &mut modules_to_import); - assert!(modules_to_import.contains("_foo")); + fn walk_class(&mut self, class: &Class) { + for method in &class.methods { + self.walk_function(method); + } + for attr in &class.attributes { + self.walk_attribute(attr); + } + } - // Basic generic - annotation_stub("typing.List[int]", &mut modules_to_import); - assert!(modules_to_import.contains("typing")); + fn walk_attribute(&mut self, attribute: &Attribute) { + if let Some(type_hint) = &attribute.annotation { + self.walk_type_hint(type_hint); + } + } - // Complex generic - annotation_stub("typing.List[foo.Bar[int]]", &mut modules_to_import); - assert!(modules_to_import.contains("foo")); + fn walk_function(&mut self, function: &Function) { + for decorator in &function.decorators { + self.builtins.insert(decorator.clone()); + } + for arg in function + .arguments + .positional_only_arguments + .iter() + .chain(&function.arguments.arguments) + .chain(&function.arguments.keyword_only_arguments) + { + if let Some(type_hint) = &arg.annotation { + self.walk_type_hint(type_hint); + } + } + for arg in function + .arguments + .vararg + .as_ref() + .iter() + .chain(&function.arguments.kwarg.as_ref()) + { + if let Some(type_hint) = &arg.annotation { + self.walk_type_hint(type_hint); + } + } + if let Some(type_hint) = &function.returns { + self.walk_type_hint(type_hint); + } + } - // Callable - annotation_stub( - "typing.Callable[[int, baz.Bar], bar.Baz[bool]]", - &mut modules_to_import, - ); - assert!(modules_to_import.contains("bar")); - assert!(modules_to_import.contains("baz")); + fn walk_type_hint(&mut self, type_hint: &TypeHint) { + if let TypeHint::Ast(type_hint) = type_hint { + self.walk_type_hint_expr(type_hint); + } + } - // Union - annotation_stub("a.B | b.C", &mut modules_to_import); - assert!(modules_to_import.contains("a")); - assert!(modules_to_import.contains("b")); + fn walk_type_hint_expr(&mut self, expr: &TypeHintExpr) { + match expr { + TypeHintExpr::Builtin { id } => { + self.builtins.insert(id.clone()); + } + TypeHintExpr::Attribute { module, attr } => { + self.module_members + .entry(module.clone()) + .or_default() + .insert(attr.clone()); + } + TypeHintExpr::Union { elts } => { + for elt in elts { + self.walk_type_hint_expr(elt) + } + } + TypeHintExpr::Subscript { value, slice } => { + self.walk_type_hint_expr(value); + for elt in slice { + self.walk_type_hint_expr(elt); + } + } + } } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::model::Arguments; #[test] fn function_stubs_with_variable_length() { @@ -328,18 +515,18 @@ mod tests { keyword_only_arguments: vec![Argument { name: "karg".into(), default_value: None, - annotation: Some("str".into()), + annotation: Some(TypeHint::Plain("str".into())), }], kwarg: Some(VariableLengthArgument { name: "kwarg".into(), - annotation: Some("str".into()), + annotation: Some(TypeHint::Plain("str".into())), }), }, - returns: Some("list[str]".into()), + returns: Some(TypeHint::Plain("list[str]".into())), }; assert_eq!( "def func(posonly, /, arg, *varargs, karg: str, **kwarg: str) -> list[str]: ...", - function_stubs(&function, &mut BTreeSet::new()) + function_stubs(&function, &Imports::default()) ) } @@ -363,7 +550,7 @@ mod tests { keyword_only_arguments: vec![Argument { name: "karg".into(), default_value: Some("\"foo\"".into()), - annotation: Some("str".into()), + annotation: Some(TypeHint::Plain("str".into())), }], kwarg: None, }, @@ -371,7 +558,81 @@ mod tests { }; assert_eq!( "def afunc(posonly=1, /, arg=True, *, karg: str = \"foo\"): ...", - function_stubs(&function, &mut BTreeSet::new()) + function_stubs(&function, &Imports::default()) ) } + + #[test] + fn test_import() { + let big_type = TypeHintExpr::Subscript { + value: Box::new(TypeHintExpr::Builtin { id: "dict".into() }), + slice: vec![ + TypeHintExpr::Attribute { + module: "foo.bar".into(), + attr: "A".into(), + }, + TypeHintExpr::Union { + elts: vec![ + TypeHintExpr::Attribute { + module: "bar".into(), + attr: "A".into(), + }, + TypeHintExpr::Attribute { + module: "foo".into(), + attr: "A.C".into(), + }, + TypeHintExpr::Attribute { + module: "foo".into(), + attr: "A.D".into(), + }, + TypeHintExpr::Attribute { + module: "foo".into(), + attr: "B".into(), + }, + TypeHintExpr::Attribute { + module: "bat".into(), + attr: "A".into(), + }, + ], + }, + ], + }; + let imports = Imports::create( + &Module { + name: "bar".into(), + modules: Vec::new(), + classes: vec![Class { + name: "A".into(), + methods: Vec::new(), + attributes: Vec::new(), + }], + functions: vec![Function { + name: String::new(), + decorators: Vec::new(), + arguments: Arguments { + positional_only_arguments: Vec::new(), + arguments: Vec::new(), + vararg: None, + keyword_only_arguments: Vec::new(), + kwarg: None, + }, + returns: Some(TypeHint::Ast(big_type.clone())), + }], + attributes: Vec::new(), + incomplete: true, + }, + &["foo"], + ); + assert_eq!( + &imports.imports, + &[ + "from _typeshed import Incomplete", + "from bat import A as A2", + "from foo import A as A3, B" + ] + ); + let mut output = String::new(); + imports.serialize_type_hint(&big_type, &mut output); + assert_eq!(output, "dict[A, A | A3.C | A3.D | B | A2]"); + } } diff --git a/pyo3-macros-backend/src/frompyobject.rs b/pyo3-macros-backend/src/frompyobject.rs index d3232e90777..841fcd2039f 100644 --- a/pyo3-macros-backend/src/frompyobject.rs +++ b/pyo3-macros-backend/src/frompyobject.rs @@ -1,7 +1,7 @@ use crate::attributes::{DefaultAttribute, FromPyWithAttribute, RenamingRule}; use crate::derive_attributes::{ContainerAttributes, FieldAttributes, FieldGetter}; #[cfg(feature = "experimental-inspect")] -use crate::introspection::{elide_lifetimes, ConcatenationBuilder}; +use crate::introspection::elide_lifetimes; use crate::utils::{self, Ctx}; use proc_macro2::TokenStream; use quote::{format_ident, quote, quote_spanned, ToTokens}; @@ -100,12 +100,11 @@ impl<'a> Enum<'a> { } #[cfg(feature = "experimental-inspect")] - fn write_input_type(&self, builder: &mut ConcatenationBuilder, ctx: &Ctx) { - for (i, var) in self.variants.iter().enumerate() { - if i > 0 { - builder.push_str(" | "); - } - var.write_input_type(builder, ctx); + fn input_type(&self, ctx: &Ctx) -> TokenStream { + let pyo3_crate_path = &ctx.pyo3_path; + let variants = self.variants.iter().map(|var| var.input_type(ctx)); + quote! { + #pyo3_crate_path::inspect::TypeHint::union(&[#(#variants),*]) } } } @@ -458,48 +457,42 @@ impl<'a> Container<'a> { } #[cfg(feature = "experimental-inspect")] - fn write_input_type(&self, builder: &mut ConcatenationBuilder, ctx: &Ctx) { + fn input_type(&self, ctx: &Ctx) -> TokenStream { + let pyo3_crate_path = &ctx.pyo3_path; match &self.ty { ContainerType::StructNewtype(_, from_py_with, ty) => { - Self::write_field_input_type(from_py_with, ty, builder, ctx); + Self::field_input_type(from_py_with, ty, ctx) } ContainerType::TupleNewtype(from_py_with, ty) => { - Self::write_field_input_type(from_py_with, ty, builder, ctx); + Self::field_input_type(from_py_with, ty, ctx) } ContainerType::Tuple(tups) => { - builder.push_str("tuple["); - for (i, TupleStructField { from_py_with, ty }) in tups.iter().enumerate() { - if i > 0 { - builder.push_str(", "); - } - Self::write_field_input_type(from_py_with, ty, builder, ctx); - } - builder.push_str("]"); + let elements = tups.iter().map(|TupleStructField { from_py_with, ty }| { + Self::field_input_type(from_py_with, ty, ctx) + }); + quote! { #pyo3_crate_path::inspect::TypeHint::subscript(&#pyo3_crate_path::inspect::TypeHint::builtin("tuple"), &[#(#elements),*]) } } ContainerType::Struct(_) => { // TODO: implement using a Protocol? - builder.push_str("_typeshed.Incomplete") + quote! { #pyo3_crate_path::inspect::TypeHint::module_attr("_typeshed", "Incomplete") } } } } #[cfg(feature = "experimental-inspect")] - fn write_field_input_type( + fn field_input_type( from_py_with: &Option, ty: &syn::Type, - builder: &mut ConcatenationBuilder, ctx: &Ctx, - ) { + ) -> TokenStream { + let pyo3_crate_path = &ctx.pyo3_path; if from_py_with.is_some() { // We don't know what from_py_with is doing - builder.push_str("_typeshed.Incomplete") + quote! { #pyo3_crate_path::inspect::TypeHint::module_attr("_typeshed", "Incomplete") } } else { let mut ty = ty.clone(); elide_lifetimes(&mut ty); - let pyo3_crate_path = &ctx.pyo3_path; - builder.push_tokens( - quote! { <#ty as #pyo3_crate_path::FromPyObject<'_, '_>>::INPUT_TYPE.as_bytes() }, - ) + quote! { <#ty as #pyo3_crate_path::FromPyObject<'_, '_>>::INPUT_TYPE } } } } @@ -569,34 +562,31 @@ pub fn build_derive_from_pyobject(tokens: &DeriveInput) -> Result { #[cfg(feature = "experimental-inspect")] let input_type = { - let mut builder = ConcatenationBuilder::default(); - if tokens + let pyo3_crate_path = &ctx.pyo3_path; + let input_type = if tokens .generics .params .iter() .all(|p| matches!(p, syn::GenericParam::Lifetime(_))) { match &tokens.data { - syn::Data::Enum(en) => { - Enum::new(en, &tokens.ident, options)?.write_input_type(&mut builder, ctx) - } + syn::Data::Enum(en) => Enum::new(en, &tokens.ident, options)?.input_type(ctx), syn::Data::Struct(st) => { let ident = &tokens.ident; Container::new(&st.fields, parse_quote!(#ident), options.clone())? - .write_input_type(&mut builder, ctx) + .input_type(ctx) } syn::Data::Union(_) => { // Not supported at this point - builder.push_str("_typeshed.Incomplete") + quote! { #pyo3_crate_path::inspect::TypeHint::module_attr("_typeshed", "Incomplete") } } } } else { // We don't know how to deal with generic parameters // Blocked by https://github.com/rust-lang/rust/issues/76560 - builder.push_str("_typeshed.Incomplete") + quote! { #pyo3_crate_path::inspect::TypeHint::module_attr("_typeshed", "Incomplete") } }; - let input_type = builder.into_token_stream(&ctx.pyo3_path); - quote! { const INPUT_TYPE: &'static str = unsafe { ::std::str::from_utf8_unchecked(#input_type) }; } + quote! { const INPUT_TYPE: #pyo3_crate_path::inspect::TypeHint = #input_type; } }; #[cfg(not(feature = "experimental-inspect"))] let input_type = quote! {}; diff --git a/pyo3-macros-backend/src/intopyobject.rs b/pyo3-macros-backend/src/intopyobject.rs index a49aaaae81d..75dfe054fa7 100644 --- a/pyo3-macros-backend/src/intopyobject.rs +++ b/pyo3-macros-backend/src/intopyobject.rs @@ -1,7 +1,7 @@ use crate::attributes::{IntoPyWithAttribute, RenamingRule}; use crate::derive_attributes::{ContainerAttributes, FieldAttributes}; #[cfg(feature = "experimental-inspect")] -use crate::introspection::{elide_lifetimes, ConcatenationBuilder}; +use crate::introspection::elide_lifetimes; use crate::utils::{self, Ctx}; use proc_macro2::{Span, TokenStream}; use quote::{format_ident, quote, quote_spanned, ToTokens}; @@ -360,52 +360,44 @@ impl<'a, const REF: bool> Container<'a, REF> { } #[cfg(feature = "experimental-inspect")] - fn write_output_type(&self, builder: &mut ConcatenationBuilder, ctx: &Ctx) { + fn output_type(&self, ctx: &Ctx) -> TokenStream { + let pyo3_crate_path = &ctx.pyo3_path; match &self.ty { ContainerType::StructNewtype(field) | ContainerType::TupleNewtype(field) => { - Self::write_field_output_type(&None, &field.ty, builder, ctx); + Self::field_output_type(&None, &field.ty, ctx) } ContainerType::Tuple(tups) => { - builder.push_str("tuple["); - for ( - i, - TupleStructField { - into_py_with, - field, + let elements = tups.iter().map( + |TupleStructField { + into_py_with, + field, + }| { + Self::field_output_type(into_py_with, &field.ty, ctx) }, - ) in tups.iter().enumerate() - { - if i > 0 { - builder.push_str(", "); - } - Self::write_field_output_type(into_py_with, &field.ty, builder, ctx); - } - builder.push_str("]"); + ); + quote! { #pyo3_crate_path::inspect::TypeHint::subscript(&#pyo3_crate_path::inspect::TypeHint::builtin("tuple"), &[#(#elements),*]) } } ContainerType::Struct(_) => { // TODO: implement using a Protocol? - builder.push_str("_typeshed.Incomplete") + quote! { #pyo3_crate_path::inspect::TypeHint::module_attr("_typeshed", "Incomplete") } } } } #[cfg(feature = "experimental-inspect")] - fn write_field_output_type( + fn field_output_type( into_py_with: &Option, ty: &syn::Type, - builder: &mut ConcatenationBuilder, ctx: &Ctx, - ) { + ) -> TokenStream { + let pyo3_crate_path = &ctx.pyo3_path; if into_py_with.is_some() { // We don't know what into_py_with is doing - builder.push_str("_typeshed.Incomplete") + quote! { #pyo3_crate_path::inspect::TypeHint::module_attr("_typeshed", "Incomplete") } } else { let mut ty = ty.clone(); elide_lifetimes(&mut ty); - let pyo3_crate_path = &ctx.pyo3_path; - builder.push_tokens( - quote! { <#ty as #pyo3_crate_path::IntoPyObject<'_>>::OUTPUT_TYPE.as_bytes() }, - ) + quote! { <#ty as #pyo3_crate_path::IntoPyObject<'_>>::OUTPUT_TYPE } } } } @@ -485,12 +477,11 @@ impl<'a, const REF: bool> Enum<'a, REF> { } #[cfg(feature = "experimental-inspect")] - fn write_output_type(&self, builder: &mut ConcatenationBuilder, ctx: &Ctx) { - for (i, var) in self.variants.iter().enumerate() { - if i > 0 { - builder.push_str(" | "); - } - var.write_output_type(builder, ctx); + fn output_type(&self, ctx: &Ctx) -> TokenStream { + let pyo3_crate_path = &ctx.pyo3_path; + let variants = self.variants.iter().map(|var| var.output_type(ctx)); + quote! { + #pyo3_crate_path::inspect::TypeHint::union(&[#(#variants),*]) } } } @@ -587,17 +578,15 @@ pub fn build_derive_into_pyobject(tokens: &DeriveInput) -> Resu #[cfg(feature = "experimental-inspect")] let output_type = { - let mut builder = ConcatenationBuilder::default(); - if tokens + let pyo3_crate_path = &ctx.pyo3_path; + let output_type = if tokens .generics .params .iter() .all(|p| matches!(p, syn::GenericParam::Lifetime(_))) { match &tokens.data { - syn::Data::Enum(en) => { - Enum::::new(en, &tokens.ident)?.write_output_type(&mut builder, ctx) - } + syn::Data::Enum(en) => Enum::::new(en, &tokens.ident)?.output_type(ctx), syn::Data::Struct(st) => { let ident = &tokens.ident; Container::::new( @@ -606,20 +595,19 @@ pub fn build_derive_into_pyobject(tokens: &DeriveInput) -> Resu parse_quote!(#ident), options, )? - .write_output_type(&mut builder, ctx) + .output_type(ctx) } syn::Data::Union(_) => { // Not supported at this point - builder.push_str("_typeshed.Incomplete") + quote! { #pyo3_crate_path::inspect::TypeHint::module_attr("_typeshed", "Incomplete") } } } } else { // We don't know how to deal with generic parameters // Blocked by https://github.com/rust-lang/rust/issues/76560 - builder.push_str("_typeshed.Incomplete") + quote! { #pyo3_crate_path::inspect::TypeHint::module_attr("_typeshed", "Incomplete") } }; - let output_type = builder.into_token_stream(&ctx.pyo3_path); - quote! { const OUTPUT_TYPE: &'static str = unsafe { ::std::str::from_utf8_unchecked(#output_type) }; } + quote! { const OUTPUT_TYPE: #pyo3_path::inspect::TypeHint = #output_type; } }; #[cfg(not(feature = "experimental-inspect"))] let output_type = quote! {}; diff --git a/pyo3-macros-backend/src/introspection.rs b/pyo3-macros-backend/src/introspection.rs index 8ca21beedbe..1aa53e516e0 100644 --- a/pyo3-macros-backend/src/introspection.rs +++ b/pyo3-macros-backend/src/introspection.rs @@ -104,11 +104,17 @@ pub fn function_introspection_code( IntrospectionNode::String(returns.to_python().into()) } else { match returns { - ReturnType::Default => IntrospectionNode::String("None".into()), + ReturnType::Default => IntrospectionNode::ConstantType { + name: "None", + module: None, + }, ReturnType::Type(_, ty) => match *ty { Type::Tuple(t) if t.elems.is_empty() => { // () is converted to None in return types - IntrospectionNode::String("None".into()) + IntrospectionNode::ConstantType { + name: "None", + module: None, + } } mut ty => { if let Some(class_type) = parent { @@ -183,7 +189,10 @@ pub fn attribute_introspection_code( // Type checkers can infer the type from the value because it's typing.Literal[value] // So, following stubs best practices, we only write typing.Final and not // typing.Final[typing.literal[value]] - IntrospectionNode::String("typing.Final".into()) + IntrospectionNode::ConstantType { + name: "Final", + module: Some("typing"), + } } else { IntrospectionNode::OutputType { rust_type, @@ -343,8 +352,18 @@ enum IntrospectionNode<'a> { String(Cow<'a, str>), Bool(bool), IntrospectionId(Option>), - InputType { rust_type: Type, nullable: bool }, - OutputType { rust_type: Type, is_final: bool }, + InputType { + rust_type: Type, + nullable: bool, + }, + OutputType { + rust_type: Type, + is_final: bool, + }, + ConstantType { + name: &'static str, + module: Option<&'static str>, + }, Map(HashMap<&'static str, IntrospectionNode<'a>>), List(Vec>), } @@ -382,34 +401,37 @@ impl IntrospectionNode<'_> { rust_type, nullable, } => { - content.push_str("\""); - content.push_tokens(quote! { + let mut annotation = quote! { <#rust_type as #pyo3_crate_path::impl_::extract_argument::PyFunctionArgument< { #[allow(unused_imports)] use #pyo3_crate_path::impl_::pyclass::Probe as _; #pyo3_crate_path::impl_::pyclass::IsFromPyObject::<#rust_type>::VALUE } - >>::INPUT_TYPE.as_bytes() - }); + >>::INPUT_TYPE + }; if nullable { - content.push_str(" | None"); + annotation = quote! { #pyo3_crate_path::inspect::TypeHint::union(&[#annotation, #pyo3_crate_path::inspect::TypeHint::builtin("None")]) }; } - content.push_str("\""); + content.push_tokens(serialize_type_hint(annotation, pyo3_crate_path)); } Self::OutputType { rust_type, is_final, } => { - content.push_str("\""); + let mut annotation = quote! { <#rust_type as #pyo3_crate_path::impl_::introspection::PyReturnType>::OUTPUT_TYPE }; if is_final { - content.push_str("typing.Final["); + annotation = quote! { #pyo3_crate_path::inspect::TypeHint::subscript(&#pyo3_crate_path::inspect::TypeHint::module_attr("typing", "Final"), &[#annotation]) }; } - content.push_tokens(quote! { <#rust_type as #pyo3_crate_path::impl_::introspection::PyReturnType>::OUTPUT_TYPE.as_bytes() }); - if is_final { - content.push_str("]"); - } - content.push_str("\""); + content.push_tokens(serialize_type_hint(annotation, pyo3_crate_path)); + } + Self::ConstantType { name, module } => { + let annotation = if let Some(module) = module { + quote! { #pyo3_crate_path::inspect::TypeHint::module_attr(#module, #name) } + } else { + quote! { #pyo3_crate_path::inspect::TypeHint::builtin(#name) } + }; + content.push_tokens(serialize_type_hint(annotation, pyo3_crate_path)); } Self::Map(map) => { content.push_str("{"); @@ -450,6 +472,19 @@ impl IntrospectionNode<'_> { } } +fn serialize_type_hint(hint: TokenStream, pyo3_crate_path: &PyO3CratePath) -> TokenStream { + quote! {{ + const TYPE_HINT: #pyo3_crate_path::inspect::TypeHint = #hint; + const TYPE_HINT_LEN: usize = #pyo3_crate_path::inspect::serialized_len_for_introspection(&TYPE_HINT); + const TYPE_HINT_SER: [u8; TYPE_HINT_LEN] = { + let mut result: [u8; TYPE_HINT_LEN] = [0; TYPE_HINT_LEN]; + #pyo3_crate_path::inspect::serialize_for_introspection(&TYPE_HINT, &mut result); + result + }; + &TYPE_HINT_SER + }} +} + struct AttributedIntrospectionNode<'a> { node: IntrospectionNode<'a>, attributes: &'a [Attribute], diff --git a/pyo3-macros-backend/src/pyclass.rs b/pyo3-macros-backend/src/pyclass.rs index 0f735d5d90b..07fccf3d655 100644 --- a/pyo3-macros-backend/src/pyclass.rs +++ b/pyo3-macros-backend/src/pyclass.rs @@ -435,13 +435,15 @@ fn get_class_python_name<'a>(cls: &'a syn::Ident, args: &'a PyClassArgs) -> Cow< .unwrap_or_else(|| Cow::Owned(cls.unraw())) } -fn get_class_python_module_and_name<'a>(cls: &'a Ident, args: &'a PyClassArgs) -> String { - let name = get_class_python_name(cls, args); +#[cfg(feature = "experimental-inspect")] +fn get_class_type_hint(cls: &Ident, args: &PyClassArgs, ctx: &Ctx) -> TokenStream { + let pyo3_path = &ctx.pyo3_path; + let name = get_class_python_name(cls, args).to_string(); if let Some(module) = &args.options.module { - let value = module.value.value(); - format!("{value}.{name}") + let module = module.value.value(); + quote! { #pyo3_path::inspect::TypeHint::module_attr(#module, #name) } } else { - name.to_string() + quote! { #pyo3_path::inspect::TypeHint::builtin(#name) } } } @@ -1117,10 +1119,9 @@ fn impl_complex_enum( } }); let output_type = if cfg!(feature = "experimental-inspect") { - let full_name = get_class_python_module_and_name(cls, &args); - quote! { const OUTPUT_TYPE: &'static str = #full_name; } + quote!(const OUTPUT_TYPE: #pyo3_path::inspect::TypeHint = <#cls as #pyo3_path::PyTypeInfo>::TYPE_HINT;) } else { - quote! {} + TokenStream::new() }; quote! { impl<'py> #pyo3_path::conversion::IntoPyObject<'py> for #cls { @@ -1958,19 +1959,20 @@ fn impl_pytypeinfo(cls: &syn::Ident, attr: &PyClassArgs, ctx: &Ctx) -> TokenStre quote! { ::core::option::Option::None } }; - let python_type = if cfg!(feature = "experimental-inspect") { - let full_name = get_class_python_module_and_name(cls, attr); - quote! { const PYTHON_TYPE: &'static str = #full_name; } - } else { - quote! {} + #[cfg(feature = "experimental-inspect")] + let type_hint = { + let type_hint = get_class_type_hint(cls, attr, ctx); + quote! { const TYPE_HINT: #pyo3_path::inspect::TypeHint = #type_hint; } }; + #[cfg(not(feature = "experimental-inspect"))] + let type_hint = quote! {}; quote! { unsafe impl #pyo3_path::type_object::PyTypeInfo for #cls { const NAME: &'static str = #cls_name; const MODULE: ::std::option::Option<&'static str> = #module; - #python_type + #type_hint #[inline] fn type_object_raw(py: #pyo3_path::Python<'_>) -> *mut #pyo3_path::ffi::PyTypeObject { @@ -2347,10 +2349,9 @@ impl<'a> PyClassImplsBuilder<'a> { // If #cls is not extended type, we allow Self->PyObject conversion if attr.options.extends.is_none() { let output_type = if cfg!(feature = "experimental-inspect") { - let full_name = get_class_python_module_and_name(cls, self.attr); - quote! { const OUTPUT_TYPE: &'static str = #full_name; } + quote!(const OUTPUT_TYPE: #pyo3_path::inspect::TypeHint = <#cls as #pyo3_path::PyTypeInfo>::TYPE_HINT;) } else { - quote! {} + TokenStream::new() }; quote! { impl<'py> #pyo3_path::conversion::IntoPyObject<'py> for #cls { @@ -2510,18 +2511,11 @@ impl<'a> PyClassImplsBuilder<'a> { } }; - let type_name = if cfg!(feature = "experimental-inspect") { - let full_name = get_class_python_module_and_name(cls, self.attr); - quote! { const TYPE_NAME: &'static str = #full_name; } - } else { - quote! {} - }; - let extract_pyclass_with_clone = if let Some(from_py_object) = self.attr.options.from_py_object { - let input_ty = if cfg!(feature = "experimental-inspect") { - quote!(const INPUT_TYPE: &'static str = <#cls as #pyo3_path::impl_::pyclass::PyClassImpl>::TYPE_NAME;) + let input_type = if cfg!(feature = "experimental-inspect") { + quote!(const INPUT_TYPE: #pyo3_path::inspect::TypeHint = <#cls as #pyo3_path::PyTypeInfo>::TYPE_HINT;) } else { TokenStream::new() }; @@ -2532,7 +2526,7 @@ impl<'a> PyClassImplsBuilder<'a> { { type Error = #pyo3_path::pyclass::PyClassGuardError<'a, 'py>; - #input_ty + #input_type fn extract(obj: #pyo3_path::Borrowed<'a, 'py, #pyo3_path::PyAny>) -> ::std::result::Result { ::std::result::Result::Ok(::std::clone::Clone::clone(&*obj.extract::<#pyo3_path::PyClassGuard<'_, #cls>>()?)) @@ -2567,8 +2561,6 @@ impl<'a> PyClassImplsBuilder<'a> { type WeakRef = #weakref; type BaseNativeType = #base_nativetype; - #type_name - fn items_iter() -> #pyo3_path::impl_::pyclass::PyClassItemsIter { use #pyo3_path::impl_::pyclass::*; let collector = PyClassImplCollector::::new(); diff --git a/pytests/Cargo.toml b/pytests/Cargo.toml index 0d9c74d21b7..152500792df 100644 --- a/pytests/Cargo.toml +++ b/pytests/Cargo.toml @@ -7,8 +7,11 @@ edition = "2021" publish = false rust-version = "1.83" +[features] +experimental-inspect = ["pyo3/experimental-inspect"] + [dependencies] -pyo3 = { path = "../", features = ["experimental-inspect"] } +pyo3.path = "../" [build-dependencies] pyo3-build-config = { path = "../pyo3-build-config" } diff --git a/pytests/src/pyfunctions.rs b/pytests/src/pyfunctions.rs index ce7494f7c5f..5cb88f976ab 100644 --- a/pytests/src/pyfunctions.rs +++ b/pytests/src/pyfunctions.rs @@ -77,6 +77,7 @@ fn with_typed_args(a: bool, b: u64, c: f64, d: &str) -> (bool, u64, f64, &str) { (a, b, c, d) } +#[cfg(feature = "experimental-inspect")] #[pyfunction(signature = (a: "int", *_args: "str", _b: "int | None" = None, **_kwargs: "bool") -> "int")] fn with_custom_type_annotations<'py>( a: Any<'py>, @@ -135,9 +136,12 @@ fn many_keyword_arguments<'py>( #[pymodule] pub mod pyfunctions { + #[cfg(feature = "experimental-inspect")] + #[pymodule_export] + use super::with_custom_type_annotations; #[pymodule_export] use super::{ args_kwargs, many_keyword_arguments, none, positional_only, simple, simple_args, - simple_args_kwargs, simple_kwargs, with_custom_type_annotations, with_typed_args, + simple_args_kwargs, simple_kwargs, with_typed_args, }; } diff --git a/pytests/stubs/__init__.pyi b/pytests/stubs/__init__.pyi index b88c3a5f3c3..0f6820f054e 100644 --- a/pytests/stubs/__init__.pyi +++ b/pytests/stubs/__init__.pyi @@ -1,3 +1,3 @@ -import _typeshed +from _typeshed import Incomplete -def __getattr__(name: str) -> _typeshed.Incomplete: ... +def __getattr__(name: str) -> Incomplete: ... diff --git a/pytests/stubs/consts.pyi b/pytests/stubs/consts.pyi index 45c1d5fcbfb..66b0672c8a5 100644 --- a/pytests/stubs/consts.pyi +++ b/pytests/stubs/consts.pyi @@ -1,8 +1,7 @@ -import consts -import typing +from typing import Final -PI: typing.Final[float] -SIMPLE: typing.Final = "SIMPLE" +PI: Final[float] +SIMPLE: Final = "SIMPLE" class ClassWithConst: - INSTANCE: typing.Final[consts.ClassWithConst] + INSTANCE: Final[ClassWithConst] diff --git a/pytests/stubs/pyclasses.pyi b/pytests/stubs/pyclasses.pyi index 7f7ee521452..e2d1f4d006b 100644 --- a/pytests/stubs/pyclasses.pyi +++ b/pytests/stubs/pyclasses.pyi @@ -1,8 +1,8 @@ -import _typeshed -import typing +from _typeshed import Incomplete +from typing import Any class AssertingBaseClass: - def __new__(cls, /, expected_type: typing.Any) -> None: ... + def __new__(cls, /, expected_type: Any) -> None: ... class ClassWithDecorators: def __new__(cls, /) -> None: ... @@ -47,5 +47,5 @@ class PyClassThreadIter: def __next__(self, /) -> int: ... def map_a_class( - cls: EmptyClass | tuple[EmptyClass, EmptyClass] | _typeshed.Incomplete, -) -> EmptyClass | tuple[EmptyClass, EmptyClass] | _typeshed.Incomplete: ... + cls: EmptyClass | tuple[EmptyClass, EmptyClass] | Incomplete, +) -> EmptyClass | tuple[EmptyClass, EmptyClass] | Incomplete: ... diff --git a/pytests/stubs/pyfunctions.pyi b/pytests/stubs/pyfunctions.pyi index 369119cd96f..9513072d023 100644 --- a/pytests/stubs/pyfunctions.pyi +++ b/pytests/stubs/pyfunctions.pyi @@ -1,46 +1,38 @@ -import typing +from typing import Any -def args_kwargs(*args, **kwargs) -> typing.Any: ... +def args_kwargs(*args, **kwargs) -> Any: ... def many_keyword_arguments( *, - ant: typing.Any | None = None, - bear: typing.Any | None = None, - cat: typing.Any | None = None, - dog: typing.Any | None = None, - elephant: typing.Any | None = None, - fox: typing.Any | None = None, - goat: typing.Any | None = None, - horse: typing.Any | None = None, - iguana: typing.Any | None = None, - jaguar: typing.Any | None = None, - koala: typing.Any | None = None, - lion: typing.Any | None = None, - monkey: typing.Any | None = None, - newt: typing.Any | None = None, - owl: typing.Any | None = None, - penguin: typing.Any | None = None, + ant: Any | None = None, + bear: Any | None = None, + cat: Any | None = None, + dog: Any | None = None, + elephant: Any | None = None, + fox: Any | None = None, + goat: Any | None = None, + horse: Any | None = None, + iguana: Any | None = None, + jaguar: Any | None = None, + koala: Any | None = None, + lion: Any | None = None, + monkey: Any | None = None, + newt: Any | None = None, + owl: Any | None = None, + penguin: Any | None = None, ) -> None: ... def none() -> None: ... -def positional_only(a: typing.Any, /, b: typing.Any) -> typing.Any: ... -def simple( - a: typing.Any, b: typing.Any | None = None, *, c: typing.Any | None = None -) -> typing.Any: ... -def simple_args( - a: typing.Any, b: typing.Any | None = None, *args, c: typing.Any | None = None -) -> typing.Any: ... +def positional_only(a: Any, /, b: Any) -> Any: ... +def simple(a: Any, b: Any | None = None, *, c: Any | None = None) -> Any: ... +def simple_args(a: Any, b: Any | None = None, *args, c: Any | None = None) -> Any: ... def simple_args_kwargs( - a: typing.Any, - b: typing.Any | None = None, - *args, - c: typing.Any | None = None, - **kwargs, -) -> typing.Any: ... + a: Any, b: Any | None = None, *args, c: Any | None = None, **kwargs +) -> Any: ... def simple_kwargs( - a: typing.Any, b: typing.Any | None = None, c: typing.Any | None = None, **kwargs -) -> typing.Any: ... + a: Any, b: Any | None = None, c: Any | None = None, **kwargs +) -> Any: ... def with_custom_type_annotations( a: int, *_args: str, _b: int | None = None, **_kwargs: bool ) -> int: ... def with_typed_args( a: bool = False, b: int = 0, c: float = 0.0, d: str = "" -) -> typing.Any: ... +) -> Any: ... diff --git a/src/conversion.rs b/src/conversion.rs index 7ff34b5fb5f..d4f5c513173 100644 --- a/src/conversion.rs +++ b/src/conversion.rs @@ -3,6 +3,8 @@ use crate::err::PyResult; use crate::impl_::pyclass::ExtractPyClassWithClone; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; +#[cfg(feature = "experimental-inspect")] +use crate::inspect::TypeHint; use crate::pyclass::boolean_struct::False; use crate::pyclass::{PyClassGuardError, PyClassGuardMutError}; use crate::types::PyTuple; @@ -56,7 +58,7 @@ pub trait IntoPyObject<'py>: Sized { /// For most types, the return value for this method will be identical to that of [`FromPyObject::INPUT_TYPE`]. /// 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")] - const OUTPUT_TYPE: &'static str = "typing.Any"; + const OUTPUT_TYPE: TypeHint = TypeHint::module_attr("typing", "Any"); /// Performs the conversion. fn into_pyobject(self, py: Python<'py>) -> Result; @@ -190,7 +192,7 @@ where type Error = <&'a T as IntoPyObject<'py>>::Error; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = <&'a T as IntoPyObject<'py>>::OUTPUT_TYPE; + const OUTPUT_TYPE: TypeHint = <&'a T as IntoPyObject<'py>>::OUTPUT_TYPE; #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { @@ -387,7 +389,7 @@ pub trait FromPyObject<'a, 'py>: Sized { /// For example, `Vec` would be `collections.abc.Sequence[int]`. /// The default value is `typing.Any`, which is correct for any type. #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = "typing.Any"; + const INPUT_TYPE: TypeHint = TypeHint::module_attr("typing", "Any"); /// Extracts `Self` from the bound smart pointer `obj`. /// @@ -520,7 +522,7 @@ where type Error = PyClassGuardError<'a, 'py>; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = ::TYPE_NAME; + const INPUT_TYPE: TypeHint = ::TYPE_HINT; fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result { Ok(obj.extract::>()?.clone()) @@ -534,7 +536,7 @@ where type Error = PyClassGuardError<'a, 'py>; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = ::TYPE_NAME; + const INPUT_TYPE: TypeHint = ::TYPE_HINT; fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result { obj.cast::() @@ -551,7 +553,7 @@ where type Error = PyClassGuardMutError<'a, 'py>; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = ::TYPE_NAME; + const INPUT_TYPE: TypeHint = ::TYPE_HINT; fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result { obj.cast::() diff --git a/src/conversions/std/cell.rs b/src/conversions/std/cell.rs index 108df8031cd..9a79479ad6f 100644 --- a/src/conversions/std/cell.rs +++ b/src/conversions/std/cell.rs @@ -1,5 +1,7 @@ use std::cell::Cell; +#[cfg(feature = "experimental-inspect")] +use crate::inspect::TypeHint; use crate::{conversion::IntoPyObject, Borrowed, FromPyObject, PyAny, Python}; impl<'py, T: Copy + IntoPyObject<'py>> IntoPyObject<'py> for Cell { @@ -8,7 +10,7 @@ impl<'py, T: Copy + IntoPyObject<'py>> IntoPyObject<'py> for Cell { type Error = T::Error; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = T::OUTPUT_TYPE; + const OUTPUT_TYPE: TypeHint = T::OUTPUT_TYPE; #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { @@ -22,7 +24,7 @@ impl<'py, T: Copy + IntoPyObject<'py>> IntoPyObject<'py> for &Cell { type Error = T::Error; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = T::OUTPUT_TYPE; + const OUTPUT_TYPE: TypeHint = T::OUTPUT_TYPE; #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { @@ -34,7 +36,7 @@ impl<'a, 'py, T: FromPyObject<'a, 'py>> FromPyObject<'a, 'py> for Cell { type Error = T::Error; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = T::INPUT_TYPE; + const INPUT_TYPE: TypeHint = T::INPUT_TYPE; fn extract(ob: Borrowed<'a, 'py, PyAny>) -> Result { ob.extract().map(Cell::new) diff --git a/src/conversions/std/num.rs b/src/conversions/std/num.rs index b27d56da343..8ecb10527f8 100644 --- a/src/conversions/std/num.rs +++ b/src/conversions/std/num.rs @@ -3,6 +3,8 @@ use crate::conversion::{FromPyObjectSequence, IntoPyObject}; use crate::ffi_ptr_ext::FfiPtrExt; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; +#[cfg(feature = "experimental-inspect")] +use crate::inspect::TypeHint; use crate::types::{PyByteArray, PyByteArrayMethods, PyBytes, PyInt}; use crate::{exceptions, ffi, Borrowed, Bound, FromPyObject, PyAny, PyErr, PyResult, Python}; use std::convert::Infallible; @@ -23,7 +25,7 @@ macro_rules! int_fits_larger_int { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = <$larger_type>::OUTPUT_TYPE; + const OUTPUT_TYPE: TypeHint = <$larger_type>::OUTPUT_TYPE; fn into_pyobject(self, py: Python<'py>) -> Result { (self as $larger_type).into_pyobject(py) @@ -41,7 +43,7 @@ macro_rules! int_fits_larger_int { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = <$larger_type>::OUTPUT_TYPE; + const OUTPUT_TYPE: TypeHint = <$larger_type>::OUTPUT_TYPE; fn into_pyobject(self, py: Python<'py>) -> Result { (*self).into_pyobject(py) @@ -57,7 +59,7 @@ macro_rules! int_fits_larger_int { type Error = PyErr; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = <$larger_type>::INPUT_TYPE; + const INPUT_TYPE: TypeHint = <$larger_type>::INPUT_TYPE; fn extract(obj: Borrowed<'_, '_, PyAny>) -> Result { let val: $larger_type = obj.extract()?; @@ -106,7 +108,7 @@ macro_rules! int_convert_u64_or_i64 { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = "int"; + const OUTPUT_TYPE: TypeHint = TypeHint::builtin("int"); fn into_pyobject(self, py: Python<'py>) -> Result { unsafe { @@ -127,7 +129,7 @@ macro_rules! int_convert_u64_or_i64 { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = "int"; + const OUTPUT_TYPE: TypeHint = <$rust_type>::OUTPUT_TYPE; #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { @@ -138,7 +140,7 @@ macro_rules! int_convert_u64_or_i64 { type Error = PyErr; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = "int"; + const INPUT_TYPE: TypeHint = TypeHint::builtin("int"); fn extract(obj: Borrowed<'_, '_, PyAny>) -> Result<$rust_type, Self::Error> { extract_int!(obj, !0, $pylong_as_ll_or_ull, $force_index_call) @@ -160,7 +162,7 @@ macro_rules! int_fits_c_long { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = "int"; + const OUTPUT_TYPE: TypeHint = TypeHint::builtin("int"); fn into_pyobject(self, py: Python<'py>) -> Result { unsafe { @@ -182,7 +184,7 @@ macro_rules! int_fits_c_long { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = "int"; + const OUTPUT_TYPE: TypeHint = <$rust_type>::OUTPUT_TYPE; #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { @@ -199,7 +201,7 @@ macro_rules! int_fits_c_long { type Error = PyErr; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = "int"; + const INPUT_TYPE: TypeHint = TypeHint::builtin("int"); fn extract(obj: Borrowed<'_, 'py, PyAny>) -> Result { let val: c_long = extract_int!(obj, -1, ffi::PyLong_AsLong)?; @@ -221,7 +223,7 @@ impl<'py> IntoPyObject<'py> for u8 { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = "int"; + const OUTPUT_TYPE: TypeHint = TypeHint::builtin("int"); fn into_pyobject(self, py: Python<'py>) -> Result { unsafe { @@ -255,7 +257,7 @@ impl<'py> IntoPyObject<'py> for &'_ u8 { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = "int"; + const OUTPUT_TYPE: TypeHint = u8::OUTPUT_TYPE; fn into_pyobject(self, py: Python<'py>) -> Result { u8::into_pyobject(*self, py) @@ -284,7 +286,7 @@ impl<'py> FromPyObject<'_, 'py> for u8 { type Error = PyErr; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = "int"; + const INPUT_TYPE: TypeHint = TypeHint::builtin("int"); fn extract(obj: Borrowed<'_, 'py, PyAny>) -> Result { let val: c_long = extract_int!(obj, -1, ffi::PyLong_AsLong)?; @@ -410,7 +412,7 @@ mod fast_128bit_int_conversion { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = "int"; + const OUTPUT_TYPE: TypeHint = TypeHint::builtin("int"); fn into_pyobject(self, py: Python<'py>) -> Result { #[cfg(Py_3_13)] @@ -437,7 +439,7 @@ mod fast_128bit_int_conversion { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = "int"; + const OUTPUT_TYPE: TypeHint = <$rust_type>::OUTPUT_TYPE; #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { @@ -454,7 +456,7 @@ mod fast_128bit_int_conversion { type Error = PyErr; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = "int"; + const INPUT_TYPE: TypeHint = TypeHint::builtin("int"); fn extract(ob: Borrowed<'_, '_, PyAny>) -> Result<$rust_type, Self::Error> { let num = @@ -559,7 +561,7 @@ mod slow_128bit_int_conversion { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = "int"; + const OUTPUT_TYPE: TypeHint = TypeHint::builtin("int"); fn into_pyobject(self, py: Python<'py>) -> Result { let lower = (self as u64).into_pyobject(py)?; @@ -587,7 +589,7 @@ mod slow_128bit_int_conversion { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = "int"; + const OUTPUT_TYPE: TypeHint = <$rust_type>::OUTPUT_TYPE; #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { @@ -604,7 +606,7 @@ mod slow_128bit_int_conversion { type Error = PyErr; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = "int"; + const INPUT_TYPE: TypeHint = TypeHint::builtin("int"); fn extract(ob: Borrowed<'_, '_, PyAny>) -> Result<$rust_type, Self::Error> { let py = ob.py(); @@ -658,7 +660,7 @@ macro_rules! nonzero_int_impl { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = "int"; + const OUTPUT_TYPE: TypeHint = TypeHint::builtin("int"); #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { @@ -677,7 +679,7 @@ macro_rules! nonzero_int_impl { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = "int"; + const OUTPUT_TYPE: TypeHint = <$nonzero_type>::OUTPUT_TYPE; #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { @@ -694,7 +696,7 @@ macro_rules! nonzero_int_impl { type Error = PyErr; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = <$primitive_type>::INPUT_TYPE; + const INPUT_TYPE: TypeHint = <$primitive_type>::INPUT_TYPE; fn extract(obj: Borrowed<'_, '_, PyAny>) -> Result { let val: $primitive_type = obj.extract()?; diff --git a/src/conversions/std/string.rs b/src/conversions/std/string.rs index 3c6a300ecdf..f46c159967d 100644 --- a/src/conversions/std/string.rs +++ b/src/conversions/std/string.rs @@ -1,11 +1,12 @@ -use std::{borrow::Cow, convert::Infallible}; - #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; +#[cfg(feature = "experimental-inspect")] +use crate::inspect::TypeHint; use crate::{ conversion::IntoPyObject, instance::Bound, types::PyString, Borrowed, FromPyObject, PyAny, PyErr, Python, }; +use std::{borrow::Cow, convert::Infallible}; impl<'py> IntoPyObject<'py> for &str { type Target = PyString; @@ -13,7 +14,7 @@ impl<'py> IntoPyObject<'py> for &str { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = String::OUTPUT_TYPE; + const OUTPUT_TYPE: TypeHint = String::OUTPUT_TYPE; #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { @@ -32,7 +33,7 @@ impl<'py> IntoPyObject<'py> for &&str { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = String::OUTPUT_TYPE; + const OUTPUT_TYPE: TypeHint = String::OUTPUT_TYPE; #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { @@ -51,7 +52,7 @@ impl<'py> IntoPyObject<'py> for Cow<'_, str> { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = String::OUTPUT_TYPE; + const OUTPUT_TYPE: TypeHint = String::OUTPUT_TYPE; #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { @@ -70,7 +71,7 @@ impl<'py> IntoPyObject<'py> for &Cow<'_, str> { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = String::OUTPUT_TYPE; + const OUTPUT_TYPE: TypeHint = String::OUTPUT_TYPE; #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { @@ -89,7 +90,7 @@ impl<'py> IntoPyObject<'py> for char { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = String::OUTPUT_TYPE; + const OUTPUT_TYPE: TypeHint = String::OUTPUT_TYPE; fn into_pyobject(self, py: Python<'py>) -> Result { let mut bytes = [0u8; 4]; @@ -108,7 +109,7 @@ impl<'py> IntoPyObject<'py> for &char { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = String::OUTPUT_TYPE; + const OUTPUT_TYPE: TypeHint = String::OUTPUT_TYPE; #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { @@ -127,7 +128,7 @@ impl<'py> IntoPyObject<'py> for String { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = "str"; + const OUTPUT_TYPE: TypeHint = TypeHint::builtin("str"); fn into_pyobject(self, py: Python<'py>) -> Result { Ok(PyString::new(py, &self)) @@ -145,7 +146,7 @@ impl<'py> IntoPyObject<'py> for &String { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = String::OUTPUT_TYPE; + const OUTPUT_TYPE: TypeHint = String::OUTPUT_TYPE; #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { @@ -163,7 +164,7 @@ impl<'a> crate::conversion::FromPyObject<'a, '_> for &'a str { type Error = PyErr; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = "str"; + const INPUT_TYPE: TypeHint = TypeHint::builtin("str"); fn extract(ob: crate::Borrowed<'a, '_, PyAny>) -> Result { ob.cast::()?.to_str() @@ -179,7 +180,7 @@ impl<'a> crate::conversion::FromPyObject<'a, '_> for Cow<'a, str> { type Error = PyErr; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = "str"; + const INPUT_TYPE: TypeHint = TypeHint::builtin("str"); fn extract(ob: crate::Borrowed<'a, '_, PyAny>) -> Result { ob.cast::()?.to_cow() @@ -197,7 +198,7 @@ impl FromPyObject<'_, '_> for String { type Error = PyErr; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = "str"; + const INPUT_TYPE: TypeHint = TypeHint::builtin("str"); fn extract(obj: Borrowed<'_, '_, PyAny>) -> Result { obj.cast::()?.to_cow().map(Cow::into_owned) @@ -213,7 +214,7 @@ impl FromPyObject<'_, '_> for char { type Error = PyErr; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = "str"; + const INPUT_TYPE: TypeHint = TypeHint::builtin("str"); fn extract(obj: Borrowed<'_, '_, PyAny>) -> Result { let s = obj.cast::()?.to_cow()?; diff --git a/src/impl_/extract_argument.rs b/src/impl_/extract_argument.rs index a0ea13e1ca6..21674e7daad 100644 --- a/src/impl_/extract_argument.rs +++ b/src/impl_/extract_argument.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "experimental-inspect")] +use crate::inspect::TypeHint; use crate::{ exceptions::PyTypeError, ffi, @@ -57,7 +59,7 @@ pub trait PyFunctionArgument<'a, 'holder, 'py, const IMPLEMENTS_FROMPYOBJECT: bo /// Provides the type hint information for which Python types are allowed. #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str; + const INPUT_TYPE: TypeHint; fn extract( obj: &'a Bound<'py, PyAny>, @@ -73,7 +75,7 @@ where type Error = T::Error; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = T::INPUT_TYPE; + const INPUT_TYPE: TypeHint = T::INPUT_TYPE; #[inline] fn extract(obj: &'a Bound<'py, PyAny>, _: &'_ mut ()) -> Result { @@ -89,7 +91,7 @@ where type Error = CastError<'a, 'py>; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = T::PYTHON_TYPE; + const INPUT_TYPE: TypeHint = T::TYPE_HINT; #[inline] fn extract(obj: &'a Bound<'py, PyAny>, _: &'_ mut ()) -> Result { @@ -106,7 +108,10 @@ where type Error = T::Error; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = "typing.Any | None"; + const INPUT_TYPE: TypeHint = TypeHint::union(&[ + TypeHint::module_attr("typing", "Any"), + TypeHint::builtin("None"), + ]); #[inline] fn extract( @@ -127,7 +132,7 @@ impl<'a, 'holder, 'py> PyFunctionArgument<'a, 'holder, 'py, false> for &'holder type Error = as FromPyObject<'a, 'py>>::Error; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = "str"; + const INPUT_TYPE: TypeHint = TypeHint::builtin("str"); #[inline] fn extract( @@ -157,7 +162,7 @@ impl<'a, 'holder, T: PyClass> PyFunctionArgument<'a, 'holder, '_, false> for &'h type Error = PyErr; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = T::PYTHON_TYPE; + const INPUT_TYPE: TypeHint = T::TYPE_HINT; #[inline] fn extract(obj: &'a Bound<'_, PyAny>, holder: &'holder mut Self::Holder) -> PyResult { @@ -172,7 +177,7 @@ impl<'a, 'holder, T: PyClass> PyFunctionArgument<'a, 'holder, '_ type Error = PyErr; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = T::PYTHON_TYPE; + const INPUT_TYPE: TypeHint = T::TYPE_HINT; #[inline] fn extract(obj: &'a Bound<'_, PyAny>, holder: &'holder mut Self::Holder) -> PyResult { diff --git a/src/impl_/introspection.rs b/src/impl_/introspection.rs index a0f3ec81942..da217f92ced 100644 --- a/src/impl_/introspection.rs +++ b/src/impl_/introspection.rs @@ -1,19 +1,20 @@ use crate::conversion::IntoPyObject; +use crate::inspect::TypeHint; /// Trait to guess a function Python return type /// /// It is useful to properly get the return type `T` when the Rust implementation returns e.g. `PyResult` pub trait PyReturnType { /// The function return type - const OUTPUT_TYPE: &'static str; + const OUTPUT_TYPE: TypeHint; } impl<'a, T: IntoPyObject<'a>> PyReturnType for T { - const OUTPUT_TYPE: &'static str = T::OUTPUT_TYPE; + const OUTPUT_TYPE: TypeHint = T::OUTPUT_TYPE; } impl PyReturnType for Result { - const OUTPUT_TYPE: &'static str = T::OUTPUT_TYPE; + const OUTPUT_TYPE: TypeHint = T::OUTPUT_TYPE; } #[repr(C)] diff --git a/src/impl_/pyclass.rs b/src/impl_/pyclass.rs index 772979136bb..5f887cce167 100644 --- a/src/impl_/pyclass.rs +++ b/src/impl_/pyclass.rs @@ -212,9 +212,6 @@ pub trait PyClassImpl: Sized + 'static { /// from the PyClassDocGenerator` type. const DOC: &'static CStr; - #[cfg(feature = "experimental-inspect")] - const TYPE_NAME: &'static str; - fn items_iter() -> PyClassItemsIter; #[inline] diff --git a/src/inspect/mod.rs b/src/inspect/mod.rs index f14f8a2d2ae..1bcc365bda0 100644 --- a/src/inspect/mod.rs +++ b/src/inspect/mod.rs @@ -1,4 +1,271 @@ //! Runtime inspection of objects exposed to Python. //! //! Tracking issue: . + +use std::fmt; +use std::fmt::Formatter; + pub mod types; + +/// A [type hint](https://docs.python.org/3/glossary.html#term-type-hint). +/// +/// This struct aims at being used in `const` contexts like in [`FromPyObject::INPUT_TYPE`](crate::FromPyObject::INPUT_TYPE) and [`IntoPyObject::OUTPUT_TYPE`](crate::IntoPyObject::OUTPUT_TYPE). +/// +/// ``` +/// use pyo3::inspect::TypeHint; +/// +/// const T: TypeHint = TypeHint::union(&[TypeHint::builtin("int"), TypeHint::module_attr("b", "B")]); +/// assert_eq!(T.to_string(), "int | b.B"); +/// ``` +#[derive(Clone, Copy)] +pub struct TypeHint { + inner: TypeHintExpr, +} + +#[derive(Clone, Copy)] +enum TypeHintExpr { + /// A built-name like `list` or `datetime`. Used for built-in types or modules. + Builtin { id: &'static str }, + /// A module member like `datetime.time` where module = `datetime` and attr = `time` + ModuleAttribute { + module: &'static str, + attr: &'static str, + }, + /// A union elts[0] | ... | elts[len] + Union { elts: &'static [TypeHint] }, + /// A subscript main[*args] + Subscript { + value: &'static TypeHint, + slice: &'static [TypeHint], + }, +} + +impl TypeHint { + /// A builtin like `int` or `list` + /// + /// ``` + /// use pyo3::inspect::TypeHint; + /// + /// const T: TypeHint = TypeHint::builtin("int"); + /// assert_eq!(T.to_string(), "int"); + /// ``` + pub const fn builtin(name: &'static str) -> Self { + Self { + inner: TypeHintExpr::Builtin { id: name }, + } + } + + /// A type contained in a module like `datetime.time` + /// + /// ``` + /// use pyo3::inspect::TypeHint; + /// + /// const T: TypeHint = TypeHint::module_attr("datetime", "time"); + /// assert_eq!(T.to_string(), "datetime.time"); + /// ``` + pub const fn module_attr(module: &'static str, attr: &'static str) -> Self { + Self { + inner: TypeHintExpr::ModuleAttribute { module, attr }, + } + } + + /// The union of multiple types + /// + /// ``` + /// use pyo3::inspect::TypeHint; + /// + /// const T: TypeHint = TypeHint::union(&[TypeHint::builtin("int"), TypeHint::builtin("float")]); + /// assert_eq!(T.to_string(), "int | float"); + /// ``` + pub const fn union(elts: &'static [TypeHint]) -> Self { + Self { + inner: TypeHintExpr::Union { elts }, + } + } + + /// A subscribed type, often a container + /// + /// ``` + /// use pyo3::inspect::TypeHint; + /// + /// const T: TypeHint = TypeHint::subscript(&TypeHint::builtin("dict"), &[TypeHint::builtin("int"), TypeHint::builtin("str")]); + /// assert_eq!(T.to_string(), "dict[int, str]"); + /// ``` + pub const fn subscript(value: &'static Self, slice: &'static [Self]) -> Self { + Self { + inner: TypeHintExpr::Subscript { value, slice }, + } + } +} + +/// Serialize the type for introspection and return the number of written bytes +/// +/// We use the same AST as Python: https://docs.python.org/3/library/ast.html#abstract-grammar +#[doc(hidden)] +pub const fn serialize_for_introspection(hint: &TypeHint, mut output: &mut [u8]) -> usize { + let original_len = output.len(); + match &hint.inner { + TypeHintExpr::Builtin { id } => { + output = write_slice_and_move_forward(b"{\"type\":\"builtin\",\"id\":\"", output); + output = write_slice_and_move_forward(id.as_bytes(), output); + output = write_slice_and_move_forward(b"\"}", output); + } + TypeHintExpr::ModuleAttribute { module, attr } => { + output = write_slice_and_move_forward(b"{\"type\":\"attribute\",\"module\":\"", output); + output = write_slice_and_move_forward(module.as_bytes(), output); + output = write_slice_and_move_forward(b"\",\"attr\":\"", output); + output = write_slice_and_move_forward(attr.as_bytes(), output); + output = write_slice_and_move_forward(b"\"}", output); + } + TypeHintExpr::Union { elts } => { + output = write_slice_and_move_forward(b"{\"type\":\"union\",\"elts\":[", output); + let mut i = 0; + while i < elts.len() { + if i > 0 { + output = write_slice_and_move_forward(b",", output); + } + output = write_type_hint_and_move_forward(&elts[i], output); + i += 1; + } + output = write_slice_and_move_forward(b"]}", output); + } + TypeHintExpr::Subscript { value, slice } => { + output = write_slice_and_move_forward(b"{\"type\":\"subscript\",\"value\":", output); + output = write_type_hint_and_move_forward(value, output); + output = write_slice_and_move_forward(b",\"slice\":[", output); + let mut i = 0; + while i < slice.len() { + if i > 0 { + output = write_slice_and_move_forward(b",", output); + } + output = write_type_hint_and_move_forward(&slice[i], output); + i += 1; + } + output = write_slice_and_move_forward(b"]}", output); + } + } + original_len - output.len() +} + +/// Length required by [`Self::serialize_for_introspection`] +#[doc(hidden)] +pub const fn serialized_len_for_introspection(hint: &TypeHint) -> usize { + match &hint.inner { + TypeHintExpr::Builtin { id } => 26 + id.len(), + TypeHintExpr::ModuleAttribute { module, attr } => 42 + module.len() + attr.len(), + TypeHintExpr::Union { elts } => { + let mut count = 26; + let mut i = 0; + while i < elts.len() { + if i > 0 { + count += 1; + } + count += serialized_len_for_introspection(&elts[i]); + i += 1; + } + count + } + TypeHintExpr::Subscript { value, slice } => { + let mut count = 40 + serialized_len_for_introspection(value); + let mut i = 0; + while i < slice.len() { + if i > 0 { + count += 1; + } + count += serialized_len_for_introspection(&slice[i]); + i += 1; + } + count + } + } +} + +impl fmt::Display for TypeHint { + #[inline] + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match &self.inner { + TypeHintExpr::Builtin { id } => id.fmt(f), + TypeHintExpr::ModuleAttribute { module, attr } => { + module.fmt(f)?; + f.write_str(".")?; + attr.fmt(f) + } + TypeHintExpr::Union { elts } => { + for (i, elt) in elts.iter().enumerate() { + if i > 0 { + f.write_str(" | ")?; + } + elt.fmt(f)?; + } + Ok(()) + } + TypeHintExpr::Subscript { value, slice } => { + value.fmt(f)?; + f.write_str("[")?; + for (i, elt) in slice.iter().enumerate() { + if i > 0 { + f.write_str(", ")?; + } + elt.fmt(f)?; + } + f.write_str("]") + } + } + } +} + +const fn write_slice_and_move_forward<'a>(value: &[u8], output: &'a mut [u8]) -> &'a mut [u8] { + // TODO: use copy_from_slice with MSRV 1.87+ + let mut i = 0; + while i < value.len() { + output[i] = value[i]; + i += 1; + } + output.split_at_mut(value.len()).1 +} + +const fn write_type_hint_and_move_forward<'a>( + value: &TypeHint, + output: &'a mut [u8], +) -> &'a mut [u8] { + let written = serialize_for_introspection(value, output); + output.split_at_mut(written).1 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_to_string() { + const T: TypeHint = TypeHint::subscript( + &TypeHint::builtin("dict"), + &[ + TypeHint::union(&[TypeHint::builtin("int"), TypeHint::builtin("float")]), + TypeHint::module_attr("datetime", "time"), + ], + ); + assert_eq!(T.to_string(), "dict[int | float, datetime.time]") + } + + #[test] + fn test_serialize_for_introspection() { + const T: TypeHint = TypeHint::subscript( + &TypeHint::builtin("dict"), + &[ + TypeHint::union(&[TypeHint::builtin("int"), TypeHint::builtin("float")]), + TypeHint::module_attr("datetime", "time"), + ], + ); + const SER_LEN: usize = serialized_len_for_introspection(&T); + const SER: [u8; SER_LEN] = { + let mut out: [u8; SER_LEN] = [0; SER_LEN]; + serialize_for_introspection(&T, &mut out); + out + }; + assert_eq!( + std::str::from_utf8(&SER).unwrap(), + r#"{"type":"subscript","value":{"type":"builtin","id":"dict"},"slice":[{"type":"union","elts":[{"type":"builtin","id":"int"},{"type":"builtin","id":"float"}]},{"type":"attribute","module":"datetime","attr":"time"}]}"# + ) + } +} diff --git a/src/instance.rs b/src/instance.rs index 09f0987cfbc..62cbf6c18bb 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -4,6 +4,8 @@ use crate::call::PyCallArgs; use crate::conversion::IntoPyObject; use crate::err::{PyErr, PyResult}; use crate::impl_::pycell::PyClassObject; +#[cfg(feature = "experimental-inspect")] +use crate::inspect::TypeHint; use crate::pycell::{PyBorrowError, PyBorrowMutError}; use crate::pyclass::boolean_struct::{False, True}; use crate::types::{any::PyAnyMethods, string::PyStringMethods, typeobject::PyTypeMethods}; @@ -2199,7 +2201,7 @@ where type Error = CastError<'a, 'py>; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = T::PYTHON_TYPE; + const INPUT_TYPE: TypeHint = T::TYPE_HINT; /// Extracts `Self` from the source `PyObject`. fn extract(ob: Borrowed<'a, 'py, PyAny>) -> Result { @@ -2214,7 +2216,7 @@ where type Error = CastError<'a, 'py>; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = T::PYTHON_TYPE; + const INPUT_TYPE: TypeHint = T::TYPE_HINT; /// Extracts `Self` from the source `PyObject`. fn extract(ob: Borrowed<'a, 'py, PyAny>) -> Result { diff --git a/src/pycell.rs b/src/pycell.rs index 2a230b05833..a9a282b24f0 100644 --- a/src/pycell.rs +++ b/src/pycell.rs @@ -205,6 +205,8 @@ use std::ops::{Deref, DerefMut}; use std::ptr::NonNull; pub(crate) mod impl_; +#[cfg(feature = "experimental-inspect")] +use crate::inspect::TypeHint; use impl_::{PyClassBorrowChecker, PyClassObjectLayout}; /// A wrapper type for an immutably borrowed value from a [`Bound<'py, T>`]. @@ -478,7 +480,7 @@ impl<'py, T: PyClass> IntoPyObject<'py> for PyRef<'py, T> { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = T::PYTHON_TYPE; + const OUTPUT_TYPE: TypeHint = T::TYPE_HINT; fn into_pyobject(self, _py: Python<'py>) -> Result { Ok(self.inner.clone()) @@ -491,7 +493,7 @@ impl<'a, 'py, T: PyClass> IntoPyObject<'py> for &'a PyRef<'py, T> { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = T::PYTHON_TYPE; + const OUTPUT_TYPE: TypeHint = T::TYPE_HINT; fn into_pyobject(self, _py: Python<'py>) -> Result { Ok(self.inner.as_borrowed()) @@ -657,7 +659,7 @@ impl<'py, T: PyClass> IntoPyObject<'py> for PyRefMut<'py, T> { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = T::PYTHON_TYPE; + const OUTPUT_TYPE: TypeHint = T::TYPE_HINT; fn into_pyobject(self, _py: Python<'py>) -> Result { Ok(self.inner.clone()) @@ -670,7 +672,7 @@ impl<'a, 'py, T: PyClass> IntoPyObject<'py> for &'a PyRefMut<'py type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = T::PYTHON_TYPE; + const OUTPUT_TYPE: TypeHint = T::TYPE_HINT; fn into_pyobject(self, _py: Python<'py>) -> Result { Ok(self.inner.as_borrowed()) diff --git a/src/pyclass/guard.rs b/src/pyclass/guard.rs index f2fd3d59b78..6a58b8a47a5 100644 --- a/src/pyclass/guard.rs +++ b/src/pyclass/guard.rs @@ -1,4 +1,6 @@ use crate::impl_::pycell::{PyClassObject, PyClassObjectLayout as _}; +#[cfg(feature = "experimental-inspect")] +use crate::inspect::TypeHint; use crate::pycell::PyBorrowMutError; use crate::pycell::{impl_::PyClassBorrowChecker, PyBorrowError}; use crate::pyclass::boolean_struct::False; @@ -307,7 +309,7 @@ impl<'a, 'py, T: PyClass> IntoPyObject<'py> for &PyClassGuard<'a, T> { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = T::PYTHON_TYPE; + const OUTPUT_TYPE: TypeHint = T::TYPE_HINT; #[inline] fn into_pyobject(self, py: crate::Python<'py>) -> Result { diff --git a/src/type_object.rs b/src/type_object.rs index f7f8d4dbfd1..e15a9c6b8c0 100644 --- a/src/type_object.rs +++ b/src/type_object.rs @@ -1,6 +1,8 @@ //! Python type object information use crate::ffi_ptr_ext::FfiPtrExt; +#[cfg(feature = "experimental-inspect")] +use crate::inspect::TypeHint; use crate::types::{PyAny, PyType}; use crate::{ffi, Bound, Python}; use std::ptr; @@ -46,9 +48,9 @@ pub unsafe trait PyTypeInfo: Sized { /// Module name, if any. const MODULE: Option<&'static str>; - /// Provides the full python type paths. + /// Provides the full python type as a type hint. #[cfg(feature = "experimental-inspect")] - const PYTHON_TYPE: &'static str = "typing.Any"; + const TYPE_HINT: TypeHint = TypeHint::module_attr("typing", "Any"); /// Returns the PyTypeObject instance for this type. fn type_object_raw(py: Python<'_>) -> *mut ffi::PyTypeObject; @@ -103,9 +105,9 @@ pub unsafe trait PyTypeCheck { )] const NAME: &'static str; - /// Provides the full python type of the allowed values. + /// Provides the full python type of the allowed values as a Python type hint. #[cfg(feature = "experimental-inspect")] - const PYTHON_TYPE: &'static str; + const TYPE_HINT: TypeHint; /// Checks if `object` is an instance of `Self`, which may include a subtype. /// @@ -125,7 +127,7 @@ where const NAME: &'static str = T::NAME; #[cfg(feature = "experimental-inspect")] - const PYTHON_TYPE: &'static str = ::PYTHON_TYPE; + const TYPE_HINT: TypeHint = ::TYPE_HINT; #[inline] fn type_check(object: &Bound<'_, PyAny>) -> bool { diff --git a/src/types/boolobject.rs b/src/types/boolobject.rs index de81684e035..c5de574db5b 100644 --- a/src/types/boolobject.rs +++ b/src/types/boolobject.rs @@ -1,13 +1,14 @@ +use super::any::PyAnyMethods; +use crate::conversion::IntoPyObject; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; +#[cfg(feature = "experimental-inspect")] +use crate::inspect::TypeHint; +use crate::PyErr; use crate::{ exceptions::PyTypeError, ffi, ffi_ptr_ext::FfiPtrExt, instance::Bound, types::typeobject::PyTypeMethods, Borrowed, FromPyObject, PyAny, Python, }; - -use super::any::PyAnyMethods; -use crate::conversion::IntoPyObject; -use crate::PyErr; use std::convert::Infallible; use std::ptr; @@ -144,7 +145,7 @@ impl<'py> IntoPyObject<'py> for bool { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = "bool"; + const OUTPUT_TYPE: TypeHint = TypeHint::builtin("bool"); #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { @@ -163,7 +164,7 @@ impl<'py> IntoPyObject<'py> for &bool { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = bool::OUTPUT_TYPE; + const OUTPUT_TYPE: TypeHint = bool::OUTPUT_TYPE; #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { @@ -183,7 +184,7 @@ impl FromPyObject<'_, '_> for bool { type Error = PyErr; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = "bool"; + const INPUT_TYPE: TypeHint = TypeHint::builtin("bool"); fn extract(obj: Borrowed<'_, '_, PyAny>) -> Result { let err = match obj.cast::() { diff --git a/src/types/float.rs b/src/types/float.rs index 6fbe2d3679b..5a3e22802aa 100644 --- a/src/types/float.rs +++ b/src/types/float.rs @@ -1,6 +1,8 @@ use crate::conversion::IntoPyObject; #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; +#[cfg(feature = "experimental-inspect")] +use crate::inspect::TypeHint; use crate::{ ffi, ffi_ptr_ext::FfiPtrExt, instance::Bound, Borrowed, FromPyObject, PyAny, PyErr, Python, }; @@ -73,7 +75,7 @@ impl<'py> IntoPyObject<'py> for f64 { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = "float"; + const OUTPUT_TYPE: TypeHint = TypeHint::builtin("float"); #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { @@ -92,7 +94,7 @@ impl<'py> IntoPyObject<'py> for &f64 { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = f64::OUTPUT_TYPE; + const OUTPUT_TYPE: TypeHint = f64::OUTPUT_TYPE; #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { @@ -109,7 +111,7 @@ impl<'py> FromPyObject<'_, 'py> for f64 { type Error = PyErr; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = "float"; + const INPUT_TYPE: TypeHint = TypeHint::builtin("float"); // PyFloat_AsDouble returns -1.0 upon failure #[allow(clippy::float_cmp)] @@ -146,7 +148,7 @@ impl<'py> IntoPyObject<'py> for f32 { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = "float"; + const OUTPUT_TYPE: TypeHint = TypeHint::builtin("float"); #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { @@ -165,7 +167,7 @@ impl<'py> IntoPyObject<'py> for &f32 { type Error = Infallible; #[cfg(feature = "experimental-inspect")] - const OUTPUT_TYPE: &'static str = f32::OUTPUT_TYPE; + const OUTPUT_TYPE: TypeHint = f32::OUTPUT_TYPE; #[inline] fn into_pyobject(self, py: Python<'py>) -> Result { @@ -182,7 +184,7 @@ impl<'a, 'py> FromPyObject<'a, 'py> for f32 { type Error = >::Error; #[cfg(feature = "experimental-inspect")] - const INPUT_TYPE: &'static str = "float"; + const INPUT_TYPE: TypeHint = TypeHint::builtin("float"); fn extract(obj: Borrowed<'_, 'py, PyAny>) -> Result { Ok(obj.extract::()? as f32) diff --git a/src/types/weakref/anyref.rs b/src/types/weakref/anyref.rs index 7e08abcb87a..efb6ed4ff41 100644 --- a/src/types/weakref/anyref.rs +++ b/src/types/weakref/anyref.rs @@ -1,5 +1,7 @@ use crate::err::PyResult; use crate::ffi_ptr_ext::FfiPtrExt; +#[cfg(feature = "experimental-inspect")] +use crate::inspect::TypeHint; use crate::sync::PyOnceLock; use crate::type_object::{PyTypeCheck, PyTypeInfo}; use crate::types::any::PyAny; @@ -22,8 +24,10 @@ unsafe impl PyTypeCheck for PyWeakref { const NAME: &'static str = "weakref"; #[cfg(feature = "experimental-inspect")] - const PYTHON_TYPE: &'static str = - "weakref.ProxyType | weakref.CallableProxyType | weakref.ReferenceType"; + const TYPE_HINT: TypeHint = TypeHint::union(&[ + PyWeakrefProxy::TYPE_HINT, + ::TYPE_HINT, + ]); #[inline] fn type_check(object: &Bound<'_, PyAny>) -> bool { diff --git a/src/types/weakref/proxy.rs b/src/types/weakref/proxy.rs index 1dc30620181..dce2291f3a5 100644 --- a/src/types/weakref/proxy.rs +++ b/src/types/weakref/proxy.rs @@ -1,6 +1,8 @@ use super::PyWeakrefMethods; use crate::err::PyResult; use crate::ffi_ptr_ext::FfiPtrExt; +#[cfg(feature = "experimental-inspect")] +use crate::inspect::TypeHint; use crate::py_result_ext::PyResultExt; use crate::sync::PyOnceLock; use crate::type_object::PyTypeCheck; @@ -24,7 +26,10 @@ unsafe impl PyTypeCheck for PyWeakrefProxy { const NAME: &'static str = "weakref.ProxyTypes"; #[cfg(feature = "experimental-inspect")] - const PYTHON_TYPE: &'static str = "weakref.ProxyType | weakref.CallableProxyType"; + const TYPE_HINT: TypeHint = TypeHint::union(&[ + TypeHint::module_attr("weakref", "ProxyType"), + TypeHint::module_attr("weakref", "CallableProxyType"), + ]); #[inline] fn type_check(object: &Bound<'_, PyAny>) -> bool { From 1a3e53ffaaa9cb9ad23582f4ac2a206bcd5cac9a Mon Sep 17 00:00:00 2001 From: David Hewitt Date: Thu, 23 Oct 2025 18:10:00 +0100 Subject: [PATCH 936/936] docs: use one sentence per line in guide (#5534) * docs: use one sentence per line in guide * formatting corrections * disable MD013 for paragraphs, tables, headings, and code blocks * remove unneeded comment --------- Co-authored-by: Icxolu <10486322+Icxolu@users.noreply.github.com> --- guide/src/advanced.md | 3 +- guide/src/async-await.md | 11 +- guide/src/building-and-distribution.md | 155 ++++--- .../multiple-python-versions.md | 30 +- guide/src/class.md | 162 ++++---- guide/src/class/call.md | 17 +- guide/src/class/numeric.md | 41 +- guide/src/class/object.md | 32 +- guide/src/class/protocols.md | 76 ++-- guide/src/class/thread-safety.md | 24 +- guide/src/conversions/tables.md | 16 +- guide/src/conversions/traits.md | 71 ++-- guide/src/debugging.md | 71 ++-- guide/src/ecosystem/async-await.md | 16 +- guide/src/ecosystem/logging.md | 18 +- guide/src/ecosystem/tracing.md | 18 +- guide/src/exception.md | 3 +- guide/src/faq.md | 40 +- guide/src/features.md | 56 ++- guide/src/free-threading.md | 208 +++------- guide/src/function.md | 27 +- guide/src/function/error-handling.md | 40 +- guide/src/function/signature.md | 32 +- guide/src/getting-started.md | 36 +- guide/src/index.md | 4 +- guide/src/migration.md | 385 +++++++++++------- guide/src/module.md | 15 +- guide/src/parallelism.md | 40 +- guide/src/performance.md | 29 +- guide/src/python-from-rust.md | 13 +- .../python-from-rust/calling-existing-code.md | 16 +- guide/src/python-from-rust/function-calls.md | 7 +- guide/src/python-typing-hints.md | 62 +-- guide/src/trait-bounds.md | 4 +- guide/src/type-stub.md | 12 +- guide/src/types.md | 96 +++-- pyproject.toml | 10 +- 37 files changed, 1073 insertions(+), 823 deletions(-) diff --git a/guide/src/advanced.md b/guide/src/advanced.md index 61dc66382f4..cd630c4f9da 100644 --- a/guide/src/advanced.md +++ b/guide/src/advanced.md @@ -4,4 +4,5 @@ PyO3 exposes much of Python's C API through the `ffi` module. -The C API is naturally unsafe and requires you to manage reference counts, errors and specific invariants yourself. Please refer to the [C API Reference Manual](https://docs.python.org/3/c-api/) and [The Rustonomicon](https://doc.rust-lang.org/nightly/nomicon/ffi.html) before using any function from that API. +The C API is naturally unsafe and requires you to manage reference counts, errors and specific invariants yourself. +Please refer to the [C API Reference Manual](https://docs.python.org/3/c-api/) and [The Rustonomicon](https://doc.rust-lang.org/nightly/nomicon/ffi.html) before using any function from that API. diff --git a/guide/src/async-await.md b/guide/src/async-await.md index 81d57ef3341..c6a6c9a00e9 100644 --- a/guide/src/async-await.md +++ b/guide/src/async-await.md @@ -25,7 +25,7 @@ async fn sleep(seconds: f64, result: Option>) -> Option> { # } ``` -*Python awaitables instantiated with this method can only be awaited in *asyncio* context. Other Python async runtime may be supported in the future.* +*Python awaitables instantiated with this method can only be awaited in `asyncio` context. Other Python async runtime may be supported in the future.* ## `Send + 'static` constraint @@ -33,7 +33,11 @@ Resulting future of an `async fn` decorated by `#[pyfunction]` must be `Send + ' As a consequence, `async fn` parameters and return types must also be `Send + 'static`, so it is not possible to have a signature like `async fn does_not_compile<'py>(arg: Bound<'py, PyAny>) -> Bound<'py, PyAny>`. -However, there is an exception for method receivers, so async methods can accept `&self`/`&mut self`. Note that this means that the class instance is borrowed for as long as the returned future is not completed, even across yield points and while waiting for I/O operations to complete. Hence, other methods cannot obtain exclusive borrows while the future is still being polled. This is the same as how async methods in Rust generally work but it is more problematic for Rust code interfacing with Python code due to pervasive shared mutability. This strongly suggests to prefer shared borrows `&self` over exclusive ones `&mut self` to avoid racy borrow check failures at runtime. +However, there is an exception for method receivers, so async methods can accept `&self`/ `&mut self`. +Note that this means that the class instance is borrowed for as long as the returned future is not completed, even across yield points and while waiting for I/O operations to complete. +Hence, other methods cannot obtain exclusive borrows while the future is still being polled. +This is the same as how async methods in Rust generally work but it is more problematic for Rust code interfacing with Python code due to pervasive shared mutability. +This strongly suggests to prefer shared borrows `&self` over exclusive ones `&mut self` to avoid racy borrow check failures at runtime. ## Implicitly attached to the interpreter @@ -98,6 +102,7 @@ async fn cancellable(#[pyo3(cancel_handle)] mut cancel: CancelHandle) { To make a Rust future awaitable in Python, PyO3 defines a [`Coroutine`]({{#PYO3_DOCS_URL}}/pyo3/coroutine/struct.Coroutine.html) type, which implements the Python [coroutine protocol](https://docs.python.org/3/library/collections.abc.html#collections.abc.Coroutine). -Each `coroutine.send` call is translated to a `Future::poll` call. If a [`CancelHandle`]({{#PYO3_DOCS_URL}}/pyo3/coroutine/struct.CancelHandle.html) parameter is declared, the exception passed to `coroutine.throw` call is stored in it and can be retrieved with [`CancelHandle::cancelled`]({{#PYO3_DOCS_URL}}/pyo3/coroutine/struct.CancelHandle.html#method.cancelled); otherwise, it cancels the Rust future, and the exception is reraised; +Each `coroutine.send` call is translated to a `Future::poll` call. +If a [`CancelHandle`]({{#PYO3_DOCS_URL}}/pyo3/coroutine/struct.CancelHandle.html) parameter is declared, the exception passed to `coroutine.throw` call is stored in it and can be retrieved with [`CancelHandle::cancelled`]({{#PYO3_DOCS_URL}}/pyo3/coroutine/struct.CancelHandle.html#method.cancelled); otherwise, it cancels the Rust future, and the exception is reraised; *The type does not yet have a public constructor until the design is finalized.* diff --git a/guide/src/building-and-distribution.md b/guide/src/building-and-distribution.md index 5cde6d546f8..e12dc18803b 100644 --- a/guide/src/building-and-distribution.md +++ b/guide/src/building-and-distribution.md @@ -1,14 +1,19 @@ # Building and distribution -This chapter of the guide goes into detail on how to build and distribute projects using PyO3. The way to achieve this is very different depending on whether the project is a Python module implemented in Rust, or a Rust binary embedding Python. For both types of project there are also common problems such as the Python version to build for and the [linker](https://en.wikipedia.org/wiki/Linker_(computing)) arguments to use. +This chapter of the guide goes into detail on how to build and distribute projects using PyO3. +The way to achieve this is very different depending on whether the project is a Python module implemented in Rust, or a Rust binary embedding Python. +For both types of project there are also common problems such as the Python version to build for and the [linker](https://en.wikipedia.org/wiki/Linker_(computing)) arguments to use. -The material in this chapter is intended for users who have already read the PyO3 [README](./index.md). It covers in turn the choices that can be made for Python modules and for Rust binaries. There is also a section at the end about cross-compiling projects using PyO3. +The material in this chapter is intended for users who have already read the PyO3 [README](./index.md). +It covers in turn the choices that can be made for Python modules and for Rust binaries. +There is also a section at the end about cross-compiling projects using PyO3. There is an additional sub-chapter dedicated to [supporting multiple Python versions](./building-and-distribution/multiple-python-versions.md). ## Configuring the Python version -PyO3 uses a build script (backed by the [`pyo3-build-config`] crate) to determine the Python version and set the correct linker arguments. By default it will attempt to use the following in order: +PyO3 uses a build script (backed by the [`pyo3-build-config`] crate) to determine the Python version and set the correct linker arguments. +By default it will attempt to use the following in order: - Any active Python virtualenv. - The `python` executable (if it's a Python 3 interpreter). @@ -18,7 +23,8 @@ You can override the Python interpreter by setting the `PYO3_PYTHON` environment Once the Python interpreter is located, `pyo3-build-config` executes it to query the information in the `sysconfig` module which is needed to configure the rest of the compilation. -To validate the configuration which PyO3 will use, you can run a compilation with the environment variable `PYO3_PRINT_CONFIG=1` set. An example output of doing this is shown below: +To validate the configuration which PyO3 will use, you can run a compilation with the environment variable `PYO3_PRINT_CONFIG=1` set. +An example output of doing this is shown below: ```console $ PYO3_PRINT_CONFIG=1 cargo build @@ -52,29 +58,40 @@ The `PYO3_ENVIRONMENT_SIGNATURE` environment variable can be used to trigger reb If you save the above output config from `PYO3_PRINT_CONFIG` to a file, it is possible to manually override the contents and feed it back into PyO3 using the `PYO3_CONFIG_FILE` env var. -If your build environment is unusual enough that PyO3's regular configuration detection doesn't work, using a config file like this will give you the flexibility to make PyO3 work for you. To see the full set of options supported, see the documentation for the [`InterpreterConfig` struct](https://docs.rs/pyo3-build-config/{{#PYO3_DOCS_VERSION}}/pyo3_build_config/struct.InterpreterConfig.html). +If your build environment is unusual enough that PyO3's regular configuration detection doesn't work, using a config file like this will give you the flexibility to make PyO3 work for you. +To see the full set of options supported, see the documentation for the [`InterpreterConfig` struct](https://docs.rs/pyo3-build-config/{{#PYO3_DOCS_VERSION}}/pyo3_build_config/struct.InterpreterConfig.html). ## Building Python extension modules -Python extension modules need to be compiled differently depending on the OS (and architecture) that they are being compiled for. As well as multiple OSes (and architectures), there are also many different Python versions which are actively supported. Packages uploaded to [PyPI](https://pypi.org/) usually want to upload prebuilt "wheels" covering many OS/arch/version combinations so that users on all these different platforms don't have to compile the package themselves. Package vendors can opt-in to the "abi3" limited Python API which allows their wheels to be used on multiple Python versions, reducing the number of wheels they need to compile, but restricts the functionality they can use. +Python extension modules need to be compiled differently depending on the OS (and architecture) that they are being compiled for. +As well as multiple OSes (and architectures), there are also many different Python versions which are actively supported. +Packages uploaded to [PyPI](https://pypi.org/) usually want to upload prebuilt "wheels" covering many OS/arch/version combinations so that users on all these different platforms don't have to compile the package themselves. +Package vendors can opt-in to the "abi3" limited Python API which allows their wheels to be used on multiple Python versions, reducing the number of wheels they need to compile, but restricts the functionality they can use. -There are many ways to go about this: it is possible to use `cargo` to build the extension module (along with some manual work, which varies with OS). The PyO3 ecosystem has two packaging tools, [`maturin`] and [`setuptools-rust`], which abstract over the OS difference and also support building wheels for PyPI upload. +There are many ways to go about this: it is possible to use `cargo` to build the extension module (along with some manual work, which varies with OS). +The PyO3 ecosystem has two packaging tools, [`maturin`] and [`setuptools-rust`], which abstract over the OS difference and also support building wheels for PyPI upload. PyO3 has some Cargo features to configure projects for building Python extension modules: - The `extension-module` feature, which must be enabled when building Python extension modules. - The `abi3` feature and its version-specific `abi3-pyXY` companions, which are used to opt-in to the limited Python API in order to support multiple Python versions in a single wheel. -This section describes each of these packaging tools before describing how to build manually without them. It then proceeds with an explanation of the `extension-module` feature. Finally, there is a section describing PyO3's `abi3` features. +This section describes each of these packaging tools before describing how to build manually without them. +It then proceeds with an explanation of the `extension-module` feature. +Finally, there is a section describing PyO3's `abi3` features. ### Packaging tools The PyO3 ecosystem has two main choices to abstract the process of developing Python extension modules: -- [`maturin`] is a command-line tool to build, package and upload Python modules. It makes opinionated choices about project layout meaning it needs very little configuration. This makes it a great choice for users who are building a Python extension from scratch and don't need flexibility. -- [`setuptools-rust`] is an add-on for `setuptools` which adds extra keyword arguments to the `setup.py` configuration file. It requires more configuration than `maturin`, however this gives additional flexibility for users adding Rust to an existing Python package that can't satisfy `maturin`'s constraints. +- [`maturin`] is a command-line tool to build, package and upload Python modules. + It makes opinionated choices about project layout meaning it needs very little configuration. + This makes it a great choice for users who are building a Python extension from scratch and don't need flexibility. +- [`setuptools-rust`] is an add-on for `setuptools` which adds extra keyword arguments to the `setup.py` configuration file. + It requires more configuration than `maturin`, however this gives additional flexibility for users adding Rust to an existing Python package that can't satisfy `maturin`'s constraints. -Consult each project's documentation for full details on how to get started using them and how to upload wheels to PyPI. It should be noted that while `maturin` is able to build [manylinux](https://github.com/pypa/manylinux)-compliant wheels out-of-the-box, `setuptools-rust` requires a bit more effort, [relying on Docker](https://setuptools-rust.readthedocs.io/en/latest/building_wheels.html) for this purpose. +Consult each project's documentation for full details on how to get started using them and how to upload wheels to PyPI. +It should be noted that while `maturin` is able to build [manylinux](https://github.com/pypa/manylinux)-compliant wheels out-of-the-box, `setuptools-rust` requires a bit more effort, [relying on Docker](https://setuptools-rust.readthedocs.io/en/latest/building_wheels.html) for this purpose. There are also [`maturin-starter`] and [`setuptools-rust-starter`] examples in the PyO3 repository. @@ -90,11 +107,14 @@ Once built, symlink (or copy) and rename the shared library from Cargo's `target You can then open a Python shell in the output directory and you'll be able to run `import your_module`. -If you're packaging your library for redistribution, you should indicate the Python interpreter your library is compiled for by including the [platform tag](#platform-tags) in its name. This prevents incompatible interpreters from trying to import your library. If you're compiling for PyPy you *must* include the platform tag, or PyPy will ignore the module. +If you're packaging your library for redistribution, you should indicate the Python interpreter your library is compiled for by including the [platform tag](#platform-tags) in its name. +This prevents incompatible interpreters from trying to import your library. +If you're compiling for PyPy you *must* include the platform tag, or PyPy will ignore the module. #### Bazel builds -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. +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. [github.com/abrisco/rules_pyo3](https://github.com/abrisco/rules_pyo3) -- General rules for building extension modules. @@ -103,7 +123,9 @@ For example see: #### Platform tags -Rather than using just the `.so` or `.pyd` extension suggested above (depending on OS), you can prefix the shared library extension with a platform tag to indicate the interpreter it is compatible with. You can query your interpreter's platform tag from the `sysconfig` module. Some example outputs of this are seen below: +Rather than using just the `.so` or `.pyd` extension suggested above (depending on OS), you can prefix the shared library extension with a platform tag to indicate the interpreter it is compatible with. +You can query your interpreter's platform tag from the `sysconfig` module. +Some example outputs of this are seen below: ```bash # CPython 3.10 on macOS @@ -175,39 +197,53 @@ Finally, don't forget that on MacOS the `extension-module` feature will cause `c PyO3's `extension-module` feature is used to disable [linking](https://en.wikipedia.org/wiki/Linker_(computing)) to `libpython` on Unix targets. -This is necessary because by default PyO3 links to `libpython`. This makes binaries, tests, and examples "just work". However, Python extensions on Unix must not link to libpython for [manylinux](https://www.python.org/dev/peps/pep-0513/) compliance. +This is necessary because by default PyO3 links to `libpython`. +This makes binaries, tests, and examples "just work". +However, Python extensions on Unix must not link to libpython for [manylinux](https://www.python.org/dev/peps/pep-0513/) compliance. -The downside of not linking to `libpython` is that binaries, tests, and examples (which usually embed Python) will fail to build. If you have an extension module as well as other outputs in a single project, you need to use optional Cargo features to disable the `extension-module` when you're not building the extension module. See [the FAQ](faq.md#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) for an example workaround. +The downside of not linking to `libpython` is that binaries, tests, and examples (which usually embed Python) will fail to build. +If you have an extension module as well as other outputs in a single project, you need to use optional Cargo features to disable the `extension-module` when you're not building the extension module. +See [the FAQ](faq.md#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) for an example workaround. ### `Py_LIMITED_API`/`abi3` -By default, Python extension modules can only be used with the same Python version they were compiled against. For example, an extension module built for Python 3.5 can't be imported in Python 3.8. [PEP 384](https://www.python.org/dev/peps/pep-0384/) introduced the idea of the limited Python API, which would have a stable ABI enabling extension modules built with it to be used against multiple Python versions. This is also known as `abi3`. +By default, Python extension modules can only be used with the same Python version they were compiled against. +For example, an extension module built for Python 3.5 can't be imported in Python 3.8. [PEP 384](https://www.python.org/dev/peps/pep-0384/) introduced the idea of the limited Python API, which would have a stable ABI enabling extension modules built with it to be used against multiple Python versions. +This is also known as `abi3`. -The advantage of building extension modules using the limited Python API is that package vendors only need to build and distribute a single copy (for each OS / architecture), and users can install it on all Python versions from the [minimum version](#minimum-python-version-for-abi3) and up. The downside of this is that PyO3 can't use optimizations which rely on being compiled against a known exact Python version. It's up to you to decide whether this matters for your extension module. It's also possible to design your extension module such that you can distribute `abi3` wheels but allow users compiling from source to benefit from additional optimizations - see the [support for multiple python versions](./building-and-distribution/multiple-python-versions.md) section of this guide, in particular the `#[cfg(Py_LIMITED_API)]` flag. +The advantage of building extension modules using the limited Python API is that package vendors only need to build and distribute a single copy (for each OS / architecture), and users can install it on all Python versions from the [minimum version](#minimum-python-version-for-abi3) and up. +The downside of this is that PyO3 can't use optimizations which rely on being compiled against a known exact Python version. +It's up to you to decide whether this matters for your extension module. +It's also possible to design your extension module such that you can distribute `abi3` wheels but allow users compiling from source to benefit from additional optimizations - see the [support for multiple python versions](./building-and-distribution/multiple-python-versions.md) section of this guide, in particular the `#[cfg(Py_LIMITED_API)]` flag. There are three steps involved in making use of `abi3` when building Python packages as wheels: -1. Enable the `abi3` feature in `pyo3`. This ensures `pyo3` only calls Python C-API functions which are part of the stable API, and on Windows also ensures that the project links against the correct shared object (no special behavior is required on other platforms): +1. Enable the `abi3` feature in `pyo3`. + This ensures `pyo3` only calls Python C-API functions which are part of the stable API, and on Windows also ensures that the project links against the correct shared object (no special behavior is required on other platforms): ```toml [dependencies] pyo3 = { {{#PYO3_CRATE_VERSION}}, features = ["abi3"] } ``` -2. Ensure that the built shared objects are correctly marked as `abi3`. This is accomplished by telling your build system that you're using the limited API. [`maturin`] >= 0.9.0 and [`setuptools-rust`] >= 0.11.4 support `abi3` wheels. +2. Ensure that the built shared objects are correctly marked as `abi3`. + This is accomplished by telling your build system that you're using the limited API. [`maturin`] >= 0.9.0 and [`setuptools-rust`] >= 0.11.4 support `abi3` wheels. See the [corresponding](https://github.com/PyO3/maturin/pull/353) [PRs](https://github.com/PyO3/setuptools-rust/pull/82) for more. -3. Ensure that the `.whl` is correctly marked as `abi3`. For projects using `setuptools`, this is accomplished by passing `--py-limited-api=cp3x` (where `x` is the minimum Python version supported by the wheel, e.g. `--py-limited-api=cp35` for Python 3.5) to `setup.py bdist_wheel`. +3. Ensure that the `.whl` is correctly marked as `abi3`. + For projects using `setuptools`, this is accomplished by passing `--py-limited-api=cp3x` (where `x` is the minimum Python version supported by the wheel, e.g. `--py-limited-api=cp35` for Python 3.5) to `setup.py bdist_wheel`. #### Minimum Python version for `abi3` Because a single `abi3` wheel can be used with many different Python versions, PyO3 has feature flags `abi3-py37`, `abi3-py38`, `abi3-py39` etc. to set the minimum required Python version for your `abi3` wheel. For example, if you set the `abi3-py37` feature, your extension wheel can be used on all Python 3 versions from Python 3.7 and up. `maturin` and `setuptools-rust` will give the wheel a name like `my-extension-1.0-cp37-abi3-manylinux2020_x86_64.whl`. -As your extension module may be run with multiple different Python versions you may occasionally find you need to check the Python version at runtime to customize behavior. See [the relevant section of this guide](./building-and-distribution/multiple-python-versions.md#checking-the-python-version-at-runtime) on supporting multiple Python versions at runtime. +As your extension module may be run with multiple different Python versions you may occasionally find you need to check the Python version at runtime to customize behavior. +See [the relevant section of this guide](./building-and-distribution/multiple-python-versions.md#checking-the-python-version-at-runtime) on supporting multiple Python versions at runtime. -PyO3 is only able to link your extension module to abi3 version up to and including your host Python version. E.g., if you set `abi3-py38` and try to compile the crate with a host of Python 3.7, the build will fail. +PyO3 is only able to link your extension module to abi3 version up to and including your host Python version. +E.g., if you set `abi3-py38` and try to compile the crate with a host of Python 3.7, the build will fail. > Note: If you set more that one of these `abi3` version feature flags the lowest version always wins. For example, with both `abi3-py37` and `abi3-py38` set, PyO3 would build a wheel which supports Python 3.7 and up. @@ -227,8 +263,8 @@ the automatic import library generation feature to work. #### Missing features -Due to limitations in the Python API, there are a few `pyo3` features that do -not work when compiling for `abi3`. These are: +Due to limitations in the Python API, there are a few `pyo3` features that do not work when compiling for `abi3`. +These are: - `#[pyo3(text_signature = "...")]` does not work on classes until Python 3.10 or greater. - The `dict` and `weakref` options on classes are not supported until Python 3.9 or greater. @@ -237,29 +273,42 @@ not work when compiling for `abi3`. These are: ## Embedding Python in Rust -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. +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. +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 -Embedding the Python interpreter dynamically is much easier than doing so statically. This is done by linking your program against a Python shared library (such as `libpython.3.9.so` on UNIX, or `python39.dll` on Windows). The implementation of the Python interpreter resides inside the shared library. This means that when the OS runs your Rust program it also needs to be able to find the Python shared library. +Embedding the Python interpreter dynamically is much easier than doing so statically. +This is done by linking your program against a Python shared library (such as `libpython.3.9.so` on UNIX, or `python39.dll` on Windows). +The implementation of the Python interpreter resides inside the shared library. +This means that when the OS runs your Rust program it also needs to be able to find the Python shared library. -This mode of embedding works well for Rust tests which need access to the Python interpreter. It is also great for Rust software which is installed inside a Python virtualenv, because the virtualenv sets up appropriate environment variables to locate the correct Python shared library. +This mode of embedding works well for Rust tests which need access to the Python interpreter. +It is also great for Rust software which is installed inside a Python virtualenv, because the virtualenv sets up appropriate environment variables to locate the correct Python shared library. For distributing your program to non-technical users, you will have to consider including the Python shared library in your distribution as well as setting up wrapper scripts to set the right environment variables (such as `LD_LIBRARY_PATH` on UNIX, or `PATH` on Windows). -Note that PyPy cannot be embedded in Rust (or any other software). Support for this is tracked on the [PyPy issue tracker](https://github.com/pypy/pypy/issues/3836). +Note that PyPy cannot be embedded in Rust (or any other software). +Support for this is tracked on the [PyPy issue tracker](https://github.com/pypy/pypy/issues/3836). ### Statically embedding the Python interpreter -Embedding the Python interpreter statically means including the contents of a Python static library directly inside your Rust binary. This means that to distribute your program you only need to ship your binary file: it contains the Python interpreter inside the binary! +Embedding the Python interpreter statically means including the contents of a Python static library directly inside your Rust binary. +This means that to distribute your program you only need to ship your binary file: it contains the Python interpreter inside the binary! -On Windows static linking is almost never done, so Python distributions don't usually include a static library. The information below applies only to UNIX. +On Windows static linking is almost never done, so Python distributions don't usually include a static library. +The information below applies only to UNIX. The Python static library is usually called `libpython.a`. -Static linking has a lot of complications, listed below. For these reasons PyO3 does not yet have first-class support for this embedding mode. See [issue 416 on PyO3's GitHub](https://github.com/PyO3/pyo3/issues/416) for more information and to discuss any issues you encounter. +Static linking has a lot of complications, listed below. +For these reasons PyO3 does not yet have first-class support for this embedding mode. +See [issue 416 on PyO3's GitHub](https://github.com/PyO3/pyo3/issues/416) for more information and to discuss any issues you encounter. The [`auto-initialize`](features.md#auto-initialize) feature is deliberately disabled when embedding the interpreter statically because this is often unintentionally done by new users to PyO3 running test programs. Trying out PyO3 is much easier using dynamic embedding. @@ -280,41 +329,43 @@ The known complications are: /usr/bin/ld: /usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/libpython3.9.a(zlibmodule.o): relocation R_X86_64_32 against `.data' can not be used when making a PIE object; recompile with -fPIE ``` -If you encounter these or other complications when linking the interpreter statically, discuss them on [issue 416 on PyO3's GitHub](https://github.com/PyO3/pyo3/issues/416). It is hoped that eventually that discussion will contain enough information and solutions that PyO3 can offer first-class support for static embedding. +If you encounter these or other complications when linking the interpreter statically, discuss them on [issue 416 on PyO3's GitHub](https://github.com/PyO3/pyo3/issues/416). +It is hoped that eventually that discussion will contain enough information and solutions that PyO3 can offer first-class support for static embedding. ### Import your module when embedding the Python interpreter -When you run your Rust binary with an embedded interpreter, any `#[pymodule]` created modules won't be accessible to import unless added to a table called `PyImport_Inittab` before the embedded interpreter is initialized. This will cause Python statements in your embedded interpreter such as `import your_new_module` to fail. You can call the macro [`append_to_inittab`]({{#PYO3_DOCS_URL}}/pyo3/macro.append_to_inittab.html) with your module before initializing the Python interpreter to add the module function into that table. (The Python interpreter will be initialized by calling `Python::initialize`, `with_embedded_python_interpreter`, or `Python::attach` with the [`auto-initialize`](features.md#auto-initialize) feature enabled.) +When you run your Rust binary with an embedded interpreter, any `#[pymodule]` created modules won't be accessible to import unless added to a table called `PyImport_Inittab` before the embedded interpreter is initialized. +This will cause Python statements in your embedded interpreter such as `import your_new_module` to fail. +You can call the macro [`append_to_inittab`]({{#PYO3_DOCS_URL}}/pyo3/macro.append_to_inittab.html) with your module before initializing the Python interpreter to add the module function into that table. (The Python interpreter will be initialized by calling `Python::initialize`, `with_embedded_python_interpreter`, or `Python::attach` with the [`auto-initialize`](features.md#auto-initialize) feature enabled.) ## Cross Compiling -Thanks to Rust's great cross-compilation support, cross-compiling using PyO3 is relatively straightforward. To get started, you'll need a few pieces of software: +Thanks to Rust's great cross-compilation support, cross-compiling using PyO3 is relatively straightforward. +To get started, you'll need a few pieces of software: - A toolchain for your target. - The appropriate options in your Cargo `.config` for the platform you're targeting and the toolchain you are using. - A Python interpreter that's already been compiled for your target (optional when building "abi3" extension modules). - A Python interpreter that is built for your host and available through the `PATH` or setting the [`PYO3_PYTHON`](#configuring-the-python-version) variable (optional when building "abi3" extension modules). -After you've obtained the above, you can build a cross-compiled PyO3 module by using Cargo's `--target` flag. PyO3's build script will detect that you are attempting a cross-compile based on your host machine and the desired target. +After you've obtained the above, you can build a cross-compiled PyO3 module by using Cargo's `--target` flag. +PyO3's build script will detect that you are attempting a cross-compile based on your host machine and the desired target. When cross-compiling, PyO3's build script cannot execute the target Python interpreter to query the configuration, so there are a few additional environment variables you may need to set: - `PYO3_CROSS`: If present this variable forces PyO3 to configure as a cross-compilation. -- `PYO3_CROSS_LIB_DIR`: This variable can 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. This variable is only needed when the output binary must link to libpython explicitly (e.g. when targeting Windows and Android or embedding a Python interpreter), or when it is absolutely required to get the interpreter configuration from `_sysconfigdata*.py`. -- `PYO3_CROSS_PYTHON_VERSION`: Major and minor version (e.g. 3.9) of the target Python installation. This variable is only needed if PyO3 cannot determine the version to target from `abi3-py3*` features, or if `PYO3_CROSS_LIB_DIR` is not set, or if there are multiple versions of Python present in `PYO3_CROSS_LIB_DIR`. -- `PYO3_CROSS_PYTHON_IMPLEMENTATION`: Python implementation name ("CPython" or "PyPy") of the target Python installation. CPython is assumed by default when this variable is not set, unless `PYO3_CROSS_LIB_DIR` is set for a Unix-like target and PyO3 can get the interpreter configuration from `_sysconfigdata*.py`. - -An experimental `pyo3` crate feature `generate-import-lib` enables the user to cross-compile -extension modules for Windows targets without setting the `PYO3_CROSS_LIB_DIR` environment -variable or providing any Windows Python library files. It uses an external [`python3-dll-a`] crate -to generate import libraries for the Python DLL for MinGW-w64 and MSVC compile targets. -`python3-dll-a` uses the binutils `dlltool` program to generate DLL import libraries for MinGW-w64 targets. -It is possible to override the default `dlltool` command name for the cross target -by setting `PYO3_MINGW_DLLTOOL` environment variable. -*Note*: MSVC targets require LLVM binutils or MSVC build tools to be available on the host system. -More specifically, `python3-dll-a` requires `llvm-dlltool` or `lib.exe` executable to be present in `PATH` when -targeting `*-pc-windows-msvc`. The Zig compiler executable can be used in place of `llvm-dlltool` when the `ZIG_COMMAND` -environment variable is set to the installed Zig program name (`"zig"` or `"python -m ziglang"`). +- `PYO3_CROSS_LIB_DIR`: This variable can 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. + This variable is only needed when the output binary must link to libpython explicitly (e.g. when targeting Windows and Android or embedding a Python interpreter), or when it is absolutely required to get the interpreter configuration from `_sysconfigdata*.py`. +- `PYO3_CROSS_PYTHON_VERSION`: Major and minor version (e.g. 3.9) of the target Python installation. + This variable is only needed if PyO3 cannot determine the version to target from `abi3-py3*` features, or if `PYO3_CROSS_LIB_DIR` is not set, or if there are multiple versions of Python present in `PYO3_CROSS_LIB_DIR`. +- `PYO3_CROSS_PYTHON_IMPLEMENTATION`: Python implementation name ("CPython" or "PyPy") of the target Python installation. + CPython is assumed by default when this variable is not set, unless `PYO3_CROSS_LIB_DIR` is set for a Unix-like target and PyO3 can get the interpreter configuration from `_sysconfigdata*.py`. + +An experimental `pyo3` crate feature `generate-import-lib` enables the user to cross-compile extension modules for Windows targets without setting the `PYO3_CROSS_LIB_DIR` environment variable or providing any Windows Python library files. +It uses an external [`python3-dll-a`] crate to generate import libraries for the Python DLL for MinGW-w64 and MSVC compile targets. `python3-dll-a` uses the binutils `dlltool` program to generate DLL import libraries for MinGW-w64 targets. +It is possible to override the default `dlltool` command name for the cross target by setting `PYO3_MINGW_DLLTOOL` environment variable. *Note*: MSVC targets require LLVM binutils or MSVC build tools to be available on the host system. +More specifically, `python3-dll-a` requires `llvm-dlltool` or `lib.exe` executable to be present in `PATH` when targeting `*-pc-windows-msvc`. +The Zig compiler executable can be used in place of `llvm-dlltool` when the `ZIG_COMMAND` environment variable is set to the installed Zig program name (`"zig"` or `"python -m ziglang"`). An example might look like the following (assuming your target's sysroot is at `/home/pyo3/cross/sysroot` and that your target is `armv7`): diff --git a/guide/src/building-and-distribution/multiple-python-versions.md b/guide/src/building-and-distribution/multiple-python-versions.md index 0f085d5bdc2..961c13b7d9c 100644 --- a/guide/src/building-and-distribution/multiple-python-versions.md +++ b/guide/src/building-and-distribution/multiple-python-versions.md @@ -1,14 +1,19 @@ # 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 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. +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. -Second, we'll show how to check the Python version at runtime. This can be useful when building for multiple versions with the `abi3` feature, where the Python API compiled against is not always the same as the one in use. +Second, we'll show how to check the Python version at runtime. +This can be useful when building for multiple versions with the `abi3` feature, where the Python API compiled against is not always the same as the one in use. ## Conditional compilation for different Python versions -The `pyo3-build-config` exposes multiple [`#[cfg]` flags](https://doc.rust-lang.org/rust-by-example/attribute/cfg.html) which can be used to conditionally compile code for a given Python version. PyO3 itself depends on this crate, so by using it you can be sure that you are configured correctly for the Python version PyO3 is building against. +The `pyo3-build-config` exposes multiple [`#[cfg]` flags](https://doc.rust-lang.org/rust-by-example/attribute/cfg.html) which can be used to conditionally compile code for a given Python version. +PyO3 itself depends on this crate, so by using it you can be sure that you are configured correctly for the Python version PyO3 is building against. This allows us to write code like the following @@ -51,13 +56,15 @@ After these steps you are ready to annotate your code! ### Common usages of `pyo3-build-cfg` flags -The `#[cfg]` flags added by `pyo3-build-cfg` can be combined with all of Rust's logic in the `#[cfg]` attribute to create very precise conditional code generation. The following are some common patterns implemented using these flags: +The `#[cfg]` flags added by `pyo3-build-cfg` can be combined with all of Rust's logic in the `#[cfg]` attribute to create very precise conditional code generation. +The following are some common patterns implemented using these flags: ```text #[cfg(Py_3_7)] ``` -This `#[cfg]` marks code that will only be present on Python 3.7 and upwards. There are similar options `Py_3_8`, `Py_3_9`, `Py_3_10` and so on for each minor version. +This `#[cfg]` marks code that will only be present on Python 3.7 and upwards. +There are similar options `Py_3_8`, `Py_3_9`, `Py_3_10` and so on for each minor version. ```text #[cfg(not(Py_3_7))] @@ -69,13 +76,15 @@ This `#[cfg]` marks code that will only be present on Python versions before (bu #[cfg(not(Py_LIMITED_API))] ``` -This `#[cfg]` marks code that is only available when building for the unlimited Python API (i.e. PyO3's `abi3` feature is not enabled). This might be useful if you want to ship your extension module as an `abi3` wheel and also allow users to compile it from source to make use of optimizations only possible with the unlimited API. +This `#[cfg]` marks code that is only available when building for the unlimited Python API (i.e. PyO3's `abi3` feature is not enabled). +This might be useful if you want to ship your extension module as an `abi3` wheel and also allow users to compile it from source to make use of optimizations only possible with the unlimited API. ```text #[cfg(any(Py_3_9, not(Py_LIMITED_API)))] ``` -This `#[cfg]` marks code which is available when running Python 3.9 or newer, or when using the unlimited API with an older Python version. Patterns like this are commonly seen on Python APIs which were added to the limited Python API in a specific minor version. +This `#[cfg]` marks code which is available when running Python 3.9 or newer, or when using the unlimited API with an older Python version. +Patterns like this are commonly seen on Python APIs which were added to the limited Python API in a specific minor version. ```text #[cfg(PyPy)] @@ -87,11 +96,14 @@ This `#[cfg]` marks code which is running on PyPy. When building with PyO3's `abi3` feature, your extension module will be compiled against a specific [minimum version](../building-and-distribution.md#minimum-python-version-for-abi3) of Python, but may be running on newer Python versions. -For example with PyO3's `abi3-py38` feature, your extension will be compiled as if it were for Python 3.8. If you were using `pyo3-build-config`, `#[cfg(Py_3_8)]` would be present. Your user could freely install and run your abi3 extension on Python 3.9. +For example with PyO3's `abi3-py38` feature, your extension will be compiled as if it were for Python 3.8. +If you were using `pyo3-build-config`, `#[cfg(Py_3_8)]` would be present. +Your user could freely install and run your abi3 extension on Python 3.9. There's no way to detect your user doing that at compile time, so instead you need to fall back to runtime checks. -PyO3 provides the APIs [`Python::version()`] and [`Python::version_info()`] to query the running Python version. This allows you to do the following, for example: +PyO3 provides the APIs [`Python::version()`] and [`Python::version_info()`] to query the running Python version. +This allows you to do the following, for example: ```rust use pyo3::Python; diff --git a/guide/src/class.md b/guide/src/class.md index 26e6880d42b..ecc68f3a8a6 100644 --- a/guide/src/class.md +++ b/guide/src/class.md @@ -2,9 +2,11 @@ PyO3 exposes a group of attributes powered by Rust's proc macro system for defining Python classes as Rust structs. -The main attribute is `#[pyclass]`, which is placed upon a Rust `struct` or `enum` to generate a Python type for it. They will usually also have *one* `#[pymethods]`-annotated `impl` block for the struct, which is used to define Python methods and constants for the generated Python type. (If the [`multiple-pymethods`] feature is enabled, each `#[pyclass]` is allowed to have multiple `#[pymethods]` blocks.) `#[pymethods]` may also have implementations for Python magic methods such as `__str__`. +The main attribute is `#[pyclass]`, which is placed upon a Rust `struct` or `enum` to generate a Python type for it. +They will usually also have *one* `#[pymethods]`-annotated `impl` block for the struct, which is used to define Python methods and constants for the generated Python type. (If the [`multiple-pymethods`] feature is enabled, each `#[pyclass]` is allowed to have multiple `#[pymethods]` blocks.) `#[pymethods]` may also have implementations for Python magic methods such as `__str__`. -This chapter will discuss the functionality and configuration these attributes offer. Below is a list of links to the relevant section of this chapter for each: +This chapter will discuss the functionality and configuration these attributes offer. +Below is a list of links to the relevant section of this chapter for each: - [`#[pyclass]`](#defining-a-new-class) - [`#[pyo3(get, set)]`](#object-properties-using-pyo3get-set) @@ -70,23 +72,31 @@ enum Shape { } ``` -The above example generates implementations for [`PyTypeInfo`] and [`PyClass`] for `MyClass`, `Number`, `MyEnum`, `HttpResponse`, and `Shape`. To see these generated implementations, refer to the [implementation details](#implementation-details) at the end of this chapter. +The above example generates implementations for [`PyTypeInfo`] and [`PyClass`] for `MyClass`, `Number`, `MyEnum`, `HttpResponse`, and `Shape`. +To see these generated implementations, refer to the [implementation details](#implementation-details) at the end of this chapter. ### Restrictions -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. +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 -Rust lifetimes are used by the Rust compiler to reason about a program's memory safety. They are a compile-time only concept; there is no way to access Rust lifetimes at runtime from a dynamic language like Python. +Rust lifetimes are used by the Rust compiler to reason about a program's memory safety. +They are a compile-time only concept; there is no way to access Rust lifetimes at runtime from a dynamic language like Python. -As soon as Rust data is exposed to Python, there is no guarantee that the Rust compiler can make on how long the data will live. Python is a reference-counted language and those references can be held for an arbitrarily long time which is untraceable by the Rust compiler. The only possible way to express this correctly is to require that any `#[pyclass]` does not borrow data for any lifetime shorter than the `'static` lifetime, i.e. the `#[pyclass]` cannot have any lifetime parameters. +As soon as Rust data is exposed to Python, there is no guarantee that the Rust compiler can make on how long the data will live. +Python is a reference-counted language and those references can be held for an arbitrarily long time which is untraceable by the Rust compiler. +The only possible way to express this correctly is to require that any `#[pyclass]` does not borrow data for any lifetime shorter than the `'static` lifetime, i.e. the `#[pyclass]` cannot have any lifetime parameters. When you need to share ownership of data between Python and Rust, instead of using borrowed references with lifetimes consider using reference-counted smart pointers such as [`Arc`] or [`Py`][`Py`]. #### No generic parameters -A Rust `struct Foo` with a generic parameter `T` generates new compiled implementations each time it is used with a different concrete type for `T`. These new implementations are generated by the compiler at each usage site. This is incompatible with wrapping `Foo` in Python, where there needs to be a single compiled implementation of `Foo` which is integrated with the Python interpreter. +A Rust `struct Foo` with a generic parameter `T` generates new compiled implementations each time it is used with a different concrete type for `T`. +These new implementations are generated by the compiler at each usage site. +This is incompatible with wrapping `Foo` in Python, where there needs to be a single compiled implementation of `Foo` which is integrated with the Python interpreter. Currently, the best alternative is to write a macro which expands to a new `#[pyclass]` for each instantiation you want: @@ -122,18 +132,20 @@ create_interface!(FloatClass, String); #### Must be thread-safe -Python objects are freely shared between threads by the Python interpreter. This means that: +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. +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 By default, it is not possible to create an instance of a custom class from Python code. -To declare a constructor, you need to define a method and annotate it with the `#[new]` -attribute. Only Python's `__new__` method can be specified, `__init__` is not available. +To declare a constructor, you need to define a method and annotate it with the `#[new]` attribute. +Only Python's `__new__` method can be specified, `__init__` is not available. ```rust # #![allow(dead_code)] @@ -203,12 +215,16 @@ mod my_module { ## Bound and interior mutability -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. +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. +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. However, the Rust borrow checker cannot reason about `&mut` references once an object's ownership has been passed to the Python interpreter. -To solve this, PyO3 does borrow checking at runtime using a scheme very similar to `std::cell::RefCell`. This is known as [interior mutability](https://doc.rust-lang.org/book/ch15-05-interior-mutability.html). +To solve this, PyO3 does borrow checking at runtime using a scheme very similar to `std::cell::RefCell`. +This is known as [interior mutability](https://doc.rust-lang.org/book/ch15-05-interior-mutability.html). Users who are familiar with `RefCell` can use `Py` and `Bound<'py, T>` just like `RefCell`. @@ -247,8 +263,8 @@ Python::attach(|py| { }); ``` -A `Bound<'py, T>` is restricted to the Python lifetime `'py`. To make the object longer lived (for example, to store it in a struct on the -Rust side), use `Py`. `Py` needs a `Python<'_>` token to allow access: +A `Bound<'py, T>` is restricted to the Python lifetime `'py`. +To make the object longer lived (for example, to store it in a struct on the Rust side), use `Py`. `Py` needs a `Python<'_>` token to allow access: ```rust # use pyo3::prelude::*; @@ -272,7 +288,9 @@ Python::attach(move |py| { ### frozen classes: Opting out of interior mutability -As detailed above, runtime borrow checking is currently enabled by default. But a class can opt of out it by declaring itself `frozen`. It can still use interior mutability via standard Rust types like `RefCell` or `Mutex`, but it is not bound to the implementation provided by PyO3 and can choose the most appropriate strategy on field-by-field basis. +As detailed above, runtime borrow checking is currently enabled by default. +But a class can opt of out it by declaring itself `frozen`. +It can still use interior mutability via standard Rust types like `RefCell` or `Mutex`, but it is not bound to the implementation provided by PyO3 and can choose the most appropriate strategy on field-by-field basis. Classes which are `frozen` and also `Sync`, e.g. they do use `Mutex` but not `RefCell`, can be accessed without needing a `Python` token via the `Bound::get` and `Py::get` methods: @@ -298,7 +316,8 @@ py_counter.get().value.fetch_add(1, Ordering::Relaxed); Python::attach(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. +Frozen classes are likely to become the default thereby guiding the PyO3 ecosystem towards a more deliberate application of interior mutability. +Eventually, this should enable further optimizations of PyO3's internals and avoid downstream code paying the cost of interior mutability when it is not actually required. ## Customizing the class @@ -322,25 +341,18 @@ Consult the table below to determine which type your constructor should return: ## Inheritance -By default, `object`, i.e. `PyAny` is used as the base class. To override this default, -use the `extends` parameter for `pyclass` with the full path to the base class. -Currently, only classes defined in Rust and builtins provided by PyO3 can be inherited -from; inheriting from other classes defined in Python is not yet supported -([#991](https://github.com/PyO3/pyo3/issues/991)). +By default, `object`, i.e. `PyAny` is used as the base class. +To override this default, use the `extends` parameter for `pyclass` with the full path to the base class. +Currently, only classes defined in Rust and builtins provided by PyO3 can be inherited from; inheriting from other classes defined in Python is not yet supported ([#991](https://github.com/PyO3/pyo3/issues/991)). For convenience, `(T, U)` implements `Into>` where `U` is the base class of `T`. But for a more deeply nested inheritance, you have to return `PyClassInitializer` 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_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 classes higher in the -inheritance hierarchy, for which you would need to chain multiple `as_super` or -`into_super` calls. +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_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 classes higher in the inheritance hierarchy, for which you would need to chain multiple `as_super` or `into_super` calls. ```rust # use pyo3::prelude::*; @@ -448,9 +460,8 @@ You can inherit native types such as `PyDict`, if they implement [`PySizedLayout`]({{#PYO3_DOCS_URL}}/pyo3/type_object/trait.PySizedLayout.html). This is not supported when building for the Python limited API (aka the `abi3` feature of PyO3). -To convert between the Rust type and its native base class, you can take -`slf` as a Python object. To access the Rust fields use `slf.borrow()` or -`slf.borrow_mut()`, and to access the base class use `slf.cast::()`. +To convert between the Rust type and its native base class, you can take `slf` as a Python object. +To access the Rust fields use `slf.borrow()` or `slf.borrow_mut()`, and to access the base class use `slf.cast::()`. ```rust # #[cfg(not(Py_LIMITED_API))] { @@ -567,7 +578,8 @@ struct MyClass { } ``` -The above would make the `num` field available for reading and writing as a `self.num` Python property. To expose the property with a different name to the field, specify this alongside the rest of the options, e.g. `#[pyo3(get, set, name = "custom_name")]`. +The above would make the `num` field available for reading and writing as a `self.num` Python property. +To expose the property with a different name to the field, specify this alongside the rest of the options, e.g. `#[pyo3(get, set, name = "custom_name")]`. Properties can be readonly or writeonly by using just `#[pyo3(get)]` or `#[pyo3(set)]` respectively. @@ -576,7 +588,8 @@ To use these annotations, your field type must implement some conversion traits: - For `get` the field type `T` must implement either `&T: IntoPyObject` or `T: IntoPyObject + Clone`. - For `set` the field type must implement `FromPyObject`. -For example, implementations of those traits are provided for the `Cell` type, if the inner type also implements the trait. This means you can use `#[pyo3(get, set)]` on fields wrapped in a `Cell`. +For example, implementations of those traits are provided for the `Cell` type, if the inner type also implements the trait. +This means you can use `#[pyo3(get, set)]` on fields wrapped in a `Cell`. ### Object properties using `#[getter]` and `#[setter]` @@ -600,14 +613,11 @@ impl MyClass { } ``` -A getter or setter's function name is used as the property name by default. There are several -ways how to override the name. +A getter or setter's function name is used as the property name by default. +There are several ways how to override the name. -If a function name starts with `get_` or `set_` for getter or setter respectively, -the descriptor name becomes the function name with this prefix removed. This is also useful in case of -Rust keywords like `type` -([raw identifiers](https://doc.rust-lang.org/edition-guide/rust-2018/module-system/raw-identifiers.html) -can be used since Rust 2018). +If a function name starts with `get_` or `set_` for getter or setter respectively, the descriptor name becomes the function name with this prefix removed. +This is also useful in case of Rust keywords like `type` ([raw identifiers](https://doc.rust-lang.org/edition-guide/rust-2018/module-system/raw-identifiers.html) can be used since Rust 2018). ```rust # use pyo3::prelude::*; @@ -658,19 +668,16 @@ impl MyClass { In this case, the property `number` is defined and available from Python code as `self.number`. -Attributes defined by `#[setter]` or `#[pyo3(set)]` will always raise `AttributeError` on `del` -operations. Support for defining custom `del` behavior is tracked in -[#1778](https://github.com/PyO3/pyo3/issues/1778). +Attributes defined by `#[setter]` or `#[pyo3(set)]` will always raise `AttributeError` on `del` operations. +Support for defining custom `del` behavior is tracked in [#1778](https://github.com/PyO3/pyo3/issues/1778). ## Instance methods -To define a Python compatible method, an `impl` block for your struct has to be annotated with the -`#[pymethods]` attribute. PyO3 generates Python compatible wrappers for all functions in this -block with some variations, like descriptors, class method static methods, etc. +To define a Python compatible method, an `impl` block for your struct has to be annotated with the `#[pymethods]` attribute. +PyO3 generates Python compatible wrappers for all functions in this block with some variations, like descriptors, class method static methods, etc. -Since Rust allows any number of `impl` blocks, you can easily split methods -between those accessible to Python (and Rust) and those accessible only to Rust. However to have multiple -`#[pymethods]`-annotated `impl` blocks for the same struct you must enable the [`multiple-pymethods`] feature of PyO3. +Since Rust allows any number of `impl` blocks, you can easily split methods between those accessible to Python (and Rust) and those accessible only to Rust. +However to have multiple `#[pymethods]`-annotated `impl` blocks for the same struct you must enable the [`multiple-pymethods`] feature of PyO3. ```rust # use pyo3::prelude::*; @@ -772,9 +779,8 @@ impl BaseClass { ## Static methods -To create a static method for a custom class, the method needs to be annotated with the -`#[staticmethod]` attribute. The return type must be `T` or `PyResult` for some `T` that implements -`IntoPyObject`. +To create a static method for a custom class, the method needs to be annotated with the `#[staticmethod]` attribute. +The return type must be `T` or `PyResult` for some `T` that implements `IntoPyObject`. ```rust # use pyo3::prelude::*; @@ -904,9 +910,11 @@ Note that `#[derive(FromPyObject)]` on a class is usually not useful as it tries ## Method arguments -Similar to `#[pyfunction]`, the `#[pyo3(signature = (...))]` attribute can be used to specify the way that `#[pymethods]` accept arguments. Consult the documentation for [`function signatures`](./function/signature.md) to see the parameters this attribute accepts. +Similar to `#[pyfunction]`, the `#[pyo3(signature = (...))]` attribute can be used to specify the way that `#[pymethods]` accept arguments. +Consult the documentation for [`function signatures`](./function/signature.md) to see the parameters this attribute accepts. -The following example defines a class `MyClass` with a method `method`. This method has a signature that sets default values for `num` and `name`, and indicates that `py_args` should collect all extra positional arguments and `py_kwargs` all extra keyword arguments: +The following example defines a class `MyClass` with a method `method`. +This method has a signature that sets default values for `num` and `name`, and indicates that `py_args` should collect all extra positional arguments and `py_kwargs` all extra keyword arguments: ```rust,no_run # use pyo3::prelude::*; @@ -1057,9 +1065,11 @@ Note that `text_signature` on `#[new]` is not compatible with compilation in ### 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. +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 Python lifetime `'py` should usually be derived from a `py: Python<'py>` token passed as an argument instead of the receiver. +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 Python lifetime `'py` should usually be derived from a `py: Python<'py>` token passed as an argument instead of the receiver. Specifically, signatures like @@ -1099,9 +1109,11 @@ Enum support in PyO3 comes in two flavors, depending on what kind of variants th ### Simple enums -A simple enum (a.k.a. C-like enum) has only unit variants. +A simple enum (a.k.a. +C-like enum) has only unit variants. -PyO3 adds a class attribute for each variant, so you can access them in Python without defining `#[new]`. PyO3 also provides default implementations of `__richcmp__` and `__int__`, so they can be compared using `==`: +PyO3 adds a class attribute for each variant, so you can access them in Python without defining `#[new]`. +PyO3 also provides default implementations of `__richcmp__` and `__int__`, so they can be compared using `==`: ```rust # use pyo3::prelude::*; @@ -1166,7 +1178,8 @@ Python::attach(|py| { }) ``` -All methods defined by PyO3 can be overridden. For example here's how you override `__repr__`: +All methods defined by PyO3 can be overridden. +For example here's how you override `__repr__`: ```rust # use pyo3::prelude::*; @@ -1264,9 +1277,11 @@ enum BadSubclass { An enum is complex if it has any non-unit (struct or tuple) variants. -PyO3 supports only struct and tuple variants in a complex enum. Unit variants aren't supported at present (the recommendation is to use an empty tuple enum instead). +PyO3 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. +PyO3 adds a class attribute for each variant, which may be used to construct values and in match patterns. +PyO3 also provides getter methods for all fields of each variant. ```rust # use pyo3::prelude::*; @@ -1312,7 +1327,9 @@ Python::attach(|py| { # .unwrap(); ``` -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`. +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::*; @@ -1331,8 +1348,9 @@ Python::attach(|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: +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::*; @@ -1375,11 +1393,15 @@ Python::attach(|py| { The `#[pyclass]` macros rely on a lot of conditional code generation: each `#[pyclass]` can optionally have a `#[pymethods]` block. -To support this flexibility the `#[pyclass]` macro expands to a blob of boilerplate code which sets up the structure for ["dtolnay specialization"](https://github.com/dtolnay/case-studies/blob/master/autoref-specialization/README.md). This implementation pattern enables the Rust compiler to use `#[pymethods]` implementations when they are present, and fall back to default (empty) definitions when they are not. +To support this flexibility the `#[pyclass]` macro expands to a blob of boilerplate code which sets up the structure for ["dtolnay specialization"](https://github.com/dtolnay/case-studies/blob/master/autoref-specialization/README.md). +This implementation pattern enables the Rust compiler to use `#[pymethods]` implementations when they are present, and fall back to default (empty) definitions when they are not. -This simple technique works for the case when there is zero or one implementations. To support multiple `#[pymethods]` for a `#[pyclass]` (in the [`multiple-pymethods`] feature), a registry mechanism provided by the [`inventory`](https://github.com/dtolnay/inventory) crate is used instead. This collects `impl`s at library load time, but isn't supported on all platforms. See [inventory: how it works](https://github.com/dtolnay/inventory#how-it-works) for more details. +This simple technique works for the case when there is zero or one implementations. +To support multiple `#[pymethods]` for a `#[pyclass]` (in the [`multiple-pymethods`] feature), a registry mechanism provided by the [`inventory`](https://github.com/dtolnay/inventory) crate is used instead. +This collects `impl`s at library load time, but isn't supported on all platforms. See [inventory: how it works](https://github.com/dtolnay/inventory#how-it-works) for more details. -The `#[pyclass]` macro expands to roughly the code seen below. The `PyClassImplCollector` is the type used internally by PyO3 for dtolnay specialization: +The `#[pyclass]` macro expands to roughly the code seen below. +The `PyClassImplCollector` is the type used internally by PyO3 for dtolnay specialization: ```rust # #[cfg(not(feature = "multiple-pymethods"))] { diff --git a/guide/src/class/call.md b/guide/src/class/call.md index f6596a61a7e..0f08bb53ce7 100644 --- a/guide/src/class/call.md +++ b/guide/src/class/call.md @@ -8,9 +8,8 @@ This method's signature must look like `__call__(, ...) -> object` - here, ## Example: Implementing a call counter -The following pyclass is a basic decorator - its constructor takes a Python object -as argument and calls that object when called. An equivalent Python implementation -is linked at the end. +The following pyclass is a basic decorator - its constructor takes a Python object as argument and calls that object when called. +An equivalent Python implementation is linked at the end. An example crate containing this pyclass can be found [here](https://github.com/PyO3/pyo3/tree/main/examples/decorator) @@ -92,9 +91,9 @@ fn __call__( } ``` -The problem with this is that the `&mut self` receiver means PyO3 has to borrow it exclusively, - and hold this borrow across the`self.wraps.call(py, args, kwargs)` call. This call returns control to the user's Python code - which is free to call arbitrary things, *including* the decorated function. If that happens PyO3 is unable to create a second unique borrow and will be forced to raise an exception. +The problem with this is that the `&mut self` receiver means PyO3 has to borrow it exclusively, and hold this borrow across the `self.wraps.call(py, args, kwargs)` call. +This call returns control to the user's Python code which is free to call arbitrary things, *including* the decorated function. +If that happens PyO3 is unable to create a second unique borrow and will be forced to raise an exception. As a result, something innocent like this will raise an exception: @@ -108,7 +107,8 @@ 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 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. +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: @@ -117,7 +117,8 @@ This shows the dangers of running arbitrary Python code - note that "running arb - 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. +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]: "Thread Safe Decorator · Discussion #2598 · PyO3/pyo3" [`AtomicU64`]: "AtomicU64 in std::sync::atomic - Rust" diff --git a/guide/src/class/numeric.md b/guide/src/class/numeric.md index f6d58e6a216..d7801b45f1a 100644 --- a/guide/src/class/numeric.md +++ b/guide/src/class/numeric.md @@ -2,16 +2,15 @@ At this point we have a `Number` class that we can't actually do any math on! -Before proceeding, we should think about how we want to handle overflows. There are three obvious solutions: - -- We can have infinite precision just like Python's `int`. However that would be quite boring - we'd - - be reinventing the wheel. +Before proceeding, we should think about how we want to handle overflows. +There are three obvious solutions: +- We can have infinite precision just like Python's `int`. + However that would be quite boring - we'd be reinventing the wheel. - We can raise exceptions whenever `Number` overflows, but that makes the API painful to use. -- We can wrap around the boundary of `i32`. This is the approach we'll take here. To do that we'll just forward to `i32`'s - - `wrapping_*` methods. +- We can wrap around the boundary of `i32`. + This is the approach we'll take here. + To do that we'll just forward to `i32`'s `wrapping_*` methods. ## Fixing our constructor @@ -30,10 +29,8 @@ Traceback (most recent call last): OverflowError: Python int too large to convert to C long ``` -Instead of relying on the default [`FromPyObject`] extraction to parse arguments, we can specify our -own extraction function, using the `#[pyo3(from_py_with = ...)]` attribute. Unfortunately PyO3 -doesn't provide a way to wrap Python integers out of the box, but we can do a Python call to mask it -and cast it to an `i32`. +Instead of relying on the default [`FromPyObject`] extraction to parse arguments, we can specify our own extraction function, using the `#[pyo3(from_py_with = ...)]` attribute. +Unfortunately PyO3 doesn't provide a way to wrap Python integers out of the box, but we can do a Python call to mask it and cast it to an `i32`. ```rust,no_run # #![allow(dead_code)] @@ -402,25 +399,27 @@ mod my_module { ## Appendix: Writing some unsafe code -At the beginning of this chapter we said that PyO3 doesn't provide a way to wrap Python integers out -of the box but that's a half truth. There's not a PyO3 API for it, but there's a Python C API -function that does: +At the beginning of this chapter we said that PyO3 doesn't provide a way to wrap Python integers out of the box but that's a half truth. +There's not a PyO3 API for it, but there's a Python C API function that does: ```c unsigned long PyLong_AsUnsignedLongMask(PyObject *obj) ``` -We can call this function from Rust by using [`pyo3::ffi::PyLong_AsUnsignedLongMask`]. This is an *unsafe* -function, which means we have to use an unsafe block to call it and take responsibility for upholding -the contracts of this function. Let's review those contracts: +We can call this function from Rust by using [`pyo3::ffi::PyLong_AsUnsignedLongMask`]. +This is an *unsafe* function, which means we have to use an unsafe block to call it and take responsibility for upholding the contracts of this function. +Let's review those contracts: -- We must be attached to the interpreter. If we're not, calling this function causes a data race. +- We must be attached to the interpreter. + If we're not, calling this function causes a data race. - The pointer must be valid, i.e. it must be properly aligned and point to a valid Python object. -Let's create that helper function. The signature has to be `fn(&Bound<'_, PyAny>) -> PyResult`. +Let's create that helper function. +The signature has to be `fn(&Bound<'_, PyAny>) -> PyResult`. - `&Bound<'_, PyAny>` represents a checked bound reference, so the pointer derived from it is valid (and not null). -- Whenever we have bound references to Python objects in scope, it is guaranteed that we're attached to the interpreter. This reference is also where we can get a [`Python`] token to use in our call to [`PyErr::take`]. +- Whenever we have bound references to Python objects in scope, it is guaranteed that we're attached to the interpreter. + This reference is also where we can get a [`Python`] token to use in our call to [`PyErr::take`]. ```rust,no_run # #![allow(dead_code)] diff --git a/guide/src/class/object.md b/guide/src/class/object.md index da98a0396c0..12f3dd7f06b 100644 --- a/guide/src/class/object.md +++ b/guide/src/class/object.md @@ -41,9 +41,9 @@ print(n) ## String representations -It can't even print an user-readable representation of itself! We can fix that by defining the -`__repr__` and `__str__` methods inside a `#[pymethods]` block. We do this by accessing the value -contained inside `Number`. +It can't even print an user-readable representation of itself! +We can fix that by defining the `__repr__` and `__str__` methods inside a `#[pymethods]` block. +We do this by accessing the value contained inside `Number`. ```rust,no_run # use pyo3::prelude::*; @@ -115,11 +115,10 @@ struct Coordinate { ### Accessing the class name -In the `__repr__`, we used a hard-coded class name. This is sometimes not ideal, -because if the class is subclassed in Python, we would like the repr to reflect -the subclass name. This is typically done in Python code by accessing -`self.__class__.__name__`. In order to be able to access the Python type information -*and* the Rust struct, we need to use a `Bound` as the `self` argument. +In the `__repr__`, we used a hard-coded class name. +This is sometimes not ideal, because if the class is subclassed in Python, we would like the repr to reflect the subclass name. +This is typically done in Python code by accessing `self.__class__.__name__`. +In order to be able to access the Python type information *and* the Rust struct, we need to use a `Bound` as the `self` argument. ```rust,no_run # use pyo3::prelude::*; @@ -142,8 +141,10 @@ impl Number { ### Hashing -Let's also implement hashing. We'll just hash the `i32`. For that we need a [`Hasher`]. The one -provided by `std` is [`DefaultHasher`], which uses the [SipHash] algorithm. +Let's also implement hashing. +We'll just hash the `i32`. +For that we need a [`Hasher`]. +The one provided by `std` is [`DefaultHasher`], which uses the [SipHash] algorithm. ```rust,no_run use std::collections::hash_map::DefaultHasher; @@ -168,10 +169,9 @@ 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" +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,no_run # use pyo3::prelude::*; @@ -214,8 +214,8 @@ struct Number(i32); ### Comparisons -PyO3 supports the usual magic comparison methods available in Python such as `__eq__`, `__lt__` -and so on. It is also possible to support all six operations at once with `__richcmp__`. +PyO3 supports the usual magic comparison methods available in Python such as `__eq__`, `__lt__` and so on. +It is also possible to support all six operations at once with `__richcmp__`. This method will be called with a value of `CompareOp` depending on the operation. ```rust,no_run diff --git a/guide/src/class/protocols.md b/guide/src/class/protocols.md index 36cd1d37e39..aa99ea95554 100644 --- a/guide/src/class/protocols.md +++ b/guide/src/class/protocols.md @@ -1,6 +1,8 @@ # Class customizations -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. +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. 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: @@ -8,13 +10,17 @@ PyO3 makes it possible for every magic method to be implemented in `#[pymethods] - `__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. +- 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 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. +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. ## 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. +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). @@ -26,9 +32,8 @@ When PyO3 handles a magic method, a couple of changes apply compared to other `# 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 - `self_: PyRefMut<'_, Self>`, as described [here](../class.md#inheritance). +- 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 `self_: PyRefMut<'_, Self>`, as described [here](../class.md#inheritance). - An optional `Python<'py>` argument is always allowed as the first argument. - Return values can be optionally wrapped in `PyResult`. - `object` means that any type is allowed that can be extracted from a Python @@ -37,9 +42,8 @@ given signatures should be interpreted as follows: `__richcmp__`'s second argument. - For the comparison and arithmetic methods, extraction errors are not propagated as exceptions, but lead to a return of `NotImplemented`. -- For some magic methods, the return values are not restricted by PyO3, but - checked by the Python interpreter. For example, `__str__` needs to return a - string object. This is indicated by `object (Python type)`. +- For some magic methods, the return values are not restricted by PyO3, but checked by the Python interpreter. + For example, `__str__` needs to return a string object. This is indicated by `object (Python type)`. ### Basic object customization @@ -181,10 +185,9 @@ impl MyIterator { } ``` -In many cases you'll have a distinction between the type being iterated over -(i.e. the *iterable*) and the iterator it provides. In this case, the iterable -only needs to implement `__iter__()` while the iterator must implement both -`__iter__()` and `__next__()`. For example: +In many cases you'll have a distinction between the type being iterated over (i.e. the *iterable*) and the iterator it provides. +In this case, the iterable only needs to implement `__iter__()` while the iterator must implement both `__iter__()` and `__next__()`. +For example: ```rust,no_run # use pyo3::prelude::*; @@ -233,10 +236,9 @@ 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. This is done by -raising a `StopIteration` exception. To express this in Rust, return `PyResult::Err` -with a `PyStopIteration` as the error. +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. +This is done by raising a `StopIteration` exception. +To express this in Rust, return `PyResult::Err` with a `PyStopIteration` as the error. ### Awaitable objects @@ -246,20 +248,28 @@ with a `PyStopIteration` as the error. ### Mapping & Sequence types -The magic methods in this section can be used to implement Python container types. They are two main categories of container in Python: "mappings" such as `dict`, with arbitrary keys, and "sequences" such as `list` and `tuple`, with integer keys. +The magic methods in this section can be used to implement Python container types. +They are two main categories of container in Python: "mappings" such as `dict`, with arbitrary keys, and "sequences" such as `list` and `tuple`, with integer keys. -The Python C-API which PyO3 is built upon has separate "slots" for sequences and mappings. When writing a `class` in pure Python, there is no such distinction in the implementation - a `__getitem__` implementation will fill the slots for both the mapping and sequence forms, for example. +The Python C-API which PyO3 is built upon has separate "slots" for sequences and mappings. +When writing a `class` in pure Python, there is no such distinction in the implementation - a `__getitem__` implementation will fill the slots for both the mapping and sequence forms, for example. -By default PyO3 reproduces the Python behaviour of filling both mapping and sequence slots. This makes sense for the "simple" case which matches Python, and also for sequences, where the mapping slot is used anyway to implement slice indexing. +By default PyO3 reproduces the Python behaviour of filling both mapping and sequence slots. +This makes sense for the "simple" case which matches Python, and also for sequences, where the mapping slot is used anyway to implement slice indexing. -Mapping types usually will not want the sequence slots filled. Having them filled will lead to outcomes which may be unwanted, such as: +Mapping types usually will not want the sequence slots filled. +Having them filled will lead to outcomes which may be unwanted, such as: -- The mapping type will successfully cast to [`PySequence`]. This may lead to consumers of the type handling it incorrectly. -- Python provides a default implementation of `__iter__` for sequences, which calls `__getitem__` with consecutive positive integers starting from 0 until an `IndexError` is returned. Unless the mapping only contains consecutive positive integer keys, this `__iter__` implementation will likely not be the intended behavior. +- The mapping type will successfully cast to [`PySequence`]. + This may lead to consumers of the type handling it incorrectly. +- Python provides a default implementation of `__iter__` for sequences, which calls `__getitem__` with consecutive positive integers starting from 0 until an `IndexError` is returned. + Unless the mapping only contains consecutive positive integer keys, this `__iter__` implementation will likely not be the intended behavior. -Use the `#[pyclass(mapping)]` annotation to instruct PyO3 to only fill the mapping slots, leaving the sequence ones empty. This will apply to `__getitem__`, `__setitem__`, and `__delitem__`. +Use the `#[pyclass(mapping)]` annotation to instruct PyO3 to only fill the mapping slots, leaving the sequence ones empty. +This will apply to `__getitem__`, `__setitem__`, and `__delitem__`. -Use the `#[pyclass(sequence)]` annotation to instruct PyO3 to fill the `sq_length` slot instead of the `mp_length` slot for `__len__`. This will help libraries such as `numpy` recognise the class as a sequence, however will also cause CPython to automatically add the sequence length to any negative indices before passing them to `__getitem__`. (`__getitem__`, `__setitem__` and `__delitem__` mapping slots are still used for sequences, for slice operations.) +Use the `#[pyclass(sequence)]` annotation to instruct PyO3 to fill the `sq_length` slot instead of the `mp_length` slot for `__len__`. +This will help libraries such as `numpy` recognise the class as a sequence, however will also cause CPython to automatically add the sequence length to any negative indices before passing them to `__getitem__`. (`__getitem__`, `__setitem__` and `__delitem__` mapping slots are still used for sequences, for slice operations.) - `__len__() -> usize` @@ -418,19 +428,13 @@ Coercions: ### Buffer objects - `__getbuffer__(, *mut ffi::Py_buffer, flags) -> ()` -- `__releasebuffer__(, *mut ffi::Py_buffer) -> ()` - Errors returned from `__releasebuffer__` will be sent to `sys.unraiseablehook`. It is strongly advised to never return an error from `__releasebuffer__`, and if it really is necessary, to make best effort to perform any required freeing operations before returning. `__releasebuffer__` will not be called a second time; anything not freed will be leaked. +- `__releasebuffer__(, *mut ffi::Py_buffer) -> ()` Errors returned from `__releasebuffer__` will be sent to `sys.unraiseablehook`. + It is strongly advised to never return an error from `__releasebuffer__`, and if it really is necessary, to make best effort to perform any required freeing operations before returning. `__releasebuffer__` will not be called a second time; anything not freed will be leaked. ### Garbage Collector Integration -If your type owns references to other Python objects, you will need to integrate -with Python's garbage collector so that the GC is aware of those references. To -do this, implement the two methods `__traverse__` and `__clear__`. These -correspond to the slots `tp_traverse` and `tp_clear` in the Python C API. -`__traverse__` must call `visit.call()` for each reference to another Python -object. `__clear__` must clear out any mutable references to other Python -objects (thus breaking reference cycles). Immutable references do not have to be -cleared, as every cycle must contain at least one mutable reference. +If your type owns references to other Python objects, you will need to integrate with Python's garbage collector so that the GC is aware of those references. To do this, implement the two methods `__traverse__` and `__clear__`. These correspond to the slots `tp_traverse` and `tp_clear` in the Python C API. `__traverse__` must call `visit.call()` for each reference to another Python object. `__clear__` must clear out any mutable references to other Python objects (thus breaking reference cycles). +Immutable references do not have to be cleared, as every cycle must contain at least one mutable reference. - `__traverse__(, pyo3::class::gc::PyVisit<'_>) -> Result<(), pyo3::class::gc::PyTraverseError>` - `__clear__() -> ()` diff --git a/guide/src/class/thread-safety.md b/guide/src/class/thread-safety.md index c9e8cd6391f..eb2b029617f 100644 --- a/guide/src/class/thread-safety.md +++ b/guide/src/class/thread-safety.md @@ -1,19 +1,24 @@ # `#[pyclass]` thread safety -Python objects are freely shared between threads by the Python interpreter. This means that: +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. +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. +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. +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: @@ -102,10 +107,15 @@ 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. Additionally, support for the `parking_lot` and `lock_api` synchronization libraries is gated behind the `parking_lot` and `lock_api` features. You can also enable the `arc_lock` feature if you need the `arc_lock` features of either library. +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. +Additionally, support for the `parking_lot` and `lock_api` synchronization libraries is gated behind the `parking_lot` and `lock_api` features. +You can also enable the `arc_lock` feature if you need the `arc_lock` features of either library. ### 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. +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. diff --git a/guide/src/conversions/tables.md b/guide/src/conversions/tables.md index 8e7c0b3dcdf..1cec2767447 100644 --- a/guide/src/conversions/tables.md +++ b/guide/src/conversions/tables.md @@ -61,24 +61,30 @@ For more detail on accepting `#[pyclass]` values as function arguments, see [the ### Using Rust library types vs Python-native types -Using Rust library types as function arguments will incur a conversion cost compared to using the Python-native types. Using the Python-native types is almost zero-cost (they just require a type check similar to the Python builtin function `isinstance()`). +Using Rust library types as function arguments will incur a conversion cost compared to using the Python-native types. +Using the Python-native types is almost zero-cost (they just require a type check similar to the Python builtin function `isinstance()`). However, once that conversion cost has been paid, the Rust standard library types offer a number of benefits: - You can write functionality in native-speed Rust code (free of Python's runtime costs). - You get better interoperability with the rest of the Rust ecosystem. - You can use `Python::detach` to detach from the interpreter and let other Python threads make progress while your Rust code is executing. -- You also benefit from stricter type checking. For example you can specify `Vec`, which will only accept a Python `list` containing integers. The Python-native equivalent, `&PyList`, would accept a Python `list` containing Python objects of any type. +- You also benefit from stricter type checking. + For example you can specify `Vec`, which will only accept a Python `list` containing integers. + The Python-native equivalent, `&PyList`, would accept a Python `list` containing Python objects of any type. -For most PyO3 usage the conversion cost is worth paying to get these benefits. As always, if you're not sure it's worth it in your case, benchmark it! +For most PyO3 usage the conversion cost is worth paying to get these benefits. +As always, if you're not sure it's worth it in your case, benchmark it! ## Returning Rust values to Python When returning values from functions callable from Python, [PyO3's smart pointers](../types.md#pyo3s-smart-pointers) (`Py`, `Bound<'py, T>`, and `Borrowed<'a, 'py, T>`) can be used with zero cost. -Because `Bound<'py, T>` and `Borrowed<'a, 'py, T>` have lifetime parameters, the Rust compiler may ask for lifetime annotations to be added to your function. See the [section of the guide dedicated to this](../types.md#function-argument-lifetimes). +Because `Bound<'py, T>` and `Borrowed<'a, 'py, T>` have lifetime parameters, the Rust compiler may ask for lifetime annotations to be added to your function. +See the [section of the guide dedicated to this](../types.md#function-argument-lifetimes). -If your function is fallible, it should return `PyResult` or `Result` where `E` implements `From for PyErr`. This will raise a `Python` exception if the `Err` variant is returned. +If your function is fallible, it should return `PyResult` or `Result` where `E` implements `From for PyErr`. +This will raise a `Python` exception if the `Err` variant is returned. Finally, the following Rust types are also able to convert to Python as return values: diff --git a/guide/src/conversions/traits.md b/guide/src/conversions/traits.md index 3aecc9d5cc2..d1cacf73591 100644 --- a/guide/src/conversions/traits.md +++ b/guide/src/conversions/traits.md @@ -34,10 +34,9 @@ and [`PyRefMut`]. They work like the reference wrappers of ### Deriving [`FromPyObject`] -[`FromPyObject`] can be automatically derived for many kinds of structs and enums -if the member types themselves implement `FromPyObject`. This even includes members -with a generic type `T: FromPyObject`. Derivation for empty enums, enum variants and -structs is not supported. +[`FromPyObject`] can be automatically derived for many kinds of structs and enums if the member types themselves implement `FromPyObject`. +This even includes members with a generic type `T: FromPyObject`. +Derivation for empty enums, enum variants and structs is not supported. ### Deriving [`FromPyObject`] for structs @@ -132,10 +131,8 @@ struct RustyStruct { # } ``` -This tries to extract `string_attr` from the attribute `name` and `string_in_mapping` -from a mapping with the key `"key"`. The arguments for `attribute` are restricted to -non-empty string literals while `item` can take any valid literal that implements -`ToBorrowedObject`. +This tries to extract `string_attr` from the attribute `name` and `string_in_mapping` from a mapping with the key `"key"`. +The arguments for `attribute` are restricted to non-empty string literals while `item` can take any valid literal that implements `ToBorrowedObject`. You can use `#[pyo3(from_item_all)]` on a struct to extract every field with `get_item` method. In this case, you can't use `#[pyo3(attribute)]` or barely use `#[pyo3(item)]` on any field. @@ -168,9 +165,8 @@ struct RustyStruct { ### Deriving [`FromPyObject`] for tuple structs -Tuple structs are also supported but do not allow customizing the extraction. The input is -always assumed to be a Python tuple with the same length as the Rust type, the `n`th field -is extracted from the `n`th item in the Python tuple. +Tuple structs are also supported but do not allow customizing the extraction. +The input is always assumed to be a Python tuple with the same length as the Rust type, the `n`th field is extracted from the `n`th item in the Python tuple. ```rust use pyo3::prelude::*; @@ -192,9 +188,8 @@ struct RustyTuple(String, String); # } ``` -Tuple structs with a single field are treated as wrapper types which are described in the -following section. To override this behaviour and ensure that the input is in fact a tuple, -specify the struct as +Tuple structs with a single field are treated as wrapper types which are described in the following section. +To override this behaviour and ensure that the input is in fact a tuple, specify the struct as ```rust use pyo3::prelude::*; @@ -217,10 +212,9 @@ struct RustyTuple((String,)); ### Deriving [`FromPyObject`] for wrapper types -The `pyo3(transparent)` attribute can be used on structs with exactly one field. This results -in extracting directly from the input object, i.e. `obj.extract()`, rather than trying to access -an item or attribute. This behaviour is enabled per default for newtype structs and tuple-variants -with a single field. +The `pyo3(transparent)` attribute can be used on structs with exactly one field. +This results in extracting directly from the input object, i.e. `obj.extract()`, rather than trying to access an item or attribute. +This behaviour is enabled per default for newtype structs and tuple-variants with a single field. ```rust use pyo3::prelude::*; @@ -252,14 +246,12 @@ struct RustyTransparentStruct { ### Deriving [`FromPyObject`] for enums -The `FromPyObject` derivation for enums generates code that tries to extract the variants in the -order of the fields. As soon as a variant can be extracted successfully, that variant is returned. +The `FromPyObject` derivation for enums generates code that tries to extract the variants in the order of the fields. +As soon as a variant can be extracted successfully, that variant is returned. This makes it possible to extract Python union types like `str | int`. -The same customizations and restrictions described for struct derivations apply to enum variants, -i.e. a tuple variant assumes that the input is a Python tuple, and a struct variant defaults to -extracting fields as attributes but can be configured in the same manner. The `transparent` -attribute can be applied to single-field-variants. +The same customizations and restrictions described for struct derivations apply to enum variants, i.e. a tuple variant assumes that the input is a Python tuple, and a struct variant defaults to extracting fields as attributes but can be configured in the same manner. +The `transparent` attribute can be applied to single-field-variants. ```rust use pyo3::prelude::*; @@ -405,10 +397,8 @@ enum RustyEnum<'py> { # } ``` -If none of the enum variants match, a `PyTypeError` containing the names of the -tested variants is returned. The names reported in the error message can be customized -through the `#[pyo3(annotation = "name")]` attribute, e.g. to use conventional Python type -names: +If none of the enum variants match, a `PyTypeError` containing the names of the tested variants is returned. +The names reported in the error message can be customized through the `#[pyo3(annotation = "name")]` attribute, e.g. to use conventional Python type names: ```rust use pyo3::prelude::*; @@ -537,7 +527,10 @@ struct RustyStruct { ### ⚠ Phase-Out of `FromPyObject` blanket implementation for cloneable PyClasses ⚠ -Historically PyO3 has provided a blanket implementation for `#[pyclass]` types that also implement `Clone`, to allow extraction of such types by value. Over time this has turned out problematic for a few reasons, the major one being the prevention of custom conversions by downstream crates if their type is `Clone`. Over the next few releases the blanket implementation is gradually phased out, and eventually replaced by an opt-in option. As a first step of this migration a new `skip_from_py_object` option for `#[pyclass]` was introduced, to opt-out of the blanket implementation and allow downstream users to provide their own implementation: +Historically PyO3 has provided a blanket implementation for `#[pyclass]` types that also implement `Clone`, to allow extraction of such types by value. +Over time this has turned out problematic for a few reasons, the major one being the prevention of custom conversions by downstream crates if their type is `Clone`. +Over the next few releases the blanket implementation is gradually phased out, and eventually replaced by an opt-in option. +As a first step of this migration a new `skip_from_py_object` option for `#[pyclass]` was introduced, to opt-out of the blanket implementation and allow downstream users to provide their own implementation: ```rust # #![allow(dead_code)] @@ -560,21 +553,24 @@ impl<'py> FromPyObject<'_, 'py> for Number { } ``` -As a second step the `from_py_object` option was introduced. This option also opts-out of the blanket implementation and instead generates a custom `FromPyObject` implementation for the pyclass which is functionally equivalent to the blanket. +As a second step the `from_py_object` option was introduced. +This option also opts-out of the blanket implementation and instead generates a custom `FromPyObject` implementation for the pyclass which is functionally equivalent to the blanket. ## `IntoPyObject` -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`. +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). +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. ### derive macro -`IntoPyObject` can be implemented using our derive macro. Both `struct`s and `enum`s are supported. +`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. @@ -635,8 +631,8 @@ 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. +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 @@ -700,7 +696,8 @@ impl<'a, 'py> IntoPyObject<'py> for &'a 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>`: +`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,no_run use pyo3::prelude::*; diff --git a/guide/src/debugging.md b/guide/src/debugging.md index 66c3c23a3b3..2b6b8d062f9 100644 --- a/guide/src/debugging.md +++ b/guide/src/debugging.md @@ -2,7 +2,8 @@ ## Macros -PyO3's attributes (`#[pyclass]`, `#[pymodule]`, etc.) are [procedural macros](https://doc.rust-lang.org/reference/procedural-macros.html), which means that they rewrite the source of the annotated item. You can view the generated source with the following command, which also expands a few other things: +PyO3's attributes (`#[pyclass]`, `#[pymodule]`, etc.) are [procedural macros](https://doc.rust-lang.org/reference/procedural-macros.html), which means that they rewrite the source of the annotated item. +You can view the generated source with the following command, which also expands a few other things: ```bash cargo rustc --profile=check -- -Z unstable-options --pretty=expanded > expanded.rs; rustfmt expanded.rs @@ -16,15 +17,18 @@ You can also debug classic `!`-macros by adding `-Z trace-macros`: cargo rustc --profile=check -- -Z unstable-options --pretty=expanded -Z trace-macros > expanded.rs; rustfmt expanded.rs ``` -Note that those commands require using the nightly build of rust and may occasionally have bugs. See [cargo expand](https://github.com/dtolnay/cargo-expand) for a more elaborate and stable version of those commands. +Note that those commands require using the nightly build of rust and may occasionally have bugs. +See [cargo expand](https://github.com/dtolnay/cargo-expand) for a more elaborate and stable version of those commands. ## Running with Valgrind Valgrind is a tool to detect memory management bugs such as memory leaks. -You first need to install a debug build of Python, otherwise Valgrind won't produce usable results. In Ubuntu there's e.g. a `python3-dbg` package. +You first need to install a debug build of Python, otherwise Valgrind won't produce usable results. +In Ubuntu there's e.g. a `python3-dbg` package. -Activate an environment with the debug interpreter and recompile. If you're on Linux, use `ldd` with the name of your binary and check that you're linking e.g. `libpython3.7d.so.1.0` instead of `libpython3.7.so.1.0`. +Activate an environment with the debug interpreter and recompile. +If you're on Linux, use `ldd` with the name of your binary and check that you're linking e.g. `libpython3.7d.so.1.0` instead of `libpython3.7.so.1.0`. [Download the suppressions file for CPython](https://raw.githubusercontent.com/python/cpython/master/Misc/valgrind-python.supp). @@ -32,7 +36,12 @@ Run Valgrind with `valgrind --suppressions=valgrind-python.supp ./my-command --w ## Getting a stacktrace -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. +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 ` @@ -48,7 +57,8 @@ The best start to investigate a crash such as an segmentation fault is a backtra ## 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. +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. @@ -64,7 +74,8 @@ For more information about how to use both `lldb` and `gdb` you can read the [gd 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. + > **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 @@ -198,7 +209,8 @@ VS Code with the Rust and Python extensions provides an integrated debugging exp 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 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). @@ -343,19 +355,12 @@ To use these functions: ## 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. +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. +[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 @@ -367,9 +372,8 @@ rustup override set nightly 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. +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 @@ -383,18 +387,9 @@ After activating a nightly Rust toolchain, you can build your project using 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. +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. diff --git a/guide/src/ecosystem/async-await.md b/guide/src/ecosystem/async-await.md index 7ce8da218fd..2e421428889 100644 --- a/guide/src/ecosystem/async-await.md +++ b/guide/src/ecosystem/async-await.md @@ -2,15 +2,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-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-async-runtimes` should work just fine with existing -Python libraries. +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-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-async-runtimes` should work just fine with existing Python libraries. ## Additional Information -- 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) +- 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/guide/src/ecosystem/logging.md b/guide/src/ecosystem/logging.md index 15ef2463d87..fa97c9bc7dd 100644 --- a/guide/src/ecosystem/logging.md +++ b/guide/src/ecosystem/logging.md @@ -3,17 +3,14 @@ It is desirable if both the Python and Rust parts of the application end up logging using the same configuration into the same place. -This section of the guide briefly discusses how to connect the two languages' -logging ecosystems together. The recommended way for Python extension modules is -to configure Rust's logger to send log messages to Python using the `pyo3-log` -crate. For users who want to do the opposite and send Python log messages to -Rust, see the note at the end of this guide. +This section of the guide briefly discusses how to connect the two languages' logging ecosystems together. +The recommended way for Python extension modules is to configure Rust's logger to send log messages to Python using the `pyo3-log` crate. +For users who want to do the opposite and send Python log messages to Rust, see the note at the end of this guide. ## Using `pyo3-log` to send Rust log messages to Python -The [pyo3-log] crate allows sending the messages from the Rust side to Python's -[logging] system. This is mostly suitable for writing native extensions for -Python programs. +The [pyo3-log] crate allows sending the messages from the Rust side to Python's [logging] system. +This is mostly suitable for writing native extensions for Python programs. Use [`pyo3_log::init`][init] to install the logger in its default configuration. It's also possible to tweak its configuration (mostly to tune its performance). @@ -51,9 +48,8 @@ logging.getLogger().setLevel(logging.INFO) my_module.log_something() ``` -It is important to initialize the Python loggers first, before calling any Rust -functions that may log. This limitation can be worked around if it is not -possible to satisfy, read the documentation about [caching]. +It is important to initialize the Python loggers first, before calling any Rust functions that may log. +This limitation can be worked around if it is not possible to satisfy, read the documentation about [caching]. ## The Python to Rust direction diff --git a/guide/src/ecosystem/tracing.md b/guide/src/ecosystem/tracing.md index 22b0aaab049..34beb03ed05 100644 --- a/guide/src/ecosystem/tracing.md +++ b/guide/src/ecosystem/tracing.md @@ -5,16 +5,13 @@ 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. +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: +[`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 @@ -54,11 +51,8 @@ pub fn initialize_tracing(py_impl: Bound<'_, PyAny>) { } ``` -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 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: diff --git a/guide/src/exception.md b/guide/src/exception.md index c63974d9624..bc7be1f65c7 100644 --- a/guide/src/exception.md +++ b/guide/src/exception.md @@ -58,7 +58,8 @@ mod mymodule { ## Raising an exception -As described in the [function error handling](./function/error-handling.md) chapter, to raise an exception from a `#[pyfunction]` or `#[pymethods]`, return an `Err(PyErr)`. PyO3 will automatically raise this exception for you when returning the result to Python. +As described in the [function error handling](./function/error-handling.md) chapter, to raise an exception from a `#[pyfunction]` or `#[pymethods]`, return an `Err(PyErr)`. +PyO3 will automatically raise this exception for you when returning the result to Python. You can also manually write and fetch errors in the Python interpreter's global state: diff --git a/guide/src/faq.md b/guide/src/faq.md index e5c39d81fe1..4fdb7f2b78c 100644 --- a/guide/src/faq.md +++ b/guide/src/faq.md @@ -1,10 +1,12 @@ # Frequently Asked Questions and troubleshooting -Sorry that you're having trouble using PyO3. If you can't find the answer to your problem in the list below, you can also reach out for help on [GitHub Discussions](https://github.com/PyO3/pyo3/discussions) and on [Discord](https://discord.gg/33kcChzH7f). +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 `std::sync::OnceLock`, `std::sync::LazyLock`, `lazy_static`, and `once_cell` -`OnceLock`, `LazyLock`, and their thirdparty predecessors use blocking to ensure only one thread ever initializes them. Because the Python interpreter can introduce additional locks (the Python GIL and GC can both require all other threads to pause) 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 interpreter can introduce additional locks (the Python GIL and GC can both require all other threads to pause) this can lead to deadlocks in the following way: 1. A thread (thread A) which is attached to the Python interpreter starts initialization of a `OnceLock` value. 2. The initialization code calls some Python API which temporarily detaches from the interpreter e.g. `Python::import`. @@ -13,7 +15,10 @@ Sorry that you're having trouble using PyO3. If you can't find the answer to you 5. On non-free-threaded Python, thread A is now also blocked, because it waits to re-attach to the interpreter (by taking the GIL which thread B still holds). 6. Deadlock. -PyO3 provides a struct [`PyOnceLock`] which implements a single-initialization API based on these types that avoids deadlocks. You can also make use of the [`OnceExt`] and [`OnceLockExt`] extension traits that enable using the standard library types for this purpose by providing new methods for these types that avoid the risk of deadlocking with the Python interpreter. This means they can be used in place of other choices when you are experiencing the deadlock described above. See the documentation for [`PyOnceLock`] and [`OnceExt`] for further details and an example how to use them. +PyO3 provides a struct [`PyOnceLock`] which implements a single-initialization API based on these types that avoids deadlocks. +You can also make use of the [`OnceExt`] and [`OnceLockExt`] extension traits that enable using the standard library types for this purpose by providing new methods for these types that avoid the risk of deadlocking with the Python interpreter. +This means they can be used in place of other choices when you are experiencing the deadlock described above. +See the documentation for [`PyOnceLock`] and [`OnceExt`] for further details and an example how to use them. [`PyOnceLock`]: {{#PYO3_DOCS_URL}}/pyo3/sync/struct.PyOnceLock.html [`OnceExt`]: {{#PYO3_DOCS_URL}}/pyo3/sync/trait.OnceExt.html @@ -21,9 +26,12 @@ PyO3 provides a struct [`PyOnceLock`] which implements a single-initialization A ## 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" -Currently, [#340](https://github.com/PyO3/pyo3/issues/340) causes `cargo test` to fail with linking errors when the `extension-module` feature is activated. Linking errors can also happen when building in a cargo workspace where a different crate also uses PyO3 (see [#2521](https://github.com/PyO3/pyo3/issues/2521)). For now, there are three ways we can work around these issues. +Currently, [#340](https://github.com/PyO3/pyo3/issues/340) causes `cargo test` to fail with linking errors when the `extension-module` feature is activated. +Linking errors can also happen when building in a cargo workspace where a different crate also uses PyO3 (see [#2521](https://github.com/PyO3/pyo3/issues/2521)). +For now, there are three ways we can work around these issues. -1. Make the `extension-module` feature optional. Build with `maturin develop --features "extension-module"` +1. Make the `extension-module` feature optional. + Build with `maturin develop --features "extension-module"` ```toml [dependencies.pyo3] @@ -33,7 +41,8 @@ Currently, [#340](https://github.com/PyO3/pyo3/issues/340) causes `cargo test` t extension-module = ["pyo3/extension-module"] ``` -2. Make the `extension-module` feature optional and default. Run tests with `cargo test --no-default-features`: +2. Make the `extension-module` feature optional and default. + Run tests with `cargo test --no-default-features`: ```toml [dependencies.pyo3] @@ -78,9 +87,11 @@ crate-type = ["cdylib", "rlib"] ## Ctrl-C doesn't do anything while my Rust code is executing -This is because Ctrl-C raises a SIGINT signal, which is handled by the calling Python process by simply setting a flag to action upon later. This flag isn't checked while Rust code called from Python is executing, only once control returns to the Python interpreter. +This is because Ctrl-C raises a SIGINT signal, which is handled by the calling Python process by simply setting a flag to action upon later. +This flag isn't checked while Rust code called from Python is executing, only once control returns to the Python interpreter. -You can give the Python interpreter a chance to process the signal properly by calling `Python::check_signals`. It's good practice to call this function regularly if you have a long-running Rust function so that your users can cancel it. +You can give the Python interpreter a chance to process the signal properly by calling `Python::check_signals`. +It's good practice to call this function regularly if you have a long-running Rust function so that your users can cancel it. ## `#[pyo3(get)]` clones my field @@ -123,7 +134,9 @@ AssertionError: a: b: ``` -This can be especially confusing if the field is mutable, as getting the field and then mutating it won't persist - you'll just get a fresh clone of the original on the next access. Unfortunately Python and Rust don't agree about ownership - if PyO3 gave out references to (possibly) temporary Rust objects to Python code, Python code could then keep that reference alive indefinitely. Therefore returning Rust objects requires cloning. +This can be especially confusing if the field is mutable, as getting the field and then mutating it won't persist - you'll just get a fresh clone of the original on the next access. +Unfortunately Python and Rust don't agree about ownership - if PyO3 gave out references to (possibly) temporary Rust objects to Python code, Python code could then keep that reference alive indefinitely. +Therefore returning Rust objects requires cloning. If you don't want that cloning to happen, a workaround is to allocate the field on the Python heap and store a reference to that, by using [`Py<...>`]({{#PYO3_DOCS_URL}}/pyo3/struct.Py.html): @@ -196,18 +209,21 @@ struct MyClass; ## I'm trying to call Python from Rust but I get `STATUS_DLL_NOT_FOUND` or `STATUS_ENTRYPOINT_NOT_FOUND` -This happens on Windows when linking to the python DLL fails or the wrong one is linked. The Python DLL on Windows will usually be called something like: +This happens on Windows when linking to the python DLL fails or the wrong one is linked. +The Python DLL on Windows will usually be called something like: - `python3X.dll` for Python 3.X, e.g. `python310.dll` for Python 3.10 - `python3.dll` when using PyO3's `abi3` feature -The DLL needs to be locatable using the [Windows DLL search order](https://learn.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-search-order#standard-search-order-for-unpackaged-apps). Some ways to achieve this are: +The DLL needs to be locatable using the [Windows DLL search order](https://learn.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-search-order#standard-search-order-for-unpackaged-apps). +Some ways to achieve this are: - Put the Python DLL in the same folder as your build artifacts - Add the directory containing the Python DLL to your `PATH` environment variable, for example `C:\Users\\AppData\Local\Programs\Python\Python310` - If this happens when you are *distributing* your program, consider using [PyOxidizer](https://github.com/indygreg/PyOxidizer) to package it with your binary. -If the wrong DLL is linked it is possible that this happened because another program added itself and its own Python DLLs to `PATH`. Rearrange your `PATH` variables to give the correct DLL priority. +If the wrong DLL is linked it is possible that this happened because another program added itself and its own Python DLLs to `PATH`. +Rearrange your `PATH` variables to give the correct DLL priority. > **Note**: Changes to `PATH` (or any other environment variable) are not visible to existing shells. Restart it for changes to take effect. diff --git a/guide/src/features.md b/guide/src/features.md index 3d9314a954d..44172b258aa 100644 --- a/guide/src/features.md +++ b/guide/src/features.md @@ -1,6 +1,7 @@ # Features reference -PyO3 provides a number of Cargo features to customize functionality. This chapter of the guide provides detail on each of them. +PyO3 provides a number of Cargo features to customize functionality. +This chapter of the guide provides detail on each of them. By default, only the `macros` feature is enabled. @@ -55,7 +56,8 @@ If you do not enable this feature, you should call `Python::initialize()` before 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. +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-inspect` @@ -63,15 +65,19 @@ This feature adds to the built binaries introspection data that can be then retr Also, this feature adds the `pyo3::inspect` module, as well as `IntoPy::type_output` and `FromPyObject::type_input` APIs to produce Python type "annotations" for Rust types. -This is a first step towards adding first-class support for generating type annotations automatically in PyO3, however work is needed to finish this off. All feedback and offers of help welcome on [issue #2454](https://github.com/PyO3/pyo3/issues/2454). +This is a first step towards adding first-class support for generating type annotations automatically in PyO3, however work is needed to finish this off. +All feedback and offers of help welcome on [issue #2454](https://github.com/PyO3/pyo3/issues/2454). ### `py-clone` -This feature was introduced to ease migration. It was found that delayed reference counting (which PyO3 used historically) could not be made sound and hence `Clone`-ing an instance of `Py` is impossible when not attached to Python interpreter (it will panic). To avoid migrations introducing new panics without warning, the `Clone` implementation itself is now gated behind this feature. +This feature was introduced to ease migration. +It was found that delayed reference counting (which PyO3 used historically) could not be made sound and hence `Clone`-ing an instance of `Py` is impossible when not attached to Python interpreter (it will panic). +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 associated overhead for the crossing the Python-Rust boundary. However, if enabled, `Drop`ping an instance of `Py` when not attached to the Python interpreter will abort the process. +This is a performance-oriented conditional compilation flag, e.g. [set via `$RUSTFLAGS`][set-configuration-options], which disabled the global reference pool and the associated overhead for the crossing the Python-Rust boundary. +However, if enabled, `Drop`ping an instance of `Py` when not attached to the Python interpreter will abort the process. ### `macros` @@ -85,7 +91,8 @@ This feature enables a dependency on the `pyo3-macros` crate, which provides the It also provides the `py_run!` macro. -These macros require a number of dependencies which may not be needed by users who just need PyO3 for Python FFI. Disabling this feature enables faster builds for those users, as these dependencies will not be built if this feature is disabled. +These macros require a number of dependencies which may not be needed by users who just need PyO3 for Python FFI. +Disabling this feature enables faster builds for those users, as these dependencies will not be built if this feature is disabled. > This feature is enabled by default. To disable it, set `default-features = false` for the `pyo3` entry in your Cargo.toml. @@ -93,19 +100,22 @@ These macros require a number of dependencies which may not be needed by users w 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`, 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. +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. ### `nightly` -The `nightly` feature needs the nightly Rust compiler. This allows PyO3 to use the `auto_traits` and `negative_impls` features to fix the `Python::detach` function. +The `nightly` feature needs the nightly Rust compiler. +This allows PyO3 to use the `auto_traits` and `negative_impls` features to fix the `Python::detach` function. ### `resolve-config` -The `resolve-config` feature of the `pyo3-build-config` crate controls whether that crate's -build script automatically resolves a Python interpreter / build configuration. This feature is primarily useful when building PyO3 -itself. By default this feature is not enabled, meaning you can freely use `pyo3-build-config` as a standalone library to read or write PyO3 build configuration files or resolve metadata about a Python interpreter. +The `resolve-config` feature of the `pyo3-build-config` crate controls whether that crate's build script automatically resolves a Python interpreter / build configuration. +This feature is primarily useful when building PyO3 itself. +By default this feature is not enabled, meaning you can freely use `pyo3-build-config` as a standalone library to read or write PyO3 build configuration files or resolve metadata about a Python interpreter. ## Optional Dependencies @@ -113,11 +123,13 @@ These features enable conversions between Python types and types from other Rust ### `anyhow` -Adds a dependency on [anyhow](https://docs.rs/anyhow). Enables a conversion from [anyhow](https://docs.rs/anyhow)’s [`Error`](https://docs.rs/anyhow/latest/anyhow/struct.Error.html) type to [`PyErr`]({{#PYO3_DOCS_URL}}/pyo3/struct.PyErr.html), for easy error handling. +Adds a dependency on [anyhow](https://docs.rs/anyhow). +Enables a conversion from [anyhow](https://docs.rs/anyhow)’s [`Error`](https://docs.rs/anyhow/latest/anyhow/struct.Error.html) type to [`PyErr`]({{#PYO3_DOCS_URL}}/pyo3/struct.PyErr.html), for easy error handling. ### `arc_lock` -Enables Pyo3's `MutexExt` trait for all Mutexes that extend on [`lock_api::Mutex`](https://docs.rs/lock_api/latest/lock_api/struct.Mutex.html) or [`parking_lot::ReentrantMutex`](https://docs.rs/lock_api/latest/lock_api/struct.ReentrantMutex.html) and are wrapped in an [`Arc`](https://doc.rust-lang.org/std/sync/struct.Arc.html) type. Like [`Arc`](https://docs.rs/parking_lot/latest/parking_lot/type.Mutex.html#method.lock_arc) +Enables Pyo3's `MutexExt` trait for all Mutexes that extend on [`lock_api::Mutex`](https://docs.rs/lock_api/latest/lock_api/struct.Mutex.html) or [`parking_lot::ReentrantMutex`](https://docs.rs/lock_api/latest/lock_api/struct.ReentrantMutex.html) and are wrapped in an [`Arc`](https://doc.rust-lang.org/std/sync/struct.Arc.html) type. +Like [`Arc`](https://docs.rs/parking_lot/latest/parking_lot/type.Mutex.html#method.lock_arc) ### `bigdecimal` @@ -129,7 +141,8 @@ Adds a dependency on [bytes](https://docs.rs/bytes/latest/bytes) and enables con ### `chrono` -Adds a dependency on [chrono](https://docs.rs/chrono). Enables a conversion from [chrono](https://docs.rs/chrono)'s types to python: +Adds a dependency on [chrono](https://docs.rs/chrono). +Enables a conversion from [chrono](https://docs.rs/chrono)'s types to python: - [TimeDelta](https://docs.rs/chrono/latest/chrono/struct.TimeDelta.html) -> [`PyDelta`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyDelta.html) - [FixedOffset](https://docs.rs/chrono/latest/chrono/offset/struct.FixedOffset.html) -> [`PyDelta`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyDelta.html) @@ -140,7 +153,8 @@ Adds a dependency on [chrono](https://docs.rs/chrono). Enables a conversion from ### `chrono-local` -Enables conversion from and to [Local](https://docs.rs/chrono/latest/chrono/struct.Local.html) timezones. The current system timezone as determined by [`iana_time_zone::get_timezone()`](https://docs.rs/iana-time-zone/latest/iana_time_zone/fn.get_timezone.html) will be used for conversions. +Enables conversion from and to [Local](https://docs.rs/chrono/latest/chrono/struct.Local.html) timezones. +The current system timezone as determined by [`iana_time_zone::get_timezone()`](https://docs.rs/iana-time-zone/latest/iana_time_zone/fn.get_timezone.html) will be used for conversions. `chrono::DateTime` will convert from either of: @@ -156,11 +170,13 @@ Enables conversion from and to [`Tz`](https://docs.rs/chrono-tz/latest/chrono_tz ### `either` -Adds a dependency on [either](https://docs.rs/either). Enables a conversions into [either](https://docs.rs/either)’s [`Either`](https://docs.rs/either/latest/either/enum.Either.html) type. +Adds a dependency on [either](https://docs.rs/either). +Enables a conversions into [either](https://docs.rs/either)’s [`Either`](https://docs.rs/either/latest/either/enum.Either.html) type. ### `eyre` -Adds a dependency on [eyre](https://docs.rs/eyre). Enables a conversion from [eyre](https://docs.rs/eyre)’s [`Report`](https://docs.rs/eyre/latest/eyre/struct.Report.html) type to [`PyErr`]({{#PYO3_DOCS_URL}}/pyo3/struct.PyErr.html), for easy error handling. +Adds a dependency on [eyre](https://docs.rs/eyre). +Enables a conversion from [eyre](https://docs.rs/eyre)’s [`Report`](https://docs.rs/eyre/latest/eyre/struct.Report.html) type to [`PyErr`]({{#PYO3_DOCS_URL}}/pyo3/struct.PyErr.html), for easy error handling. ### `hashbrown` @@ -172,7 +188,8 @@ Adds a dependency on [indexmap](https://docs.rs/indexmap) and enables conversion ### `jiff-02` -Adds a dependency on [jiff@0.2](https://docs.rs/jiff/0.2). Enables a conversion from [jiff](https://docs.rs/jiff)'s types to python: +Adds a dependency on [jiff@0.2](https://docs.rs/jiff/0.2). +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) @@ -217,7 +234,8 @@ Adds a dependency on [rust_decimal](https://docs.rs/rust_decimal) and enables co ### `time` -Adds a dependency on [time](https://docs.rs/time). Enables conversions between [time](https://docs.rs/time)'s types and Python: +Adds a dependency on [time](https://docs.rs/time). +Enables conversions between [time](https://docs.rs/time)'s types and Python: - [Date](https://docs.rs/time/0.3.38/time/struct.Date.html) -> [`PyDate`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyDate.html) - [Time](https://docs.rs/time/0.3.38/time/struct.Time.html) -> [`PyTime`]({{#PYO3_DOCS_URL}}/pyo3/types/struct.PyTime.html) diff --git a/guide/src/free-threading.md b/guide/src/free-threading.md index e145a273946..5222ff2202b 100644 --- a/guide/src/free-threading.md +++ b/guide/src/free-threading.md @@ -1,10 +1,7 @@ # Supporting Free-Threaded CPython -CPython 3.14 declared support for the "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. Since version 0.23, PyO3 -supports building Rust extensions for the free-threaded Python build and -calling into free-threaded Python from Rust. +CPython 3.14 declared support for the "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. +Since version 0.23, PyO3 supports building Rust extensions for the free-threaded Python build and 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/whatsnew/3.13.html#whatsnew313-free-threaded-cpython) @@ -17,57 +14,32 @@ in the community-maintained Python free-threading guide, 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 only choice before the "free-threaded" build was introduced), -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`](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`](https://doc.rust-lang.org/nomicon/send-and-sync.html) traits. +In the GIL-enabled build (the only choice before the "free-threaded" build was introduced), 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`](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`](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. ## 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)]`. +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)]`. 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 -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 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). +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 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 @@ -99,9 +71,8 @@ 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. +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) @@ -110,44 +81,27 @@ 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. +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. Many existing extensions -providing mutable data structures relied on the GIL to lock Python objects and -make interior mutability thread-safe. - -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 analogous to releasing and acquiring the GIL in the GIL-enabled build. - -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 free-threaded interpreter does not have a GIL. +Many existing extensions providing mutable data structures relied on the GIL to lock Python objects and make interior mutability thread-safe. + +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 analogous to releasing and acquiring the GIL in the GIL-enabled build. + +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. ### Attaching to the runtime -You still need to obtain a `'py` lifetime 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::attach`] -function. Threads created via the Python [`threading`] module do not need to -do this, and pyo3 will handle setting up the [`Python<'py>`] token when CPython -calls into your extension. +You still need to obtain a `'py` lifetime 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::attach`] function. +Threads created via the Python [`threading`] module do not need to do this, and pyo3 will handle setting up the [`Python<'py>`] token when CPython calls into your extension. ### Detaching to avoid hangs and deadlocks @@ -165,34 +119,17 @@ situations: 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::detach`] 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. +This means that you should detach from the interpreter runtime using [`Python::detach`] 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 -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 [`Python::detach`] 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 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) in multiple threads. For -example, consider the following implementation: +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 [`Python::detach`] 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 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) in multiple threads. +For example, consider the following implementation: ```rust,no_run # use pyo3::prelude::*; @@ -241,19 +178,13 @@ Traceback (most recent call last) RuntimeError: Already borrowed ``` -We may allow user-selectable semantics for mutable pyclass definitions in a -future version of PyO3, 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 the GIL. +We may allow user-selectable semantics for mutable pyclass definitions in a future version of PyO3, 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 the GIL. ### 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. +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 upload only one wheel for each release of your @@ -266,14 +197,9 @@ the free-threaded build. ### Thread-safe single initialization -To initialize data exactly once, use the [`PyOnceLock`] type, which is a close equivalent -to [`std::sync::OnceLock`][`OnceLock`] that also helps avoid deadlocks by detaching from -the Python interpreter when threads are blocking waiting for another thread to -complete initialization. If already using [`OnceLock`] and it is impractical -to replace with a [`PyOnceLock`], there is the [`OnceLockExt`] extension trait -which adds [`OnceLockExt::get_or_init_py_attached`] to detach from the interpreter -when blocking in the same fashion as [`PyOnceLock`]. Here is an example using -[`PyOnceLock`] to single-initialize a runtime cache holding a `Py`: +To initialize data exactly once, use the [`PyOnceLock`] type, which is a close equivalent to [`std::sync::OnceLock`][`OnceLock`] that also helps avoid deadlocks by detaching from the Python interpreter when threads are blocking waiting for another thread to complete initialization. +If already using [`OnceLock`] and it is impractical to replace with a [`PyOnceLock`], there is the [`OnceLockExt`] extension trait which adds [`OnceLockExt::get_or_init_py_attached`] to detach from the interpreter when blocking in the same fashion as [`PyOnceLock`]. +Here is an example using [`PyOnceLock`] to single-initialize a runtime cache holding a `Py`: ```rust # use pyo3::prelude::*; @@ -288,16 +214,11 @@ Python::attach(|py| { }); ``` -In cases where a function must run exactly once, you can bring -the [`OnceExt`] trait 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 thread is attached to the Python interpreter. These functions are analogous to -[`Once::call_once`], [`Once::call_once_force`] except they accept a [`Python<'py>`] -token in addition to an `FnOnce`. All of these functions detach from the -interpreter before blocking and re-attach before executing the function, -avoiding deadlocks that are possible without using the PyO3 -extension traits. Here the same example as above built using a [`Once`] instead of a +In cases where a function must run exactly once, you can bring the [`OnceExt`] trait 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 thread is attached to the Python interpreter. +These functions are analogous to [`Once::call_once`], [`Once::call_once_force`] except they accept a [`Python<'py>`] token in addition to an `FnOnce`. +All of these functions detach from the interpreter before blocking and re-attach before executing the function, avoiding deadlocks that are possible without using the PyO3 extension traits. +Here the same example as above built using a [`Once`] instead of a [`PyOnceLock`]: ```rust @@ -326,13 +247,9 @@ Python::attach(|py| { ### `GILProtected` is not exposed -[`GILProtected`] is a (deprecated) 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`](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. +[`GILProtected`] is a (deprecated) 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`](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: @@ -378,11 +295,8 @@ Python::attach(|py| { # } ``` -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. +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. [`GILProtected`]: https://docs.rs/pyo3/0.22/pyo3/sync/struct.GILProtected.html [`MutexExt`]: {{#PYO3_DOCS_URL}}/pyo3/sync/trait.MutexExt.html diff --git a/guide/src/function.md b/guide/src/function.md index 60e23781393..3eb13058c57 100644 --- a/guide/src/function.md +++ b/guide/src/function.md @@ -1,6 +1,7 @@ # Python functions -The `#[pyfunction]` attribute is used to define a Python function from a Rust function. Once defined, the function needs to be added to a [module](./module.md). +The `#[pyfunction]` attribute is used to define a Python function from a Rust function. +Once defined, the function needs to be added to a [module](./module.md). The following example defines a function called `double` in a Python module called `my_extension`: @@ -16,7 +17,8 @@ mod my_extension { } ``` -This chapter of the guide explains full usage of the `#[pyfunction]` attribute. In this first section, the following topics are covered: +This chapter of the guide explains full usage of the `#[pyfunction]` attribute. +In this first section, the following topics are covered: - [Function options](#function-options) - [`#[pyo3(name = "...")]`](#name) @@ -34,7 +36,8 @@ There are also additional sections on the following topics: ## Function options -The `#[pyo3]` attribute can be used to modify properties of the generated Python function. It can take any combination of the following options: +The `#[pyo3]` attribute can be used to modify properties of the generated Python function. +It can take any combination of the following options: - `#[pyo3(name = "...")]` @@ -200,7 +203,8 @@ The `#[pyo3]` attribute can be used to modify properties of the generated Python ## Per-argument options -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: +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 = ...)]` @@ -234,11 +238,9 @@ You can pass Python `def`'d functions and built-in functions to Rust functions [ corresponds to regular Python functions while [`PyCFunction`] describes built-ins such as `repr()`. -You can also use [`Bound<'_, PyAny>::is_callable`] to check if you have a callable object. `is_callable` -will return `true` for functions (including lambdas), methods and objects with a `__call__` method. -You can call the object with [`Bound<'_, PyAny>::call`] with the args as first parameter and the kwargs -(or `None`) as second parameter. There are also [`Bound<'_, PyAny>::call0`] with no args and -[`Bound<'_, PyAny>::call1`] with only positional args. +You can also use [`Bound<'_, PyAny>::is_callable`] to check if you have a callable object. `is_callable` will return `true` for functions (including lambdas), methods and objects with a `__call__` method. +You can call the object with [`Bound<'_, PyAny>::call`] with the args as first parameter and the kwargs (or `None`) as second parameter. +There are also [`Bound<'_, PyAny>::call0`] with no args and [`Bound<'_, PyAny>::call1`] with only positional args. ### Calling Rust functions in Python @@ -251,11 +253,8 @@ The ways to convert a Rust function into a Python object vary depending on the f ### Accessing the FFI functions -In order to make Rust functions callable from Python, PyO3 generates an `extern "C"` -function whose exact signature depends on the Rust signature. (PyO3 chooses the optimal -Python argument passing convention.) It then embeds the call to the Rust function inside this -FFI-wrapper function. This wrapper handles extraction of the regular arguments and the keyword -arguments from the input `PyObject`s. +In order to make Rust functions callable from Python, PyO3 generates an `extern "C"` function whose exact signature depends on the Rust signature. (PyO3 chooses the optimal Python argument passing convention.) It then embeds the call to the Rust function inside this FFI-wrapper function. +This wrapper handles extraction of the regular arguments and the keyword arguments from the input `PyObject`s. The `wrap_pyfunction` macro can be used to directly get a `Bound` given a `#[pyfunction]` and a `Bound`: `wrap_pyfunction!(rust_fun, module)`. diff --git a/guide/src/function/error-handling.md b/guide/src/function/error-handling.md index cb0eb177d61..5db344bb6c1 100644 --- a/guide/src/function/error-handling.md +++ b/guide/src/function/error-handling.md @@ -8,9 +8,11 @@ There is a later section of the guide on [Python exceptions](../exception.md) wh ## Representing Python exceptions -Rust code uses the generic [`Result`] enum to propagate errors. The error type `E` is chosen by the code author to describe the possible errors which can happen. +Rust code uses the generic [`Result`] enum to propagate errors. +The error type `E` is chosen by the code author to describe the possible errors which can happen. -PyO3 has the [`PyErr`] type which represents a Python exception. If a PyO3 API could result in a Python exception being raised, the return type of that `API` will be [`PyResult`], which is an alias for the type `Result`. +PyO3 has the [`PyErr`] type which represents a Python exception. +If a PyO3 API could result in a Python exception being raised, the return type of that `API` will be [`PyResult`], which is an alias for the type `Result`. In summary: @@ -24,7 +26,8 @@ In summary: As indicated in the previous section, when a `PyResult` containing an `Err` crosses from Rust to Python, PyO3 will raise the exception contained within. -Accordingly, to raise an exception from a `#[pyfunction]`, change the return type `T` to `PyResult`. When the function returns an `Err` it will raise a Python exception. (Other `Result` types can be used as long as the error `E` has a `From` conversion for `PyErr`, see [custom Rust error types](#custom-rust-error-types) below.) +Accordingly, to raise an exception from a `#[pyfunction]`, change the return type `T` to `PyResult`. +When the function returns an `Err` it will raise a Python exception. (Other `Result` types can be used as long as the error `E` has a `From` conversion for `PyErr`, see [custom Rust error types](#custom-rust-error-types) below.) This also works for functions in `#[pymethods]`. @@ -52,11 +55,13 @@ fn check_positive(x: i32) -> PyResult<()> { # } ``` -All built-in Python exception types are defined in the [`pyo3::exceptions`] module. They have a `new_err` constructor to directly build a `PyErr`, as seen in the example above. +All built-in Python exception types are defined in the [`pyo3::exceptions`] module. +They have a `new_err` constructor to directly build a `PyErr`, as seen in the example above. ## Custom Rust error types -PyO3 will automatically convert a `Result` returned by a `#[pyfunction]` into a `PyResult` as long as there is an implementation of `std::from::From for PyErr`. Many error types in the Rust standard library have a [`From`] conversion defined in this way. +PyO3 will automatically convert a `Result` returned by a `#[pyfunction]` into a `PyResult` as long as there is an implementation of `std::from::From for PyErr`. +Many error types in the Rust standard library have a [`From`] conversion defined in this way. If the type `E` you are handling is defined in a third-party crate, see the section on [foreign rust error types](#foreign-rust-error-types) below for ways to work with this error. @@ -89,7 +94,8 @@ Traceback (most recent call last): ValueError: invalid digit found in string ``` -As a more complete example, the following snippet defines a Rust error named `CustomIOError`. It then defines a `From for PyErr`, which returns a `PyErr` representing Python's `OSError`. +As a more complete example, the following snippet defines a Rust error named `CustomIOError`. +It then defines a `From for PyErr`, which returns a `PyErr` representing Python's `OSError`. Therefore, it can use this error in the result of a `#[pyfunction]` directly, relying on the conversion if it has to be propagated into a Python exception. ```rust @@ -140,13 +146,11 @@ fn main() { } ``` -If lazy construction of the Python exception instance is desired, the -[`PyErrArguments`]({{#PYO3_DOCS_URL}}/pyo3/trait.PyErrArguments.html) -trait can be implemented instead of `From`. In that case, actual exception argument creation is delayed -until the `PyErr` is needed. +If lazy construction of the Python exception instance is desired, the [`PyErrArguments`]({{#PYO3_DOCS_URL}}/pyo3/trait.PyErrArguments.html) trait can be implemented instead of `From`. +In that case, actual exception argument creation is delayed until the `PyErr` is needed. -A final note is that any errors `E` which have a `From` conversion can be used with the `?` -("try") operator with them. An alternative implementation of the above `parse_int` which instead returns `PyResult` is below: +A final note is that any errors `E` which have a `From` conversion can be used with the `?` ("try") operator with them. +An alternative implementation of the above `parse_int` which instead returns `PyResult` is below: ```rust use pyo3::prelude::*; @@ -182,10 +186,13 @@ The Rust compiler will not permit implementation of traits for types outside of Given a type `OtherError` which is defined in third-party code, there are two main strategies available to integrate it with PyO3: -- Create a newtype wrapper, e.g. `MyOtherError`. Then implement `From for PyErr` (or `PyErrArguments`), as well as `From` for `MyOtherError`. -- Use Rust's Result combinators such as `map_err` to write code freely to convert `OtherError` into whatever is needed. This requires boilerplate at every usage however gives unlimited flexibility. +- Create a newtype wrapper, e.g. `MyOtherError`. + Then implement `From for PyErr` (or `PyErrArguments`), as well as `From` for `MyOtherError`. +- Use Rust's Result combinators such as `map_err` to write code freely to convert `OtherError` into whatever is needed. + This requires boilerplate at every usage however gives unlimited flexibility. -To detail the newtype strategy a little further, the key trick is to return `Result` from the `#[pyfunction]`. This means that PyO3 will make use of `From for PyErr` to create Python exceptions while the `#[pyfunction]` implementation can use `?` to convert `OtherError` to `MyOtherError` automatically. +To detail the newtype strategy a little further, the key trick is to return `Result` from the `#[pyfunction]`. +This means that PyO3 will make use of `From for PyErr` to create Python exceptions while the `#[pyfunction]` implementation can use `?` to convert `OtherError` to `MyOtherError` automatically. The following example demonstrates this for some imaginary third-party crate `some_crate` with a function `get_x` returning `Result`: @@ -234,7 +241,8 @@ fn wrapped_get_x() -> Result { ## Notes -In Python 3.11 and up, notes can be added to Python exceptions to provide additional debugging information when printing the exception. In PyO3, you can use the `add_note` method on `PyErr` to accomplish this functionality. +In Python 3.11 and up, notes can be added to Python exceptions to provide additional debugging information when printing the exception. +In PyO3, you can use the `add_note` method on `PyErr` to accomplish this functionality. [`From`]: https://doc.rust-lang.org/stable/std/convert/trait.From.html [`Result`]: https://doc.rust-lang.org/stable/std/result/enum.Result.html diff --git a/guide/src/function/signature.md b/guide/src/function/signature.md index 393bf3a779b..a185bb541c0 100644 --- a/guide/src/function/signature.md +++ b/guide/src/function/signature.md @@ -1,8 +1,12 @@ # Function signatures -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. +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. All arguments are required by default. 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 = "...")]` @@ -28,12 +32,13 @@ Just like in Python, the following constructs can be part of the signature:: - `/`: positional-only arguments separator, each parameter defined before `/` is a positional-only parameter. - `*`: var arguments separator, each parameter defined after `*` is a keyword-only parameter. -- `*args`: "args" is var args. Type of the `args` parameter has to be `&Bound<'_, PyTuple>`. -- `**kwargs`: "kwargs" receives keyword arguments. The type of the `kwargs` parameter has to be `Option<&Bound<'_, PyDict>>`. +- `*args`: "args" is var args. + Type of the `args` parameter has to be `&Bound<'_, PyTuple>`. +- `**kwargs`: "kwargs" receives keyword arguments. + The type of the `kwargs` parameter has to be `Option<&Bound<'_, PyDict>>`. - `arg=Value`: arguments with default value. If the `arg` argument is defined after var arguments, it is treated as a keyword-only argument. - Note that `Value` has to be valid rust code, PyO3 just inserts it into the generated - code unmodified. + Note that `Value` has to be valid rust code, PyO3 just inserts it into the generated code unmodified. Example: @@ -88,7 +93,8 @@ pub fn simple_python_bound_function(py: Python<'_>, lambda: Py) -> PyResu } ``` -N.B. the position of the `/` and `*` arguments (if included) control the system of handling positional and keyword arguments. In Python: +N.B. the position of the `/` and `*` arguments (if included) control the system of handling positional and keyword arguments. +In Python: ```python import mymodule @@ -125,9 +131,11 @@ num=44 ## 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. +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. -This automatic generation can only display the value of default arguments for strings, integers, boolean types, and `None`. Any other default arguments will be displayed as `...`. (`.pyi` type stub files commonly also use `...` for default arguments in the same way.) +This automatic generation can only display the value of default arguments for strings, integers, boolean types, and `None`. +Any other default arguments will be displayed as `...`. (`.pyi` type stub files commonly also use `...` for default arguments in the same way.) In cases where the automatically-generated signature needs adjusting, it can [be overridden](#overriding-the-generated-signature) using the `#[pyo3(text_signature)]` option.) @@ -210,7 +218,8 @@ fn add(a: u64, b: u64) -> u64 { # } ``` -PyO3 will include the contents of the annotation unmodified as the `__text_signature__`. Below shows how IPython will now present this (see the default value of 0 for b): +PyO3 will include the contents of the annotation unmodified as the `__text_signature__`. +Below shows how IPython will now present this (see the default value of 0 for b): ```text >>> pyo3_test.add.__text_signature__ @@ -221,7 +230,8 @@ Docstring: This function adds two unsigned 64-bit integers. Type: builtin_function_or_method ``` -If no signature is wanted at all, `#[pyo3(text_signature = None)]` will disable the built-in signature. The snippet below demonstrates use of this: +If no signature is wanted at all, `#[pyo3(text_signature = None)]` will disable the built-in signature. +The snippet below demonstrates use of this: ```rust use pyo3::prelude::*; diff --git a/guide/src/getting-started.md b/guide/src/getting-started.md index f3a548eba3a..a621b97ca1a 100644 --- a/guide/src/getting-started.md +++ b/guide/src/getting-started.md @@ -1,24 +1,32 @@ # Installation -To get started using PyO3 you will need three things: a Rust toolchain, a Python environment, and a way to build. We'll cover each of these below. +To get started using PyO3 you will need three things: a Rust toolchain, a Python environment, and a way to build. +We'll cover each of these below. > If you'd like to chat to the PyO3 maintainers and other PyO3 users, consider joining the [PyO3 Discord server](https://discord.gg/33kcChzH7f). We're keen to hear about your experience getting started, so we can make PyO3 as accessible as possible for everyone! ## Rust -First, make sure you have Rust installed on your system. If you haven't already done so, try following the instructions [here](https://www.rust-lang.org/tools/install). PyO3 runs on both the `stable` and `nightly` versions so you can choose whichever one fits you best. The minimum required Rust version is 1.83. +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.83. If you can run `rustc --version` and the version is new enough you're good to go! ## Python -To use PyO3, you need at least Python 3.7. While you can simply use the default Python interpreter on your system, it is recommended to use a virtual environment. +To use PyO3, you need at least Python 3.7. +While you can simply use the default Python interpreter on your system, it is recommended to use a virtual environment. ## Virtualenvs -While you can use any virtualenv manager you like, we recommend the use of `pyenv` in particular if you want to develop or test for multiple different Python versions, so that is what the examples in this book will use. The installation instructions for `pyenv` can be found [here](https://github.com/pyenv/pyenv#a-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.) +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#a-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.) -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. +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: @@ -28,7 +36,9 @@ pyenv install 3.12 --keep ### 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. +There are a number of build and Python package management systems such as [`setuptools-rust`](https://github.com/PyO3/setuptools-rust) or [manually](./building-and-distribution.md#manual-builds). +We recommend the use of `maturin`, which you can install [here](https://maturin.rs/installation.html). +It is developed to work with PyO3 and provides the most "batteries included" experience, especially if you are aiming to publish to PyPI. `maturin` is just a Python package, so you can add it in the same way you already install Python packages. System Python: @@ -59,7 +69,8 @@ After installation, you can run `maturin --version` to check that you have corre ## Starting a new project -First you should create the folder and virtual environment that are going to contain your new project. Here we will use the recommended `pyenv`: +First you should create the folder and virtual environment that are going to contain your new project. +Here we will use the recommended `pyenv`: ```bash mkdir pyo3-example @@ -68,7 +79,9 @@ pyenv virtualenv pyo3 pyenv local pyo3 ``` -After this, you should install your build manager. In this example, we will use `maturin`. After you've activated your virtualenv, add `maturin` to it: +After this, you should install your build manager. +In this example, we will use `maturin`. +After you've activated your virtualenv, add `maturin` to it: ```bash pip install maturin @@ -100,7 +113,9 @@ If you opt for the second option, here are the things you need to pay attention ## Cargo.toml -Make sure that the Rust crate you want to be able to access from Python is compiled into a library. You can have a binary output as well, but the code you want to access from Python has to be in the library part. Also, make sure that the crate type is `cdylib` and add PyO3 as a dependency as so: +Make sure that the Rust crate you want to be able to access from Python is compiled into a library. +You can have a binary output as well, but the code you want to access from Python has to be in the library part. +Also, make sure that the crate type is `cdylib` and add PyO3 as a dependency as so: ```toml # If you already have [package] information in `Cargo.toml`, you can ignore @@ -179,4 +194,5 @@ For more instructions on how to use Python code from Rust, see the [Python from ## 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. +In development, any changes in the code would require running `maturin develop` before testing. +To streamline the development process, you may want to install [Maturin Import Hook](https://github.com/PyO3/maturin-import-hook) which will run `maturin develop` automatically when the library with code changes is being imported. diff --git a/guide/src/index.md b/guide/src/index.md index 4e85d25d52b..309809ad9cb 100644 --- a/guide/src/index.md +++ b/guide/src/index.md @@ -1,6 +1,8 @@ # The PyO3 user guide -Welcome to the PyO3 user guide! This book is a companion to [PyO3's API docs](https://docs.rs/pyo3). It contains examples and documentation to explain all of PyO3's use cases in detail. +Welcome to the PyO3 user guide! +This book is a companion to [PyO3's API docs](https://docs.rs/pyo3). +It contains examples and documentation to explain all of PyO3's use cases in detail. The rough order of material in this user guide is as follows: 1. Getting started diff --git a/guide/src/migration.md b/guide/src/migration.md index 68543e8654d..c92fed5126d 100644 --- a/guide/src/migration.md +++ b/guide/src/migration.md @@ -10,22 +10,21 @@ For a detailed list of all changes, see the [CHANGELOG](changelog.md).
Click to expand -With the removal of the `gil-ref` API in PyO3 0.23 it is now possible to fully split the Python lifetime -`'py` and the input lifetime `'a`. This allows borrowing from the input data without extending the -lifetime of being attached to the interpreter. +With the removal of the `gil-ref` API in PyO3 0.23 it is now possible to fully split the Python lifetime `'py` and the input lifetime `'a`. +This allows borrowing from the input data without extending the lifetime of being attached to the interpreter. -`FromPyObject` now takes an additional lifetime `'a` describing the input lifetime. The argument -type of the `extract` method changed from `&Bound<'py, PyAny>` to `Borrowed<'a, 'py, PyAny>`. This was -done because `&'a Bound<'py, PyAny>` would have an implicit restriction `'py: 'a` due to the reference type. +`FromPyObject` now takes an additional lifetime `'a` describing the input lifetime. +The argument type of the `extract` method changed from `&Bound<'py, PyAny>` to `Borrowed<'a, 'py, PyAny>`. +This was done because `&'a Bound<'py, PyAny>` would have an implicit restriction `'py: 'a` due to the reference type. This new form was partly implemented already in 0.22 using the internal `FromPyObjectBound` trait and is now extended to all types. Most implementations can just add an elided lifetime to migrate. -Additionally `FromPyObject` gained an associated type `Error`. This is the error type that can be used -in case of a conversion error. During migration using `PyErr` is a good default, later a custom error -type can be introduced to prevent unneccessary creation of Python exception objects and improved type safety. +Additionally `FromPyObject` gained an associated type `Error`. +This is the error type that can be used in case of a conversion error. +During migration using `PyErr` is a good default, later a custom error type can be introduced to prevent unneccessary creation of Python exception objects and improved type safety. Before: @@ -51,8 +50,9 @@ impl<'py> FromPyObject<'_, 'py> for IpAddr { } ``` -Occasionally, more steps are necessary. For generic types, the bounds need to be adjusted. The -correct bound depends on how the type is used. +Occasionally, more steps are necessary. +For generic types, the bounds need to be adjusted. +The correct bound depends on how the type is used. For simple wrapper types usually it's possible to just forward the bound. @@ -89,10 +89,10 @@ where } ``` -Container types that need to create temporary Python references during extraction, for example -extracing from a `PyList`, requires a stronger bound. For these the `FromPyObjectOwned` trait was -introduced. It is automatically implemented for any type that implements `FromPyObject` and does not -borrow from the input. It is intended to be used as a trait bound in these situations. +Container types that need to create temporary Python references during extraction, for example extracing from a `PyList`, requires a stronger bound. +For these the `FromPyObjectOwned` trait was introduced. +It is automatically implemented for any type that implements `FromPyObject` and does not borrow from the input. +It is intended to be used as a trait bound in these situations. Before: @@ -143,9 +143,12 @@ This is very similar to `serde`s [`Deserialize`] and [`DeserializeOwned`] traits ## `.downcast()` and `DowncastError` replaced with `.cast()` and `CastError` -The `.downcast()` family of functions were only available on `Bound`. In corner cases (particularly related to `.downcast_into()`) this would require use of `.as_any().downcast()` or `.into_any().downcast_into()` chains. Additionally, `DowncastError` produced Python exception messages which are not very Pythonic due to use of Rust type names in the error messages. +The `.downcast()` family of functions were only available on `Bound`. +In corner cases (particularly related to `.downcast_into()`) this would require use of `.as_any().downcast()` or `.into_any().downcast_into()` chains. +Additionally, `DowncastError` produced Python exception messages which are not very Pythonic due to use of Rust type names in the error messages. -The `.cast()` family of functions are available on all `Bound` and `Borrowed` smart pointers, whatever the type, and have error messages derived from the actual type at runtime. This produces a nicer experience for both PyO3 module authors and consumers. +The `.cast()` family of functions are available on all `Bound` and `Borrowed` smart pointers, whatever the type, and have error messages derived from the actual type at runtime. +This produces a nicer experience for both PyO3 module authors and consumers. To migrate, replace `.downcast()` with `.cast()` and `DowncastError` with `CastError` (and similar with `.downcast_into()` / `DowncastIntoError` etc). @@ -164,10 +167,12 @@ This should always have been the case, it was an unfortunate omission from its o
Click to expand -The names for these APIs were created when the global interpreter lock (GIL) was mandatory. With the introduction of free-threading in Python 3.13 this is no longer the case, and the naming has no universal meaning anymore. +The names for these APIs were created when the global interpreter lock (GIL) was mandatory. +With the introduction of free-threading in Python 3.13 this is no longer the case, and the naming has no universal meaning anymore. For this reason, we chose to rename these to more modern terminology introduced in free-threading: -- `Python::with_gil` is now called `Python::attach`, it attaches a Python thread-state to the current thread. In GIL enabled builds there can only be 1 thread attached to the interpreter, in free-threading there can be more. +- `Python::with_gil` is now called `Python::attach`, it attaches a Python thread-state to the current thread. + In GIL enabled builds there can only be 1 thread attached to the interpreter, in free-threading there can be more. - `Python::allow_threads` is now called `Python::detach`, it detaches a previously attached thread-state. - `pyo3::prepare_freethreaded_python` is now called `Python::initialize`.
@@ -177,7 +182,9 @@ For this reason, we chose to rename these to more modern terminology introduced
Click to expand -The type alias `PyObject` (aka `Py`) is often confused with the identically named FFI definition `pyo3::ffi::PyObject`. For this reason we are deprecating its usage. To migrate simply replace its usage by the target type `Py`. +The type alias `PyObject` (aka `Py`) is often confused with the identically named FFI definition `pyo3::ffi::PyObject`. +For this reason we are deprecating its usage. +To migrate simply replace its usage by the target type `Py`.
### Replacement of `GILOnceCell` with `PyOnceLock` @@ -185,9 +192,11 @@ The type alias `PyObject` (aka `Py`) is often confused with the identical
Click to expand -Similar to the above renaming of `Python::with_gil` and related APIs, the `GILOnceCell` type was designed for a Python interpreter which was limited by the GIL. Aside from its name, it allowed for the "once" initialization to race because the racing was mediated by the GIL and was extremely unlikely to manifest in practice. +Similar to the above renaming of `Python::with_gil` and related APIs, the `GILOnceCell` type was designed for a Python interpreter which was limited by the GIL. +Aside from its name, it allowed for the "once" initialization to race because the racing was mediated by the GIL and was extremely unlikely to manifest in practice. -With the introduction of free-threaded Python the racy initialization behavior is more likely to be problematic and so a new type `PyOnceLock` has been introduced which performs true single-initialization correctly while attached to the Python interpreter. It exposes the same API as `GILOnceCell`, so should be a drop-in replacement with the notable exception that if the racy initialization of `GILOnceCell` was inadvertently relied on (e.g. due to circular references) then the stronger once-ever guarantee of `PyOnceLock` may lead to deadlocking which requires refactoring. +With the introduction of free-threaded Python the racy initialization behavior is more likely to be problematic and so a new type `PyOnceLock` has been introduced which performs true single-initialization correctly while attached to the Python interpreter. +It exposes the same API as `GILOnceCell`, so should be a drop-in replacement with the notable exception that if the racy initialization of `GILOnceCell` was inadvertently relied on (e.g. due to circular references) then the stronger once-ever guarantee of `PyOnceLock` may lead to deadlocking which requires refactoring. Before: @@ -227,7 +236,8 @@ DECIMAL_TYPE.import(py, "decimal", "Decimal")?;
Click to expand -As another cleanup related to concurrency primitives designed for a Python constrained by the GIL, the `GILProtected` type is now deprecated. Prefer to use concurrency primitives which are compatible with free-threaded Python, such as [`std::sync::Mutex`](https://doc.rust-lang.org/std/sync/struct.Mutex.html) (in combination with PyO3's [`MutexExt`]({{#PYO3_DOCS_URL}}/pyo3/sync/trait.MutexExt.html) trait). +As another cleanup related to concurrency primitives designed for a Python constrained by the GIL, the `GILProtected` type is now deprecated. +Prefer to use concurrency primitives which are compatible with free-threaded Python, such as [`std::sync::Mutex`](https://doc.rust-lang.org/std/sync/struct.Mutex.html) (in combination with PyO3's [`MutexExt`]({{#PYO3_DOCS_URL}}/pyo3/sync/trait.MutexExt.html) trait). Before: @@ -271,8 +281,10 @@ Python::attach(|py| {
Click to expand -Previously, converting a `PyMemoryError` into a Rust `io::Error` would result in an error with kind `Other`. Now, it produces an error with kind `OutOfMemory`. -Similarly, converting an `io::Error` with kind `OutOfMemory` back into a Python error would previously yield a generic `PyOSError`. Now, it yields a `PyMemoryError`. +Previously, converting a `PyMemoryError` into a Rust `io::Error` would result in an error with kind `Other`. +Now, it produces an error with kind `OutOfMemory`. +Similarly, converting an `io::Error` with kind `OutOfMemory` back into a Python error would previously yield a generic `PyOSError`. +Now, it yields a `PyMemoryError`. This change makes error conversions more precise and matches the semantics of out-of-memory errors between Python and Rust.
@@ -285,9 +297,13 @@ This change makes error conversions more precise and matches the semantics of ou Click to expand The `AsPyPointer` trait is mostly a leftover from the now removed gil-refs API. The last remaining uses were the GC API, namely `PyVisit::call`, and identity comparison (`PyAnyMethods::is` and `Py::is`). -`PyVisit::call` has been updated to take `T: Into>>`, which allows for arguments of type `&Py`, `&Option>` and `Option<&Py>`. It is unlikely any changes are needed here to migrate. +`PyVisit::call` has been updated to take `T: Into>>`, which allows for arguments of type `&Py`, `&Option>` and `Option<&Py>`. +It is unlikely any changes are needed here to migrate. -`PyAnyMethods::is`/`Py::is` has been updated to take `T: AsRef>>`. Additionally `AsRef>>` implementations were added for `Py`, `Bound` and `Borrowed`. Because of the existing `AsRef> for Bound` implementation this may cause inference issues in non-generic code. This can be easily migrated by switching to `as_any` instead of `as_ref` for these calls. +`PyAnyMethods::is`/ `Py::is` has been updated to take `T: AsRef>>`. +Additionally `AsRef>>` implementations were added for `Py`, `Bound` and `Borrowed`. +Because of the existing `AsRef> for Bound` implementation this may cause inference issues in non-generic code. +This can be easily migrated by switching to `as_any` instead of `as_ref` for these calls.
## from 0.23.* to 0.24 @@ -307,10 +323,14 @@ 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: +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. +- 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. @@ -326,7 +346,10 @@ CPython 3.13, aka "3.13t". 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`. -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. +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. Future PyO3 versions will likely add more traits and data structures to make working with free-threaded Python easier. @@ -335,8 +358,8 @@ 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 -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. +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.
@@ -357,10 +380,9 @@ Notable features of this new trait include: - 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` -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). +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. @@ -441,7 +463,8 @@ impl<'a, 'py> IntoPyObject<'py> for &'a MyPyObjectWrapper {
Click to expand -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)]`. +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 @@ -449,8 +472,8 @@ This change has an effect on functions and methods returning _byte_ collections - `[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`. +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,no_run # #![allow(dead_code)] @@ -484,7 +507,8 @@ This is purely additional and should just extend the possible return types. 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 have been freed up. For example, `PyTuple::new_bound` is now just `PyTuple::new` (the existing name remains but is deprecated). +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). Before: @@ -515,9 +539,11 @@ let tup = PyTuple::new(py, [1, 2, 3]); #### `IntoPyDict` trait adjusted for removal of `gil-refs` -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. +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. +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: @@ -588,9 +614,11 @@ See the 0.21 migration entry for help upgrading.
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. +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: @@ -623,11 +651,18 @@ fn increment(x: u64, amount: Option) -> u64 { 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. +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. +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. +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 @@ -635,7 +670,10 @@ Related to this, we also added a `pyo3_disable_reference_pool` conditional compi
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. +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. @@ -671,8 +709,8 @@ enum SimpleEnum {
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. +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>`. @@ -729,9 +767,16 @@ Python::with_gil(|py| {
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 . +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 . +For a full history of discussion see . -The "GIL Ref" `&'py PyAny` and similar types such as `&'py PyDict` continue to be available as a deprecated API. Due to the advantages of the new API it is advised that all users make the effort to upgrade as soon as possible. +The "GIL Ref" `&'py PyAny` and similar types such as `&'py PyDict` continue to be available as a deprecated API. +Due to the advantages of the new API it is advised that all users make the effort to upgrade as soon as possible. In addition to the major API type overhaul, PyO3 has needed to make a few small breaking adjustments to other APIs to close correctness and soundness gaps. @@ -749,7 +794,9 @@ The following sections are laid out in this order.
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. +To make the transition for the PyO3 ecosystem away from the GIL Refs API as smooth as possible, in PyO3 0.21 no APIs consuming or producing GIL Refs have been altered. +Instead, variants using `Bound` smart pointers have been introduced, for example `PyTuple::new_bound` which returns `Bound` is the replacement form of `PyTuple::new`. +The GIL Ref APIs have been deprecated, but to make migration easier it is possible to disable these deprecation warnings by enabling the `gil-refs` feature. > The one single exception where an existing API was changed in-place is the `pyo3::intern!` macro. Almost all uses of this macro did not need to update code to account it changing to return `&Bound` immediately, and adding an `intern_bound!` replacement was perceived as adding more work for users. @@ -778,9 +825,11 @@ pyo3 = { version = "0.21", features = ["gil-refs"] }
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`. +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` and `PyAny::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`). @@ -823,7 +872,10 @@ Python::with_gil(|py| {
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. +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. Starting with an implementation of a Python iterator using `IterNextOutput`, e.g. @@ -872,7 +924,8 @@ impl PyClassIter { } ``` -This form also has additional benefits: It has already worked in previous PyO3 versions, it matches the signature of Rust's [`Iterator` trait](https://doc.rust-lang.org/stable/std/iter/trait.Iterator.html) and it allows using a fast path in CPython which completely avoids the cost of raising a `StopIteration` exception. Note that using [`Option::transpose`](https://doc.rust-lang.org/stable/std/option/enum.Option.html#method.transpose) and the `Result, E>` variant, this form can also be used to wrap fallible iterators. +This form also has additional benefits: It has already worked in previous PyO3 versions, it matches the signature of Rust's [`Iterator` trait](https://doc.rust-lang.org/stable/std/iter/trait.Iterator.html) and it allows using a fast path in CPython which completely avoids the cost of raising a `StopIteration` exception. +Note that using [`Option::transpose`](https://doc.rust-lang.org/stable/std/option/enum.Option.html#method.transpose) and the `Result, E>` variant, this form can also be used to wrap fallible iterators. Alternatively, the implementation can also be done as it would in Python itself, i.e. by "raising" a `StopIteration` exception @@ -946,7 +999,8 @@ impl PyClassAsyncIter {
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. +`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 @@ -954,7 +1008,9 @@ impl PyClassAsyncIter {
Click to expand -Interactions with Python objects implemented in Rust no longer need to go though `PyCell`. Instead interactions 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. +Interactions with Python objects implemented in Rust no longer need to go though `PyCell`. +Instead interactions 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` @@ -962,9 +1018,12 @@ Interactions with Python objects implemented in Rust no longer need to go though
Click to expand -To minimise breakage of code using the GIL Refs API, the `Bound` smart pointer has been introduced by adding complements to all functions which accept or return GIL Refs. This allows code to migrate by replacing the deprecated APIs with the new ones. +To minimise breakage of code using the GIL Refs API, the `Bound` smart pointer has been introduced by adding complements to all functions which accept or return GIL Refs. +This allows code to migrate by replacing the deprecated APIs with the new ones. -To identify what to migrate, temporarily switch off the `gil-refs` feature to see deprecation warnings on [almost](#cases-where-pyo3-cannot-emit-gil-ref-deprecation-warnings) all uses of APIs accepting and producing GIL Refs . Over one or more PRs it should be possible to follow the deprecation hints to update code. Depending on your development environment, switching off the `gil-refs` feature may introduce [some very targeted breakages](#deactivating-the-gil-refs-feature), so you may need to fixup those first. +To identify what to migrate, temporarily switch off the `gil-refs` feature to see deprecation warnings on [almost](#cases-where-pyo3-cannot-emit-gil-ref-deprecation-warnings) all uses of APIs accepting and producing GIL Refs . +Over one or more PRs it should be possible to follow the deprecation hints to update code. +Depending on your development environment, switching off the `gil-refs` feature may introduce [some very targeted breakages](#deactivating-the-gil-refs-feature), so you may need to fixup those first. For example, the following APIs have gained updated variants: @@ -976,10 +1035,15 @@ Because the new `Bound` API brings ownership out of the PyO3 framework and in - Code will need to add the occasional `&` to borrow the new smart pointer as `&Bound` to pass these types around (or use `.clone()` at the very small cost of increasing the Python reference count) - `Bound` and `Bound` cannot support indexing with `list[0]`, you should use `list.get_item(0)` instead. -- `Bound::iter_borrowed` is slightly more efficient than `Bound::iter`. The default iteration of `Bound` cannot return borrowed references because Rust does not (yet) have "lending iterators". Similarly `Bound::get_borrowed_item` is more efficient than `Bound::get_item` for the same reason. -- `&Bound` does not implement `FromPyObject` (although it might be possible to do this in the future once the GIL Refs API is completely removed). Use `bound_any.downcast::()` instead of `bound_any.extract::<&Bound>()`. +- `Bound::iter_borrowed` is slightly more efficient than `Bound::iter`. + The default iteration of `Bound` cannot return borrowed references because Rust does not (yet) have "lending iterators". + Similarly `Bound::get_borrowed_item` is more efficient than `Bound::get_item` for the same reason. +- `&Bound` does not implement `FromPyObject` (although it might be possible to do this in the future once the GIL Refs API is completely removed). + Use `bound_any.downcast::()` instead of `bound_any.extract::<&Bound>()`. - `Bound::to_str` now borrows from the `Bound` rather than from the `'py` lifetime, so code will need to store the smart pointer as a value in some cases where previously `&PyString` was just used as a temporary. (There are some more details relating to this in [the section below](#deactivating-the-gil-refs-feature).) -- `.extract::<&str>()` now borrows from the source Python object. The simplest way to update is to change to `.extract::()`, which retains ownership of the Python reference. See more information [in the section on deactivating the `gil-refs` feature](#deactivating-the-gil-refs-feature). +- `.extract::<&str>()` now borrows from the source Python object. + The simplest way to update is to change to `.extract::()`, which retains ownership of the Python reference. + See more information [in the section on deactivating the `gil-refs` feature](#deactivating-the-gil-refs-feature). To convert between `&PyAny` and `&Bound` use the `as_borrowed()` method: @@ -1022,7 +1086,8 @@ let obj: Py = bound.unbind(); #### Migrating `FromPyObject` implementations -`FromPyObject` has had a new method `extract_bound` which takes `&Bound<'py, PyAny>` as an argument instead of `&PyAny`. Both `extract` and `extract_bound` have been given default implementations in terms of the other, to avoid breaking code immediately on update to 0.21. +`FromPyObject` has had a new method `extract_bound` which takes `&Bound<'py, PyAny>` as an argument instead of `&PyAny`. +Both `extract` and `extract_bound` have been given default implementations in terms of the other, to avoid breaking code immediately on update to 0.21. All implementations of `FromPyObject` should be switched from `extract` to `extract_bound`. @@ -1050,7 +1115,8 @@ The expectation is that in 0.22 `extract_bound` will have the default implementa #### Cases where PyO3 cannot emit GIL Ref deprecation warnings -Despite a large amount of deprecations warnings produced by PyO3 to aid with the transition from GIL Refs to the Bound API, there are a few cases where PyO3 cannot automatically warn on uses of GIL Refs. It is worth checking for these cases manually after the deprecation warnings have all been addressed: +Despite a large amount of deprecations warnings produced by PyO3 to aid with the transition from GIL Refs to the Bound API, there are a few cases where PyO3 cannot automatically warn on uses of GIL Refs. +It is worth checking for these cases manually after the deprecation warnings have all been addressed: - Individual implementations of the `FromPyObject` trait cannot be deprecated, so PyO3 cannot warn about uses of code patterns like `.extract<&PyAny>()` which produce a GIL Ref. - GIL Refs in `#[pyfunction]` arguments emit a warning, but if the GIL Ref is wrapped inside another container such as `Vec<&PyAny>` then PyO3 cannot warn against this. @@ -1065,15 +1131,21 @@ Despite a large amount of deprecations warnings produced by PyO3 to aid with the As a final step of migration, deactivating the `gil-refs` feature will set up code for best performance and is intended to set up a forward-compatible API for PyO3 0.22. -At this point code that needed to manage GIL Ref memory can safely remove uses of `GILPool` (which are constructed by calls to `Python::new_pool` and `Python::with_pool`). Deprecation warnings will highlight these cases. +At this point code that needed to manage GIL Ref memory can safely remove uses of `GILPool` (which are constructed by calls to `Python::new_pool` and `Python::with_pool`). +Deprecation warnings will highlight these cases. -There is just one case of code that changes upon disabling these features: `FromPyObject` trait implementations for types that borrow directly from the input data cannot be implemented by PyO3 without GIL Refs (while the GIL Refs API is in the process of being removed). The main types affected are `&str`, `Cow<'_, str>`, `&[u8]`, `Cow<'_, u8>`. +There is just one case of code that changes upon disabling these features: `FromPyObject` trait implementations for types that borrow directly from the input data cannot be implemented by PyO3 without GIL Refs (while the GIL Refs API is in the process of being removed). +The main types affected are `&str`, `Cow<'_, str>`, `&[u8]`, `Cow<'_, u8>`. -To make PyO3's core functionality continue to work while the GIL Refs API is in the process of being removed, disabling the `gil-refs` feature moves the implementations of `FromPyObject` for `&str`, `Cow<'_, str>`, `&[u8]`, `Cow<'_, u8>` to a new temporary trait `FromPyObjectBound`. This trait is the expected future form of `FromPyObject` and has an additional lifetime `'a` to enable these types to borrow data from Python objects. +To make PyO3's core functionality continue to work while the GIL Refs API is in the process of being removed, disabling the `gil-refs` feature moves the implementations of `FromPyObject` for `&str`, `Cow<'_, str>`, `&[u8]`, `Cow<'_, u8>` to a new temporary trait `FromPyObjectBound`. +This trait is the expected future form of `FromPyObject` and has an additional lifetime `'a` to enable these types to borrow data from Python objects. -PyO3 0.21 has introduced the [`PyBackedStr`]({{#PYO3_DOCS_URL}}/pyo3/pybacked/struct.PyBackedStr.html) and [`PyBackedBytes`]({{#PYO3_DOCS_URL}}/pyo3/pybacked/struct.PyBackedBytes.html) types to help with this case. The easiest way to avoid lifetime challenges from extracting `&str` is to use these. For more complex types like `Vec<&str>`, is now impossible to extract directly from a Python object and `Vec` is the recommended upgrade path. +PyO3 0.21 has introduced the [`PyBackedStr`]({{#PYO3_DOCS_URL}}/pyo3/pybacked/struct.PyBackedStr.html) and [`PyBackedBytes`]({{#PYO3_DOCS_URL}}/pyo3/pybacked/struct.PyBackedBytes.html) types to help with this case. +The easiest way to avoid lifetime challenges from extracting `&str` is to use these. +For more complex types like `Vec<&str>`, is now impossible to extract directly from a Python object and `Vec` is the recommended upgrade path. -A key thing to note here is because extracting to these types now ties them to the input lifetime, some extremely common patterns may need to be split into multiple Rust lines. For example, the following snippet of calling `.extract::<&str>()` directly on the result of `.getattr()` needs to be adjusted when deactivating the `gil-refs` feature. +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: @@ -1111,7 +1183,11 @@ assert_eq!(name, "list"); # } ``` -To avoid needing to worry about lifetimes at all, it is also possible to use the new `PyBackedStr` type, which stores a reference to the Python `str` without a lifetime attachment. In particular, `PyBackedStr` helps for `abi3` builds for Python older than 3.10. Due to limitations in the `abi3` CPython API for those older versions, PyO3 cannot offer a `FromPyObjectBound` implementation for `&str` on those versions. The easiest way to migrate for older `abi3` builds is to replace any cases of `.extract::<&str>()` with `.extract::()`. Alternatively, use `.extract::>()`, `.extract::()` to copy the data into Rust. +To avoid needing to worry about lifetimes at all, it is also possible to use the new `PyBackedStr` type, which stores a reference to the Python `str` without a lifetime attachment. +In particular, `PyBackedStr` helps for `abi3` builds for Python older than 3.10. +Due to limitations in the `abi3` CPython API for those older versions, PyO3 cannot offer a `FromPyObjectBound` implementation for `&str` on those versions. +The easiest way to migrate for older `abi3` builds is to replace any cases of `.extract::<&str>()` with `.extract::()`. +Alternatively, use `.extract::>()`, `.extract::()` to copy the data into Rust. The following example uses the same snippet as those just above, but this time the final extracted type is `PyBackedStr`: @@ -1137,7 +1213,8 @@ assert_eq!(&*name, "list");
Click to expand -PyO3 0.20 has increased minimum Rust version to 1.56. This enables use of newer language features and simplifies maintenance of the project. +PyO3 0.20 has increased minimum Rust version to 1.56. +This enables use of newer language features and simplifies maintenance of the project.
### `PyDict::get_item` now returns a `Result` @@ -1145,7 +1222,8 @@ PyO3 0.20 has increased minimum Rust version to 1.56. This enables use of newer
Click to expand -`PyDict::get_item` in PyO3 0.19 and older was implemented using a Python API which would suppress all exceptions and return `None` in those cases. This included errors in `__hash__` and `__eq__` implementations of the key being looked up. +`PyDict::get_item` in PyO3 0.19 and older was implemented using a Python API which would suppress all exceptions and return `None` in those cases. +This included errors in `__hash__` and `__eq__` implementations of the key being looked up. Newer recommendations by the Python core developers advise against using these APIs which suppress exceptions, instead allowing exceptions to bubble upwards. `PyDict::get_item_with_error` already implemented this recommended behavior, so that API has been renamed to `PyDict::get_item`. @@ -1206,7 +1284,9 @@ Python::with_gil(|py| -> PyResult<()> {
Click to expand -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. +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: @@ -1237,7 +1317,8 @@ fn x_or_y(x: Option, y: u64) -> u64 {
Click to expand -In PyO3 0.18 the `#[args]` attribute for `#[pymethods]`, and directly specifying the function signature in `#[pyfunction]`, was deprecated. This functionality has been removed in PyO3 0.20. +In PyO3 0.18 the `#[args]` attribute for `#[pymethods]`, and directly specifying the function signature in `#[pyfunction]`, was deprecated. +This functionality has been removed in PyO3 0.20. Before: @@ -1287,11 +1368,13 @@ The trait `AsPyPointer` is now `unsafe trait`, meaning any external implementati
Click to expand -During `__traverse__` implementations for Python's Garbage Collection it is forbidden to do anything other than visit the members of the `#[pyclass]` being traversed. This means making Python function calls or other API calls are forbidden. +During `__traverse__` implementations for Python's Garbage Collection it is forbidden to do anything other than visit the members of the `#[pyclass]` being traversed. +This means making Python function calls or other API calls are forbidden. Previous versions of PyO3 would allow access to `Python` (e.g. via `Python::with_gil`), which could cause the Python interpreter to crash or otherwise confuse the garbage collection algorithm. -Attempts to acquire the GIL will now panic. See [#3165](https://github.com/PyO3/pyo3/issues/3165) for more detail. +Attempts to acquire the GIL will now panic. +See [#3165](https://github.com/PyO3/pyo3/issues/3165) for more detail. ```rust,ignore # use pyo3::prelude::*; @@ -1422,7 +1505,10 @@ 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 Python token supplied by `Python::with_gil`, the problem is avoided as the Python 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 Python token `Python<'py>` via the [`PyAny::py`](https://docs.rs/pyo3/0.22.5/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 Python token supplied by `Python::with_gil`, the problem is avoided as the Python 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 Python 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 @@ -1470,7 +1556,9 @@ fn required_argument_after_option_b(x: Option, y: i32) {} The [`#[pyo3(text_signature = "...")]` option](./function/signature.md#making-the-function-signature-available-to-python) was previously the only supported way to set the `__text_signature__` attribute on generated Python functions. -PyO3 is now able to automatically populate `__text_signature__` for all functions automatically based on their Rust signature (or the [new `#[pyo3(signature = (...))]` option](./function/signature.md)). These automatically-generated `__text_signature__` values will currently only render `...` for all default values. Many `#[pyo3(text_signature = "...")]` options can be removed from functions when updating to PyO3 0.18, however in cases with default values a manual implementation may still be preferred for now. +PyO3 is now able to automatically populate `__text_signature__` for all functions automatically based on their Rust signature (or the [new `#[pyo3(signature = (...))]` option](./function/signature.md)). +These automatically-generated `__text_signature__` values will currently only render `...` for all default values. +Many `#[pyo3(text_signature = "...")]` options can be removed from functions when updating to PyO3 0.18, however in cases with default values a manual implementation may still be preferred for now. As examples: @@ -1512,19 +1600,12 @@ Unfortunately these functions are not sufficient for distinguishing such types, leading to inconsistent behavior (see [pyo3/pyo3#2072](https://github.com/PyO3/pyo3/issues/2072)). -PyO3 0.17 changes these downcast checks to explicitly test if the type is a -subclass of the corresponding abstract base class `collections.abc.Mapping` or -`collections.abc.Sequence`. Note this requires calling into Python, which may -incur a performance penalty over the previous method. If this performance -penalty is a problem, you may be able to perform your own checks and use -`try_from_unchecked` (unsafe). +PyO3 0.17 changes these downcast checks to explicitly test if the type is a subclass of the corresponding abstract base class `collections.abc.Mapping` or `collections.abc.Sequence`. +Note this requires calling into Python, which may incur a performance penalty over the previous method. +If this performance penalty is a problem, you may be able to perform your own checks and use `try_from_unchecked` (unsafe). -Another side-effect is that a pyclass defined in Rust with PyO3 will need to -be _registered_ with the corresponding Python abstract base class for -downcasting to succeed. `PySequence::register` and `PyMapping:register` have -been added to make it easy to do this from Rust code. These are equivalent to -calling `collections.abc.Mapping.register(MappingPyClass)` or -`collections.abc.Sequence.register(SequencePyClass)` from Python. +Another side-effect is that a pyclass defined in Rust with PyO3 will need to be _registered_ with the corresponding Python abstract base class for downcasting to succeed. `PySequence::register` and `PyMapping:register` have been added to make it easy to do this from Rust code. +These are equivalent to calling `collections.abc.Mapping.register(MappingPyClass)` or `collections.abc.Sequence.register(SequencePyClass)` from Python. For example, for a mapping class defined in Rust: @@ -1563,8 +1644,8 @@ Note that this requirement may go away in the future when a pyclass is able to i
Click to expand -Due to limitations in the `inventory` crate which the `multiple-pymethods` feature depends on, this feature now -requires Rust 1.62. For more information see [dtolnay/inventory#32](https://github.com/dtolnay/inventory/issues/32). +Due to limitations in the `inventory` crate which the `multiple-pymethods` feature depends on, this feature now requires Rust 1.62. +For more information see [dtolnay/inventory#32](https://github.com/dtolnay/inventory/issues/32).
### Added `impl IntoPy> for &str` @@ -1607,7 +1688,8 @@ Python::with_gil(|py| {
Click to expand -In preparation for removing the deprecated `#[pyproto]` attribute macro in a future PyO3 version, it is now gated behind an opt-in feature flag. This also gives a slight saving to compile times for code which does not use the deprecated macro. +In preparation for removing the deprecated `#[pyproto]` attribute macro in a future PyO3 version, it is now gated behind an opt-in feature flag. +This also gives a slight saving to compile times for code which does not use the deprecated macro.
### `PyTypeObject` trait has been deprecated @@ -1615,7 +1697,8 @@ In preparation for removing the deprecated `#[pyproto]` attribute macro in a fut
Click to expand -The `PyTypeObject` trait already was near-useless; almost all functionality was already on the `PyTypeInfo` trait, which `PyTypeObject` had a blanket implementation based upon. In PyO3 0.17 the final method, `PyTypeObject::type_object` was moved to `PyTypeInfo::type_object`. +The `PyTypeObject` trait already was near-useless; almost all functionality was already on the `PyTypeInfo` trait, which `PyTypeObject` had a blanket implementation based upon. +In PyO3 0.17 the final method, `PyTypeObject::type_object` was moved to `PyTypeInfo::type_object`. To migrate, update trait bounds and imports from `PyTypeObject` to `PyTypeInfo`. @@ -1651,7 +1734,8 @@ fn get_type_object(py: Python<'_>) -> &PyType {
Click to expand -If this leads to errors, simply implement `IntoPy`. Because pyclasses already implement `IntoPy`, you probably don't need to worry about this. +If this leads to errors, simply implement `IntoPy`. +Because pyclasses already implement `IntoPy`, you probably don't need to worry about this.
### Each `#[pymodule]` can now only be initialized once per process @@ -1659,7 +1743,8 @@ If this leads to errors, simply implement `IntoPy`. Because pyclasses already im
Click to expand -To make PyO3 modules sound in the presence of Python sub-interpreters, for now it has been necessary to explicitly disable the ability to initialize a `#[pymodule]` more than once in the same process. Attempting to do this will now raise an `ImportError`. +To make PyO3 modules sound in the presence of Python sub-interpreters, for now it has been necessary to explicitly disable the ability to initialize a `#[pymodule]` more than once in the same process. +Attempting to do this will now raise an `ImportError`.
## from 0.15.* to 0.16 @@ -1673,7 +1758,8 @@ To make PyO3 modules sound in the presence of Python sub-interpreters, for now i
Click to expand -PyO3 0.16 has increased minimum Rust version to 1.48 and minimum Python version to 3.7. This enables use of newer language features (enabling some of the other additions in 0.16) and simplifies maintenance of the project. +PyO3 0.16 has increased minimum Rust version to 1.48 and minimum Python version to 3.7. +This enables use of newer language features (enabling some of the other additions in 0.16) and simplifies maintenance of the project.
### `#[pyproto]` has been deprecated @@ -1681,9 +1767,12 @@ PyO3 0.16 has increased minimum Rust version to 1.48 and minimum Python version
Click to expand -In PyO3 0.15, the `#[pymethods]` attribute macro gained support for implementing "magic methods" such as `__str__` (aka "dunder" methods). This implementation was not quite finalized at the time, with a few edge cases to be decided upon. The existing `#[pyproto]` attribute macro was left untouched, because it covered these edge cases. +In PyO3 0.15, the `#[pymethods]` attribute macro gained support for implementing "magic methods" such as `__str__` (aka "dunder" methods). +This implementation was not quite finalized at the time, with a few edge cases to be decided upon. +The existing `#[pyproto]` attribute macro was left untouched, because it covered these edge cases. -In PyO3 0.16, the `#[pymethods]` implementation has been completed and is now the preferred way to implement magic methods. To allow the PyO3 project to move forward, `#[pyproto]` has been deprecated (with expected removal in PyO3 0.18). +In PyO3 0.16, the `#[pymethods]` implementation has been completed and is now the preferred way to implement magic methods. +To allow the PyO3 project to move forward, `#[pyproto]` has been deprecated (with expected removal in PyO3 0.18). Migration from `#[pyproto]` to `#[pymethods]` is straightforward; copying the existing methods directly from the `#[pyproto]` trait implementation is all that is needed in most cases. @@ -1757,9 +1846,11 @@ method `eq()`.
Click to expand -In PyO3 0.15, `__getitem__`, `__setitem__` and `__delitem__` in `#[pymethods]` would generate only the _mapping_ implementation for a `#[pyclass]`. To match the Python behavior, these methods now generate both the _mapping_ **and** _sequence_ implementations. +In PyO3 0.15, `__getitem__`, `__setitem__` and `__delitem__` in `#[pymethods]` would generate only the _mapping_ implementation for a `#[pyclass]`. +To match the Python behavior, these methods now generate both the _mapping_ **and** _sequence_ implementations. -This means that classes implementing these `#[pymethods]` will now also be treated as sequences, same as a Python `class` would be. Small differences in behavior may result: +This means that classes implementing these `#[pymethods]` will now also be treated as sequences, same as a Python `class` would be. +Small differences in behavior may result: - PyO3 will allow instances of these classes to be cast to `PySequence` as well as `PyMapping`. - Python will provide a default implementation of `__iter__` (if the class did not have one) which repeatedly calls `__getitem__` with integers (starting at 0) until an `IndexError` is raised. @@ -1780,9 +1871,12 @@ class ExampleContainer: This class implements a Python [sequence](https://docs.python.org/3/glossary.html#term-sequence). -The `__len__` and `__getitem__` methods are also used to implement a Python [mapping](https://docs.python.org/3/glossary.html#term-mapping). In the Python C-API, these methods are not shared: the sequence `__len__` and `__getitem__` are defined by the `sq_length` and `sq_item` slots, and the mapping equivalents are `mp_length` and `mp_subscript`. There are similar distinctions for `__setitem__` and `__delitem__`. +The `__len__` and `__getitem__` methods are also used to implement a Python [mapping](https://docs.python.org/3/glossary.html#term-mapping). +In the Python C-API, these methods are not shared: the sequence `__len__` and `__getitem__` are defined by the `sq_length` and `sq_item` slots, and the mapping equivalents are `mp_length` and `mp_subscript`. +There are similar distinctions for `__setitem__` and `__delitem__`. -Because there is no such distinction from Python, implementing these methods will fill the mapping and sequence slots simultaneously. A Python class with `__len__` implemented, for example, will have both the `sq_length` and `mp_length` slots filled. +Because there is no such distinction from Python, implementing these methods will fill the mapping and sequence slots simultaneously. +A Python class with `__len__` implemented, for example, will have both the `sq_length` and `mp_length` slots filled. The PyO3 behavior in 0.16 has been changed to be closer to this Python behavior by default.
@@ -1893,9 +1987,11 @@ For projects embedding Python in Rust, PyO3 no longer automatically initializes
Click to expand -`#[pymethods]` have been reworked with a simpler default implementation which removes the dependency on the `inventory` crate. This reduces dependencies and compile times for the majority of users. +`#[pymethods]` have been reworked with a simpler default implementation which removes the dependency on the `inventory` crate. +This reduces dependencies and compile times for the majority of users. -The limitation of the new default implementation is that it cannot support multiple `#[pymethods]` blocks for the same `#[pyclass]`. If you need this functionality, you must enable the `multiple-pymethods` feature which will switch `#[pymethods]` to the inventory-based implementation. +The limitation of the new default implementation is that it cannot support multiple `#[pymethods]` blocks for the same `#[pyclass]`. +If you need this functionality, you must enable the `multiple-pymethods` feature which will switch `#[pymethods]` to the inventory-based implementation.
### Deprecated `#[pyproto]` methods @@ -1903,7 +1999,8 @@ The limitation of the new default implementation is that it cannot support multi
Click to expand -Some protocol (aka `__dunder__`) methods such as `__bytes__` and `__format__` have been possible to implement two ways in PyO3 for some time: via a `#[pyproto]` (e.g. `PyObjectProtocol` for the methods listed here), or by writing them directly in `#[pymethods]`. This is only true for a handful of the `#[pyproto]` methods (for technical reasons to do with the way PyO3 currently interacts with the Python C-API). +Some protocol (aka `__dunder__`) methods such as `__bytes__` and `__format__` have been possible to implement two ways in PyO3 for some time: via a `#[pyproto]` (e.g. `PyObjectProtocol` for the methods listed here), or by writing them directly in `#[pymethods]`. +This is only true for a handful of the `#[pyproto]` methods (for technical reasons to do with the way PyO3 currently interacts with the Python C-API). In the interest of having only one way to do things, the `#[pyproto]` forms of these methods have been deprecated. @@ -1951,7 +2048,8 @@ impl MyClass {
Click to expand -PyO3 `0.13` makes use of new Rust language features stabilized between Rust 1.40 and Rust 1.45. If you are using a Rust compiler older than Rust 1.45, you will need to update your toolchain to be able to continue using PyO3. +PyO3 `0.13` makes use of new Rust language features stabilized between Rust 1.40 and Rust 1.45. +If you are using a Rust compiler older than Rust 1.45, you will need to update your toolchain to be able to continue using PyO3.
### Runtime changes to support the CPython limited API @@ -1959,9 +2057,11 @@ PyO3 `0.13` makes use of new Rust language features stabilized between Rust 1.40
Click to expand -In PyO3 `0.13` support was added for compiling against the CPython limited API. This had a number of implications for _all_ PyO3 users, described here. +In PyO3 `0.13` support was added for compiling against the CPython limited API. +This had a number of implications for _all_ PyO3 users, described here. -The largest of these is that all types created from PyO3 are what CPython calls "heap" types. The specific implications of this are: +The largest of these is that all types created from PyO3 are what CPython calls "heap" types. +The specific implications of this are: - If you wish to subclass one of these types _from Rust_ you must mark it `#[pyclass(subclass)]`, as you would if you wished to allow subclassing it from Python code. - Type objects are now mutable - Python code can set attributes on them. @@ -1975,13 +2075,11 @@ The largest of these is that all types created from PyO3 are what CPython calls
Click to expand -In PyO3 `0.12` the `PyErr` type has been re-implemented to be significantly more compatible with -the standard Rust error handling ecosystem. Specifically `PyErr` now implements -`Error + Send + Sync`, which are the standard traits used for error types. +In PyO3 `0.12` the `PyErr` type has been re-implemented to be significantly more compatible with the standard Rust error handling ecosystem. +Specifically `PyErr` now implements `Error + Send + Sync`, which are the standard traits used for error types. -While this has necessitated the removal of a number of APIs, the resulting `PyErr` type should now -be much more easier to work with. The following sections list the changes in detail and how to -migrate to the new APIs. +While this has necessitated the removal of a number of APIs, the resulting `PyErr` type should now be much more easier to work with. +The following sections list the changes in detail and how to migrate to the new APIs.
#### `PyErr::new` and `PyErr::from_type` now require `Send + Sync` for their argument @@ -1989,9 +2087,8 @@ migrate to the new APIs.
Click to expand -For most uses no change will be needed. If you are trying to construct `PyErr` from a value that is -not `Send + Sync`, you will need to first create the Python object and then use -`PyErr::from_instance`. +For most uses no change will be needed. +If you are trying to construct `PyErr` from a value that is not `Send + Sync`, you will need to first create the Python object and then use `PyErr::from_instance`. Similarly, any types which implemented `PyErrArguments` will now need to be `Send + Sync`.
@@ -2021,7 +2118,8 @@ Exception types](#exception-types-have-been-reworked)).
Click to expand -This implementation was redundant. Just construct the `Result::Err` variant directly. +This implementation was redundant. +Just construct the `Result::Err` variant directly. Before: @@ -2043,11 +2141,11 @@ let result: PyResult<()> = Err(PyTypeError::new_err("error message"));
Click to expand -Previously exception types were zero-sized marker types purely used to construct `PyErr`. In PyO3 -0.12, these types have been replaced with full definitions and are usable in the same way as `PyAny`, `PyDict` etc. This -makes it possible to interact with Python exception objects. +Previously exception types were zero-sized marker types purely used to construct `PyErr`. +In PyO3 0.12, these types have been replaced with full definitions and are usable in the same way as `PyAny`, `PyDict` etc. This makes it possible to interact with Python exception objects. -The new types also have names starting with the "Py" prefix. For example, before: +The new types also have names starting with the "Py" prefix. +For example, before: ```rust,ignore let err: PyErr = TypeError::py_err("error message"); @@ -2081,9 +2179,8 @@ assert_eq!(
Click to expand -To simplify the PyO3 conversion traits, the `FromPy` trait has been removed. Previously there were -two ways to define the to-Python conversion for a type: -`FromPy for PyObject` and `IntoPy for T`. +To simplify the PyO3 conversion traits, the `FromPy` trait has been removed. +Previously there were two ways to define the to-Python conversion for a type: `FromPy for PyObject` and `IntoPy for T`. Now there is only one way to define the conversion, `IntoPy`, so downstream crates may need to adjust accordingly. @@ -2144,8 +2241,8 @@ let obj: PyObject = 1.234.into_py(py);
Click to expand -This should change very little from a usage perspective. If you implemented traits for both -`PyObject` and `Py`, you may find you can just remove the `PyObject` implementation. +This should change very little from a usage perspective. +If you implemented traits for both `PyObject` and `Py`, you may find you can just remove the `PyObject` implementation.
### `AsPyRef` has been removed @@ -2153,9 +2250,8 @@ This should change very little from a usage perspective. If you implemented trai
Click to expand -As `PyObject` has been changed to be just a type alias, the only remaining implementor of `AsPyRef` -was `Py`. This removed the need for a trait, so the `AsPyRef::as_ref` method has been moved to -`Py::as_ref`. +As `PyObject` has been changed to be just a type alias, the only remaining implementor of `AsPyRef` was `Py`. +This removed the need for a trait, so the `AsPyRef::as_ref` method has been moved to `Py::as_ref`. This should require no code changes except removing `use pyo3::AsPyRef` for code which did not use `pyo3::prelude::*`. @@ -2189,7 +2285,8 @@ let list_ref: &PyList = list_py.as_ref(py);
Click to expand -PyO3 now supports the stable Rust toolchain. The minimum required version is 1.39.0. +PyO3 now supports the stable Rust toolchain. +The minimum required version is 1.39.0.
### `#[pyclass]` structs must now be `Send` or `unsendable` @@ -2205,8 +2302,8 @@ This may "break" some code which previously was accepted, even though it could b There can be two fixes: 1. If you think that your `#[pyclass]` actually must be `Send`able, then let's implement `Send`. - A common, safer way is using thread-safe types. E.g., `Arc` instead of `Rc`, `Mutex` instead of - `RefCell`, and `Box` instead of `Box`. + A common, safer way is using thread-safe types. + E.g., `Arc` instead of `Rc`, `Mutex` instead of `RefCell`, and `Box` instead of `Box`. Before: @@ -2236,14 +2333,12 @@ There can be two fixes: } ``` - In situations where you cannot change your `#[pyclass]` to automatically implement `Send` - (e.g., when it contains a raw pointer), you can use `unsafe impl Send`. + In situations where you cannot change your `#[pyclass]` to automatically implement `Send` (e.g., when it contains a raw pointer), you can use `unsafe impl Send`. In such cases, care should be taken to ensure the struct is actually thread safe. See [the Rustonomicon](https://doc.rust-lang.org/nomicon/send-and-sync.html) for more. -2. If you think that your `#[pyclass]` should not be accessed by another thread, you can use - `unsendable` flag. A class marked with `unsendable` panics when accessed by another thread, - making it thread-safe to expose an unsendable object to the Python interpreter. +2. If you think that your `#[pyclass]` should not be accessed by another thread, you can use `unsendable` flag. + A class marked with `unsendable` panics when accessed by another thread, making it thread-safe to expose an unsendable object to the Python interpreter. Before: @@ -2275,8 +2370,8 @@ There can be two fixes:
Click to expand -Previously, a few methods such as `Object::get_refcnt` did not take `Python` as an argument (to -ensure that the Python GIL was held by the current thread). Technically, this was not sound. +Previously, a few methods such as `Object::get_refcnt` did not take `Python` as an argument (to ensure that the Python GIL was held by the current thread). +Technically, this was not sound. To migrate, just pass a `py` argument to any calls to these methods. Before: diff --git a/guide/src/module.md b/guide/src/module.md index d1fd8deeaee..79d7fbdad32 100644 --- a/guide/src/module.md +++ b/guide/src/module.md @@ -30,8 +30,8 @@ mod my_extension { The `#[pymodule]` procedural macro takes care of creating the initialization function of your module and exposing it to Python. -The module's name defaults to the name of the Rust module. You can override the module name by -using `#[pyo3(name = "custom_name")]`: +The module's name defaults to the name of the Rust module. +You can override the module name by using `#[pyo3(name = "custom_name")]`: ```rust,no_run # mod declarative_module_custom_name_test { @@ -50,9 +50,8 @@ mod my_extension { # } ``` -The name of the module must match the name of the `.so` or `.pyd` -file. Otherwise, you will get an import error in Python with the following message: -`ImportError: dynamic module does not define module export function (PyInit_name_of_your_module)` +The name of the module must match the name of the `.so` or `.pyd` file. +Otherwise, you will get an import error in Python with the following message: `ImportError: dynamic module does not define module export function (PyInit_name_of_your_module)` To import the module, either: @@ -109,10 +108,8 @@ fn func() -> String { } ``` -Note that this does not define a package, so this won’t allow Python code to directly import -submodules by using `from parent_module import child_module`. For more information, see -[#759](https://github.com/PyO3/pyo3/issues/759) and -[#1517](https://github.com/PyO3/pyo3/issues/1517#issuecomment-808664021). +Note that this does not define a package, so this won’t allow Python code to directly import submodules by using `from parent_module import child_module`. +For more information, see [#759](https://github.com/PyO3/pyo3/issues/759) and [#1517](https://github.com/PyO3/pyo3/issues/1517#issuecomment-808664021). You can provide the `submodule` argument to `#[pymodule()]` for modules that are not top-level modules in order for them to properly generate the `#[pyclass]` `module` attribute automatically. diff --git a/guide/src/parallelism.md b/guide/src/parallelism.md index 2d3940a39c2..6f36e1cf956 100644 --- a/guide/src/parallelism.md +++ b/guide/src/parallelism.md @@ -1,8 +1,10 @@ # Parallelism -Historically, CPython was limited by the [global interpreter lock](https://docs.python.org/3/glossary.html#term-global-interpreter-lock) (GIL), which only allowed a single thread to drive the Python interpreter at a time. This made threading in Python a bad fit for [CPU-bound](https://en.wikipedia.org/wiki/CPU-bound) tasks and often forced developers to accept the overhead of multiprocessing. +Historically, CPython was limited by the [global interpreter lock](https://docs.python.org/3/glossary.html#term-global-interpreter-lock) (GIL), which only allowed a single thread to drive the Python interpreter at a time. +This made threading in Python a bad fit for [CPU-bound](https://en.wikipedia.org/wiki/CPU-bound) tasks and often forced developers to accept the overhead of multiprocessing. -Rust is well-suited to multithreaded code, and libraries like [`rayon`] can help you leverage safe parallelism with minimal effort. The [`Python::detach`] method can be used to allow the Python interpreter to do other work while the Rust work is ongoing. +Rust is well-suited to multithreaded code, and libraries like [`rayon`] can help you leverage safe parallelism with minimal effort. +The [`Python::detach`] method can be used to allow the Python interpreter to do other work while the Rust work is ongoing. To enable full parallelism in your application, consider also using [free-threaded Python](./free-threading.md) which is supported since Python 3.14. @@ -38,7 +40,8 @@ fn search(contents: &str, needle: &str) -> usize { } ``` -But let's assume you have a long running Rust function which you would like to execute several times in parallel. For the sake of example let's take a sequential version of the word count: +But let's assume you have a long running Rust function which you would like to execute several times in parallel. +For the sake of example let's take a sequential version of the word count: ```rust,no_run # #![allow(dead_code)] @@ -57,7 +60,8 @@ fn search_sequential(contents: &str, needle: &str) -> usize { } ``` -To enable parallel execution of this function, the [`Python::detach`] method can be used to temporarily release the GIL, thus allowing other Python threads to run. We then have a function exposed to the Python runtime which calls `search_sequential` inside a closure passed to [`Python::detach`] to enable true parallelism: +To enable parallel execution of this function, the [`Python::detach`] method can be used to temporarily release the GIL, thus allowing other Python threads to run. +We then have a function exposed to the Python runtime which calls `search_sequential` inside a closure passed to [`Python::detach`] to enable true parallelism: ```rust,no_run # #![allow(dead_code)] @@ -130,11 +134,9 @@ You can see that the Python threaded version is not much slower than the Rust se ## 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. +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 @@ -171,19 +173,13 @@ let allowed_ids: Vec = Python::attach(|outer_py| { assert!(allowed_ids.into_iter().filter(|b| *b).count() == 4); ``` -It's important to note that there is an `outer_py` Python token as well as -an `inner_py` token. Sharing Python tokens between threads is not allowed -and threads must individually attach to the interpreter to access data wrapped by a Python -object. - -It's also important to see that this example uses [`Python::detach`] to -wrap the code that spawns OS threads via `rayon`. If this example didn't use -`detach`, 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 `detach` allows the GIL to be released in the thread -collecting the results from the worker threads. You should always call -`detach` in situations that spawn worker threads, but especially so in -cases where worker threads need to acquire the GIL, to prevent deadlocks. +It's important to note that there is an `outer_py` Python token as well as an `inner_py` token. +Sharing Python tokens between threads is not allowed and threads must individually attach to the interpreter to access data wrapped by a Python object. + +It's also important to see that this example uses [`Python::detach`] to wrap the code that spawns OS threads via `rayon`. +If this example didn't use `detach`, 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 `detach` allows the GIL to be released in the thread collecting the results from the worker threads. +You should always call `detach` in situations that spawn worker threads, but especially so in cases where worker threads need to acquire the GIL, to prevent deadlocks. [`Python::detach`]: {{#PYO3_DOCS_URL}}/pyo3/marker/struct.Python.html#method.detach [`rayon`]: https://github.com/rayon-rs/rayon diff --git a/guide/src/performance.md b/guide/src/performance.md index 8a62212e471..86912829797 100644 --- a/guide/src/performance.md +++ b/guide/src/performance.md @@ -4,7 +4,8 @@ To achieve the best possible performance, it is useful to be aware of several tr ## `extract` versus `cast` -Pythonic API implemented using PyO3 are often polymorphic, i.e. they will accept `&Bound<'_, PyAny>` and try to turn this into multiple more concrete types to which the requested operation is applied. This often leads to chains of calls to `extract`, e.g. +Pythonic API implemented using PyO3 are often polymorphic, i.e. they will accept `&Bound<'_, PyAny>` and try to turn this into multiple more concrete types to which the requested operation is applied. +This often leads to chains of calls to `extract`, e.g. ```rust,no_run # #![allow(dead_code)] @@ -31,7 +32,9 @@ fn frobnicate<'py>(value: &Bound<'py, PyAny>) -> PyResult> { } ``` -This suboptimal as the `FromPyObject` trait requires `extract` to have a `Result` return type. For native types like `PyList`, it faster to use `cast` (which `extract` calls internally) when the error value is ignored. This avoids the costly conversion of a `PyDowncastError` to a `PyErr` required to fulfil the `FromPyObject` contract, i.e. +This suboptimal as the `FromPyObject` trait requires `extract` to have a `Result` return type. +For native types like `PyList`, it faster to use `cast` (which `extract` calls internally) when the error value is ignored. +This avoids the costly conversion of a `PyDowncastError` to a `PyErr` required to fulfil the `FromPyObject` contract, i.e. ```rust,no_run # #![allow(dead_code)] @@ -55,7 +58,8 @@ fn frobnicate<'py>(value: &Bound<'py, PyAny>) -> PyResult> { ## Access to Bound implies access to Python token -Calling `Python::attach` is effectively a no-op when we're already attached to the interpreter, but checking that this is the case still has a cost. If an existing Python token can not be accessed, for example when implementing a pre-existing trait, but a Python-bound reference is available, this cost can be avoided by exploiting that access to Python-bound reference gives zero-cost access to a Python token via `Bound::py`. +Calling `Python::attach` is effectively a no-op when we're already attached to the interpreter, but checking that this is the case still has a cost. +If an existing Python token can not be accessed, for example when implementing a pre-existing trait, but a Python-bound reference is available, this cost can be avoided by exploiting that access to Python-bound reference gives zero-cost access to a Python token via `Bound::py`. For example, instead of writing @@ -101,13 +105,16 @@ impl PartialEq for FooBound<'_> { 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 +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. +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 @@ -126,11 +133,17 @@ As a rule of thumb, attaching and detaching from the Python interpreter takes le ## 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 being attached to the interpreter. The necessary synchronization to obtain and apply these reference count updates when PyO3-based code next attaches to the interpreter is somewhat expensive and can become a significant part of the cost of crossing the Python-Rust boundary. +PyO3 uses global mutable state to keep track of deferred reference count updates implied by `impl Drop for Py` being called without being attached to the interpreter. +The necessary synchronization to obtain and apply these reference count updates when PyO3-based code next attaches to the interpreter 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 being attached to the interpreter. If `pyo3_leak_on_drop_without_reference_pool` is additionally enabled, objects dropped without being attached to Python will be leaked instead which is always sound but might have determinal effects like resource exhaustion in the long term. +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 being attached to the interpreter. +If `pyo3_leak_on_drop_without_reference_pool` is additionally enabled, objects dropped without being attached to Python 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::attach` without making sure to re-attach beforehand. For example, the following code +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::attach` without making sure to re-attach beforehand. +For example, the following code ```rust,ignore # use pyo3::prelude::*; diff --git a/guide/src/python-from-rust.md b/guide/src/python-from-rust.md index 80df8066c04..87a80b95a72 100644 --- a/guide/src/python-from-rust.md +++ b/guide/src/python-from-rust.md @@ -23,7 +23,8 @@ The `Python<'py>` token serves three purposes: - It can be passed to functions that require a proof of attachment, such as [`Py::clone_ref`][clone_ref]. - Its lifetime `'py` is used to bind many of PyO3's types to the Python interpreter, such as [`Bound<'py, T>`][Bound]. -PyO3's types that are bound to the `'py` lifetime, for example `Bound<'py, T>`, all contain a `Python<'py>` token. This means they have full access to the Python interpreter and offer a complete API for interacting with Python objects. +PyO3's types that are bound to the `'py` lifetime, for example `Bound<'py, T>`, all contain a `Python<'py>` token. +This means they have full access to the Python interpreter and offer a complete API for interacting with Python objects. Consult [PyO3's API documentation][obtaining-py] to learn how to acquire one of these tokens. @@ -35,7 +36,8 @@ Historically, Rust code was able to use the GIL as a synchronization guarantee, The [`pyo3::sync`] module offers synchronization tools which abstract over both Python builds. -To enable any parallelism on the GIL-enabled build, and best throughput on the free-threaded build, non-Python operations (system calls and native Rust code) should consider detaching from the Python interpreter to allow other work to proceed. See [the section on parallelism](parallelism.md) for how to do that using PyO3's API. +To enable any parallelism on the GIL-enabled build, and best throughput on the free-threaded build, non-Python operations (system calls and native Rust code) should consider detaching from the Python interpreter to allow other work to proceed. +See [the section on parallelism](parallelism.md) for how to do that using PyO3's API. ## Python's memory model @@ -44,9 +46,12 @@ Python's memory model differs from Rust's memory model in two key ways: - There is no concept of ownership; all Python objects are shared and usually implemented via reference counting - There is no concept of exclusive (`&mut`) references; any reference can mutate a Python object -PyO3's API reflects this by providing [smart pointer][smart-pointers] types, `Py`, `Bound<'py, T>`, and (the very rarely used) `Borrowed<'a, 'py, T>`. These smart pointers all use Python reference counting. See the [subchapter on types](./types.md) for more detail on these types. +PyO3's API reflects this by providing [smart pointer][smart-pointers] types, `Py`, `Bound<'py, T>`, and (the very rarely used) `Borrowed<'a, 'py, T>`. +These smart pointers all use Python reference counting. +See the [subchapter on types](./types.md) for more detail on these types. -Because of the lack of exclusive `&mut` references, PyO3's APIs for Python objects, for example [`PyListMethods::append`], use shared references. This is safe because Python objects have internal mechanisms to prevent data races (as of time of writing, the Python GIL). +Because of the lack of exclusive `&mut` references, PyO3's APIs for Python objects, for example [`PyListMethods::append`], use shared references. +This is safe because Python objects have internal mechanisms to prevent data races (as of time of writing, the Python GIL). [attached]: https://docs.python.org/3.14/glossary.html#term-attached-thread-state [global interpreter lock]: https://docs.python.org/3/c-api/init.html#thread-state-and-the-global-interpreter-lock diff --git a/guide/src/python-from-rust/calling-existing-code.md b/guide/src/python-from-rust/calling-existing-code.md index 8ecef32ec5a..b54aee76dcf 100644 --- a/guide/src/python-from-rust/calling-existing-code.md +++ b/guide/src/python-from-rust/calling-existing-code.md @@ -4,8 +4,8 @@ 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`] 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. +[`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 use pyo3::prelude::*; @@ -145,9 +145,8 @@ An import in Python will first attempt to lookup the module from this dict, and if not present will use various strategies to attempt to locate and load the module. -The [`append_to_inittab`]({{#PYO3_DOCS_URL}}/pyo3/macro.append_to_inittab.html) -macro can be used to add additional `#[pymodule]` modules to an embedded -Python interpreter. The macro **must** be invoked _before_ initializing Python. +The [`append_to_inittab`]({{#PYO3_DOCS_URL}}/pyo3/macro.append_to_inittab.html) macro can be used to add additional `#[pymodule]` modules to an embedded Python interpreter. +The macro **must** be invoked _before_ initializing Python. As an example, the below adds the module `foo` to the embedded interpreter: @@ -210,8 +209,8 @@ You can include a file at compile time by using Or you can load a file at runtime by using [`std::fs::read_to_string`](https://doc.rust-lang.org/std/fs/fn.read_to_string.html) function. -Many Python files can be included and loaded as modules. If one file depends on -another you must preserve correct order while declaring `PyModule`. +Many Python files can be included and loaded as modules. +If one file depends on another you must preserve correct order while declaring `PyModule`. Example directory structure: @@ -379,7 +378,8 @@ class House(object): ## Handling system signals/interrupts (Ctrl-C) -The best way to handle system signals when running Rust code is to periodically call `Python::check_signals` to handle any signals captured by Python's signal handler. See also [the FAQ entry](../faq.md#ctrl-c-doesnt-do-anything-while-my-rust-code-is-executing). +The best way to handle system signals when running Rust code is to periodically call `Python::check_signals` to handle any signals captured by Python's signal handler. +See also [the FAQ entry](../faq.md#ctrl-c-doesnt-do-anything-while-my-rust-code-is-executing). Alternatively, set Python's `signal` module to take the default action for a signal: diff --git a/guide/src/python-from-rust/function-calls.md b/guide/src/python-from-rust/function-calls.md index 692c524ec6d..6574b86a356 100644 --- a/guide/src/python-from-rust/function-calls.md +++ b/guide/src/python-from-rust/function-calls.md @@ -7,7 +7,8 @@ PyO3 offers two APIs to make function calls: - [`call`]({{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.call) - call any callable Python object. - [`call_method`]({{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.call_method) - call a method on the Python object. -Both of these APIs take `args` and `kwargs` arguments (for positional and keyword arguments respectively). There are variants for less complex calls: +Both of these APIs take `args` and `kwargs` arguments (for positional and keyword arguments respectively). +There are variants for less complex calls: - [`call1`]({{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.call1) and [`call_method1`]({{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.call_method1) to call only with positional `args`. - [`call0`]({{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.call0) and [`call_method0`]({{#PYO3_DOCS_URL}}/pyo3/types/trait.PyAnyMethods.html#tymethod.call_method0) to call with no arguments. @@ -58,7 +59,9 @@ fn main() -> PyResult<()> { ## Creating keyword arguments -For the `call` and `call_method` APIs, `kwargs` are `Option<&Bound<'py, PyDict>>`, so can either be `None` or `Some(&dict)`. You can use the [`IntoPyDict`]({{#PYO3_DOCS_URL}}/pyo3/types/trait.IntoPyDict.html) trait to convert other dict-like containers, e.g. `HashMap` or `BTreeMap`, as well as tuples with up to 10 elements and `Vec`s where each element is a two-element tuple. To pass keyword arguments of different types, construct a `PyDict` object. +For the `call` and `call_method` APIs, `kwargs` are `Option<&Bound<'py, PyDict>>`, so can either be `None` or `Some(&dict)`. +You can use the [`IntoPyDict`]({{#PYO3_DOCS_URL}}/pyo3/types/trait.IntoPyDict.html) trait to convert other dict-like containers, e.g. `HashMap` or `BTreeMap`, as well as tuples with up to 10 elements and `Vec`s where each element is a two-element tuple. +To pass keyword arguments of different types, construct a `PyDict` object. ```rust use pyo3::prelude::*; diff --git a/guide/src/python-typing-hints.md b/guide/src/python-typing-hints.md index 0e5b881615b..7f6661a2743 100644 --- a/guide/src/python-typing-hints.md +++ b/guide/src/python-typing-hints.md @@ -1,20 +1,25 @@ # Typing and IDE hints for your Python package -PyO3 provides an easy to use interface to code native Python libraries in Rust. The accompanying Maturin allows you to build and publish them as a package. Yet, for a better user experience, Python libraries should provide typing hints and documentation for all public entities, so that IDEs can show them during development and type analyzing tools such as `mypy` can use them to properly verify the code. +PyO3 provides an easy to use interface to code native Python libraries in Rust. +The accompanying Maturin allows you to build and publish them as a package. +Yet, for a better user experience, Python libraries should provide typing hints and documentation for all public entities, so that IDEs can show them during development and type analyzing tools such as `mypy` can use them to properly verify the code. Currently the best solution for the problem is to manually maintain `*.pyi` files and ship them along with the package. -PyO3 is working on automated their generation. See the [type stub generation](type-stub.md) documentation for a description of the current state of automated generation. +PyO3 is working on automated their generation. +See the [type stub generation](type-stub.md) documentation for a description of the current state of automated generation. ## Introduction to `pyi` files -`pyi` files (an abbreviation for `Python Interface`) are called "stub files" in most of the documentation related to them. A very good definition of what it is can be found in [old MyPy documentation](https://github.com/python/mypy/wiki/Creating-Stubs-For-Python-Modules): +`pyi` files (an abbreviation for `Python Interface`) are called "stub files" in most of the documentation related to them. +A very good definition of what it is can be found in [old MyPy documentation](https://github.com/python/mypy/wiki/Creating-Stubs-For-Python-Modules): > A stubs file only contains a description of the public interface of the module without any implementations. There is also [extensive documentation on type stubs on the official Python typing documentation](https://typing.readthedocs.io/en/latest/source/stubs.html). -Most Python developers probably already encountered them when trying to use their IDE's "Go to Definition" function on any builtin type. For example, the definitions of a few standard exceptions look like this: +Most Python developers probably already encountered them when trying to use their IDE's "Go to Definition" function on any builtin type. +For example, the definitions of a few standard exceptions look like this: ```python class BaseException(object): @@ -37,7 +42,8 @@ class StopIteration(Exception): value: Any ``` -As we can see, those are not full definitions containing implementation, but just a description of the interface. It is usually all that the user of the library needs. +As we can see, those are not full definitions containing implementation, but just a description of the interface. +It is usually all that the user of the library needs. ### What do the PEPs say? @@ -53,7 +59,8 @@ At the time of writing this documentation, the `pyi` files are referenced in fou It contains a specification for them (highly recommended reading, since it contains at least one thing that is not used in normal Python code) and also some general information about where to store the stub files. -[PEP561 - Distributing and Packaging Type Information](https://www.python.org/dev/peps/pep-0561/) describes in detail how to build packages that will enable type checking. In particular it contains information about how the stub files must be distributed in order for type checkers to use them. +[PEP561 - Distributing and Packaging Type Information](https://www.python.org/dev/peps/pep-0561/) describes in detail how to build packages that will enable type checking. +In particular it contains information about how the stub files must be distributed in order for type checkers to use them. [PEP560 - Core support for typing module and generic types](https://www.python.org/dev/peps/pep-0560/) describes the details on how Python's type system internally supports generics, including both runtime behavior and integration with static type checkers. @@ -65,19 +72,24 @@ It contains a specification for them (highly recommended reading, since it conta - `separate package with stub files` - the typing is placed in `pyi` files distributed in their own, separate package; - `in-package stub files` - the typing is placed in `pyi` files distributed in the same package as source files. -The first way is tricky with PyO3 since we do not have `py` files. When it has been investigated and necessary changes are implemented, this document will be updated. +The first way is tricky with PyO3 since we do not have `py` files. +When it has been investigated and necessary changes are implemented, this document will be updated. -The second way is easy to do, and the whole work can be fully separated from the main library code. The example repo for the package with stub files can be found in [PEP561 references section](https://www.python.org/dev/peps/pep-0561/#references): [Stub package repository](https://github.com/ethanhs/stub-package) +The second way is easy to do, and the whole work can be fully separated from the main library code. +The example repo for the package with stub files can be found in [PEP561 references section](https://www.python.org/dev/peps/pep-0561/#references): [Stub package repository](https://github.com/ethanhs/stub-package) The third way is described below. ### Including `pyi` files in your PyO3/Maturin build package -When source files are in the same package as stub files, they should be placed next to each other. We need a way to do that with Maturin. Also, in order to mark our package as typing-enabled we need to add an empty file named `py.typed` to the package. +When source files are in the same package as stub files, they should be placed next to each other. +We need a way to do that with Maturin. +Also, in order to mark our package as typing-enabled we need to add an empty file named `py.typed` to the package. #### If you do not have other Python files -If you do not need to add any other Python files apart from `pyi` to the package, Maturin provides a way to do most of the work for you. As documented in the [Maturin Guide](https://github.com/PyO3/maturin/#mixed-rustpython-projects), the only thing you need to do is to create a stub file for your module named `.pyi` in your project root and Maturin will do the rest. +If you do not need to add any other Python files apart from `pyi` to the package, Maturin provides a way to do most of the work for you. +As documented in the [Maturin Guide](https://github.com/PyO3/maturin/#mixed-rustpython-projects), the only thing you need to do is to create a stub file for your module named `.pyi` in your project root and Maturin will do the rest. ```text my-rust-project/ @@ -92,7 +104,9 @@ For an example `pyi` file see the [`my_project.pyi` content](#my_projectpyi-cont #### If you need other Python files -If you need to add other Python files apart from `pyi` to the package, you can do it also, but that requires some more work. Maturin provides an easy way to add files to a package ([documentation](https://github.com/PyO3/maturin/blob/0dee40510083c03607834c821eea76964140a126/Readme.md#mixed-rustpython-projects)). You just need to create a folder with the name of your module next to the `Cargo.toml` file (for customization see documentation linked above). +If you need to add other Python files apart from `pyi` to the package, you can do it also, but that requires some more work. +Maturin provides an easy way to add files to a package ([documentation](https://github.com/PyO3/maturin/blob/0dee40510083c03607834c821eea76964140a126/Readme.md#mixed-rustpython-projects)). +You just need to create a folder with the name of your module next to the `Cargo.toml` file (for customization see documentation linked above). The folder structure would be: @@ -114,7 +128,9 @@ Let's go a little bit more into detail regarding the files inside the package fo ##### `__init__.py` content -As we now specify our own package content, we have to provide the `__init__.py` file, so the folder is treated as a package and we can import things from it. We can always use the same content that Maturin creates for us if we do not specify a Python source folder. For PyO3 bindings it would be: +As we now specify our own package content, we have to provide the `__init__.py` file, so the folder is treated as a package and we can import things from it. +We can always use the same content that Maturin creates for us if we do not specify a Python source folder. +For PyO3 bindings it would be: ```python from .my_project import * @@ -127,7 +143,8 @@ That way everything that is exposed by our native module can be imported directl As stated in [PEP561](https://www.python.org/dev/peps/pep-0561/): > Package maintainers who wish to support type checking of their code MUST add a marker file named py.typed to their package supporting typing. This marker applies recursively: if a top-level package includes it, all its sub-packages MUST support type checking as well. -If we do not include that file, some IDEs might still use our `pyi` files to show hints, but the type checkers might not. MyPy will raise an error in this situation: +If we do not include that file, some IDEs might still use our `pyi` files to show hints, but the type checkers might not. +MyPy will raise an error in this situation: ```text error: Skipping analyzing "my_project": found module but no type hints or library stubs @@ -137,7 +154,8 @@ The file is just a marker file, so it should be empty. ##### `my_project.pyi` content -Our module stub file. This document does not aim at describing how to write them, since you can find a lot of documentation on it, starting from the already quoted [PEP484](https://www.python.org/dev/peps/pep-0484/#stub-files). +Our module stub file. +This document does not aim at describing how to write them, since you can find a lot of documentation on it, starting from the already quoted [PEP484](https://www.python.org/dev/peps/pep-0484/#stub-files). The example can look like this: @@ -170,9 +188,9 @@ class Car: ### Supporting Generics -Type annotations can also be made generic in Python. They are useful for working -with different types while maintaining type safety. Usually, generic classes -inherit from the `typing.Generic` metaclass. +Type annotations can also be made generic in Python. +They are useful for working with different types while maintaining type safety. +Usually, generic classes inherit from the `typing.Generic` metaclass. Take for example the following `.pyi` file that specifies a `Car` that can accept multiple types of wheels: @@ -204,8 +222,8 @@ class Car[W]: #### Runtime Behaviour -Stub files (`pyi`) are only useful for static type checkers and ignored at runtime. Therefore, -PyO3 classes do not inherit from `typing.Generic` even if specified in the stub files. +Stub files (`pyi`) are only useful for static type checkers and ignored at runtime. +Therefore, PyO3 classes do not inherit from `typing.Generic` even if specified in the stub files. This can cause some runtime issues, as annotating a variable like `f1_car: Car[AlloyWheel] = ...` can make Python call magic methods that are not defined. @@ -218,10 +236,8 @@ To overcome this limitation, implementers can pass the `generic` parameter to `p #### Advanced Users -`#[pyclass(generic)]` implements a very simple runtime behavior that accepts -any generic argument. Advanced users can opt to manually implement -[`__class_geitem__`](https://docs.python.org/3/reference/datamodel.html#emulating-generic-types) -for the generic class to have more control. +`#[pyclass(generic)]` implements a very simple runtime behavior that accepts any generic argument. +Advanced users can opt to manually implement [`__class_geitem__`](https://docs.python.org/3/reference/datamodel.html#emulating-generic-types) for the generic class to have more control. ```rust ignore impl MyClass { diff --git a/guide/src/trait-bounds.md b/guide/src/trait-bounds.md index 4c5aab84d52..578019c8dfc 100644 --- a/guide/src/trait-bounds.md +++ b/guide/src/trait-bounds.md @@ -160,8 +160,8 @@ impl Model for UserModel { } ``` -However, the previous code will not compile. The compilation error is the following one: -`error: #[pymethods] cannot be used on trait impl blocks` +However, the previous code will not compile. +The compilation error is the following one: `error: #[pymethods] cannot be used on trait impl blocks` That's a bummer! However, we can write a second wrapper around these functions to call them directly. diff --git a/guide/src/type-stub.md b/guide/src/type-stub.md index f7641b8f074..8214984a6a7 100644 --- a/guide/src/type-stub.md +++ b/guide/src/type-stub.md @@ -76,8 +76,12 @@ This is useful when PyO3 is not able to derive proper type annotations by itself ## Constraints and limitations - The `experimental-inspect` feature is required to generate the introspection fragments. -- Lots of features are not implemented yet. See [the related issue](https://github.com/PyO3/pyo3/issues/5137) for a list of them. -- Introspection only works with Python modules declared with an inline Rust module. Modules declared using a function are not supported. +- Lots of features are not implemented yet. + See [the related issue](https://github.com/PyO3/pyo3/issues/5137) for a list of them. +- Introspection only works with Python modules declared with an inline Rust module. + Modules declared using a function are not supported. - `FromPyObject::INPUT_TYPE` and `IntoPyObject::OUTPUT_TYPE` must be implemented for PyO3 to get the proper input/output type annotations to use. -- Because `FromPyObject::INPUT_TYPE` and `IntoPyObject::OUTPUT_TYPE` are `const` it is not possible to build yet smart generic annotations for containers like `concat!("list[", T::OUTPUT_TYPE, "]")`. See [this tracking issue](https://github.com/rust-lang/rust/issues/76560). -- PyO3 is not able to introspect the content of `#[pymodule]` and `#[pymodule_init]` functions. If they are present, the module is tagged as incomplete using a fake `def __getattr__(name: str) -> Incomplete: ...` function [following best practices](https://typing.python.org/en/latest/guides/writing_stubs.html#incomplete-stubs). +- Because `FromPyObject::INPUT_TYPE` and `IntoPyObject::OUTPUT_TYPE` are `const` it is not possible to build yet smart generic annotations for containers like `concat!("list[", T::OUTPUT_TYPE, "]")`. + See [this tracking issue](https://github.com/rust-lang/rust/issues/76560). +- PyO3 is not able to introspect the content of `#[pymodule]` and `#[pymodule_init]` functions. + If they are present, the module is tagged as incomplete using a fake `def __getattr__(name: str) -> Incomplete: ...` function [following best practices](https://typing.python.org/en/latest/guides/writing_stubs.html#incomplete-stubs). diff --git a/guide/src/types.md b/guide/src/types.md index 1e94aa58482..585a895f9e1 100644 --- a/guide/src/types.md +++ b/guide/src/types.md @@ -1,49 +1,69 @@ # Python object types -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. +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 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. +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. ## 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>`. +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>`. These smart pointers behave differently due to their lifetime parameters. `Py` has no lifetime parameters, `Bound<'py, T>` has [the `'py` lifetime](./python-from-rust.md#the-py-lifetime) as a parameter, and `Borrowed<'a, 'py, T>` has the `'py` lifetime plus an additional lifetime `'a` to denote the lifetime it is borrowing data for. (You can read more about these lifetimes in the subsections below). -Python objects are reference counted, like [`std::sync::Arc`](https://doc.rust-lang.org/stable/std/sync/struct.Arc.html). A major reason for these smart pointers is to bring Python's reference counting to a Rust API. +Python objects are reference counted, like [`std::sync::Arc`](https://doc.rust-lang.org/stable/std/sync/struct.Arc.html). +A major reason for these smart pointers is to bring Python's reference counting to a Rust API. The recommendation of when to use each of these smart pointers is as follows: - Use `Bound<'py, T>` for as much as possible, as it offers the most efficient and complete API. - Use `Py` mostly just for storage inside Rust `struct`s which do not want to or can't add a lifetime parameter for `Bound<'py, T>`. -- `Borrowed<'a, 'py, T>` is almost never used. It is occasionally present at the boundary between Rust and the Python interpreter, for example when borrowing data from Python tuples (which is safe because they are immutable). +- `Borrowed<'a, 'py, T>` is almost never used. + It is occasionally present at the boundary between Rust and the Python interpreter, for example when borrowing data from Python tuples (which is safe because they are immutable). The sections below also explain these smart pointers in a little more detail. ### `Py` -[`Py`][Py] is the foundational smart pointer in PyO3's API. The type parameter `T` denotes the type of the Python object. Very frequently this is `PyAny`, meaning any Python object. +[`Py`][Py] is the foundational smart pointer in PyO3's API. +The type parameter `T` denotes the type of the Python object. +Very frequently this is `PyAny`, meaning any Python object. -Because `Py` is not bound to [the `'py` lifetime](./python-from-rust.md#the-py-lifetime), it is the type to use when storing a Python object inside a Rust `struct` or `enum` which do not want to have a lifetime parameter. In particular, [`#[pyclass]`][pyclass] types are not permitted to have a lifetime, so `Py` is the correct type to store Python objects inside them. +Because `Py` is not bound to [the `'py` lifetime](./python-from-rust.md#the-py-lifetime), it is the type to use when storing a Python object inside a Rust `struct` or `enum` which do not want to have a lifetime parameter. +In particular, [`#[pyclass]`][pyclass] types are not permitted to have a lifetime, so `Py` is the correct type to store Python objects inside them. The lack of binding to the `'py` lifetime also carries drawbacks: - Almost all methods on `Py` require a `Python<'py>` token as the first argument - Other functionality, such as [`Drop`][Drop], needs to check at runtime for attachment to the Python interpreter, 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 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`]. ### `Bound<'py, T>` -[`Bound<'py, T>`][Bound] is the counterpart to `Py` which is also bound to the `'py` lifetime. It can be thought of as equivalent to the Rust tuple `(Python<'py>, Py)`. +[`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 `Bound<'py, T>` should usually be used whenever carrying this lifetime is acceptable, and `Py` otherwise. +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). +`Bound<'py, T>` engages in Python reference counting. +This means that `Bound<'py, T>` owns a Python object. +Rust code which just wants to borrow a Python object should use a shared reference `&Bound<'py, T>`. +Just like `std::sync::Arc`, using `.clone()` and `drop()` will cheaply increment and decrement the reference count of the object (just in this case, the reference counting is implemented by the Python interpreter itself). To give an example of how `Bound<'py, T>` is PyO3's primary API type, consider the following Python code: @@ -89,7 +109,8 @@ fn example(py: Python<'_>) -> PyResult<()> { #### Function argument lifetimes -Because the `'py` lifetime often appears in many function arguments as part of the `Bound<'py, T>` smart pointer, the Rust compiler will often require annotations of input and output lifetimes. This occurs when the function output has at least one lifetime, and there is more than one lifetime present on the inputs. +Because the `'py` lifetime often appears in many function arguments as part of the `Bound<'py, T>` smart pointer, the Rust compiler will often require annotations of input and output lifetimes. +This occurs when the function output has at least one lifetime, and there is more than one lifetime present on the inputs. To demonstrate, consider this function which takes accepts Python objects and applies the [Python `+` operation][PyAnyMethods::add] to them: @@ -100,9 +121,14 @@ fn add(left: &'_ Bound<'_, PyAny>, right: &'_ Bound<'_, PyAny>) -> PyResult>`. It doesn't need ownership of the inputs, so it takes `&Bound<'_, PyAny>` shared references. To demonstrate the point, all lifetimes have used the wildcard `'_` to allow the Rust compiler to attempt to infer them. Because there are four input lifetimes (two lifetimes of the shared references, and two `'py` lifetimes unnamed inside the `Bound<'_, PyAny>` pointers), the compiler cannot reason about which must be connected to the output. +Because the Python `+` operation might raise an exception, this function returns `PyResult>`. +It doesn't need ownership of the inputs, so it takes `&Bound<'_, PyAny>` shared references. +To demonstrate the point, all lifetimes have used the wildcard `'_` to allow the Rust compiler to attempt to infer them. +Because there are four input lifetimes (two lifetimes of the shared references, and two `'py` lifetimes unnamed inside the `Bound<'_, PyAny>` pointers), the compiler cannot reason about which must be connected to the output. -The correct way to solve this is to add the `'py` lifetime as a parameter for the function, and name all the `'py` lifetimes inside the `Bound<'py, PyAny>` smart pointers. For the shared references, it's also fine to reduce `&'_` to just `&`. The working end result is below: +The correct way to solve this is to add the `'py` lifetime as a parameter for the function, and name all the `'py` lifetimes inside the `Bound<'py, PyAny>` smart pointers. +For the shared references, it's also fine to reduce `&'_` to just `&`. +The working end result is below: ```rust # use pyo3::prelude::*; @@ -118,7 +144,8 @@ fn add<'py>( # }) ``` -If naming the `'py` lifetime adds unwanted complexity to the function signature, it is also acceptable to return `Py`, which has no lifetime. The cost is instead paid by a slight increase in implementation complexity, as seen by the introduction of a call to [`Bound::unbind`]: +If naming the `'py` lifetime adds unwanted complexity to the function signature, it is also acceptable to return `Py`, which has no lifetime. +The cost is instead paid by a slight increase in implementation complexity, as seen by the introduction of a call to [`Bound::unbind`]: ```rust # use pyo3::prelude::*; @@ -134,7 +161,9 @@ fn add(left: &Bound<'_, PyAny>, right: &Bound<'_, PyAny>) -> PyResult> ### `Borrowed<'a, 'py, T>` -[`Borrowed<'a, 'py, T>`][Borrowed] is an advanced type used just occasionally at the edge of interaction with the Python interpreter. It can be thought of as analogous to the shared reference `&'a Bound<'py, T>`. The difference is that `Borrowed<'a, 'py, T>` is just a smart pointer rather than a reference-to-a-smart-pointer, which is a helpful reduction in indirection in specific interactions with the Python interpreter. +[`Borrowed<'a, 'py, T>`][Borrowed] is an advanced type used just occasionally at the edge of interaction with the Python interpreter. +It can be thought of as analogous to the shared reference `&'a Bound<'py, T>`. +The difference is that `Borrowed<'a, 'py, T>` is just a smart pointer rather than a reference-to-a-smart-pointer, which is a helpful reduction in indirection in specific interactions with the Python interpreter. `Borrowed<'a, 'py, T>` dereferences to `Bound<'py, T>`, so all methods on `Bound<'py, T>` are available on `Borrowed<'a, 'py, T>`. @@ -161,7 +190,8 @@ for i in 0..=2 { ### Casting between smart pointer types -To convert between `Py` and `Bound<'py, T>` use the `bind()` / `into_bound()` methods. Use the `as_unbound()` / `unbind()` methods to go back from `Bound<'py, T>` to `Py`. +To convert between `Py` and `Bound<'py, T>` use the `bind()` / `into_bound()` methods. +Use the `as_unbound()` / `unbind()` methods to go back from `Bound<'py, T>` to `Py`. ```rust,ignore let obj: Py = ...; @@ -172,7 +202,8 @@ let obj: &Py = bound.as_unbound(); let obj: Py = bound.unbind(); ``` -To convert between `Bound<'py, T>` and `Borrowed<'a, 'py, T>` use the `as_borrowed()` method. `Borrowed<'a, 'py, T>` has a deref coercion to `Bound<'py, T>`. Use the `to_owned()` method to increment the Python reference count and to create a new `Bound<'py, T>` from the `Borrowed<'a, 'py, T>`. +To convert between `Bound<'py, T>` and `Borrowed<'a, 'py, T>` use the `as_borrowed()` method. `Borrowed<'a, 'py, T>` has a deref coercion to `Bound<'py, T>`. +Use the `to_owned()` method to increment the Python reference count and to create a new `Bound<'py, T>` from the `Borrowed<'a, 'py, T>`. ```rust,ignore let bound: Bound<'py, PyAny> = ...; @@ -185,7 +216,8 @@ let bound: &Bound<'py, PyAny> = &borrowed; let bound: Bound<'py, PyAny> = borrowed.to_owned(); ``` -To convert between `Py` and `Borrowed<'a, 'py, T>` use the `bind_borrowed()` method. Use either `as_unbound()` or `.to_owned().unbind()` to go back to `Py` from `Borrowed<'a, 'py, T>`, via `Bound<'py, T>`. +To convert between `Py` and `Borrowed<'a, 'py, T>` use the `bind_borrowed()` method. +Use either `as_unbound()` or `.to_owned().unbind()` to go back to `Py` from `Borrowed<'a, 'py, T>`, via `Bound<'py, T>`. ```rust,ignore let obj: Py = ...; @@ -218,10 +250,13 @@ The following subsections covers some further detail about how to work with thes Each concrete Python type such as `PyAny`, `PyTuple` and `PyDict` exposes its API on the corresponding bound smart pointer `Bound<'py, PyAny>`, `Bound<'py, PyTuple>` and `Bound<'py, PyDict>`. -Each type's API is exposed as a trait: [`PyAnyMethods`], [`PyTupleMethods`], [`PyDictMethods`], and so on for all concrete types. Using traits rather than associated methods on the `Bound` smart pointer is done for a couple of reasons: +Each type's API is exposed as a trait: [`PyAnyMethods`], [`PyTupleMethods`], [`PyDictMethods`], and so on for all concrete types. +Using traits rather than associated methods on the `Bound` smart pointer is done for a couple of reasons: -- Clarity of documentation: each trait gets its own documentation page in the PyO3 API docs. If all methods were on the `Bound` smart pointer directly, the vast majority of PyO3's API would be on a single, extremely long, documentation page. -- Consistency: downstream code implementing Rust APIs for existing Python types can also follow this pattern of using a trait. Downstream code would not be allowed to add new associated methods directly on the `Bound` type. +- Clarity of documentation: each trait gets its own documentation page in the PyO3 API docs. + If all methods were on the `Bound` smart pointer directly, the vast majority of PyO3's API would be on a single, extremely long, documentation page. +- Consistency: downstream code implementing Rust APIs for existing Python types can also follow this pattern of using a trait. + Downstream code would not be allowed to add new associated methods directly on the `Bound` type. - Future design: it is hoped that a future Rust with [arbitrary self types](https://github.com/rust-lang/rust/issues/44874) will remove the need for these traits in favour of placing the methods directly on `PyAny`, `PyTuple`, `PyDict`, and so on. These traits are all included in the `pyo3::prelude` module, so with the glob import `use pyo3::prelude::*` the full PyO3 API is made available to downstream code. @@ -243,7 +278,10 @@ fn get_first_item<'py>(list: &Bound<'py, PyList>) -> PyResult> ### Casting between Python object types -To cast `Bound<'py, T>` smart pointers to some other type, use the [`.cast()`][Bound::cast] family of functions. This converts `&Bound<'py, T>` to a different `&Bound<'py, U>`, without transferring ownership. There is also [`.cast_into()`][Bound::cast_into] to convert `Bound<'py, T>` to `Bound<'py, U>` with transfer of ownership. These methods are available for all types `T` which implement the [`PyTypeCheck`] trait. +To cast `Bound<'py, T>` smart pointers to some other type, use the [`.cast()`][Bound::cast] family of functions. +This converts `&Bound<'py, T>` to a different `&Bound<'py, U>`, without transferring ownership. +There is also [`.cast_into()`][Bound::cast_into] to convert `Bound<'py, T>` to `Bound<'py, U>` with transfer of ownership. +These methods are available for all types `T` which implement the [`PyTypeCheck`] trait. Casting to `Bound<'py, PyAny>` can be done with `.as_any()` or `.into_any()`. @@ -266,7 +304,8 @@ let _: Bound<'py, PyTuple> = obj.cast_into()?; # Python::attach(example).unwrap() ``` -Custom [`#[pyclass]`][pyclass] types implement [`PyTypeCheck`], so `.cast()` also works for these types. The snippet below is the same as the snippet above casting instead to a custom type `MyClass`: +Custom [`#[pyclass]`][pyclass] types implement [`PyTypeCheck`], so `.cast()` also works for these types. +The snippet below is the same as the snippet above casting instead to a custom type `MyClass`: ```rust use pyo3::prelude::*; @@ -290,7 +329,8 @@ let _: Bound<'py, MyClass> = obj.cast_into()?; ### Extracting Rust data from Python objects -To extract Rust data from Python objects, use [`.extract()`][PyAnyMethods::extract] instead of `.cast()`. This method is available for all types which implement the [`FromPyObject`] trait. +To extract Rust data from Python objects, use [`.extract()`][PyAnyMethods::extract] instead of `.cast()`. +This method is available for all types which implement the [`FromPyObject`] trait. For example, the following snippet extracts a Rust tuple of integers from a Python tuple: @@ -309,8 +349,8 @@ assert_eq!((x, y, z), (1, 2, 3)); # Python::attach(example).unwrap() ``` -To avoid copying data, [`#[pyclass]`][pyclass] types can directly reference Rust data stored within the Python objects without needing to `.extract()`. See the [corresponding documentation in the class section of the guide](./class.md#bound-and-interior-mutability) -for more detail. +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. [Bound]: {{#PYO3_DOCS_URL}}/pyo3/struct.Bound.html [`Bound::unbind`]: {{#PYO3_DOCS_URL}}/pyo3/struct.Bound.html#method.unbind diff --git a/pyproject.toml b/pyproject.toml index ccb4c50a280..2b9915ffb45 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,8 +28,6 @@ name = "Fixed" [tool.rumdl] disable = [ - # TODO: enable in a follow-up, probably with `sentence-per-line` reflow - "MD013", # TODO: what to do about inline HTML, probably allow? "MD033", # TODO: {{#PYO3_DOCS_URL}} placeholder confuses rumdl, change syntax perhaps? @@ -47,3 +45,11 @@ exclude = [ [tool.rumdl.MD004] style = "dash" + +[tool.rumdl.MD013] +paragraphs = false +code_blocks = false +tables = false +headings = false +reflow = true +reflow_mode = "sentence-per-line"
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 422/936] 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 423/936] 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 424/936] 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 425/936] 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 426/936] 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 427/936] 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 428/936] 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 429/936] 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 430/936] 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 431/936] 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 432/936] 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 433/936] 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 434/936] 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 435/936] 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 436/936] 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 437/936] 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 438/936] 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 439/936] 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 440/936] 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 441/936] 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 442/936] 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 443/936] 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 444/936] 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 445/936] 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 446/936] 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 447/936] 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 448/936] 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 449/936] 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 450/936] 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 451/936] 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 452/936] 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 453/936] 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 454/936] 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 455/936] 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 456/936] 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 457/936] 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. +