PyO3 / pyo3

Rust bindings for the Python interpreter
https://pyo3.rs
Apache License 2.0
12.18k stars 751 forks source link

IntoPy for #[pyclass(extends=...)] ? #1836

Open dbr opened 3 years ago

dbr commented 3 years ago

Context

I am trying to wrap a few Rust objects in a Python API - basically an enum of a few different types.

enum Things {
    Obj1(String),
    Obj2(f64),
}

I want to represent this as, roughly, something like this:

class BaseThing:
    def as_obj1(self): ...
    def as_obj2(self): ...

class Obj1(BaseThing): ..
class Obj2(BaseThing): ...

So, I created a base pyclass,

#[pyclass(name="BaseThing", subclass)]
#[derive(Debug, Clone)]
pub struct PyBaseThing {
}

..and pyclass for each of the variants:

#[pyclass(name="Obj1", extends=MyBase)]
#[derive(Debug, Clone)]
pub struct PyObj1 {
}

#[pyclass(name="Obj2", extends=MyBase)]
#[derive(Debug, Clone)]
pub struct MyObj2 {
}

All good so far (this explanation is missing a bunch of detail and isn't an entirely correct summary of the actual code, but hopefully enough to give context for my question)

Problem

Problem is when I try to implement the fn as_obj1(&self, py: Python) -> PyResult<PyObj1> { Ok(PyObj1{}.into_py(py)) } , I can't get past the error:

method cannot be called on PyObj1 due to unsatisfied trait bounds because

    = note: the following trait bounds were not satisfied:
            `PyObj1 AsPyPointer`
            which is required by `&PyObj1: pyo3::IntoPy<pyo3::Py<pyo3::PyAny>>`

If I change #[pyclass(name="Obj1", extends=MyBase)] to #[pyclass(name="Obj1")] then all compiles and works, except the Obj1 is of course not a subclass of my BaseThing

After a bit of poking around and rereading the docs several times, I spotted the sentence

All types in PyO3 implement this trait, as does a #[pyclass] which doesn't use extends.

..however this a bit cryptic to me. Is there a reason the "pyclass-with-extends" cannot implement IntoPy? Is there another conversion trait I should instead?

Not urgent as in my case the subclass hierarchy is just a "nice to have" thing, and I can just drop the extends=... for now

davidhewitt commented 3 years ago

This is a bit thorny. The reason that "pyclass-with-extends" doesn't implement IntoPy is because we also need the base class struct which it extends from to complete the conversion.

If the base class was stored inside the subclass struct (see https://github.com/PyO3/pyo3/issues/1637#issuecomment-849444381), then it should be possible for the subclass to implement IntoPy.

CLOVIS-AI commented 2 years ago

Currently stuck on the same thing (https://github.com/PyO3/pyo3/discussions/2294). What is the correct way to create a Python instance of a class with inheritance then? Or can they only be instantiated from Python code?

davidhewitt commented 2 years ago

With apologies this is still a very unpolished part of PyO3.

You can use .into_new_object method of PyClassInitializer to create a new Python object from the initializer. However that's really an internal API we use from macro code. If someone was interested in working on this area of PyO3, help would be welcome.