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:
parent
87e42c96be
commit
8133aaa5d8
|
@ -0,0 +1 @@
|
|||
Values of type `bool` can now be extracted from all Python values defining a `__bool__` magic method.
|
|
@ -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>>,
|
||||
|
|
|
@ -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",
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue