PyO3 / PyO3/pyo3

Creating instances of python sub classes of a Rust struct from factory methods on the struct

Open
#4,608 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Rust
Stars
16.2k
Forks
1k
Avg merge
2d 6h
Merged PRs (30d)
66

Description

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!

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by examining PyO3's handling of #[classmethod] and #[pyclass(subclass)], using the minimal Rust/Python example as a reproduction. Compare the existing #[new] subclass behavior with the proposed factory path; done means a classmethod can construct the requested Python subclass from an existing Rust struct without requiring a costly new method, while still allowing the base type to be returned when desired.

Written by the indexing model from the issue text.

Assessment

Tech stack
python, rust
Domain
backend-api-design
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.