Skip to content

Commit 3a94a66

Browse files
davidhewittIcxolu
andcommitted
add IntoPyObjectExt trait (#4708)
* add `IntoPyObjectExt` trait * adjust method names, more docs & usage internally * more uses of `IntoPyObjectExt` * guide docs * newsfragment * fixup doctest * Update guide/src/conversions/traits.md Co-authored-by: Icxolu <[email protected]> --------- Co-authored-by: Icxolu <[email protected]>
1 parent 11b9086 commit 3a94a66

File tree

21 files changed

+218
-333
lines changed

21 files changed

+218
-333
lines changed

guide/src/conversions/traits.md

Lines changed: 45 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -490,9 +490,11 @@ If the input is neither a string nor an integer, the error message will be:
490490
- the function signature must be `fn(&Bound<PyAny>) -> PyResult<T>` where `T` is the Rust type of the argument.
491491

492492
### `IntoPyObject`
493-
This trait defines the to-python conversion for a Rust type. All types in PyO3 implement this trait,
493+
The ['IntoPyObject'] trait defines the to-python conversion for a Rust type. All types in PyO3 implement this trait,
494494
as does a `#[pyclass]` which doesn't use `extends`.
495495

496+
This trait defines a single method, `into_pyobject()`, which returns a [`Result`] with `Ok` and `Err` types depending on the input value. For convenience, there is a companion [`IntoPyObjectExt`] trait which adds methods such as `into_py_any()` which converts the `Ok` and `Err` types to commonly used types (in the case of `into_py_any()`, `Py<PyAny>` and `PyErr` respectively).
497+
496498
Occasionally you may choose to implement this for custom types which are mapped to Python types
497499
_without_ having a unique python type.
498500

@@ -510,7 +512,7 @@ into `PyTuple` with the fields in declaration order.
510512

511513
// structs convert into `PyDict` with field names as keys
512514
#[derive(IntoPyObject)]
513-
struct Struct {
515+
struct Struct {
514516
count: usize,
515517
obj: Py<PyAny>,
516518
}
@@ -532,11 +534,11 @@ forward the implementation to the inner type.
532534

533535
// newtype tuple structs are implicitly `transparent`
534536
#[derive(IntoPyObject)]
535-
struct TransparentTuple(PyObject);
537+
struct TransparentTuple(PyObject);
536538

537539
#[derive(IntoPyObject)]
538540
#[pyo3(transparent)]
539-
struct TransparentStruct<'py> {
541+
struct TransparentStruct<'py> {
540542
inner: Bound<'py, PyAny>, // `'py` lifetime will be used as the Python lifetime
541543
}
542544
```
@@ -582,7 +584,7 @@ impl<'py> IntoPyObject<'py> for MyPyObjectWrapper {
582584
}
583585
}
584586

585-
// equivalent to former `ToPyObject` implementations
587+
// equivalent to former `ToPyObject` implementations
586588
impl<'a, 'py> IntoPyObject<'py> for &'a MyPyObjectWrapper {
587589
type Target = PyAny;
588590
type Output = Borrowed<'a, 'py, Self::Target>; // `Borrowed` can be used to optimized reference counting
@@ -594,38 +596,6 @@ impl<'a, 'py> IntoPyObject<'py> for &'a MyPyObjectWrapper {
594596
}
595597
```
596598

597-
### `IntoPy<T>`
598-
599-
<div class="warning">
600-
601-
⚠️ Warning: API update in progress 🛠️
602-
603-
PyO3 0.23 has introduced `IntoPyObject` as the new trait for to-python conversions. While `#[pymethods]` and `#[pyfunction]` contain a compatibility layer to allow `IntoPy<PyObject>` as a return type, all Python API have been migrated to use `IntoPyObject`. To migrate implement `IntoPyObject` for your type.
604-
</div>
605-
606-
607-
This trait defines the to-python conversion for a Rust type. It is usually implemented as
608-
`IntoPy<PyObject>`, which is the trait needed for returning a value from `#[pyfunction]` and
609-
`#[pymethods]`.
610-
611-
All types in PyO3 implement this trait, as does a `#[pyclass]` which doesn't use `extends`.
612-
613-
Occasionally you may choose to implement this for custom types which are mapped to Python types
614-
_without_ having a unique python type.
615-
616-
```rust
617-
use pyo3::prelude::*;
618-
# #[allow(dead_code)]
619-
struct MyPyObjectWrapper(PyObject);
620-
621-
#[allow(deprecated)]
622-
impl IntoPy<PyObject> for MyPyObjectWrapper {
623-
fn into_py(self, py: Python<'_>) -> PyObject {
624-
self.0
625-
}
626-
}
627-
```
628-
629599
#### `BoundObject` for conversions that may be `Bound` or `Borrowed`
630600

631601
`IntoPyObject::into_py_object` returns either `Bound` or `Borrowed` depending on the implementation for a concrete type. For example, the `IntoPyObject` implementation for `u32` produces a `Bound<'py, PyInt>` and the `bool` implementation produces a `Borrowed<'py, 'py, PyBool>`:
@@ -672,6 +642,8 @@ where
672642
the_vec.iter()
673643
.map(|x| {
674644
Ok(
645+
// Note: the below is equivalent to `x.into_py_any()`
646+
// from the `IntoPyObjectExt` trait
675647
x.into_pyobject(py)
676648
.map_err(Into::into)?
677649
.into_any()
@@ -693,6 +665,38 @@ let vec_of_pyobjs: Vec<Py<PyAny>> = Python::with_gil(|py| {
693665

694666
In the example above we used `BoundObject::into_any` and `BoundObject::unbind` to manipulate the python types and smart pointers into the result type we wanted to produce from the function.
695667

668+
### `IntoPy<T>`
669+
670+
<div class="warning">
671+
672+
⚠️ Warning: API update in progress 🛠️
673+
674+
PyO3 0.23 has introduced `IntoPyObject` as the new trait for to-python conversions. While `#[pymethods]` and `#[pyfunction]` contain a compatibility layer to allow `IntoPy<PyObject>` as a return type, all Python API have been migrated to use `IntoPyObject`. To migrate implement `IntoPyObject` for your type.
675+
</div>
676+
677+
678+
This trait defines the to-python conversion for a Rust type. It is usually implemented as
679+
`IntoPy<PyObject>`, which is the trait needed for returning a value from `#[pyfunction]` and
680+
`#[pymethods]`.
681+
682+
All types in PyO3 implement this trait, as does a `#[pyclass]` which doesn't use `extends`.
683+
684+
Occasionally you may choose to implement this for custom types which are mapped to Python types
685+
_without_ having a unique python type.
686+
687+
```rust
688+
use pyo3::prelude::*;
689+
# #[allow(dead_code)]
690+
struct MyPyObjectWrapper(PyObject);
691+
692+
#[allow(deprecated)]
693+
impl IntoPy<PyObject> for MyPyObjectWrapper {
694+
fn into_py(self, py: Python<'_>) -> PyObject {
695+
self.0
696+
}
697+
}
698+
```
699+
696700
### The `ToPyObject` trait
697701

698702
<div class="warning">
@@ -710,8 +714,12 @@ same purpose, except that it consumes `self`.
710714
[`IntoPy`]: {{#PYO3_DOCS_URL}}/pyo3/conversion/trait.IntoPy.html
711715
[`FromPyObject`]: {{#PYO3_DOCS_URL}}/pyo3/conversion/trait.FromPyObject.html
712716
[`ToPyObject`]: {{#PYO3_DOCS_URL}}/pyo3/conversion/trait.ToPyObject.html
717+
[`IntoPyObject`]: {{#PYO3_DOCS_URL}}/pyo3/conversion/trait.IntoPyObject.html
718+
[`IntoPyObjectExt`]: {{#PYO3_DOCS_URL}}/pyo3/conversion/trait.IntoPyObjectExt.html
713719
[`PyObject`]: {{#PYO3_DOCS_URL}}/pyo3/type.PyObject.html
714720

715721
[`PyRef`]: {{#PYO3_DOCS_URL}}/pyo3/pycell/struct.PyRef.html
716722
[`PyRefMut`]: {{#PYO3_DOCS_URL}}/pyo3/pycell/struct.PyRefMut.html
717723
[`BoundObject`]: {{#PYO3_DOCS_URL}}/pyo3/instance/trait.BoundObject.html
724+
725+
[`Result`]: https://doc.rust-lang.org/stable/std/result/enum.Result.html

newsfragments/4708.added.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add `IntoPyObjectExt` trait.

pyo3-macros-backend/src/pyclass.rs

Lines changed: 7 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1367,10 +1367,7 @@ fn impl_complex_enum_tuple_variant_getitem(
13671367
.map(|i| {
13681368
let field_access = format_ident!("_{}", i);
13691369
quote! { #i =>
1370-
#pyo3_path::IntoPyObject::into_pyobject(#variant_cls::#field_access(slf)?, py)
1371-
.map(#pyo3_path::BoundObject::into_any)
1372-
.map(#pyo3_path::BoundObject::unbind)
1373-
.map_err(::std::convert::Into::into)
1370+
#pyo3_path::IntoPyObjectExt::into_py_any(#variant_cls::#field_access(slf)?, py)
13741371
}
13751372
})
13761373
.collect();
@@ -1852,16 +1849,10 @@ fn pyclass_richcmp_arms(
18521849
.map(|span| {
18531850
quote_spanned! { span =>
18541851
#pyo3_path::pyclass::CompareOp::Eq => {
1855-
#pyo3_path::IntoPyObject::into_pyobject(self_val == other, py)
1856-
.map(#pyo3_path::BoundObject::into_any)
1857-
.map(#pyo3_path::BoundObject::unbind)
1858-
.map_err(::std::convert::Into::into)
1852+
#pyo3_path::IntoPyObjectExt::into_py_any(self_val == other, py)
18591853
},
18601854
#pyo3_path::pyclass::CompareOp::Ne => {
1861-
#pyo3_path::IntoPyObject::into_pyobject(self_val != other, py)
1862-
.map(#pyo3_path::BoundObject::into_any)
1863-
.map(#pyo3_path::BoundObject::unbind)
1864-
.map_err(::std::convert::Into::into)
1855+
#pyo3_path::IntoPyObjectExt::into_py_any(self_val != other, py)
18651856
},
18661857
}
18671858
})
@@ -1876,28 +1867,16 @@ fn pyclass_richcmp_arms(
18761867
.map(|ord| {
18771868
quote_spanned! { ord.span() =>
18781869
#pyo3_path::pyclass::CompareOp::Gt => {
1879-
#pyo3_path::IntoPyObject::into_pyobject(self_val > other, py)
1880-
.map(#pyo3_path::BoundObject::into_any)
1881-
.map(#pyo3_path::BoundObject::unbind)
1882-
.map_err(::std::convert::Into::into)
1870+
#pyo3_path::IntoPyObjectExt::into_py_any(self_val > other, py)
18831871
},
18841872
#pyo3_path::pyclass::CompareOp::Lt => {
1885-
#pyo3_path::IntoPyObject::into_pyobject(self_val < other, py)
1886-
.map(#pyo3_path::BoundObject::into_any)
1887-
.map(#pyo3_path::BoundObject::unbind)
1888-
.map_err(::std::convert::Into::into)
1873+
#pyo3_path::IntoPyObjectExt::into_py_any(self_val < other, py)
18891874
},
18901875
#pyo3_path::pyclass::CompareOp::Le => {
1891-
#pyo3_path::IntoPyObject::into_pyobject(self_val <= other, py)
1892-
.map(#pyo3_path::BoundObject::into_any)
1893-
.map(#pyo3_path::BoundObject::unbind)
1894-
.map_err(::std::convert::Into::into)
1876+
#pyo3_path::IntoPyObjectExt::into_py_any(self_val <= other, py)
18951877
},
18961878
#pyo3_path::pyclass::CompareOp::Ge => {
1897-
#pyo3_path::IntoPyObject::into_pyobject(self_val >= other, py)
1898-
.map(#pyo3_path::BoundObject::into_any)
1899-
.map(#pyo3_path::BoundObject::unbind)
1900-
.map_err(::std::convert::Into::into)
1879+
#pyo3_path::IntoPyObjectExt::into_py_any(self_val >= other, py)
19011880
},
19021881
}
19031882
})

pyo3-macros-backend/src/pyimpl.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -213,10 +213,7 @@ pub fn gen_py_const(cls: &syn::Type, spec: &ConstSpec, ctx: &Ctx) -> MethodAndMe
213213

214214
let associated_method = quote! {
215215
fn #wrapper_ident(py: #pyo3_path::Python<'_>) -> #pyo3_path::PyResult<#pyo3_path::PyObject> {
216-
#pyo3_path::IntoPyObject::into_pyobject(#cls::#member, py)
217-
.map(#pyo3_path::BoundObject::into_any)
218-
.map(#pyo3_path::BoundObject::unbind)
219-
.map_err(::std::convert::Into::into)
216+
#pyo3_path::IntoPyObjectExt::into_py_any(#cls::#member, py)
220217
}
221218
};
222219

src/conversion.rs

Lines changed: 65 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -178,8 +178,23 @@ pub trait IntoPy<T>: Sized {
178178

179179
/// Defines a conversion from a Rust type to a Python object, which may fail.
180180
///
181+
/// This trait has `#[derive(IntoPyObject)]` to automatically implement it for simple types and
182+
/// `#[derive(IntoPyObjectRef)]` to implement the same for references.
183+
///
181184
/// It functions similarly to std's [`TryInto`] trait, but requires a [GIL token](Python)
182185
/// as an argument.
186+
///
187+
/// The [`into_pyobject`][IntoPyObject::into_pyobject] method is designed for maximum flexibility and efficiency; it
188+
/// - allows for a concrete Python type to be returned (the [`Target`][IntoPyObject::Target] associated type)
189+
/// - allows for the smart pointer containing the Python object to be either `Bound<'py, Self::Target>` or `Borrowed<'a, 'py, Self::Target>`
190+
/// to avoid unnecessary reference counting overhead
191+
/// - allows for a custom error type to be returned in the event of a conversion error to avoid
192+
/// unnecessarily creating a Python exception
193+
///
194+
/// # See also
195+
///
196+
/// - The [`IntoPyObjectExt`] trait, which provides convenience methods for common usages of
197+
/// `IntoPyObject` which erase type information and convert errors to `PyErr`.
183198
#[cfg_attr(
184199
diagnostic_namespace,
185200
diagnostic::on_unimplemented(
@@ -227,12 +242,7 @@ pub trait IntoPyObject<'py>: Sized {
227242
I: IntoIterator<Item = Self> + AsRef<[Self]>,
228243
I::IntoIter: ExactSizeIterator<Item = Self>,
229244
{
230-
let mut iter = iter.into_iter().map(|e| {
231-
e.into_pyobject(py)
232-
.map(BoundObject::into_any)
233-
.map(BoundObject::into_bound)
234-
.map_err(Into::into)
235-
});
245+
let mut iter = iter.into_iter().map(|e| e.into_bound_py_any(py));
236246
let list = crate::types::list::try_new_from_iter(py, &mut iter);
237247
list.map(Bound::into_any)
238248
}
@@ -250,12 +260,7 @@ pub trait IntoPyObject<'py>: Sized {
250260
I: IntoIterator<Item = Self> + AsRef<[<Self as private::Reference>::BaseType]>,
251261
I::IntoIter: ExactSizeIterator<Item = Self>,
252262
{
253-
let mut iter = iter.into_iter().map(|e| {
254-
e.into_pyobject(py)
255-
.map(BoundObject::into_any)
256-
.map(BoundObject::into_bound)
257-
.map_err(Into::into)
258-
});
263+
let mut iter = iter.into_iter().map(|e| e.into_bound_py_any(py));
259264
let list = crate::types::list::try_new_from_iter(py, &mut iter);
260265
list.map(Bound::into_any)
261266
}
@@ -347,6 +352,54 @@ where
347352
}
348353
}
349354

355+
mod into_pyobject_ext {
356+
pub trait Sealed {}
357+
impl<'py, T> Sealed for T where T: super::IntoPyObject<'py> {}
358+
}
359+
360+
/// Convenience methods for common usages of [`IntoPyObject`]. Every type that implements
361+
/// [`IntoPyObject`] also implements this trait.
362+
///
363+
/// These methods:
364+
/// - Drop type information from the output, returning a `PyAny` object.
365+
/// - Always convert the `Error` type to `PyErr`, which may incur a performance penalty but it
366+
/// more convenient in contexts where the `?` operator would produce a `PyErr` anyway.
367+
pub trait IntoPyObjectExt<'py>: IntoPyObject<'py> + into_pyobject_ext::Sealed {
368+
/// Converts `self` into an owned Python object, dropping type information.
369+
#[inline]
370+
fn into_bound_py_any(self, py: Python<'py>) -> PyResult<Bound<'py, PyAny>> {
371+
match self.into_pyobject(py) {
372+
Ok(obj) => Ok(obj.into_any().into_bound()),
373+
Err(err) => Err(err.into()),
374+
}
375+
}
376+
377+
/// Converts `self` into an owned Python object, dropping type information and unbinding it
378+
/// from the `'py` lifetime.
379+
#[inline]
380+
fn into_py_any(self, py: Python<'py>) -> PyResult<Py<PyAny>> {
381+
match self.into_pyobject(py) {
382+
Ok(obj) => Ok(obj.into_any().unbind()),
383+
Err(err) => Err(err.into()),
384+
}
385+
}
386+
387+
/// Converts `self` into a Python object.
388+
///
389+
/// This is equivalent to calling [`into_pyobject`][IntoPyObject::into_pyobject] followed
390+
/// with `.map_err(Into::into)` to convert the error type to [`PyErr`]. This is helpful
391+
/// for generic code which wants to make use of the `?` operator.
392+
#[inline]
393+
fn into_pyobject_or_pyerr(self, py: Python<'py>) -> PyResult<Self::Output> {
394+
match self.into_pyobject(py) {
395+
Ok(obj) => Ok(obj),
396+
Err(err) => Err(err.into()),
397+
}
398+
}
399+
}
400+
401+
impl<'py, T> IntoPyObjectExt<'py> for T where T: IntoPyObject<'py> {}
402+
350403
/// Extract a type from a Python object.
351404
///
352405
///

src/conversions/either.rs

Lines changed: 6 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@
4747
#[cfg(feature = "experimental-inspect")]
4848
use crate::inspect::types::TypeInfo;
4949
use crate::{
50-
conversion::IntoPyObject, exceptions::PyTypeError, types::any::PyAnyMethods, Bound,
51-
BoundObject, FromPyObject, PyAny, PyErr, PyObject, PyResult, Python,
50+
exceptions::PyTypeError, types::any::PyAnyMethods, Bound, FromPyObject, IntoPyObject,
51+
IntoPyObjectExt, PyAny, PyErr, PyObject, PyResult, Python,
5252
};
5353
#[allow(deprecated)]
5454
use crate::{IntoPy, ToPyObject};
@@ -82,16 +82,8 @@ where
8282

8383
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
8484
match self {
85-
Either::Left(l) => l
86-
.into_pyobject(py)
87-
.map(BoundObject::into_any)
88-
.map(BoundObject::into_bound)
89-
.map_err(Into::into),
90-
Either::Right(r) => r
91-
.into_pyobject(py)
92-
.map(BoundObject::into_any)
93-
.map(BoundObject::into_bound)
94-
.map_err(Into::into),
85+
Either::Left(l) => l.into_bound_py_any(py),
86+
Either::Right(r) => r.into_bound_py_any(py),
9587
}
9688
}
9789
}
@@ -108,16 +100,8 @@ where
108100

109101
fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
110102
match self {
111-
Either::Left(l) => l
112-
.into_pyobject(py)
113-
.map(BoundObject::into_any)
114-
.map(BoundObject::into_bound)
115-
.map_err(Into::into),
116-
Either::Right(r) => r
117-
.into_pyobject(py)
118-
.map(BoundObject::into_any)
119-
.map(BoundObject::into_bound)
120-
.map_err(Into::into),
103+
Either::Left(l) => l.into_bound_py_any(py),
104+
Either::Right(r) => r.into_bound_py_any(py),
121105
}
122106
}
123107
}

src/coroutine.rs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use crate::{
1515
exceptions::{PyAttributeError, PyRuntimeError, PyStopIteration},
1616
panic::PanicException,
1717
types::{string::PyStringMethods, PyIterator, PyString},
18-
Bound, BoundObject, IntoPyObject, Py, PyAny, PyErr, PyObject, PyResult, Python,
18+
Bound, IntoPyObject, IntoPyObjectExt, Py, PyAny, PyErr, PyObject, PyResult, Python,
1919
};
2020

2121
pub(crate) mod cancel;
@@ -60,10 +60,7 @@ impl Coroutine {
6060
let wrap = async move {
6161
let obj = future.await.map_err(Into::into)?;
6262
// SAFETY: GIL is acquired when future is polled (see `Coroutine::poll`)
63-
obj.into_pyobject(unsafe { Python::assume_gil_acquired() })
64-
.map(BoundObject::into_any)
65-
.map(BoundObject::unbind)
66-
.map_err(Into::into)
63+
obj.into_py_any(unsafe { Python::assume_gil_acquired() })
6764
};
6865
Self {
6966
name: name.map(Bound::unbind),

0 commit comments

Comments
 (0)