Skip to content

Commit

Permalink
pyerr: improve debug & display impls
Browse files Browse the repository at this point in the history
  • Loading branch information
davidhewitt committed Nov 15, 2020
1 parent 8b61830 commit 6f99945
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 40 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- `#[pyclass(subclass)]` is now required for subclassing from Rust (was previously just required for subclassing from Python). [#1152](https://github.com/PyO3/pyo3/pull/1152)
- Change `PyIterator` to be consistent with other native types: it is now used as `&PyIterator` instead of `PyIterator<'a>`. [#1176](https://github.com/PyO3/pyo3/pull/1176)
- Change formatting of `PyDowncastError` messages to be closer to Python's builtin error messages. [#1212](https://github.com/PyO3/pyo3/pull/1212)
- Change `Debug` and `Display` impls for `PyException` to be consistent with `PyAny`. [#1275](https://github.com/PyO3/pyo3/pull/1275)
- Change `Debug` impl of `PyErr` to output more helpful information (acquiring the GIL if necessary). [#1275](https://github.com/PyO3/pyo3/pull/1275)

### Removed
- Remove deprecated ffi definitions `PyUnicode_AsUnicodeCopy`, `PyUnicode_GetMax`, `_Py_CheckRecursionLimit`, `PyObject_AsCharBuffer`, `PyObject_AsReadBuffer`, `PyObject_CheckReadBuffer` and `PyObject_AsWriteBuffer`, which will be removed in Python 3.10. [#1217](https://github.com/PyO3/pyo3/pull/1217)
Expand Down
72 changes: 68 additions & 4 deletions src/err/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ impl PyErr {
/// use pyo3::{Python, PyErr, exceptions::PyTypeError, types::PyType};
/// Python::with_gil(|py| {
/// let err = PyTypeError::new_err(("some type error",));
/// assert_eq!(err.pvalue(py).to_string(), "TypeError: some type error");
/// assert_eq!(format!("{:?}", err.pvalue(py)), "TypeError('some type error')");
/// });
/// ```
pub fn pvalue<'py>(&'py self, py: Python<'py>) -> &'py PyBaseException {
Expand Down Expand Up @@ -447,13 +447,28 @@ impl PyErr {

impl std::fmt::Debug for PyErr {
fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
f.write_str(format!("PyErr {{ type: {:?} }}", self.ptype_ptr()).as_str())
Python::with_gil(|py| {
f.debug_struct("PyErr")
.field("type", self.ptype(py))
.field("value", self.pvalue(py))
.field("traceback", &self.ptraceback(py))
.finish()
})
}
}

impl std::fmt::Display for PyErr {
fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
Python::with_gil(|py| self.instance(py).fmt(f))
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
Python::with_gil(|py| {
let instance = self.instance(py);
let type_name = instance.get_type().name().map_err(|_| std::fmt::Error)?;
write!(f, "{}", type_name)?;
if let Ok(s) = instance.str() {
write!(f, ": {}", &s.to_string_lossy())
} else {
write!(f, ": <exception str() failed>")
}
})
}
}

Expand Down Expand Up @@ -559,6 +574,55 @@ mod tests {
let _ = PyErr::fetch(py);
}

#[test]
fn err_debug() {
// Debug representation should be like the following (without the newlines):
// PyErr {
// type: <class 'Exception'>,
// value: Exception('banana'),
// traceback: Some(<traceback object at 0x..)"
// }

let gil = Python::acquire_gil();
let py = gil.python();
let err = py
.run("raise Exception('banana')", None, None)
.expect_err("raising should have given us an error");

let debug_str = format!("{:?}", err);
assert!(debug_str.starts_with("PyErr { "));
assert!(debug_str.ends_with(" }"));

let mut fields = debug_str
.strip_prefix("PyErr { ")
.unwrap()
.strip_suffix(" }")
.unwrap()
.split(", ");

assert_eq!(fields.next().unwrap(), "type: <class 'Exception'>");
#[cfg(not(Py_3_7))] // Python 3.6 and below formats the repr differently
assert_eq!(fields.next().unwrap(), ("value: Exception('banana',)"));
#[cfg(Py_3_7)]
assert_eq!(fields.next().unwrap(), "value: Exception('banana')");

let traceback = fields.next().unwrap();
assert!(traceback.starts_with("traceback: Some(<traceback object at 0x"));
assert!(traceback.ends_with(">)"));

assert!(fields.next().is_none());
}

#[test]
fn err_display() {
let gil = Python::acquire_gil();
let py = gil.python();
let err = py
.run("raise Exception('banana')", None, None)
.expect_err("raising should have given us an error");
assert_eq!(err.to_string(), "Exception: banana");
}

#[test]
fn test_pyerr_send_sync() {
fn is_send<T: Send>() {}
Expand Down
65 changes: 33 additions & 32 deletions src/exceptions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ use std::os::raw::c_char;
#[macro_export]
macro_rules! impl_exception_boilerplate {
($name: ident) => {
$crate::pyobject_native_type_fmt!($name);

impl std::convert::From<&$name> for $crate::PyErr {
fn from(err: &$name) -> $crate::PyErr {
$crate::PyErr::from_instance(err)
Expand All @@ -27,27 +29,6 @@ macro_rules! impl_exception_boilerplate {
}
}

impl std::fmt::Debug for $name {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let type_name = self.get_type().name().map_err(|_| std::fmt::Error)?;
f.debug_struct(type_name)
// TODO: print out actual fields!
.finish()
}
}

impl std::fmt::Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let type_name = self.get_type().name().map_err(|_| std::fmt::Error)?;
write!(f, "{}", type_name)?;
if let Ok(s) = self.str() {
write!(f, ": {}", &s.to_string_lossy())
} else {
write!(f, ": <exception str() failed>")
}
}
}

impl std::error::Error for $name {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
unsafe {
Expand Down Expand Up @@ -493,7 +474,6 @@ mod test {
use super::{PyException, PyUnicodeDecodeError};
use crate::types::{IntoPyDict, PyDict};
use crate::{PyErr, Python};
use std::error::Error;

import_exception!(socket, gaierror);
import_exception!(email.errors, MessageError);
Expand Down Expand Up @@ -580,8 +560,12 @@ mod test {
let exc = py
.run("raise Exception('banana')", None, None)
.expect_err("raising should have given us an error")
.into_instance(py);
assert_eq!(format!("{:?}", exc.as_ref(py)), "Exception");
.into_instance(py)
.into_ref(py);
assert_eq!(
format!("{:?}", exc),
exc.repr().unwrap().extract::<String>().unwrap()
);
}

#[test]
Expand All @@ -591,12 +575,18 @@ mod test {
let exc = py
.run("raise Exception('banana')", None, None)
.expect_err("raising should have given us an error")
.into_instance(py);
assert_eq!(exc.to_string(), "Exception: banana");
.into_instance(py)
.into_ref(py);
assert_eq!(
exc.to_string(),
exc.str().unwrap().extract::<String>().unwrap()
);
}

#[test]
fn native_exception_chain() {
use std::error::Error;

let gil = Python::acquire_gil();
let py = gil.python();
let exc = py
Expand All @@ -606,10 +596,21 @@ mod test {
None,
)
.expect_err("raising should have given us an error")
.into_instance(py);
assert_eq!(exc.to_string(), "Exception: banana");
let source = exc.as_ref(py).source().expect("cause should exist");
assert_eq!(source.to_string(), "TypeError: peach");
.into_instance(py)
.into_ref(py);

#[cfg(Py_3_7)]
assert_eq!(format!("{:?}", exc), "Exception('banana')");
#[cfg(not(Py_3_7))]
assert_eq!(format!("{:?}", exc), "Exception('banana',)");

let source = exc.source().expect("cause should exist");

#[cfg(Py_3_7)]
assert_eq!(format!("{:?}", source), "TypeError('peach')");
#[cfg(not(Py_3_7))]
assert_eq!(format!("{:?}", source), "TypeError('peach',)");

let source_source = source.source();
assert!(source_source.is_none(), "source_source should be None");
}
Expand All @@ -621,8 +622,8 @@ mod test {
Python::with_gil(|py| {
let decode_err = PyUnicodeDecodeError::new_utf8(py, invalid_utf8, err).unwrap();
assert_eq!(
decode_err.to_string(),
"UnicodeDecodeError: \'utf-8\' codec can\'t decode byte 0xd8 in position 2: invalid utf-8"
format!("{:?}", decode_err),
"UnicodeDecodeError('utf-8', b'fo\\xd8o', 2, 3, 'invalid utf-8')"
);

// Restoring should preserve the same error
Expand Down
5 changes: 1 addition & 4 deletions tests/test_inheritance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,7 @@ fn mutation_fails() {
let e = py
.run("obj.base_set(lambda: obj.sub_set_and_ret(1))", global, None)
.unwrap_err();
assert_eq!(
&e.instance(py).to_string(),
"RuntimeError: Already borrowed"
)
assert_eq!(&e.to_string(), "RuntimeError: Already borrowed")
}

#[pyclass(subclass)]
Expand Down

0 comments on commit 6f99945

Please sign in to comment.