Jim
Jim

Reputation: 769

pyo3: Passing a reference of Rust's Self into a method of Self with attribute `#[classmethod]`

Context of my question:

In pydantic's, you can define a CustomType, which is any python class with the following class method named __get_validators__(cls) defined. The objective is to have pydantic validate the input to the initialization of the CustomType instance.

class CustomType:

    @classmethod
    def __get_validators__(cls):
        yield cls.validate_func_one

I am attempting to create a struct in Rust, mapping it to Python class using the familiar attribute macro #[pyclass]. My objective is to create from Rust, a Python class that is compatible with pydantic validation scheme, e.g. having the classmethod(__get_validators__) defined.

Here is my attempt:

#[pyclass(name = "MyPythonClass")]
#[derive(Debug, Clone)]
pub struct PyClass {
    pub rust_object: MyRustStruct,
}

The implementation of PyClass is as follows:

#[pymethods]
impl PyClass {
    #[new]
    fn new(string: String) -> PyResult<PyClass > {
        Ok(PyClass {
            rust_object: MyRustStruct::new(string).unwrap(),
        })
    }

    // intended to be the `cls.validate` function which is yielded
    fn __call__(_cls: &PyType, v: &PyString, _values: &PyDict) -> PyResult<PyClass> {
        let v: &str = v.extract()?;
        Ok(PyClass{
            // PyClass::new does all the validations required in rust
            rust_object: PyClass::new(v).unwrap(),
        })
    }

    #[classmethod] // not sure how to write this...
    fn __get_validator__(_cls: &PyType) -> PyResult<PyClassContainer> {
      Ok(Self::get_container())
    }

PyClass is a wrapper over the underlying Rust struct MyRustStruct, such that MyRustStruct can be used for other purposes, e.g. to build a binary etc. This is standard practice.

To be able to yield, I would need two things: first, a struct that is an Iterator in rust and last, that struct has to implement

First:

#[pyclass(name = "PyClassContainer")]
#[derive(Debug)]
struct PyClassContainer{
    iter: Vec<PyClass>,
}

To trivially 'implement' the rust Iterator trait, I dump the PyClass object into a Vector.

Then lastly, for the implementations of __iter__ and __next__ of PyClassContainer:

#[pymethods]
impl PyClassContainer{

    fn __iter__(slf: PyRef<'_, Self>) -> PyResult<Py<PyClassContainer>> {
        let iter = PyClassContainer{
            iter: slf.iter.clone().into_iter(),
        };
        Py::new(slf.py(), iter)
    }

    // to use `yield`, the `__next__` method has to return an `IterNextOutput<T, U>` enum
    // https://docs.rs/pyo3/latest/pyo3/pyclass/enum.IterNextOutput.html
    fn __next__(slf: PyRef<'_, Self>) -> IterNextOutput<Result<PyClass, &'static str>, PyErr> {
        match self.iter.into_iter() {
            Some(Ok(item)) => IterNextOutput::Yield(Ok(item)),
            Some(Err(err)) => IterNextOutput::Return(PyStopIteration::new_err(err)),
    }
  }
}

My intent is to have __next__ to 'yield' Ok(item) from the .iter field of PyClassContainer which will return me an instance of the python object of type PyClass when PyClass.__get_validator__ is called.

However, the signature of attribute macro #[classmethod] does not allow the passing in of a reference of self, which makes it impossible for me to 'clone' the fields that are already available in self.

Upvotes: 1

Views: 520

Answers (0)

Related Questions