Skip to content

Commit

Permalink
Allow other Result types in #[pyfunction] (#1118)
Browse files Browse the repository at this point in the history
* Added a couple basic tests

* Implemented suggested change

* Fixed type inference

* cargo fmt

* Finished tests and removed warnings

* Include in CHANGELOG.md

* Moved test into separate file

* &'static str and function rename

* Mention in the book
  • Loading branch information
Mario authored Aug 29, 2020
1 parent 1631129 commit 608aea7
Show file tree
Hide file tree
Showing 9 changed files with 95 additions and 6 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Add `PyBytes::new_with` and `PyByteArray::new_with` for initialising Python-allocated bytes and bytearrays using a closure. [#1074](https://github.com/PyO3/pyo3/pull/1074)
- Add `Py::as_ref` and `Py::into_ref`. [#1098](https://github.com/PyO3/pyo3/pull/1098)
- Add optional implementations of `ToPyObject`, `IntoPy`, and `FromPyObject` for [hashbrown](https://crates.io/crates/hashbrown)'s `HashMap` and `HashSet` types. The `hashbrown` feature must be enabled for these implementations to be built. [#1114](https://github.com/PyO3/pyo3/pull/1114/)
- Allow other `Result` types when using `#[pyfunction]`. [#1106](https://github.com/PyO3/pyo3/issues/1106).

### Changed
- Exception types have been renamed from e.g. `RuntimeError` to `PyRuntimeError`, and are now only accessible by `&T` or `Py<T>` similar to other Python-native types. The old names continue to exist but are deprecated. [#1024](https://github.com/PyO3/pyo3/pull/1024)
Expand Down
2 changes: 1 addition & 1 deletion guide/src/conversions.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ When returning values from functions callable from Python, Python-native types (

Because these types are references, in some situations the Rust compiler may ask for lifetime annotations. If this is the case, you should use `Py<PyAny>`, `Py<PyDict>` etc. instead - which are also zero-cost. For all of these Python-native types `T`, `Py<T>` can be created from `T` with an `.into()` conversion.

If your function is fallible, it should return `PyResult<T>`, which will raise a `Python` exception if the `Err` variant is returned.
If your function is fallible, it should return `PyResult<T>` or `Result<T, E>` where `E` implements `From<E> for PyErr`. This will raise a `Python` exception if the `Err` variant is returned.

Finally, the following Rust types are also able to convert to Python as return values:

Expand Down
7 changes: 4 additions & 3 deletions src/callback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

//! Utilities for a Python callable object that invokes a Rust function.

use crate::err::PyResult;
use crate::err::{PyErr, PyResult};
use crate::exceptions::PyOverflowError;
use crate::ffi::{self, Py_hash_t};
use crate::IntoPyPointer;
Expand Down Expand Up @@ -37,12 +37,13 @@ pub trait IntoPyCallbackOutput<Target> {
fn convert(self, py: Python) -> PyResult<Target>;
}

impl<T, U> IntoPyCallbackOutput<U> for PyResult<T>
impl<T, E, U> IntoPyCallbackOutput<U> for Result<T, E>
where
T: IntoPyCallbackOutput<U>,
E: Into<PyErr>,
{
fn convert(self, py: Python) -> PyResult<U> {
self.and_then(|t| t.convert(py))
self.map_err(Into::into).and_then(|t| t.convert(py))
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/class/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ macro_rules! py_binary_self_func {
let arg = py.from_borrowed_ptr::<$crate::PyAny>(arg);
call_operator_mut!(py, slf_, $f, arg).convert(py)?;
ffi::Py_INCREF(slf);
Ok(slf)
Ok::<_, $crate::err::PyErr>(slf)
})
}
Some(wrap::<$class>)
Expand Down
3 changes: 2 additions & 1 deletion src/class/number.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
//! Trait and support implementation for implementing number protocol

use crate::callback::IntoPyCallbackOutput;
use crate::err::PyErr;
use crate::{ffi, FromPyObject, PyClass, PyObject};

/// Number interface
Expand Down Expand Up @@ -941,7 +942,7 @@ impl ffi::PyNumberMethods {
let other = py.from_borrowed_ptr::<crate::PyAny>(other);
call_operator_mut!(py, slf_cell, __ipow__, other).convert(py)?;
ffi::Py_INCREF(slf);
Ok(slf)
Ok::<_, PyErr>(slf)
})
}
self.nb_inplace_power = Some(wrap_ipow::<T>);
Expand Down
1 change: 1 addition & 0 deletions tests/test_compile_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ fn test_compile_errors() {
t.compile_fail("tests/ui/missing_clone.rs");
t.compile_fail("tests/ui/reject_generics.rs");
t.compile_fail("tests/ui/wrong_aspyref_lifetimes.rs");
t.compile_fail("tests/ui/invalid_result_conversion.rs");

skip_min_stable(&t);

Expand Down
40 changes: 40 additions & 0 deletions tests/test_various.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ use pyo3::types::IntoPyDict;
use pyo3::types::{PyDict, PyTuple};
use pyo3::{py_run, wrap_pyfunction, PyCell};

use std::fmt;

mod common;

#[pyclass]
Expand Down Expand Up @@ -168,3 +170,41 @@ fn test_pickle() {
"#
);
}

/// Testing https://github.com/PyO3/pyo3/issues/1106. A result type that
/// implements `From<MyError> for PyErr` should be automatically converted
/// when using `#[pyfunction]`.
///
/// This only makes sure that valid `Result` types do work. For an invalid
/// enum type, see `ui/invalid_result_conversion.py`.
#[derive(Debug)]
struct MyError {
pub descr: &'static str,
}

impl fmt::Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "My error message: {}", self.descr)
}
}

/// Important for the automatic conversion to `PyErr`.
impl From<MyError> for PyErr {
fn from(err: MyError) -> pyo3::PyErr {
pyo3::exceptions::PyOSError::py_err(err.to_string())
}
}

#[pyfunction]
fn result_conversion_function() -> Result<(), MyError> {
Err(MyError {
descr: "something went wrong",
})
}

#[test]
fn test_result_conversion() {
let gil = Python::acquire_gil();
let py = gil.python();
wrap_pyfunction!(result_conversion_function)(py);
}
31 changes: 31 additions & 0 deletions tests/ui/invalid_result_conversion.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//! Testing https://github.com/PyO3/pyo3/issues/1106. A result type that
//! *doesn't* implement `From<MyError> for PyErr` won't be automatically
//! converted when using `#[pyfunction]`.
use pyo3::prelude::*;
use pyo3::wrap_pyfunction;

use std::fmt;

/// A basic error type for the tests. It's missing `From<MyError> for PyErr`,
/// though, so it shouldn't work.
#[derive(Debug)]
struct MyError {
pub descr: &'static str,
}

impl fmt::Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "My error message: {}", self.descr)
}
}

#[pyfunction]
fn should_not_work() -> Result<(), MyError> {
Err(MyError { descr: "something went wrong" })
}

fn main() {
let gil = Python::acquire_gil();
let py = gil.python();
wrap_pyfunction!(should_not_work)(py);
}
14 changes: 14 additions & 0 deletions tests/ui/invalid_result_conversion.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
error[E0277]: the trait bound `std::result::Result<(), MyError>: pyo3::callback::IntoPyCallbackOutput<_>` is not satisfied
--> $DIR/invalid_result_conversion.rs:22:1
|
22 | #[pyfunction]
| ^^^^^^^^^^^^^ the trait `pyo3::callback::IntoPyCallbackOutput<_>` is not implemented for `std::result::Result<(), MyError>`
|
::: $WORKSPACE/src/callback.rs
|
| T: IntoPyCallbackOutput<U>,
| ----------------------- required by this bound in `pyo3::callback::convert`
|
= help: the following implementations were found:
<std::result::Result<T, E> as pyo3::callback::IntoPyCallbackOutput<U>>
= note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info)

0 comments on commit 608aea7

Please sign in to comment.