Search code examples
pythonrustpyo3

Retrieve a #[pyclass] from an attribute of an arbitrary PyAny


Let us say I have the following Rust struct which becomes a Python class:

#[pyclass]
struct MyRustClass {
    foo: i32
}

#[pymethods]
impl MyRustClass {
    #[new]
    fn new(foo: i32) {
        MyRustClass { foo }
    }
}

This class is made available to Python, and a new Python class, which is not implemented in Rust at all, holds an instance of this class in an attribute like so, for the purposes of this question, assume that this class is impossible to implement in Rust, and must be entirely Python side:

class MyPythonClass:
    def __init__(self, foo: int):
        self.my_rust_class = MyRustClass(foo)

Now what I want to do, is be able to expose a #[pyfunction] which accepts an instance of MyPythonClass as an argument, and retrieves the MyRustClass struct from the MyPythonClass.my_rust_class attribute. This is the part I'm not sure about how to accomplish, but the function signature is something like below, where the python_class argument is an instance MyPythonClass:

#[pyfunction]
fn my_cool_function(python_class: PyAny) {
    // Get some sort of reference to the MyRustClass struct from the MyPythonClass.my_rust_class attribute
}

Solution

  • PyAny has a getattr method (as well as hasattr, setattr, delattr) https://docs.rs/pyo3/latest/pyo3/struct.PyAny.html#method.getattr

    So with a class like this:

    #[pyclass]
    #[derive(Debug, Clone)]
    pub struct TestClass {
        pub value: i32,
    }
    
    #[pymethods]
    impl TestClass {
        #[new]
        fn new(value: i32) -> Self {
            Self { value: value }
        }
    }
    

    You can create a function to try and extract the rust_class (or any number of different types) from a PyAny:

    #[pyfunction]
    fn get_int_from_class(python_class: &PyAny) -> PyResult<i32> {
        let pyany_rust_class = python_class.getattr("rust_class")?;
        let rust_class: TestClass = pyany_rust_class.extract()?;
        Ok(rust_class.value)
    }
    

    Then in python:

    from unwrap_test import TestClass, get_int_from_class
    
    class PythonClass:
        def __init__(self, value:int) -> None:
            self.rust_class = TestClass(value)
    
    python_class = PythonClass(3)
    print(get_int_from_class(python_class)) # 3
    

    It'll even handle the exceptions nicely:

    get_int_from_class('not even a class')
    # AttributeError: 'str' object has no attribute 'rust_class'
    

    and:

    class WrongClass:
        def __init__(self) -> None:
            self.rust_class = 'not a rust class'
    
    get_int_from_class(WrongClass())
    # TypeError: 'str' object cannot be converted to 'TestClass'