guide: additional detail on how to handle foreign errors
This commit is contained in:
parent
580e747521
commit
fb05e1d7a7
|
@ -8,6 +8,7 @@
|
||||||
- [Python modules](module.md)
|
- [Python modules](module.md)
|
||||||
- [Python functions](function.md)
|
- [Python functions](function.md)
|
||||||
- [Function signatures](function/signature.md)
|
- [Function signatures](function/signature.md)
|
||||||
|
- [Error handling](function/error_handling.md)
|
||||||
- [Python classes](class.md)
|
- [Python classes](class.md)
|
||||||
- [Class customizations](class/protocols.md)
|
- [Class customizations](class/protocols.md)
|
||||||
- [Basic object customization](class/object.md)
|
- [Basic object customization](class/object.md)
|
||||||
|
|
|
@ -52,35 +52,9 @@ fn mymodule(py: Python<'_>, m: &PyModule) -> PyResult<()> {
|
||||||
|
|
||||||
## Raising an exception
|
## Raising an exception
|
||||||
|
|
||||||
To raise an exception from `pyfunction`s and `pymethods`, you should return an `Err(PyErr)`.
|
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 returned to Python code, this [`PyErr`] will then be raised as a Python exception. Many PyO3 APIs also return [`PyResult`].
|
|
||||||
|
|
||||||
If a Rust type exists for the exception, then it is possible to use the `new_err` method.
|
You can also manually write and fetch errors in the Python interpreter's global state:
|
||||||
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<i32> {
|
|
||||||
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:
|
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use pyo3::{Python, PyErr};
|
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
|
## Checking exception types
|
||||||
|
|
||||||
Python has an [`isinstance`](https://docs.python.org/3/library/functions.html#isinstance) method to check an object's type.
|
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::<PyTypeError>(py);
|
||||||
# });
|
# });
|
||||||
```
|
```
|
||||||
|
|
||||||
## Handling Rust errors
|
|
||||||
|
|
||||||
The vast majority of operations in this library will return
|
|
||||||
[`PyResult<T>`]({{#PYO3_DOCS_URL}}/pyo3/prelude/type.PyResult.html),
|
|
||||||
which is an alias for the type `Result<T, PyErr>`.
|
|
||||||
|
|
||||||
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<MyError> 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<CustomIOError> 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<CustomIOError> for PyErr {
|
|
||||||
fn from(err: CustomIOError) -> PyErr {
|
|
||||||
PyOSError::new_err(err.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Connection { /* ... */}
|
|
||||||
|
|
||||||
fn bind(addr: String) -> Result<Connection, CustomIOError> {
|
|
||||||
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::<PyOSError>(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<usize> {
|
|
||||||
Ok(s.parse::<usize>()?)
|
|
||||||
}
|
|
||||||
#
|
|
||||||
# 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::<PyValueError>(py));
|
|
||||||
# assert!(parse_int(String::from("foo"))
|
|
||||||
# .unwrap_err()
|
|
||||||
# .is_instance_of::<PyValueError>(py));
|
|
||||||
# assert!(parse_int(String::from("13.37"))
|
|
||||||
# .unwrap_err()
|
|
||||||
# .is_instance_of::<PyValueError>(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
|
## Using exceptions defined in Python code
|
||||||
|
|
||||||
It is possible to use an exception defined in Python code as a native Rust type.
|
It is possible to use an exception defined in Python code as a native Rust type.
|
||||||
|
|
|
@ -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<T, E>`] 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<T>`], which is an alias for the type `Result<T, PyErr>`.
|
||||||
|
|
||||||
|
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<T>`. When the function returns an `Err` it will raise a Python exception. (Other `Result<T, E>` 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<T, E>` returned by a `#[pyfunction]` into a `PyResult<T>` as long as there is an implementation of `std::from::From<E> 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<ParseIntError> 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<usize, ParseIntError> {
|
||||||
|
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 "<stdin>", line 1, in <module>
|
||||||
|
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<CustomIOError> 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<CustomIOError> for PyErr {
|
||||||
|
fn from(err: CustomIOError) -> PyErr {
|
||||||
|
PyOSError::new_err(err.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Connection { /* ... */}
|
||||||
|
|
||||||
|
fn bind(addr: String) -> Result<Connection, CustomIOError> {
|
||||||
|
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::<PyOSError>(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<usize> {
|
||||||
|
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::<PyValueError>(py));
|
||||||
|
# assert!(parse_int(String::from("foo"))
|
||||||
|
# .unwrap_err()
|
||||||
|
# .is_instance_of::<PyValueError>(py));
|
||||||
|
# assert!(parse_int(String::from("13.37"))
|
||||||
|
# .unwrap_err()
|
||||||
|
# .is_instance_of::<PyValueError>(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<MyOtherError> for PyErr` (or `PyErrArguments`), as well as `From<OtherError>` 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<T, MyOtherError>` from the `#[pyfunction]`. This means that PyO3 will make use of `From<MyOtherError> 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<i32, OtherError>`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
# mod some_crate {
|
||||||
|
# struct OtherError(());
|
||||||
|
# impl OtherError {
|
||||||
|
# fn message() -> &'static str { "some error occurred" }
|
||||||
|
# }
|
||||||
|
# fn get_x() -> Result<i32, OtherError>
|
||||||
|
# }
|
||||||
|
|
||||||
|
use pyo3::prelude::*;
|
||||||
|
use some_crate::{OtherError, get_x};
|
||||||
|
|
||||||
|
struct MyOtherError(OtherError);
|
||||||
|
|
||||||
|
impl From<MyOtherError> for PyErr {
|
||||||
|
fn from(error: MyOtherError) -> Self {
|
||||||
|
PyValueError::new_err(self.0.message())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<OtherError> for MyOtherError {
|
||||||
|
fn from(other: OtherError) -> Self {
|
||||||
|
Self(other)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[pyfunction]
|
||||||
|
fn wrapped_get_x() -> Result<i32, MyOtherError> {
|
||||||
|
// get_x is a function returning Result<i32, OtherError>
|
||||||
|
let x: i32 = get_x()?;
|
||||||
|
Ok(x)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
[`From`]: https://doc.rust-lang.org/stable/std/convert/trait.From.html
|
||||||
|
[`Result<T, E>`]: https://doc.rust-lang.org/stable/std/result/enum.Result.html
|
||||||
|
|
||||||
|
[`PyResult<T>`]: {{#PYO3_DOCS_URL}}/pyo3/prelude/type.PyResult.html
|
||||||
|
[`PyResult<T>`]: {{#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
|
Loading…
Reference in New Issue