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 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)
|
||||
|
|
|
@ -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<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:
|
||||
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::<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
|
||||
|
||||
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