2

I'm trying to build a Python package from Rust using PyO3 (version: 0.13.2). Right now I'm stuck trying to get the conversion working for enums. I have a simple enum like so:

#[derive(FromPyObject)]
#[derive(Copy,Clone,PartialEq,Eq)]
enum Direction {
    Left,
    Right,
    Up,
    Down
}

I added #[derive(FromPyObject)] as per the documentation, however, I get the following error:

error: cannot derive FromPyObject for empty structs and variants --> src/main.rs:3:10 | 3 | #[derive(FromPyObject)] |
^^^^^^^^^^^^ | = note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info)

In the example, all of the enum values have types associated with them. If this is the source of the error, is there any way around it so it'll work with the enum I have?

Thanks for any help.

SOLUTION

Here is the solution I ended up with. I'm new to Rust, so use it at your own risk. Thanks to Ahmed Mehrez from this question for providing the basis for the macro.

You'll need the following dependencies.

[dependencies]
num-traits = "0.2"
num-derive = "0.3"

The macro implements IntoPy and FromPyObject for the enum. It converts to/from an int. Plus, you can now iterate the enum!

use pyo3::prelude::*;

#[macro_use]
extern crate num_derive;
use num_traits::FromPrimitive;

// https://stackoverflow.com/questions/21371534/in-rust-is-there-a-way-to-iterate-through-the-values-of-an-enum
macro_rules! simple_enum {

    ($visibility:vis, $name:ident, $($member:tt),*) => {

        #[derive(Copy,Clone)]
        $visibility enum $name {$($member),*}

        impl $name {
            fn iterate() -> Vec<$name> {
                vec![$($name::$member,) *]
            }
        }

        impl IntoPy<PyObject> for $name {
            fn into_py(self, py: Python) -> PyObject {
                (self as u8).into_py(py)
            }
        }

        impl FromPyObject<'_> for $name {

            fn extract(ob: &'_ PyAny) -> PyResult<$name> {
        
                let value: u8 = ob.extract().unwrap();

                if let Some(val) = FromPrimitive::from_u8(value) {
                    for member in $name::iterate() {
                        if (member as u8) == val {
                            return Ok(member);
                        }
                    }
                }

                panic!("Invalid value ({}).", value);
            }
        }
    };

    ($name:ident, $($member:tt),*) => {
        simple_enum!(, $name, $($member),*)
    };
}

// Example
simple_enum!(pub, Direction, 
    Left, 
    Right, 
    Up, 
    Down
);

From Python, you'll want to redefine your enum and use the value with your Rust module.

from enum import Enum
    
class Direction(Enum):
    Left = 1
    Right = 2
    Up = 3
    Down = 4
    
// Direction.Left.value
interrupt0
  • 25
  • 5

1 Answers1

4

There is currently no derivation for this type of enum. The FromPyObject derivation is designed to handle polymorphic input from the Python side, not to discriminate between unit types.

However, there has been a stale PR for adding general enum support on PyO3 since last year's summer. If this gets some movement, you might be able to deal with Python Enums in the future.

Until then, you'll need to implement FromPyObject by hand and decide what inputs map to which variant.

If you want to pass a string from Python and get the matching enum variant in Rust from it, you could also make your interface take a String in Rust, add a impl TryFrom<&str> for Direction and try the conversion in your interfacing function.

sebpuetz
  • 2,430
  • 1
  • 7
  • 15