Closest implementation for enums with fields in pyO3

As of PyO3 version 0.18.1, only fieldless enums are supported.

What are the potential implementations to support enums with fields using PyO3 if implemented manually?? pyclass is of no use here (Correct me if I am wrong).

For instance if I have to implement for following enum?

pub enum Prop {
    Str(String),
    I32(i32),
    I64(i64),
    U32(u32),
    U64(u64),
    F32(f32),
    F64(f64),
    Bool(bool),
}

Please suggest.

I imagine you have to construct some dynamic Python object. With Rust you can't use enums with fields and you can't use types with generic parameters. I think you can construct a python dictionary (or class if you prefer that, though this requires more effort and writing that class in Python, I assume). What I'd do is construct a dictionary with the fields type and value, for example:

d1 = {
    "type": "str",
    "value": "hello", 
}

d2 = {
    "type": "bool",
    "value": True,
}

I think this can be accomplished by implementing the ToPyObject trait for Prop, something like this:

use pyo3::conversion::ToPyObject;
use pyo3::{types::PyDict, PyObject, Python};

pub enum Prop {
    Str(String),
    I32(i32),
    I64(i64),
    U32(u32),
    U64(u64),
    F32(f32),
    F64(f64),
    Bool(bool),
}

impl Prop {
    fn ty(&self) -> String {
        match self {
            Self::Str(_) => "str",
            Self::I32(_) => "i32",
            Self::I64(_) => "i64",
            Self::U32(_) => "u32",
            Self::U64(_) => "u64",
            Self::F32(_) => "f32",
            Self::F64(_) => "f64",
            Self::Bool(_) => "bool",
        }
        .to_owned()
    }

    fn value(&self) -> PyObject {
        Python::with_gil(|py| match self {
            Self::Str(s) => s.to_object(py),
            Self::I32(i) => i.to_object(py),
            Self::I64(i) => i.to_object(py),
            Self::U32(u) => u.to_object(py),
            Self::U64(u) => u.to_object(py),
            Self::F32(f) => f.to_object(py),
            Self::F64(f) => f.to_object(py),
            Self::Bool(b) => b.to_object(py),
        })
    }
}

impl ToPyObject for Prop {
    fn to_object(&self, py: Python<'_>) -> PyObject {
        let obj = PyDict::new(py);

        obj.set_item("type", self.ty()).unwrap();
        obj.set_item("value", self.value()).unwrap();

        obj.into()
    }
}

I didn't test it, so try out with caution.

Thanks for your reply @jofas.

I was thinking in the line of class/subclass like so:

#[pyclass(subclass)]
#[derive(Clone)]
struct Prop;

#[pymethods]
impl Prop {
    #[new]
    fn new() -> Self {
        Prop
    }

    pub fn method(&self) {}
}

#[pyclass(extends=Prop, subclass)]
#[derive(Clone)]
struct Str {
    value: String,
}

#[pymethods]
impl Str {
    #[new]
    fn new(value: String) -> (Self, Prop) {
        (Str { value }, Prop::new())
    }

    pub fn method(&self) {
        println!("value = {}", self.value)
    }
}

#[pyfunction]
fn print_prop(s: Prop) {
    s.method()
}

#[pyfunction]
fn print_str(s: Str) {
    println!("{}", s.value)
}

This allows me to keep the API similar to how I am using enums (with fields) in rust.

>>> import pyo3_example
>>> s = pyo3_example.Str("pomtery")
>>> pyo3_example.print_prop(s)
>>> pyo3_example.print_str(s)
pomtery
>>>

This seems to look nice but the problem I am facing is related to overriding methods!
In the print_prop function if I could pass any subtype and then call derived functions it would work. But it doesn't seem to be working. Any idea how can I fix this? And also what do you think of this approach?

Why do you need print_prop? Have you tried calling the method directly? Like this:

>>> import pyo3_example
>>> s = pyo3_example.Str("pomtery")
>>> s.method()

It's type-safer than what I had in mind, but will require significantly more boilerplate. In the end what matters is how happy you and your users are. I'd prioritize the interface and try to make it feel as pythonic as possible.

Okay, so in the wider context. I want user to pass a list of Prop and on the rust end I would invoke convert to convert it into field based enum which is already being used in the rest of the library.

To make is more vivid, the pseudo code would look something like this:

// python
get_props([Str("pometry"), Int(32)])

// rust
get_props(props: Vec<Prop>) -> Vec<EnumProp> {
  props.iter().map(|prop| {
    prop.convert()
  }).collect()
}

Makes sense?

Makes perfect sense. Have you considered a python class like this?

class Property:
    def __init__(self, ty, val):
        self.ty = ty
        self.val = val
    
    @staticmethod
    def str(s):
        return Property("str", s)

    @staticmethod
    def int(i):
        return Property("int", i)

get_props([Property.str("pometry"), Property.int(32)])

Note that you can make get_props even more pythonic with *args : Python Classes - PyO3 user guide

get_props(Property.str("pometry"), Property.int(32))

Going forward with your suggestion on keeping APIs more pythonic I managed to implement this as follows:

use pyo3::prelude::*;
use std::collections::HashMap;

#[derive(FromPyObject, Debug)]
pub enum Prop {
    Int(usize),
    String(String),
    Vec(Vec<usize>),
}

#[pyfunction]
pub fn get_props(props: HashMap<String, Prop>) -> PyResult<()> {
    let v = props.into_iter().collect::<Vec<(String, Prop)>>();
    for i in v {
        println!("K = {}, V = {:?}", i.0, i.1)
    }
    Ok(())
}

From python:

import pyo3_example

pyo3_example.get_props({
                   "name": "Shivam Kapoor", 
                   "age": 35,  
                   "hobbies": [1, 2, 3]
                 })
# K = name, V = String("Shivam Kapoor")
# K = age, V = Int(35)
# K = hobbies, V = Vec([1, 2, 3])

Thanks for all the help! @jofas

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.