Skip to content

Commit 9e348a9

Browse files
committed
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
1 parent 24d9113 commit 9e348a9

File tree

3 files changed

+59
-4
lines changed

3 files changed

+59
-4
lines changed

newsfragments/3638.changed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Values of type `bool` can now be extracted from all Python values defining a `__bool__` magic method.

src/types/any.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,6 @@ impl PyAny {
144144
///
145145
/// To avoid repeated temporary allocations of Python strings, the [`intern!`] macro can be used
146146
/// to intern `attr_name`.
147-
#[allow(dead_code)] // Currently only used with num-complex+abi3, so dead without that.
148147
pub(crate) fn lookup_special<N>(&self, attr_name: N) -> PyResult<Option<&PyAny>>
149148
where
150149
N: IntoPy<Py<PyString>>,

src/types/boolobject.rs

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
#[cfg(feature = "experimental-inspect")]
22
use crate::inspect::types::TypeInfo;
3-
use crate::{ffi, FromPyObject, IntoPy, PyAny, PyObject, PyResult, Python, ToPyObject};
3+
use crate::{
4+
exceptions::PyTypeError, ffi, intern, FromPyObject, IntoPy, PyAny, PyObject, PyResult, Python,
5+
ToPyObject,
6+
};
47

58
/// Represents a Python `bool`.
69
#[repr(transparent)]
@@ -56,7 +59,16 @@ impl IntoPy<PyObject> for bool {
5659
/// Fails with `TypeError` if the input is not a Python `bool`.
5760
impl<'source> FromPyObject<'source> for bool {
5861
fn extract(obj: &'source PyAny) -> PyResult<Self> {
59-
Ok(obj.downcast::<PyBool>()?.is_true())
62+
if let Ok(obj) = obj.downcast::<PyBool>() {
63+
return Ok(obj.is_true());
64+
}
65+
66+
let meth = obj
67+
.lookup_special(intern!(obj.py(), "__bool__"))?
68+
.ok_or_else(|| PyTypeError::new_err("object has no __bool__ magic method"))?;
69+
70+
let obj = meth.call0()?.downcast::<PyBool>()?;
71+
Ok(obj.is_true())
6072
}
6173

6274
#[cfg(feature = "experimental-inspect")]
@@ -67,7 +79,7 @@ impl<'source> FromPyObject<'source> for bool {
6779

6880
#[cfg(test)]
6981
mod tests {
70-
use crate::types::{PyAny, PyBool};
82+
use crate::types::{PyAny, PyBool, PyModule};
7183
use crate::Python;
7284
use crate::ToPyObject;
7385

@@ -90,4 +102,47 @@ mod tests {
90102
assert!(false.to_object(py).is(PyBool::new(py, false)));
91103
});
92104
}
105+
106+
#[test]
107+
fn test_magic_method() {
108+
Python::with_gil(|py| {
109+
let module = PyModule::from_code(
110+
py,
111+
r#"
112+
class A:
113+
def __bool__(self): return True
114+
class B:
115+
def __bool__(self): return "not a bool"
116+
class C:
117+
def __len__(self): return 23
118+
class D:
119+
pass
120+
"#,
121+
"test.py",
122+
"test",
123+
)
124+
.unwrap();
125+
126+
let a = module.getattr("A").unwrap().call0().unwrap();
127+
assert!(a.extract::<bool>().unwrap());
128+
129+
let b = module.getattr("B").unwrap().call0().unwrap();
130+
assert_eq!(
131+
b.extract::<bool>().unwrap_err().to_string(),
132+
"TypeError: 'str' object cannot be converted to 'PyBool'",
133+
);
134+
135+
let c = module.getattr("C").unwrap().call0().unwrap();
136+
assert_eq!(
137+
c.extract::<bool>().unwrap_err().to_string(),
138+
"TypeError: object has no __bool__ magic method",
139+
);
140+
141+
let d = module.getattr("D").unwrap().call0().unwrap();
142+
assert_eq!(
143+
d.extract::<bool>().unwrap_err().to_string(),
144+
"TypeError: object has no __bool__ magic method",
145+
);
146+
});
147+
}
93148
}

0 commit comments

Comments
 (0)