From 463442fd188260bee850834c8efd9b1f7145bb01 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Fri, 3 Apr 2026 14:26:39 +0200 Subject: [PATCH 1/6] add `PyObject_HasAttrWithError` & use it in `PyAnyMethods::hasattr` --- newsfragments/5944.added.md | 1 + newsfragments/5944.changed.md | 1 + pyo3-ffi/src/compat/py_3_13.rs | 12 ++++++++++++ src/types/any.rs | 26 ++++++++++++++------------ 4 files changed, 28 insertions(+), 12 deletions(-) create mode 100644 newsfragments/5944.added.md create mode 100644 newsfragments/5944.changed.md diff --git a/newsfragments/5944.added.md b/newsfragments/5944.added.md new file mode 100644 index 00000000000..1cd74a33698 --- /dev/null +++ b/newsfragments/5944.added.md @@ -0,0 +1 @@ +add `pyo3-ffi::compat::PyObject_HasAttrWithError` diff --git a/newsfragments/5944.changed.md b/newsfragments/5944.changed.md new file mode 100644 index 00000000000..c39e081ff5a --- /dev/null +++ b/newsfragments/5944.changed.md @@ -0,0 +1 @@ +use `PyObject_HasAttrWithError` in `PyAnyMethods::hasattr` diff --git a/pyo3-ffi/src/compat/py_3_13.rs b/pyo3-ffi/src/compat/py_3_13.rs index 08bdf5cba18..0b943c762e0 100644 --- a/pyo3-ffi/src/compat/py_3_13.rs +++ b/pyo3-ffi/src/compat/py_3_13.rs @@ -130,3 +130,15 @@ compat_function!( crate::_PyThreadState_UncheckedGet() } ); + +compat_function!( + originally_defined_for(Py_3_13); + + #[inline] + pub unsafe fn PyObject_HasAttrWithError(obj: *mut crate::PyObject, attr: *mut crate::PyObject) -> std::ffi::c_int { + let res: *mut crate::PyObject = std::ptr::null_mut(); + let rc = crate::PyObject_GetOptionalAttr(obj, attr, &mut result); + crate::Py_XDECREF(res); + rc + } +); diff --git a/src/types/any.rs b/src/types/any.rs index 8923f429efa..2e05fa413a0 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -1,8 +1,8 @@ use crate::call::PyCallArgs; use crate::class::basic::CompareOp; use crate::conversion::{FromPyObject, IntoPyObject}; -use crate::err::{PyErr, PyResult}; -use crate::exceptions::{PyAttributeError, PyTypeError}; +use crate::err::{error_on_minusone, PyErr, PyResult}; +use crate::exceptions::PyTypeError; use crate::ffi_ptr_ext::FfiPtrExt; use crate::impl_::pycell::PyStaticClassObject; use crate::instance::Bound; @@ -11,7 +11,7 @@ use crate::py_result_ext::PyResultExt; use crate::type_object::{PyTypeCheck, PyTypeInfo}; use crate::types::PySuper; use crate::types::{PyDict, PyIterator, PyList, PyString, PyType}; -use crate::{err, ffi, Borrowed, BoundObject, IntoPyObjectExt, Py, Python}; +use crate::{err, ffi, Borrowed, BoundObject, IntoPyObjectExt, Py}; #[allow(deprecated)] use crate::{DowncastError, DowncastIntoError}; use std::cell::UnsafeCell; @@ -982,15 +982,17 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { { // 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. - fn inner(py: Python<'_>, getattr_result: PyResult>) -> PyResult { - match getattr_result { - Ok(_) => Ok(true), - Err(err) if err.is_instance_of::(py) => Ok(false), - Err(e) => Err(e), - } - } - - inner(self.py(), self.getattr(attr_name)) + let result = unsafe { + ffi::compat::PyObject_HasAttrWithError( + self.as_ptr(), + attr_name + .into_pyobject(self.py()) + .map_err(Into::into)? + .as_ptr(), + ) + }; + error_on_minusone(self.py(), result)?; + Ok(result > 0) } fn getattr(&self, attr_name: N) -> PyResult> From e2686d4c847def13e90833499f50b263305d24f9 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Fri, 3 Apr 2026 16:49:25 +0200 Subject: [PATCH 2/6] Add 'PyObject_GetOptionalAttr' compat --- newsfragments/5944.added.md | 2 +- pyo3-ffi/src/compat/py_3_13.rs | 25 ++++++++++++-- src/types/any.rs | 59 ++++++++++++---------------------- 3 files changed, 43 insertions(+), 43 deletions(-) diff --git a/newsfragments/5944.added.md b/newsfragments/5944.added.md index 1cd74a33698..1065ef3d9db 100644 --- a/newsfragments/5944.added.md +++ b/newsfragments/5944.added.md @@ -1 +1 @@ -add `pyo3-ffi::compat::PyObject_HasAttrWithError` +add `pyo3-ffi::compat::PyObject_HasAttrWithError` & `pyo3-ffi::compat::PyObject_GetOptionalAttr` diff --git a/pyo3-ffi/src/compat/py_3_13.rs b/pyo3-ffi/src/compat/py_3_13.rs index 0b943c762e0..ae622d4d20f 100644 --- a/pyo3-ffi/src/compat/py_3_13.rs +++ b/pyo3-ffi/src/compat/py_3_13.rs @@ -135,10 +135,29 @@ compat_function!( originally_defined_for(Py_3_13); #[inline] - pub unsafe fn PyObject_HasAttrWithError(obj: *mut crate::PyObject, attr: *mut crate::PyObject) -> std::ffi::c_int { - let res: *mut crate::PyObject = std::ptr::null_mut(); - let rc = crate::PyObject_GetOptionalAttr(obj, attr, &mut result); + pub unsafe fn PyObject_HasAttrWithError(obj: *mut crate::PyObject, attr_name: *mut crate::PyObject) -> std::ffi::c_int { + let mut res: *mut crate::PyObject = std::ptr::null_mut(); + let rc = crate::compat::PyObject_GetOptionalAttr(obj, attr_name, &mut res); crate::Py_XDECREF(res); rc } ); + +compat_function!( + originally_defined_for(Py_3_13); + + #[inline] + pub unsafe fn PyObject_GetOptionalAttr(obj: *mut crate::PyObject, attr_name: *mut crate::PyObject, result: *mut *mut crate::PyObject,) -> std::ffi::c_int { + *result = crate::PyObject_GetAttr(obj, attr_name); + if !(*result).is_null() { + 1 + } else if crate::PyErr_Occurred().is_null() { + 0 + } else if crate::PyErr_ExceptionMatches(crate::PyExc_AttributeError) > 0 { + crate::PyErr_Clear(); + 0 + } else { + -1 + } + } +); diff --git a/src/types/any.rs b/src/types/any.rs index 2e05fa413a0..632ebce7751 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -1022,48 +1022,29 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { 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())), - } + let py = self.py(); + let mut resp_ptr: *mut ffi::PyObject = std::ptr::null_mut(); + match unsafe { + ffi::compat::PyObject_GetOptionalAttr( + self.as_ptr(), + attr_name + .into_pyobject(py) + .map_err(Into::into)? + .as_ptr(), + &mut resp_ptr, + ) + } { + // Attribute found, result is a new strong reference + 1 => { + let bound = unsafe { Bound::from_owned_ptr(py, resp_ptr) }; + Ok(Some(bound)) } + // Attribute not found, result is NULL + 0 => Ok(None), - #[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), - } - } - } - } + // An error occurred (other than AttributeError) + _ => Err(PyErr::fetch(py)), } - - let py = self.py(); - inner(self, attr_name.into_pyobject_or_pyerr(py)?.as_borrowed()) } fn setattr(&self, attr_name: N, value: V) -> PyResult<()> From c484a751fd29ae520f024803eecaa01e8e14c19b Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Fri, 3 Apr 2026 17:16:42 +0200 Subject: [PATCH 3/6] Remove comment --- src/types/any.rs | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/types/any.rs b/src/types/any.rs index 632ebce7751..00421d0017d 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -980,18 +980,14 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { where N: IntoPyObject<'py, Target = PyString>, { - // 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. + let py = self.py(); let result = unsafe { ffi::compat::PyObject_HasAttrWithError( self.as_ptr(), - attr_name - .into_pyobject(self.py()) - .map_err(Into::into)? - .as_ptr(), + attr_name.into_pyobject(py).map_err(Into::into)?.as_ptr(), ) }; - error_on_minusone(self.py(), result)?; + error_on_minusone(py, result)?; Ok(result > 0) } @@ -1027,10 +1023,7 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { match unsafe { ffi::compat::PyObject_GetOptionalAttr( self.as_ptr(), - attr_name - .into_pyobject(py) - .map_err(Into::into)? - .as_ptr(), + attr_name.into_pyobject(py).map_err(Into::into)?.as_ptr(), &mut resp_ptr, ) } { From 17b8f317efeed9e7a62a6b9416c6f6538b90e546 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Fri, 1 May 2026 15:30:06 +0200 Subject: [PATCH 4/6] Update changelog --- newsfragments/5944.added.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/newsfragments/5944.added.md b/newsfragments/5944.added.md index 1065ef3d9db..1cd74a33698 100644 --- a/newsfragments/5944.added.md +++ b/newsfragments/5944.added.md @@ -1 +1 @@ -add `pyo3-ffi::compat::PyObject_HasAttrWithError` & `pyo3-ffi::compat::PyObject_GetOptionalAttr` +add `pyo3-ffi::compat::PyObject_HasAttrWithError` From 6b2ae497b0d35572127161c9430c532f1e795ba7 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Fri, 1 May 2026 15:33:04 +0200 Subject: [PATCH 5/6] Fix --- src/types/any.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/any.rs b/src/types/any.rs index 9c3344983b6..7f1cc1517cd 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -11,7 +11,7 @@ use crate::py_result_ext::PyResultExt; use crate::type_object::{PyTypeCheck, PyTypeInfo}; use crate::types::PySuper; use crate::types::{PyDict, PyIterator, PyList, PyString, PyType}; -use crate::{err, ffi, Borrowed, BoundObject, IntoPyObjectExt, Py, Python}; +use crate::{err, ffi, Borrowed, BoundObject, IntoPyObjectExt, Py}; #[allow(deprecated)] use crate::{DowncastError, DowncastIntoError}; use std::cell::UnsafeCell; From 93314b5fa193dc1bcff6ab649a1b8260204d0b56 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers Date: Fri, 1 May 2026 15:41:57 +0200 Subject: [PATCH 6/6] Add back `inner` function --- src/types/any.rs | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/types/any.rs b/src/types/any.rs index 7f1cc1517cd..877c3fbd1a2 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -980,15 +980,23 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { where N: IntoPyObject<'py, Target = PyString>, { - let py = self.py(); - let result = unsafe { - ffi::compat::PyObject_HasAttrWithError( - self.as_ptr(), - attr_name.into_pyobject(py).map_err(Into::into)?.as_ptr(), - ) - }; - error_on_minusone(py, result)?; - Ok(result > 0) + fn inner<'py>( + any: &Bound<'py, PyAny>, + attr_name: Borrowed<'_, '_, PyString>, + ) -> PyResult { + let result = + unsafe { ffi::compat::PyObject_HasAttrWithError(any.as_ptr(), attr_name.as_ptr()) }; + error_on_minusone(any.py(), result)?; + Ok(result > 0) + } + + inner( + self, + attr_name + .into_pyobject(self.py()) + .map_err(Into::into)? + .as_borrowed(), + ) } fn getattr(&self, attr_name: N) -> PyResult>