Search code examples
pythonrustpyo3

TypeError in Python using PyAny in reflected numeric emulator (e.g. __radd__) for a Rust pyo3 pyclass struct


I've created a Rust library for python using pyo3. This contains a pyclass struct that implements several of PyNumberProtocol methods like __add__, __sub__, etc... to allow python operators like + and - to work on the class. I'm using PyAny as the 'other' object in most of these as I want to support lots of different types. That's working fine but when I try and implement the reflected methods like __radd__ and __rsub__, python throws a TypeError. The TypeError that's thrown has no arguments or message, it's just an empty TypeError. The method itself works if I call myitem.__radd__(other) but other + myitem fails. I stripped out everything except __add__ and __radd__ for i64 as an example (TestClass1 below).

I can implement the reflected methods for a specific type, for example i64 (see TestClass2 below). But obviously this doesn't allow any different types (float, lists, classes, etc...). I couldn't find any generic types that would work, nor any way to overload the __radd__ method. So my question is, is there a way to implement __radd__ to accept multiple types from python? I'm pretty new to Rust so I've probably missed something obvious...

Rust example library:

use pyo3::exceptions::TypeError;
use pyo3::prelude::*;
use pyo3::PyNumberProtocol;

macro_rules! create_test_class {
    ($name: ident) => {
        #[pyclass]
        #[derive(PartialEq, Debug, Clone)]
        pub struct $name {
            #[pyo3(get, set)]
            value: i64,
        }
        #[pymethods]
        impl $name {
            #[new]
            pub fn from_value(value: i64) -> $name {
                $name { value: value }
            }
        }
    };
}

create_test_class!(TestClass1);
create_test_class!(TestClass2);

#[pyproto]
impl PyNumberProtocol for TestClass1 {
    fn __add__(lhs: TestClass1, rhs: &PyAny) -> PyResult<TestClass1> {
        let pynum_result: Result<i64, _> = rhs.extract();
        if let Ok(pynum) = pynum_result {
            Ok(TestClass1 {
                value: lhs.value + pynum,
            })
        } else {
            Err(TypeError::py_err("Not implemented for this type!"))
        }
    }
    fn __radd__(self, other: &PyAny) -> PyResult<TestClass1> {
        let pynum_result: Result<i64, _> = other.extract();
        if let Ok(pynum) = pynum_result {
            Ok(TestClass1 {
                value: self.value + pynum,
            })
        } else {
            Err(TypeError::py_err("Not implemented for this type!"))
        }
    }
}

#[pyproto]
impl PyNumberProtocol for TestClass2 {
    fn __radd__(self, other: i64) -> PyResult<TestClass2> {
        Ok(TestClass2 {
            value: self.value + other,
        })
    }
}

#[pymodule]
fn test_class(_py: Python, m: &PyModule) -> PyResult<()> {
    m.add_class::<TestClass1>()?;
    m.add_class::<TestClass2>()?;
    Ok(())
}

Python example, all the print statements work as expected except the last line:

from test_class import TestClass1, TestClass2

tc2 = TestClass2(10)
print(tc2.__radd__(3).value)  # 13
print((3 + tc2).value)        # 13
try:
    3.0 + tc2                 # expected TypeError
except TypeError as e:
    print(repr(e))            # TypeError("'float' object cannot be interpreted as an integer")

tc1 = TestClass1(10)
print((tc1 + 3).value)        # 13
print(tc1.__radd__(3).value)  # 13
print((3 + tc1).value)        # unexpected, empty TypeError 

I'm using Rust 1.45.2, pyo3 0.11.1, python 3.7.3


Solution

  • After some more digging it looks like it's a limitation of the current version of pyo3: https://github.com/PyO3/pyo3/issues/844

    Also it had nothing to do with PyAny, my test was too simple. TestClass2 worked not because it used i64 instead of &PyAny but because it didn't have __add__! I added a simple __add__ method and sure enough that broke it.

    Anyway, from the chatter in the github discussion it looks like this will be working in pyo3 0.12.