From 4867ef8dd3fc719b92d9aee7c2409b7a2b6c0808 Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Sun, 25 Jun 2023 19:12:14 +0100 Subject: [PATCH] stop suppressing unrelated exceptions in `PyAny::hasattr` --- newsfragments/3271.fixed.md | 1 + src/types/any.rs | 85 +++++++++++++++++++++++++++++++++---- 2 files changed, 78 insertions(+), 8 deletions(-) create mode 100644 newsfragments/3271.fixed.md diff --git a/newsfragments/3271.fixed.md b/newsfragments/3271.fixed.md new file mode 100644 index 00000000..ff3fe715 --- /dev/null +++ b/newsfragments/3271.fixed.md @@ -0,0 +1 @@ +Stop suppressing unrelated exceptions in `PyAny::hasattr`. diff --git a/src/types/any.rs b/src/types/any.rs index c9dbd4c4..4a91e88d 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -1,7 +1,7 @@ use crate::class::basic::CompareOp; use crate::conversion::{AsPyPointer, FromPyObject, IntoPy, IntoPyPointer, PyTryFrom, ToPyObject}; use crate::err::{PyDowncastError, PyErr, PyResult}; -use crate::exceptions::PyTypeError; +use crate::exceptions::{PyAttributeError, PyTypeError}; use crate::type_object::PyTypeInfo; #[cfg(not(PyPy))] use crate::types::PySuper; @@ -79,14 +79,37 @@ impl PyAny { /// /// 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::{intern, pyfunction, types::PyModule, Python, PyResult}; + /// # + /// #[pyfunction] + /// fn has_version(sys: &PyModule) -> PyResult { + /// sys.hasattr(intern!(sys.py(), "version")) + /// } + /// # + /// # Python::with_gil(|py| { + /// # let sys = py.import("sys").unwrap(); + /// # has_version(sys).unwrap(); + /// # }); + /// ``` pub fn hasattr(&self, attr_name: N) -> PyResult where N: IntoPy>, { - let py = self.py(); - let attr_name = attr_name.into_py(py); + fn inner(any: &PyAny, attr_name: Py) -> PyResult { + // 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. + match any._getattr(attr_name) { + Ok(_) => Ok(true), + Err(err) if err.is_instance_of::(any.py()) => Ok(false), + Err(e) => Err(e), + } + } - unsafe { Ok(ffi::PyObject_HasAttr(self.as_ptr(), attr_name.as_ptr()) != 0) } + inner(self, attr_name.into_py(self.py())) } /// Retrieves an attribute value. @@ -115,12 +138,20 @@ impl PyAny { where N: IntoPy>, { - let py = self.py(); - let attr_name = attr_name.into_py(py); + fn inner(any: &PyAny, attr_name: Py) -> PyResult<&PyAny> { + any._getattr(attr_name) + .map(|object| object.into_ref(any.py())) + } + inner(self, attr_name.into_py(self.py())) + } + + fn _getattr(&self, attr_name: Py) -> PyResult { unsafe { - let ret = ffi::PyObject_GetAttr(self.as_ptr(), attr_name.as_ptr()); - py.from_owned_ptr_or_err(ret) + Py::from_owned_ptr_or_err( + self.py(), + ffi::PyObject_GetAttr(self.as_ptr(), attr_name.as_ptr()), + ) } } @@ -1160,6 +1191,44 @@ class SimpleClass: }); } + #[test] + fn test_hasattr() { + Python::with_gil(|py| { + let x = 5.to_object(py).into_ref(py); + assert!(x.is_instance_of::()); + + assert!(x.hasattr("to_bytes").unwrap()); + assert!(!x.hasattr("bbbbbbytes").unwrap()); + }) + } + + #[cfg(feature = "macros")] + #[test] + fn test_hasattr_error() { + use crate::exceptions::PyValueError; + use crate::prelude::*; + + #[pyclass(crate = "crate")] + struct GetattrFail; + + #[pymethods(crate = "crate")] + impl GetattrFail { + fn __getattr__(&self, attr: PyObject) -> PyResult { + Err(PyValueError::new_err(attr)) + } + } + + Python::with_gil(|py| { + let obj = Py::new(py, GetattrFail).unwrap(); + let obj = obj.as_ref(py).as_ref(); + + assert!(obj + .hasattr("foo") + .unwrap_err() + .is_instance_of::(py)); + }) + } + #[test] fn test_nan_eq() { Python::with_gil(|py| {