Skip to content

Commit

Permalink
Try harder by looking for a __bool__ magic method when extracing bool…
Browse files Browse the repository at this point in the history
… 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
  • Loading branch information
adamreichold committed Dec 10, 2023
1 parent 24d9113 commit f02d0d1
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 4 deletions.
1 change: 1 addition & 0 deletions newsfragments/3638.changed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Values of type `bool` can now be extracted from all Python values defining a `__bool__` magic method.
1 change: 0 additions & 1 deletion src/types/any.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<N>(&self, attr_name: N) -> PyResult<Option<&PyAny>>
where
N: IntoPy<Py<PyString>>,
Expand Down
61 changes: 58 additions & 3 deletions src/types/boolobject.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand Down Expand Up @@ -56,7 +59,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")]
Expand All @@ -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;

Expand All @@ -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::<bool>().unwrap(), true);

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!(
c.extract::<bool>().unwrap_err().to_string(),
"TypeError: object has no __bool__ magic method",
);
});
}
}

0 comments on commit f02d0d1

Please sign in to comment.