Skip to content

Commit

Permalink
Upgrade to [email protected] (#10)
Browse files Browse the repository at this point in the history
  • Loading branch information
LukeMathWalker authored Jan 3, 2025
1 parent 685ee04 commit a9dff83
Show file tree
Hide file tree
Showing 25 changed files with 147 additions and 81 deletions.
89 changes: 48 additions & 41 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
[workspace]
members = ["exercises/*/*", "patcher"]
resolver = "2"

[workspace.dependencies]
anyhow = "1"
pyo3 = "0.23.3"
semver = "1.0.23"
serde = "1.0.204"
serde_json = "1.0.120"
2 changes: 1 addition & 1 deletion book/src/01_intro/01_setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ name = "setup"
crate-type = ["cdylib"]

[dependencies]
pyo3 = "0.21.1"
pyo3 = "0.23.0"
```

Two things stand out in this file compared to a regular Rust project:
Expand Down
2 changes: 1 addition & 1 deletion book/src/01_intro/04_arguments.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,4 @@ Don't try to use them to solve the exercise for this section: we'll cover them i

## References

- [The `FromPyObject` trait](https://docs.rs/pyo3/0.22.0/pyo3/conversion/trait.FromPyObject.html)
- [The `FromPyObject` trait](https://docs.rs/pyo3/0.23.3/pyo3/conversion/trait.FromPyObject.html)
4 changes: 2 additions & 2 deletions book/src/01_intro/05_gil.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,8 @@ when we're interacting with the Python object during the conversion.

## References

- [`FromPyObject`](https://docs.rs/pyo3/0.22.1/pyo3/conversion/trait.FromPyObject.html)
- [`Python<'py>`](https://docs.rs/pyo3/0.22.1/pyo3/marker/struct.Python.html)
- [`FromPyObject`](https://docs.rs/pyo3/0.23.3/pyo3/conversion/trait.FromPyObject.html)
- [`Python<'py>`](https://docs.rs/pyo3/0.23.3/pyo3/marker/struct.Python.html)
- [Global Interpreter Lock](https://docs.python.org/3/c-api/init.html#thread-state-and-the-global-interpreter-lock)
- [Official guidance on Python-native vs Rust-native types](https://pyo3.rs/v0.22.0/conversions/tables#using-rust-library-types-vs-python-native-types)

Expand Down
78 changes: 65 additions & 13 deletions book/src/01_intro/06_output.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,46 +3,98 @@
We've gone deep into the weeds of how `pyo3` handles arguments to your `#[pyfunction]`s.
Let's now move our focus to output values: how do you return _something_ from your Rust functions to Python?

## `IntoPy`
## `IntoPyObject`

Guess what? There's a trait for that too!\
`IntoPy` is the counterpart of `FromPyObject`. It converts Rust values into Python objects:
`IntoPyObject` is the counterpart of `FromPyObject`. It converts Rust values into Python objects:

```rust
pub trait IntoPy<T>: Sized {
fn into_py(self, py: Python<'_>) -> T;
pub trait IntoPyObject<'py>: Sized {
type Target;
type Output: BoundObject<'py, Self::Target>;
type Error: Into<PyErr>;

fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error>;
}
```

The output type of your `#[pyfunction]` must implement `IntoPy`.
The output type of your `#[pyfunction]` must implement `IntoPyObject`.

### `IntoPy::into_py`
### `IntoPyObject::into_pyobject`

`IntoPy::into_py` expects two arguments:
`IntoPyObject::into_pyobject` expects two arguments:

- `self`: the Rust value you want to convert into a Python object.
- `Python<'_>`: a GIL token that you can use to create new Python objects.
- `Python<'py>`: a GIL token that you can use to create new Python objects.

The conversion can fail, so the method returns a `Result`.\
The output type itself is more complex, so let's break it down using an example.

## Case study: a newtype

Let's look at a simple example: a newtype that wraps a `u64`.
We want it to be represented as a "plain" integer in Python.

```rust
use std::convert::Infallible;
use pyo3::prelude::*;
use pyo3::types::PyInt;

struct MyType {
value: u64,
}

impl IntoPy<Py<PyAny>> for MyType {
fn into_py(self, py: Python<'_>) -> Py<PyAny> {
self.value.to_object(py)
impl<'py> IntoPyObject<'py> for MyType {
/// `Target` is the **concrete** Python type we want to use
/// to represent our Rust value.
/// The underlying Rust type is a `u64`, so we'll convert it to a `PyInt`,
/// a Python integer.
type Target = PyInt;
/// `Output`, instead, is a **wrapper** around the concrete type.
/// It captures the ownership relationship between the Python object
/// and the Python runtime.
/// In this case, we're using a `Bound` smart pointer to a `PyInt`.
/// The `'py` lifetime ensures that the Python object is owned
/// by the Python runtime.
type Output = Bound<'py, PyInt>;
/// Since the conversion can fail, we need to specify an error type.
/// We can't fail to convert a `u64` into a Python integer,
/// so we'll use `Infallible` as the error type.
type Error = Infallible;

fn into_pyobject(self, py: Python<'py>) -> Result<Self::Output, Self::Error> {
// `u64` already implements `IntoPyObject`, so we delegate
// to its implementation to do the actual conversion.
self.value.into_pyobject(py)
}
}
```

### The `Output` associated type

Let's focus on the `Output` associated type for a moment.\
In almost all cases, you'll be setting `Output` to `Bound<'py, Self::Target>`[^syntax]. You're creating a new Python
object and its lifetime is tied to the Python runtime.

In a few cases, you might be able to rely on [`Borrowed<'a, 'py, Self::Target>`](https://docs.rs/pyo3/0.23.3/pyo3/prelude/struct.Borrowed.html)
instead.
It's slightly faster[^conversation], but it's limited to scenarios where you are borrowing from an existing Python object—fairly
rare for an `IntoPyObject` implementation.

There are no other options for `Output`, since `Output` must implement
[the `BoundObject` trait](https://docs.rs/pyo3/0.23.3/pyo3/trait.BoundObject.html),
the trait is [sealed](https://predr.ag/blog/definitive-guide-to-sealed-traits-in-rust/) and
those two types are the only implementors within `pyo3`.\
If it helps, think of `Output` as an enum with two variants: `Bound` and `Borrowed`.

## Provided implementations

`pyo3` provides out-of-the-box implementations of `IntoPy` for many Rust types, as well as for all `Py*` types.
Check out [its documentation](https://docs.rs/pyo3/0.22.0/pyo3/conversion/trait.IntoPy.html#) for an exhaustive list.
`pyo3` provides out-of-the-box implementations of `IntoPyObject` for many Rust types, as well as for all `Py*` types.
Check out [its documentation](https://docs.rs/pyo3/0.23.3/pyo3/conversion/trait.IntoPyObject.html#foreign-impls)
for an exhaustive list.

[^syntax]: The actual syntax is a bit more complex: `type Output = Bound<'py, <Self as IntoPyObject<'py>>::Target>>;`.
We've simplified it for clarity.

[^conversation]: In addition to its documentation, you may find [this issue](https://github.com/PyO3/pyo3/issues/4467)
useful to understand the trade-offs between `&Bound` and `Borrowed`.
4 changes: 2 additions & 2 deletions book/src/02_classes/00_pyclass.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ fn my_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
}
```

## `IntoPy`
## `IntoPyObject`

Rust types that have been annotated with `#[pyclass]` automatically implement the `IntoPy` trait, thus
Rust types that have been annotated with `#[pyclass]` automatically implement the `IntoPyObject` trait, thus
allowing you to return them from your `#[pyfunction]`s.

For example, you can define a function that creates a new `Wallet` instance:
Expand Down
Loading

0 comments on commit a9dff83

Please sign in to comment.