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
This commit is contained in:
Adam Reichold 2023-12-10 16:55:13 +01:00
parent 87e42c96be
commit 8133aaa5d8
3 changed files with 57 additions and 4 deletions

View File

@ -0,0 +1 @@
Values of type `bool` can now be extracted from all Python values defining a `__bool__` magic method.

View File

@ -146,7 +146,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<N>(&self, attr_name: N) -> PyResult<Option<&PyAny>>
where
N: IntoPy<Py<PyString>>,

View File

@ -1,7 +1,8 @@
#[cfg(feature = "experimental-inspect")]
use crate::inspect::types::TypeInfo;
use crate::{
ffi, instance::Py2, FromPyObject, IntoPy, PyAny, PyObject, PyResult, Python, ToPyObject,
exceptions::PyTypeError, ffi, instance::Py2, FromPyObject, IntoPy, PyAny, PyObject, PyResult,
Python, ToPyObject,
};
/// Represents a Python `bool`.
@ -76,7 +77,16 @@ impl IntoPy<PyObject> 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<Self> {
Ok(obj.downcast::<PyBool>()?.is_true())
if let Ok(obj) = obj.downcast::<PyBool>() {
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::<PyBool>()?;
Ok(obj.is_true())
}
#[cfg(feature = "experimental-inspect")]
@ -87,7 +97,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;
@ -110,4 +120,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!(a.extract::<bool>().unwrap());
let b = module.getattr("B").unwrap().call0().unwrap();
assert_eq!(
b.extract::<bool>().unwrap_err().to_string(),
"TypeError: 'str' object cannot be converted to 'PyBool'",
);
let c = module.getattr("C").unwrap().call0().unwrap();
assert_eq!(
c.extract::<bool>().unwrap_err().to_string(),
"TypeError: object has no __bool__ magic method",
);
let d = module.getattr("D").unwrap().call0().unwrap();
assert_eq!(
d.extract::<bool>().unwrap_err().to_string(),
"TypeError: object has no __bool__ magic method",
);
});
}
}