PyO3 / pyo3

Rust bindings for the Python interpreter
https://pyo3.rs
Apache License 2.0
12.32k stars 760 forks source link

Export something like `impl_to_pyerr` to allow easy conversion between a extern error to PyErr #2270

Closed leoleoasd closed 2 years ago

leoleoasd commented 2 years ago

Implementing std::convert::From for PyErr is required a lot when calling rust functions from python. PyO3 does implement errors from std using impl_to_pyerr macro, but this macro isn't exported, so users need to make their own implementation for custom rust errors.

Also, users can't implement std::convert::From<ExternalType> for PyErr, they need to wrap ExternalType and add error conversion everywhere the error is encountered.

For example: I created a macro to automate this wrapping process:

macro_rules! wrap_err {
    ($base: ty, $pybase: ty, $name: ident, $e: ident, $code: block) => {
        #[repr(transparent)]
        #[derive(Debug)]
        struct $name($base);

        impl std::convert::From<$name> for PyErr {
            fn from($e: $name) -> PyErr $code
        }
    }
}

Basically this macro wraps the given error and implements std::convert::From<$name> for PyErr. And when I need to use it:

create_exception!(my_module, MyError, PyException);

wrap_err!(ndarray::ShapeError, MyError, MyShapeError, err, {
    MyError::new_err(format!("Shape error: {}", err.0.to_string()))
});

// somewhere else
array.push_row(ArrayView::from(&[1,2,3])).or_else(|e| Err(MyShapeError(e)))?;

I need this ugly or_else statement for EVERY SINGLE ERROR HANDLING.

I don't know much about rust macros, for example, If PyO3 exports impl_to_pyerr and I call that macro from another package, can I break the "only traits defined in the current crate can be implemented for arbitrary types define and implement a trait or new type instead" rule? If this is possible, I can avoid the or_else statement, and if not, I think lots user needs to handle errors other than std's, so they needs wrap_err! macro.

davidhewitt commented 2 years ago

@leoleoasd I think you can already achieve what you want by implementing From<ExternalType> for MyShapeError, and returning MyShapeError from your #[pyfunction] implementations. If you also implement From<MyShapeError> for PyErr, then PyO3 will accept that as the return type.

Something like this:

#[pyfunction]
fn function_returning_custom_error() -> Result<(), MyShapeError> {  // Uses From<MyShapeError> for PyErr
    array.push_row(ArrayView::from(&[1,2,3]))?;  // Uses From<ExternalType> for MyShapeError
    Ok(())
}
leoleoasd commented 2 years ago

What if my function may return multiple types of errors?

davidhewitt commented 2 years ago

You can still write a custom error enum (e.g. maybe by thiserror) with the same idea; as many custom error types as you like can convert to the custom enum and then you can implement a conversion from the enum to PyErr.

leoleoasd commented 2 years ago

Would this method still require a type conversion between custom error with custom enum?

EDIT: No.

leoleoasd commented 2 years ago

I solved it! Just define a error enum like this:

// use quick_error package to generate some code
quick_error! {
    #[derive(Debug)]
    pub enum PreprocessErrorWrap {
        Metis(err: metis::Error) {
            // generate error source
            source(err)
            // and implement From<metis::Error> for PreprocessErrorWrap
            from()
        }
        Shape(err: ndarray::ShapeError) {
            source(err)
            from()
        }
        IO(err: std::io::Error){
            source(err)
            from()
        }
        Python(err: pyo3::PyErr) {
            source(err)
            from()
        }
    }
}

And implement From for PyErr:

impl std::convert::From<PreprocessErrorWrap> for PyErr {
    fn from(err: PreprocessErrorWrap) -> PyErr {
        match err {
            PreprocessErrorWrap::Python(err) => {
                err
            }
            _ => {
                PreprocessError::new_err(err.to_string())
            }
        }
    }
}

Then, a pyfunction returns Result<_, PreprocessErrorWrap> would be accepted by PyO3 and any error in that enum would also be accepted.