PyO3 / pyo3

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

Extend Python Class from Rust #991

Open davidhewitt opened 4 years ago

davidhewitt commented 4 years ago

A user asked a question on Gitter just now about extending a Python class from Rust.

While I think it's possible to do this by hand with a lot of unsafe code by implementing PyTypeInfo for the base type manually, it's pretty complicated.

This is an issue to think one day about how to make this easier. Possibly it's good enough to document the current process, or maybe there are design changes we can make internally.

nekitdev commented 4 years ago

I had an initial idea of calling PyType_Type, just like from python, in order to construct new classes with multiple inheritance:

class A:
    a = 13

class B:
    b = 42

C = type("C", (A, B), {})
davidhewitt commented 4 years ago

Interesting. You might find #1152 interesting which I think might use that or something similar (haven't had a chance to review it yet)

chinedufn commented 3 years ago

Possibly it's good enough to document the current process

Are there any existing examples that you can point me to of how this is done now?

Thanks!

LegNeato commented 3 years ago

I recently hit this and asked in gitter too

"How do I use abstract base classes / make my Rust code with the same api as a built in class pass an isinstance check? I can't seem to find it in the docs or any examples. I specifically want my Rust class to pass isinstance(i, uuid.UUID) where i is a instance created by my Rust code. (if it isn't supported, happy to put up a PR if someone will point me in the right direction and it is reasonable for a pyo3 beginner)"

David Hewitt @davidhewitt Feb 20 22:50 @LegNeato you're looking at a special case of PyO3/pyo3#991. Unfortunately this is not supported at the moment. It's quite a hard issue to solve, but if you're interested in implementing it I can discuss the design and implementation steps with you on that issue.

@davidhewitt I likely won't have time soon due to work so don't sweat it, but if you find some cycles and can throw some information here I can possibly take a crack at it in the future when work settles down.

LegNeato commented 3 years ago

Potentially relevant: https://www.python.org/dev/peps/pep-0253/

calbaker commented 1 year ago

I tried using subclass in a pyclass in https://github.com/NREL/fastsim (can grant access to this private repo if needed), and when I tried:

class ChildVehicle(fsr.RustVehicle):
    def get_veh_kg(self) -> float:
        return self.veh_kg

veh = ChildVehicle.from_file(str(fsim.package_root() / 'resources/vehdb/2012_Ford_Fusion.yaml'))

veh.get_veh_kg()

this happened:

---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In [12], line 7
      3         return self.veh_kg
      5 veh = ChildVehicle.from_file(str(fsim.package_root() / 'resources/vehdb/2012_Ford_Fusion.yaml'))
----> 7 veh.get_veh_kg()

AttributeError: 'fastsimrust.RustVehicle' object has no attribute 'get_veh_kg'

Note that on the Rust side, we use a proc macro to add the pyo3 attributes so it'll look a little wonky to anyone who looks into the Rust code.

birkenfeld commented 1 year ago

@calbaker please open this as a separate discussion/issue - this issue is about extending the other way.

caniko commented 12 months ago

I had this issue about half a year ago, and came up with an idea (link to pydantic-core issue) where we use BaseModel from Pydantic as an interface between the two languages.

Half a year later, I need this feature again in a completely different project; sad to see it being so far away (very understandable).

A big +1 from me.

ghost commented 11 months ago

How could this be implemented? Wouldn't it require evaluating the python code with an from module import SomeClass and generating a rust struct that's then imported into rust?

Or would the macro generate the equivalent CPython commands to import the class and then subclass it dynamically, thus making the code unsafe?

caniko commented 11 months ago

How could this be implemented? Wouldn't it require evaluating the python code with an from module import SomeClass and generating a rust struct that's then imported into rust?

Or would the macro generate the equivalent CPython commands to import the class and then subclass it dynamically, thus making the code unsafe?

You are welcome to join the discussion @LoveIsGrief

ghost commented 11 months ago

Just a dump of my thoughts @nekitdev already found it, but classes / types in python can be created dynamically https://github.com/PyO3/pyo3/issues/991#issuecomment-703310594. (relevant python doc). They generate a PyTypeObject (documentation on creating new types in C).

So it should be possible to dynamically create types in rust from imported python classes. The downside is they will be dynamic / at runtime and thus cannot be used for static typing elsewhere in Rust code. As a first step, that might be OK and it might even already be possible (still have to try it out to confirm).

Making it static though would be the most difficult, I imagine. One would have to somehow evaluate the python code before compilation or during compilation to generate either:

@caniko I'm not sure why this should be dependent on pydantic. Could shed some light on that? Shouldn't PyO3 be independent of third-party python libs?

caniko commented 11 months ago

Making it static though would be the most difficult

@LoveIsGrief, you are already thinking about the issue. The Pydantic way would make it static. :two_men_holding_hands:

davidhewitt commented 11 months ago

@LoveIsGrief agreed that making dynamic subtyping is a fine first step, I think all we would need to do to achieve that is give #[pyclass] some way to load the base type, probably by a PyTypeInfo implementation. There is also work that would need to be done in the pyclass internals. If you're interested in helping I can try to write up what's needed.

To make it static I think we can borrow inspiration from Duchess which has a java_package! macro. I understand this can do compile-time reflection using javap; we could consider invoking Python in the same way to introspect.

@caniko I think the pydantic approach you propose may be suitable for some use-cases but I don't think it's necessary for a barebones implementation here.

AtomicGamer9523 commented 9 months ago

I have a similar issue, however mine might be even more complex. Basically: mymodule.pyi:

import abc as _abc
class Entity(_abc.ABC):
    """An entity"""
    @_abc.abstractproperty
    def uuid(self) -> str:
        """Unique ID of this entity"""
        ...

And then I also have implementations of that class (also in .pyi). But the users might want to create their own class (in python), that extends the Entity class, and define the property for it. And my rust code needs to handle that accordingly. How would I go about doing this?

Edit: I will attempt to acknowledge the issue of abstract classes in this repo.

ghost commented 9 months ago

@davidhewitt time flew by! A quick click through the #[pyclass] macro had my head spinning. However, if you have time to write up what's needed, I'd be happy to give this a shot in 2024 if I find the time to!

kythyria commented 3 months ago

In my case I'm generating Rust-facing wrappers via macro anyway, so the boilerplate being boilerplatey is less of an issue than the lack of documentation on what it needs to be: I've narrowed it down to needing PyTypeInfo and PyClassBaseImpl on the wrapper, but don't understand the latter enough to know what it is that's actually causing me problems.