From fb05e1d7a7e71ade994d97041d35362ea8c0af62 Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Sun, 21 Aug 2022 16:23:34 +0100 Subject: [PATCH] guide: additional detail on how to handle foreign errors --- guide/src/SUMMARY.md | 1 + guide/src/exception.md | 130 +--------------- guide/src/function/error_handling.md | 222 +++++++++++++++++++++++++++ 3 files changed, 225 insertions(+), 128 deletions(-) create mode 100644 guide/src/function/error_handling.md diff --git a/guide/src/SUMMARY.md b/guide/src/SUMMARY.md index e07b41a7..2198c379 100644 --- a/guide/src/SUMMARY.md +++ b/guide/src/SUMMARY.md @@ -8,6 +8,7 @@ - [Python modules](module.md) - [Python functions](function.md) - [Function signatures](function/signature.md) + - [Error handling](function/error_handling.md) - [Python classes](class.md) - [Class customizations](class/protocols.md) - [Basic object customization](class/object.md) diff --git a/guide/src/exception.md b/guide/src/exception.md index 518122de..385be998 100644 --- a/guide/src/exception.md +++ b/guide/src/exception.md @@ -52,35 +52,9 @@ fn mymodule(py: Python<'_>, m: &PyModule) -> PyResult<()> { ## Raising an exception -To raise an exception from `pyfunction`s and `pymethods`, you should return an `Err(PyErr)`. -If returned to Python code, this [`PyErr`] will then be raised as a Python exception. Many PyO3 APIs also return [`PyResult`]. +As described in the [function error handling](./function/error_handling.md) chapter, to raise an exception from a `#[pyfunction]` or `#[pymethods]`, return an `Err(PyErr)`. PyO3 will automatically raise this exception for you when returing the result to Python. -If a Rust type exists for the exception, then it is possible to use the `new_err` method. -For example, each standard exception defined in the `pyo3::exceptions` module -has a corresponding Rust type and exceptions defined by [`create_exception!`] and [`import_exception!`] macro have Rust types as well. - -```rust -use pyo3::exceptions::PyZeroDivisionError; -use pyo3::prelude::*; - -#[pyfunction] -fn divide(a: i32, b: i32) -> PyResult { - match a.checked_div(b) { - Some(q) => Ok(q), - None => Err(PyZeroDivisionError::new_err("division by zero")), - } -} -# -# fn main(){ -# Python::with_gil(|py|{ -# let fun = pyo3::wrap_pyfunction!(divide, py).unwrap(); -# fun.call1((1,0)).unwrap_err(); -# fun.call1((1,1)).unwrap(); -# }); -# } -``` - -You can manually write and fetch errors in the Python interpreter's global state: +You can also manually write and fetch errors in the Python interpreter's global state: ```rust use pyo3::{Python, PyErr}; @@ -93,8 +67,6 @@ Python::with_gil(|py| { }); ``` -If you already have a Python exception object, you can use [`PyErr::from_value`] to create a `PyErr` from it. - ## Checking exception types Python has an [`isinstance`](https://docs.python.org/3/library/functions.html#isinstance) method to check an object's type. @@ -123,104 +95,6 @@ err.is_instance_of::(py); # }); ``` -## Handling Rust errors - -The vast majority of operations in this library will return -[`PyResult`]({{#PYO3_DOCS_URL}}/pyo3/prelude/type.PyResult.html), -which is an alias for the type `Result`. - -A [`PyErr`] represents a Python exception. Errors within the PyO3 library are also exposed as -Python exceptions. - -If your code has a custom error type, adding an implementation of `std::convert::From for PyErr` -is usually enough. PyO3 will then automatically convert your error to a Python exception when needed. - -The following code snippet defines a Rust error named `CustomIOError`. In its `From for PyErr` -implementation it returns a `PyErr` representing Python's `OSError`. - -```rust -use pyo3::exceptions::PyOSError; -use pyo3::prelude::*; -use std::fmt; - -#[derive(Debug)] -struct CustomIOError; - -impl std::error::Error for CustomIOError {} - -impl fmt::Display for CustomIOError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Oh no!") - } -} - -impl std::convert::From for PyErr { - fn from(err: CustomIOError) -> PyErr { - PyOSError::new_err(err.to_string()) - } -} - -pub struct Connection { /* ... */} - -fn bind(addr: String) -> Result { - if &addr == "0.0.0.0"{ - Err(CustomIOError) - } else { - Ok(Connection{ /* ... */}) - } -} - -#[pyfunction] -fn connect(s: String) -> Result<(), CustomIOError> { - bind(s)?; - Ok(()) -} - -fn main() { - Python::with_gil(|py| { - let fun = pyo3::wrap_pyfunction!(connect, py).unwrap(); - let err = fun.call1(("0.0.0.0",)).unwrap_err(); - assert!(err.is_instance_of::(py)); - }); -} -``` - -This has been implemented for most of Rust's standard library errors, so that you can use the `?` -("try") operator with them. The following code snippet will raise a `ValueError` in Python if -`String::parse()` returns an error. - -```rust -use pyo3::prelude::*; - -fn parse_int(s: String) -> PyResult { - Ok(s.parse::()?) -} -# -# use pyo3::exceptions::PyValueError; -# -# fn main() { -# Python::with_gil(|py| { -# assert_eq!(parse_int(String::from("1")).unwrap(), 1); -# assert_eq!(parse_int(String::from("1337")).unwrap(), 1337); -# -# assert!(parse_int(String::from("-1")) -# .unwrap_err() -# .is_instance_of::(py)); -# assert!(parse_int(String::from("foo")) -# .unwrap_err() -# .is_instance_of::(py)); -# assert!(parse_int(String::from("13.37")) -# .unwrap_err() -# .is_instance_of::(py)); -# }) -# } -``` - -If lazy construction of the Python exception instance is desired, the -[`PyErrArguments`]({{#PYO3_DOCS_URL}}/pyo3/trait.PyErrArguments.html) -trait can be implemented. In that case, actual exception argument creation is delayed -until the `PyErr` is needed. - ## Using exceptions defined in Python code It is possible to use an exception defined in Python code as a native Rust type. diff --git a/guide/src/function/error_handling.md b/guide/src/function/error_handling.md new file mode 100644 index 00000000..6e722568 --- /dev/null +++ b/guide/src/function/error_handling.md @@ -0,0 +1,222 @@ +# Error handling + +This chapter contains a little background of error handling in Rust and how PyO3 integrates this with Python exceptions. + +This covers enough detail to create a `#[pyfunction]` which raises Python exceptions from errors originating in Rust. + +There is a later section of the guide on [Python exceptions](../exception.md) which covers exception types in more detail. + +## Representing Python exceptions + +Rust code uses the generic [`Result`] enum to propagate errors. The error type `E` is chosen by the code author to describe the possible errors which can happen. + +PyO3 has the [`PyErr`] type which represents a Python exception. If a PyO3 API could result in a Python exception being raised, the return type of that `API` will be [`PyResult`], which is an alias for the type `Result`. + +In summary: +- When Python exceptions are raised and caught by PyO3, the exception will stored in the `Err` variant of the `PyResult`. +- Passing Python exceptions through Rust code then uses all the "normal" techniques such as the `?` operator, with `PyErr` as the error type. +- Finally, when a `PyResult` crosses from Rust back to Python via PyO3, if the result is an `Err` variant the contained exception will be raised. + +(There are many great tutorials on Rust error handling and the `?` operator, so this guide will not go into detail on Rust-specific topics.) + +## Raising an exception from a function + +As indicated in the previous section, when a `PyResult` containing an `Err` crosses from Rust to Python, PyO3 will raise the exception contained within. + +Accordingly, to raise an exception from a `#[pyfunction]`, change the return type `T` to `PyResult`. When the function returns an `Err` it will raise a Python exception. (Other `Result` types can be used as long as the error `E` has a `From` conversion for `PyErr`, see [implementing a conversion](#implementing-an-error-conversion) below.) + +This also works for functions in `#[pymethods]`. + +For example, the following `check_positive` function raises a `ValueError` when the input is negative: + +```rust +use pyo3::exceptions::PyValueError; +use pyo3::prelude::*; + +#[pyfunction] +fn check_positive(x: i32) -> PyResult<()> { + if x < 0 { + Err(PyValueError::new_err("x is negative")) + } else { + Ok(()) + } +} +# +# fn main(){ +# Python::with_gil(|py|{ +# let fun = pyo3::wrap_pyfunction!(check_positive, py).unwrap(); +# fun.call1((-1,)).unwrap_err(); +# fun.call1((1)).unwrap(); +# }); +# } +``` + +All built-in Python exception types are defined in the [`pyo3::exceptions`] module. They have a `new_err` constructor to directly build a `PyErr`, as seen in the example above. + +## Custom Rust error types + +PyO3 will automatically convert a `Result` returned by a `#[pyfunction]` into a `PyResult` as long as there is an implementation of `std::from::From for PyErr`. Many error types in the Rust standard library have a [`From`] conversion defined in this way. + +If the type `E` you are handling is defined in a third-party crate, see the section on [foreign rust error types](#foreign-rust-error-types) below for ways to work with this error. + +The following example makes use of the implementation of `From for PyErr` to raise exceptions encountered when parsing strings as integers: + +```rust +# use pyo3::prelude::*; +use std::num::ParseIntError; + +#[pyfunction] +fn parse_int(x: &str) -> Result { + x.parse() +} +``` + +When passed a string which doesn't contain a floating-point number, the exception raised will look like the below: + +```python +>>> parse_int("bar") +Traceback (most recent call last): + File "", line 1, in +ValueError: invalid digit found in string +``` + +As a more complete example, the following snippet defines a Rust error named `CustomIOError`. It then defines a `From for PyErr`, which returns a `PyErr` representing Python's `OSError`. Finally, it + +```rust +use pyo3::exceptions::PyOSError; +use pyo3::prelude::*; +use std::fmt; + +#[derive(Debug)] +struct CustomIOError; + +impl std::error::Error for CustomIOError {} + +impl fmt::Display for CustomIOError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Oh no!") + } +} + +impl std::convert::From for PyErr { + fn from(err: CustomIOError) -> PyErr { + PyOSError::new_err(err.to_string()) + } +} + +pub struct Connection { /* ... */} + +fn bind(addr: String) -> Result { + if &addr == "0.0.0.0"{ + Err(CustomIOError) + } else { + Ok(Connection{ /* ... */}) + } +} + +#[pyfunction] +fn connect(s: String) -> Result<(), CustomIOError> { + bind(s)?; + Ok(()) +} + +fn main() { + Python::with_gil(|py| { + let fun = pyo3::wrap_pyfunction!(connect, py).unwrap(); + let err = fun.call1(("0.0.0.0",)).unwrap_err(); + assert!(err.is_instance_of::(py)); + }); +} +``` + +If lazy construction of the Python exception instance is desired, the +[`PyErrArguments`]({{#PYO3_DOCS_URL}}/pyo3/trait.PyErrArguments.html) +trait can be implemented instead of `From`. In that case, actual exception argument creation is delayed +until the `PyErr` is needed. + +A final note is that any errors `E` which have a `From` conversion can be used with the `?` +("try") operator with them. An alternative implementation of the above `parse_int` which instead returns `PyResult` is below: + +```rust +use pyo3::prelude::*; + +fn parse_int(s: String) -> PyResult { + let x = s.parse()?; + Ok(x) +} +# +# use pyo3::exceptions::PyValueError; +# +# fn main() { +# Python::with_gil(|py| { +# assert_eq!(parse_int(String::from("1")).unwrap(), 1); +# assert_eq!(parse_int(String::from("1337")).unwrap(), 1337); +# +# assert!(parse_int(String::from("-1")) +# .unwrap_err() +# .is_instance_of::(py)); +# assert!(parse_int(String::from("foo")) +# .unwrap_err() +# .is_instance_of::(py)); +# assert!(parse_int(String::from("13.37")) +# .unwrap_err() +# .is_instance_of::(py)); +# }) +# } +``` + +## Foreign Rust error types + +The Rust compiler will not permit implementation of traits for types outside of the crate where the type is defined. (This is known as the "orphan rule".) + +Given a type `OtherError` which is defined in thirdparty code, there are two main strategies available to integrate it with PyO3: + +- Create a newtype wrapper, e.g. `MyOtherError`. Then implement `From for PyErr` (or `PyErrArguments`), as well as `From` for `MyOtherError`. +- Use Rust's Result combinators such as `map_err` to write code freely to convert `OtherError` into whatever is needed. This requires boilerplate at every usage however gives unlimited flexibility. + +To detail the newtype strategy a little further, the key trick is to return `Result` from the `#[pyfunction]`. This means that PyO3 will make use of `From for PyErr` to create Python exceptions while the `#[pyfunction]` implementation can use `?` to convert `OtherError` to `MyOtherError` automatically. + +The following example demonstrates this for some imaginary thirdparty crate `some_crate` with a function `get_x` returning `Result`: + +```rust +# mod some_crate { +# struct OtherError(()); +# impl OtherError { +# fn message() -> &'static str { "some error occurred" } +# } +# fn get_x() -> Result +# } + +use pyo3::prelude::*; +use some_crate::{OtherError, get_x}; + +struct MyOtherError(OtherError); + +impl From for PyErr { + fn from(error: MyOtherError) -> Self { + PyValueError::new_err(self.0.message()) + } +} + +impl From for MyOtherError { + fn from(other: OtherError) -> Self { + Self(other) + } +} + +#[pyfunction] +fn wrapped_get_x() -> Result { + // get_x is a function returning Result + let x: i32 = get_x()?; + Ok(x) +} +``` + + +[`From`]: https://doc.rust-lang.org/stable/std/convert/trait.From.html +[`Result`]: https://doc.rust-lang.org/stable/std/result/enum.Result.html + +[`PyResult`]: {{#PYO3_DOCS_URL}}/pyo3/prelude/type.PyResult.html +[`PyResult`]: {{#PYO3_DOCS_URL}}/pyo3/prelude/type.PyResult.html +[`PyErr`]: {{#PYO3_DOCS_URL}}/pyo3/struct.PyErr.html +[`pyo3::exceptions`]: {{#PYO3_DOCS_URL}}/pyo3/exceptions/index.html