pyo3 icon indicating copy to clipboard operation
pyo3 copied to clipboard

Support returning &mut Self for method chaining

Open Walter-Reactor opened this issue 1 year ago • 2 comments

It would be extremely helpful to support easily binding APIs which rely on method chaining (obj.a().b().c() where a, b, and c are instance methods). The builder pattern is one of the more frequent use cases, though it's far from the only one.

In Rust, these methods look like pub fn foo(&mut self, ...) -> &mut Self {...}. Unfortunately such functions can't be used in a #[pymethods] block at the moment

I'm still a little new to rust, but it seems strange to me that this doesn't work since Self since we know already that Self is a PyObject. Unfortunately, I have no real idea how something like this might be implemented, or if there's language reasons why this can't be done.

Example:


#[pyclass]
pub struct SimpleChaining{}

#[pymethods]
impl SimpleChaining{
    pub fn a(&mut self) -> &mut Self {
        self
    }
}

Currently fails with

error[E0277]: the trait bound `&mut SimpleChaining: OkWrap<&mut SimpleChaining>` is not satisfied
   --> lib\generators\src\scenario_generator.rs:122:1
    |
122 | #[pymethods]
    | ^^^^^^^^^^^^ the trait `IntoPy<Py<PyAny>>` is not implemented for `&mut SimpleChaining`, which is required by `&mut SimpleChaining: OkWrap<_>`
    |
    = help: the trait `IntoPy<Py<PyAny>>` is implemented for `SimpleChaining`
    = note: required for `&mut SimpleChaining` to implement `OkWrap<&mut SimpleChaining>`
    = note: this error originates in the attribute macro `pymethods` (in Nightly builds, run with -Z macro-backtrace for more info)

Walter-Reactor avatar Jul 10 '24 21:07 Walter-Reactor

For various reasons this isn't particularly practical -- pyo3 has no way of mapping the &mut reference that's returned back to the original instance. The right way to do this is something like:

fn method(slf: pyo3::PyRef<'_, Self>) -> pyo3::PyRef<'_, Self> {
    self
}

alex avatar Jul 14 '24 14:07 alex

Adding on to alex's point, you can instead use PyRefMut for both the argument and the return value to let you mutate the type (since PyRefMut<T> dereferences to &mut T), which is typically what you want for builders.

For example, a basic builder pattern might look something like this:

use pyo3::prelude::*;
use pyo3::exceptions::PyValueError;

#[pyclass]
struct MyProduct {
    name: String,
    value: i32,
}

#[pyclass]
struct MyBuilder {
    name: Option<String>,
    value: Option<i32>,
}
#[pymethods]
impl MyBuilder {
    #[new]
    fn new() -> Self {
        Self { name: None, value: None }
    }
    fn name(mut slf: PyRefMut<Self>, name: String) -> PyRefMut<Self> {
        slf.name = Some(name);
        slf
    }
    fn value(mut slf: PyRefMut<Self>, value: i32) -> PyRefMut<Self> {
        slf.value = Some(value);
        slf
    }
    fn build(slf: PyRef<Self>) -> PyResult<MyProduct> {
        let name = slf.name.as_ref().cloned()
            .ok_or_else(|| PyValueError::new_err("name not set"))?;
        let value = slf.value.as_ref().cloned()
            .ok_or_else(|| PyValueError::new_err("value not set"))?;
        Ok(MyProduct { name, value })
    }
}

Which you could then use from Python like this:

product = MyBuilder().name("fred").value(10).build()

JRRudy1 avatar Jul 14 '24 23:07 JRRudy1

Closing as resolved.

davidhewitt avatar Oct 11 '24 20:10 davidhewitt