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
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.