From f02d0d17e9fc4a87e4338540c1455c3ae7c11a21 Mon Sep 17 00:00:00 2001 From: Adam Reichold Date: Sun, 10 Dec 2023 16:55:13 +0100 Subject: [PATCH] Try harder by looking for a __bool__ magic method when extracing bool values from Python objects. I decided to not implement the full protocol for truth value testing [1] as it seems confusing in the context of function arguments if basically any instance of custom class or non-empty collection turns into `true`. [1] https://docs.python.org/3/library/stdtypes.html#truth --- newsfragments/3638.changed.md | 1 + src/types/any.rs | 1 - src/types/boolobject.rs | 61 +++++++++++++++++++++++++++++++++-- 3 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 newsfragments/3638.changed.md diff --git a/newsfragments/3638.changed.md b/newsfragments/3638.changed.md new file mode 100644 index 00000000000..83f0bd74d08 --- /dev/null +++ b/newsfragments/3638.changed.md @@ -0,0 +1 @@ +Values of type `bool` can now be extracted from all Python values defining a `__bool__` magic method. diff --git a/src/types/any.rs b/src/types/any.rs index dc29b1dea56..43bce57d3c6 100644 --- a/src/types/any.rs +++ b/src/types/any.rs @@ -144,7 +144,6 @@ impl PyAny { /// /// To avoid repeated temporary allocations of Python strings, the [`intern!`] macro can be used /// to intern `attr_name`. - #[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>, diff --git a/src/types/boolobject.rs b/src/types/boolobject.rs index 1f42db90c3b..39945d599b6 100644 --- a/src/types/boolobject.rs +++ b/src/types/boolobject.rs @@ -1,6 +1,9 @@ #[cfg(feature = "experimental-inspect")] use crate::inspect::types::TypeInfo; -use crate::{ffi, FromPyObject, IntoPy, PyAny, PyObject, PyResult, Python, ToPyObject}; +use crate::{ + exceptions::PyTypeError, ffi, intern, FromPyObject, IntoPy, PyAny, PyObject, PyResult, Python, + ToPyObject, +}; /// Represents a Python `bool`. #[repr(transparent)] @@ -56,7 +59,16 @@ impl IntoPy for bool { /// Fails with `TypeError` if the input is not a Python `bool`. impl<'source> FromPyObject<'source> for bool { fn extract(obj: &'source PyAny) -> PyResult { - Ok(obj.downcast::()?.is_true()) + if let Ok(obj) = obj.downcast::() { + return Ok(obj.is_true()); + } + + let meth = obj + .lookup_special(intern!(obj.py(), "__bool__"))? + .ok_or_else(|| PyTypeError::new_err("object has no __bool__ magic method"))?; + + let obj = meth.call0()?.downcast::()?; + Ok(obj.is_true()) } #[cfg(feature = "experimental-inspect")] @@ -67,7 +79,7 @@ impl<'source> FromPyObject<'source> for bool { #[cfg(test)] mod tests { - use crate::types::{PyAny, PyBool}; + use crate::types::{PyAny, PyBool, PyModule}; use crate::Python; use crate::ToPyObject; @@ -90,4 +102,47 @@ mod tests { assert!(false.to_object(py).is(PyBool::new(py, false))); }); } + + #[test] + fn test_magic_method() { + Python::with_gil(|py| { + let module = PyModule::from_code( + py, + r#" +class A: + def __bool__(self): return True +class B: + def __bool__(self): return "not a bool" +class C: + def __len__(self): return 23 +class D: + pass + "#, + "test.py", + "test", + ) + .unwrap(); + + let a = module.getattr("A").unwrap().call0().unwrap(); + assert_eq!(a.extract::().unwrap(), true); + + let b = module.getattr("B").unwrap().call0().unwrap(); + assert_eq!( + b.extract::().unwrap_err().to_string(), + "TypeError: 'str' object cannot be converted to 'PyBool'", + ); + + let c = module.getattr("C").unwrap().call0().unwrap(); + assert_eq!( + c.extract::().unwrap_err().to_string(), + "TypeError: object has no __bool__ magic method", + ); + + let d = module.getattr("D").unwrap().call0().unwrap(); + assert_eq!( + c.extract::().unwrap_err().to_string(), + "TypeError: object has no __bool__ magic method", + ); + }); + } }