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

Fix IntoPyCallbackOutput paper cuts #2326

Merged
merged 9 commits into from May 9, 2022
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -11,12 +11,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- Implement `ToPyObject` for `[T; N]`. [#2313](https://github.com/PyO3/pyo3/pull/2313)
- Added the internal `IntoPyResult` trait to give better error messages when function return types do not implement `IntoPy`. [#2326](https://github.com/PyO3/pyo3/pull/2326)

### Changed

- Move `PyTypeObject::type_object` method to `PyTypeInfo` trait, and deprecate `PyTypeObject` trait. [#2287](https://github.com/PyO3/pyo3/pull/2287)
- The deprecated `pyproto` feature is now disabled by default. [#2322](https://github.com/PyO3/pyo3/pull/2322)
- Deprecate `ToBorrowedObject` trait (it is only used as a wrapper for `ToPyObject`). [#2333](https://github.com/PyO3/pyo3/pull/2333)
- `impl<T, const N: usize> IntoPy<PyObject> for [T; N]` now requires `T: IntoPy` rather than `T: ToPyObject`. [#2326](https://github.com/PyO3/pyo3/pull/2326)

## [0.16.4] - 2022-04-14

Expand Down
4 changes: 4 additions & 0 deletions guide/src/migration.md
Expand Up @@ -40,6 +40,10 @@ fn get_type_object<T: PyTypeInfo>(py: Python<'_>) -> &PyType {
# Python::with_gil(|py| { get_type_object::<pyo3::types::PyList>(py); });
```

### `impl<T, const N: usize> IntoPy<PyObject> for [T; N]` now requires `T: IntoPy` rather than `T: ToPyObject`

If this leads to errors, simply implement `IntoPy`. Because pyclasses already implement `IntoPy`, you probably don't need to worry about this.

## from 0.15.* to 0.16

### Drop support for older technologies
Expand Down
12 changes: 10 additions & 2 deletions pyo3-macros-backend/src/method.rs
Expand Up @@ -478,8 +478,16 @@ impl<'a> FnSpec<'a> {
} else {
quote!(#func_name)
};
let rust_call =
quote! { _pyo3::callback::convert(#py, #rust_name(#self_arg #(#arg_names),*)) };

// The method call is necessary to generate a decent error message.
let rust_call = quote! {
let ret = #rust_name(#self_arg #(#arg_names),*);

use _pyo3::callback::IntoPyResult;
let ret: _pyo3::PyResult<_> = ret.into_py_result();

_pyo3::callback::convert(#py, ret)
};
let krate = &self.krate;
Ok(match self.convention {
CallingConvention::Noargs => {
Expand Down
19 changes: 18 additions & 1 deletion pyo3-macros-backend/src/pymethod.rs
Expand Up @@ -495,6 +495,22 @@ pub fn impl_py_getter_def(cls: &syn::Type, property_type: PropertyType<'_>) -> R
self_type.receiver(cls, ExtractErrorMode::Raise)
}
};

let conversion = match property_type {
PropertyType::Descriptor { .. } => {
quote! {
let item: _pyo3::Py<_pyo3::PyAny> = _pyo3::IntoPy::into_py(item, _py);
::std::result::Result::Ok(_pyo3::conversion::IntoPyPointer::into_ptr(item))
}
}
// Forward to `IntoPyCallbackOutput`, to handle `#[getter]`s returning results.
PropertyType::Function { .. } => {
quote! {
_pyo3::callback::convert(_py, item)
}
}
};

Ok(quote! {
_pyo3::class::PyMethodDefType::Getter({
#deprecations
Expand All @@ -509,7 +525,8 @@ pub fn impl_py_getter_def(cls: &syn::Type, property_type: PropertyType<'_>) -> R
let _py = gil.python();
_pyo3::callback::panic_result_into_callback_output(_py, ::std::panic::catch_unwind(move || -> _pyo3::PyResult<_> {
#slf
_pyo3::callback::convert(_py, #getter_impl)
let item = #getter_impl;
#conversion
}))
}
__wrap
Expand Down
25 changes: 25 additions & 0 deletions src/callback.rs
Expand Up @@ -35,6 +35,31 @@ impl PyCallbackOutput for () {
const ERR_VALUE: Self = ();
}

/// A helper trait to result-wrap the return values of functions and methods.
mejrs marked this conversation as resolved.
Show resolved Hide resolved
#[doc(hidden)]
pub trait IntoPyResult<R> {
fn into_py_result(self) -> R;
}
mejrs marked this conversation as resolved.
Show resolved Hide resolved
mejrs marked this conversation as resolved.
Show resolved Hide resolved

impl<T> IntoPyResult<PyResult<T>> for T
where
T: IntoPy<PyObject>,
{
fn into_py_result(self) -> PyResult<Self> {
Ok(self)
}
}

impl<T, E> IntoPyResult<PyResult<T>> for Result<T, E>
where
T: IntoPy<PyObject>,
E: Into<PyErr>,
{
fn into_py_result(self) -> PyResult<T> {
self.map_err(Into::into)
}
}

/// Convert the result of callback function into the appropriate return value.
pub trait IntoPyCallbackOutput<Target> {
fn convert(self, py: Python<'_>) -> PyResult<Target>;
Expand Down
119 changes: 111 additions & 8 deletions src/conversions/array.rs
Expand Up @@ -3,14 +3,39 @@ use crate::{exceptions, PyErr};
#[cfg(min_const_generics)]
mod min_const_generics {
use super::invalid_sequence_length;
use crate::{FromPyObject, IntoPy, PyAny, PyObject, PyResult, PyTryFrom, Python, ToPyObject};
use crate::conversion::IntoPyPointer;
use crate::{
ffi, FromPyObject, IntoPy, Py, PyAny, PyObject, PyResult, PyTryFrom, Python, ToPyObject,
};

impl<T, const N: usize> IntoPy<PyObject> for [T; N]
where
T: ToPyObject,
T: IntoPy<PyObject>,
mejrs marked this conversation as resolved.
Show resolved Hide resolved
{
fn into_py(self, py: Python<'_>) -> PyObject {
self.as_ref().to_object(py)
unsafe {
#[allow(deprecated)] // we're not on edition 2021 yet
let elements = std::array::IntoIter::new(self);
let len = N as ffi::Py_ssize_t;

let ptr = ffi::PyList_New(len);

// We create the `Py` pointer here for two reasons:
// - panics if the ptr is null
// - its Drop cleans up the list if user code panics.
let list: Py<PyAny> = Py::from_owned_ptr(py, ptr);

for (i, obj) in (0..len).zip(elements) {
let obj = obj.into_py(py).into_ptr();

#[cfg(not(Py_LIMITED_API))]
ffi::PyList_SET_ITEM(ptr, i, obj);
#[cfg(Py_LIMITED_API)]
ffi::PyList_SetItem(ptr, i, obj);
}

list
}
}
}

Expand Down Expand Up @@ -149,17 +174,69 @@ mod min_const_generics {
#[cfg(not(min_const_generics))]
mejrs marked this conversation as resolved.
Show resolved Hide resolved
mod array_impls {
use super::invalid_sequence_length;
use crate::{FromPyObject, IntoPy, PyAny, PyObject, PyResult, PyTryFrom, Python, ToPyObject};
use crate::conversion::IntoPyPointer;
use crate::{
ffi, FromPyObject, IntoPy, Py, PyAny, PyObject, PyResult, PyTryFrom, Python, ToPyObject,
};
use std::mem::{transmute_copy, ManuallyDrop};

macro_rules! array_impls {
($($N:expr),+) => {
$(
impl<T> IntoPy<PyObject> for [T; $N]
where
T: ToPyObject
T: IntoPy<PyObject>
{
fn into_py(self, py: Python<'_>) -> PyObject {
self.as_ref().to_object(py)

struct ArrayGuard<T> {
elements: [ManuallyDrop<T>; $N],
start: usize,
}

impl<T> Drop for ArrayGuard<T> {
fn drop(&mut self) {
unsafe {
let needs_drop = self.elements.get_mut(self.start..).unwrap();
for item in needs_drop{
ManuallyDrop::drop(item);
}
}
}
}

unsafe {
let ptr = ffi::PyList_New($N as ffi::Py_ssize_t);

// We create the `Py` pointer here for two reasons:
// - panics if the ptr is null
// - its Drop cleans up the list if user code panics.
let list: Py<PyAny> = Py::from_owned_ptr(py, ptr);

let slf = ManuallyDrop::new(self);

let mut guard = ArrayGuard{
// the transmute size check is _very_ dumb around generics
elements: transmute_copy(&slf),
mejrs marked this conversation as resolved.
Show resolved Hide resolved
start: 0
};

for i in 0..$N {
let obj: T = ManuallyDrop::take(&mut guard.elements[i]);
guard.start += 1;

let obj = obj.into_py(py).into_ptr();

#[cfg(not(Py_LIMITED_API))]
ffi::PyList_SET_ITEM(ptr, i as ffi::Py_ssize_t, obj);
#[cfg(Py_LIMITED_API)]
ffi::PyList_SetItem(ptr, i as ffi::Py_ssize_t, obj);
}

std::mem::forget(guard);

list
}
}
}

Expand Down Expand Up @@ -218,7 +295,7 @@ fn invalid_sequence_length(expected: usize, actual: usize) -> PyErr {

#[cfg(test)]
mod tests {
use crate::{types::PyList, PyResult, Python};
use crate::{types::PyList, IntoPy, PyResult, Python, ToPyObject};

#[test]
fn test_extract_small_bytearray_to_array() {
Expand All @@ -233,7 +310,6 @@ mod tests {
}
#[test]
fn test_topyobject_array_conversion() {
use crate::ToPyObject;
Python::with_gil(|py| {
let array: [f32; 4] = [0.0, -16.0, 16.0, 42.0];
let pyobject = array.to_object(py);
Expand All @@ -258,4 +334,31 @@ mod tests {
);
})
}

#[test]
fn test_intopy_array_conversion() {
Python::with_gil(|py| {
let array: [f32; 4] = [0.0, -16.0, 16.0, 42.0];
let pyobject = array.into_py(py);
let pylist: &PyList = pyobject.extract(py).unwrap();
assert_eq!(pylist[0].extract::<f32>().unwrap(), 0.0);
assert_eq!(pylist[1].extract::<f32>().unwrap(), -16.0);
assert_eq!(pylist[2].extract::<f32>().unwrap(), 16.0);
assert_eq!(pylist[3].extract::<f32>().unwrap(), 42.0);
});
}

#[cfg(feature = "macros")]
#[test]
fn test_pyclass_intopy_array_conversion() {
#[crate::pyclass(crate = "crate")]
struct Foo;

Python::with_gil(|py| {
let array: [Foo; 8] = [Foo, Foo, Foo, Foo, Foo, Foo, Foo, Foo];
let pyobject = array.into_py(py);
let list: &PyList = pyobject.cast_as(py).unwrap();
let _cell: &crate::PyCell<Foo> = list.get_item(4).unwrap().extract().unwrap();
});
}
}
5 changes: 3 additions & 2 deletions src/types/list.rs
Expand Up @@ -33,8 +33,9 @@ fn new_from_iter(

let ptr = ffi::PyList_New(len);

// - Panics if the ptr is null
// - Cleans up the list if `convert` or the asserts panic
// We create the `Py` pointer here for two reasons:
// - panics if the ptr is null
// - its Drop cleans up the list if user code or the asserts panic.
let list: Py<PyList> = Py::from_owned_ptr(py, ptr);

let mut counter: Py_ssize_t = 0;
Expand Down
1 change: 1 addition & 0 deletions tests/test_compile_error.rs
Expand Up @@ -96,6 +96,7 @@ fn _test_compile_errors() {
fn tests_rust_1_60(t: &trybuild::TestCases) {
t.compile_fail("tests/ui/invalid_immutable_pyclass_borrow.rs");
t.compile_fail("tests/ui/invalid_pymethod_receiver.rs");
t.compile_fail("tests/ui/missing_intopy.rs");
}

#[rustversion::before(1.60)]
Expand Down
26 changes: 12 additions & 14 deletions tests/ui/invalid_result_conversion.stderr
@@ -1,14 +1,12 @@
error[E0277]: the trait bound `Result<(), MyError>: IntoPyCallbackOutput<_>` is not satisfied
--> tests/ui/invalid_result_conversion.rs:21:1
|
21 | #[pyfunction]
| ^^^^^^^^^^^^^ the trait `IntoPyCallbackOutput<_>` is not implemented for `Result<(), MyError>`
|
= help: the following implementations were found:
<Result<T, E> as IntoPyCallbackOutput<U>>
note: required by a bound in `pyo3::callback::convert`
--> src/callback.rs
|
| T: IntoPyCallbackOutput<U>,
| ^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `pyo3::callback::convert`
= note: this error originates in the attribute macro `pyfunction` (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0599]: the method `into_py_result` exists for enum `Result<(), MyError>`, but its trait bounds were not satisfied
--> tests/ui/invalid_result_conversion.rs:21:1
|
21 | #[pyfunction]
| ^^^^^^^^^^^^^ method cannot be called on `Result<(), MyError>` due to unsatisfied trait bounds
|
= note: the following trait bounds were not satisfied:
`&Result<(), MyError>: IntoPy<Py<PyAny>>`
which is required by `&Result<(), MyError>: IntoPyResult<Result<&Result<(), MyError>, PyErr>>`
`&mut Result<(), MyError>: IntoPy<Py<PyAny>>`
which is required by `&mut Result<(), MyError>: IntoPyResult<Result<&mut Result<(), MyError>, PyErr>>`
mejrs marked this conversation as resolved.
Show resolved Hide resolved
= note: this error originates in the attribute macro `pyfunction` (in Nightly builds, run with -Z macro-backtrace for more info)
8 changes: 8 additions & 0 deletions tests/ui/missing_intopy.rs
@@ -0,0 +1,8 @@
struct Blah;

#[pyo3::pyfunction]
fn blah() -> Blah{
Blah
}

fn main(){}
29 changes: 29 additions & 0 deletions tests/ui/missing_intopy.stderr
@@ -0,0 +1,29 @@
error[E0599]: the method `into_py_result` exists for struct `Blah`, but its trait bounds were not satisfied
--> tests/ui/missing_intopy.rs:3:1
|
1 | struct Blah;
| ------------
| |
| method `into_py_result` not found for this
| doesn't satisfy `Blah: IntoPy<Py<PyAny>>`
| doesn't satisfy `Blah: IntoPyResult<Result<Blah, PyErr>>`
2 |
3 | #[pyo3::pyfunction]
| ^^^^^^^^^^^^^^^^^^^ method cannot be called on `Blah` due to unsatisfied trait bounds
|
= note: the following trait bounds were not satisfied:
`Blah: IntoPy<Py<PyAny>>`
which is required by `Blah: IntoPyResult<Result<Blah, PyErr>>`
`&Blah: IntoPy<Py<PyAny>>`
which is required by `&Blah: IntoPyResult<Result<&Blah, PyErr>>`
`&mut Blah: IntoPy<Py<PyAny>>`
which is required by `&mut Blah: IntoPyResult<Result<&mut Blah, PyErr>>`
note: the following trait must be implemented
--> src/conversion.rs
|
| / pub trait IntoPy<T>: Sized {
| | /// Performs the conversion.
| | fn into_py(self, py: Python<'_>) -> T;
| | }
| |_^
= note: this error originates in the attribute macro `pyo3::pyfunction` (in Nightly builds, run with -Z macro-backtrace for more info)