diff --git a/newsfragments/5985.fixed.md b/newsfragments/5985.fixed.md new file mode 100644 index 00000000000..052cec3ed7e --- /dev/null +++ b/newsfragments/5985.fixed.md @@ -0,0 +1 @@ +`getattr_opt` now correctly treats `AttributeError` subclasses as missing attributes on Python < 3.13. diff --git a/pyo3-ffi/src/compat/py_3_13.rs b/pyo3-ffi/src/compat/py_3_13.rs index 26b9f2698fb..0c766b0f86a 100644 --- a/pyo3-ffi/src/compat/py_3_13.rs +++ b/pyo3-ffi/src/compat/py_3_13.rs @@ -177,3 +177,28 @@ compat_function!( 0 } ); + +compat_function!( + originally_defined_for(Py_3_13); + + #[inline] + pub unsafe fn PyObject_GetOptionalAttr( + obj: *mut crate::PyObject, + name: *mut crate::PyObject, + result: *mut *mut crate::PyObject, + ) -> std::ffi::c_int { + use crate::{PyErr_Clear, PyErr_ExceptionMatches, PyExc_AttributeError, PyObject_GetAttr}; + + let attr = PyObject_GetAttr(obj, name); + if !attr.is_null() { + *result = attr; + return 1; // found + } + *result = std::ptr::null_mut(); + if PyErr_ExceptionMatches(PyExc_AttributeError) != 0 { + PyErr_Clear(); + return 0; // not found + } + -1 // other error + } +); diff --git a/src/types/any.rs b/src/types/any.rs index 8923f429efa..9ae17d35dbc 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -1024,39 +1024,20 @@ impl<'py> PyAnyMethods<'py> for Bound<'py, PyAny> { 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 mut resp_ptr: *mut ffi::PyObject = std::ptr::null_mut(); + match unsafe { + ffi::compat::PyObject_GetOptionalAttr( + any.as_ptr(), + attr_name.as_ptr(), + &mut resp_ptr, + ) + } { + // Attribute found, result is a new strong reference + 1 => Ok(Some(unsafe { Bound::from_owned_ptr(any.py(), resp_ptr) })), + // Attribute not found + 0 => Ok(None), + // An error occurred (other than AttributeError) + _ => Err(PyErr::fetch(any.py())), } } @@ -1789,6 +1770,33 @@ class Test: }); } + #[test] + fn test_getattr_opt_attribute_error_subclass() { + Python::attach(|py| { + let module = PyModule::from_code( + py, + cr#" +class CustomAttrError(AttributeError): + pass + +class Obj: + @property + def missing(self): + raise CustomAttrError("not here") + "#, + c"test.py", + &generate_unique_module_name("test"), + ) + .unwrap(); + + let obj = module.getattr("Obj").unwrap().call0().unwrap(); + + // An AttributeError subclass should be treated as "attribute not found" + let result = obj.getattr_opt("missing").unwrap(); + assert!(result.is_none()); + }); + } + #[test] fn test_call_for_non_existing_method() { Python::attach(|py| {