Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow other Result types in #[pyfunction] #1118

Merged
merged 10 commits into from Aug 29, 2020
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Implement type information for conversion failures. [#1050](https://github.com/PyO3/pyo3/pull/1050)
- 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)
- 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
7 changes: 4 additions & 3 deletions src/callback.rs
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
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
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
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
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: String,
}

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".to_string(),
})
}

#[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
@@ -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: String,
}

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

#[pyfunction]
fn should_work() -> Result<(), MyError> {
marioortizmanero marked this conversation as resolved.
Show resolved Hide resolved
Err(MyError { descr: "something went wrong".to_string() })
}

fn main() {
let gil = Python::acquire_gil();
let py = gil.python();
wrap_pyfunction!(should_not_work)(py);
}
25 changes: 25 additions & 0 deletions tests/ui/invalid_result_conversion.stderr
@@ -0,0 +1,25 @@
error[E0425]: cannot find value `__pyo3_get_function_should_not_work` in this scope
--> $DIR/invalid_result_conversion.rs:30:5
|
22 | #[pyfunction]
| ------------- similarly named function `__pyo3_get_function_should_work` defined here
...
30 | wrap_pyfunction!(should_not_work)(py);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ help: a function with a similar name exists: `__pyo3_get_function_should_work`
|
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

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)