Creating instances of python sub classes of a Rust struct from factory methods on the struct
The Problem
I have a struct in rust that is intended to be sub classed by python classes, and I need to provide factory class methods to this struct. Currently, there seems to be no direct way to instantiate an instance of a sub class with an already existing instance of the Rust struct.
Minimal Example
use pyo3::prelude::*;
use pyo3::types::PyType;
use rand::random;
#[pyclass(subclass)]
pub struct Spam {
#[pyo3(get)]
eggs: i32,
}
#[pymethods]
impl Spam {
#[classmethod]
fn mk<'py>(cls: Bound<'py, PyType>) -> Self {
// Naive attempt to return Spam, considering it may
// automatically get converted to subtype like `#[new]`
Spam {
eggs: random(),
}
}
}
and in python:
from test_pyo3 import Spam
class Child(Spam):
pass
print(Child.mk()) # <builtins.Spam object at 0x74993d11bf10>
The mk function doesn't automatically convert the returned Spam into a Child instance
Current Workaround
The best I've managed to do is the following:
#[pymethods]
impl Spam {
#[new]
fn new_py() -> Self {
Spam {
eggs: -1,
}
}
#[classmethod]
fn mk<'py>(cls: Bound<'py, PyType>) -> PyResult<Bound<'py, PyAny>> {
let spam = Spam {
eggs: random(),
};
let sub = cls.call0()?;
let mut parent_spam = sub.downcast::<Spam>()?.borrow_mut();
*parent_spam = spam;
Ok(sub)
}
}
print(Child.mk()) # <__main__.Child object at 0x744d73506d50>
However, this is not ideal, since I had to define a new, and in a real scenario if new is non trivial to call/or is costly, this is not feasible.
Suggestion
Would it be possible to add something like this:
#[pymethods]
impl Spam {
#[classmethod]
fn mk<'py>(cls: Bound<'py, PyType>) -> Bound<'py, PyAny> {
cls.from_super(Spam {
eggs: random(),
})
}
}
Note: This still keeps it possible to return the base type if so desired, since it doesn't automatically converting a classmethod's return value to the subtype like #[new]
Implementation
I'd be happy to give implementing this a go, but I'm not sure how hard it might be, and may need some guidance/mentoring!
Great question. Closely related to this is other subclassing issues like #4443, #1647, #1644 and https://github.com/PyO3/pyo3/issues/1637#issuecomment-849444381
I think the API you propose fits a very relevant need, but I'd slightly prefer if instead of adding one new function a bit ad-hoc we tried to do a more complete review of what we are currently missing and where we don't match Python semantics.
Ideally we can then do a small API refresh to close a bunch of related awkward cases. Maybe targeting 0.24