11

I am using the pyo3 rust crate (version 0.11.1) in order to port rust code, into cpython (version 3.8.2) code. I have created a class called my_class and defined the following functions: new, __str__, and __repr__.

TL;DR: The __str__ function exists on a class ported from rust using the pyo3 crate, but doesn't get printed when just using print(obj), and instead having to write print(obj.__str__())

The my_class definition is here:

use pyo3::prelude::*;

#[pyclass]
struct my_class {
    #[pyo3(get, set)]
    num: i32,
    #[pyo3(get, set)]
    debug: bool,
}

#[pymethods]
impl my_class {
    #[new]
    fn new(num: i32, debug: bool) -> Self {
        my_class {num, debug}
    }
    fn __str__(&self) -> PyResult<String>   {
        Ok(format!("[__str__] Num: {}, Debug: {}", self.num, self.debug))
    }

    fn __repr__(&self) -> PyResult<String> {
        Ok(format!("[__repr__] Num: {}, Debug: {}", self.num, self.debug))
    }
}

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

I build this (into release mode), and test the code with the following code:

from my_module import my_class

def main():
    dsa = my_class(1, True)
    print(dsa)
    print(dsa.__str__())
    
    
if __name__ == "__main__":
    main()

When running the test python code, I get the following output:

<my_class object at 0x7fb7828ae950>
[__str__] Num: 1, Debug: true

Now I have thought of possible solutions to this. One solution might be the pyo3 rust crate actually acts as a proxy, and in order to port classes into python might implement some sort of object which transfers all actions over to the ported class. So it might not implement its own __str__ therefore not giving me what I want. The second possible solution I thought of was I might not be overloading the __str__ function properly, therefore when python tries to use the print function it doesn't access the correct function and just does the default behavior.

Thanks for reading so far, hope I can find an answer since I didn't find anything online for this.

Mike Pennington
  • 41,899
  • 19
  • 136
  • 174
fdsa
  • 113
  • 1
  • 6
  • The chain of calls is `print` -> `str` -> `__str__`. What does `print(str(dsa))` output? – mportes Jun 30 '20 at 22:39
  • Well, print(str(dsa)) prints ``, but the `str` function should call the `__str__` function internally shouldn't it? According to the documentation "If neither encoding nor errors is given, str(object) returns object.__str__()...", here we don't have any encoding nor errors so it matches the description – fdsa Jun 30 '20 at 22:55
  • 1
    The proxy object theory sounds plausible: What you see seems to be just a generic `__repr__()` of `my_class`... print should try to call perform `__str__()` conversion, but fail over to `__repr__()` if not defined. – Ondrej K. Jun 30 '20 at 22:57
  • @OndrejK.Yup, that's exactly why I'm so conflicted on this matter. – fdsa Jun 30 '20 at 23:02

2 Answers2

12

I'm pretty sure this is because you need to implement these methods through the PyObjectProtocol trait.

Many Python __magic__ methods correspond to C-level function pointer slots in a type object's memory layout. A type implemented in C needs to provide a function pointer in the slot, and Python will automatically generate a method to wrap the pointer for explicit method calls. A type implemented in Python will automatically have function pointers inserted that delegate to the magic methods.

The Python internals will usually look for the function pointer rather than the corresponding magic method, and if Python doesn't find the function pointer, it will behave as though the method doesn't exist. That's why, for example, you had to use #[new] to mark your constructor instead of implementing a __new__ static method.

__str__ and __repr__ also correspond to function pointers - specifically, tp_str and tp_repr. If you just try to implement them as regular methods, pyo3 won't generate the function pointers needed. PyObjectProtocol is the pyo3 interface to go through for that.

user2357112
  • 260,549
  • 28
  • 431
  • 505
  • That's exactly it! I just found it as well, thanks for the detailed answer :) – fdsa Jul 01 '20 at 00:43
  • 1
    @fdsa: Rather than editing the code in the question, it's better to post an answer for that. Editing the question makes it hard to tell what the problem was and what the answers were trying to fix. – user2357112 Jul 01 '20 at 00:54
  • Yup, I inserted it at the bottom :) – fdsa Jul 01 '20 at 00:57
  • 1
    @fdsa i think they mean as an actual answer rather than as an addendum to the question, so that people looking at the question know where to look for the answer, and others can up/downvote – joel Jul 01 '20 at 12:09
0

This is one possible solution…

The python importing the rust code…

from my_module import my_class

def main():
    dsa = my_class(1, True)
    print(dsa)
    print(dsa.__str__())


if __name__ == "__main__":
    main()

The rust source code…

use pyo3::prelude::*;
use pyo3::PyObjectProtocol;

#[pyclass]
struct my_class {
    #[pyo3(get, set)]
    num: i32,
    #[pyo3(get, set)]
    debug: bool,
}

#[pymethods]
impl my_class {
    #[new]
    fn new(num: i32, debug: bool) -> Self {
        my_class {num, debug}
    }
    fn __str__(&self) -> PyResult<String>   {
        Ok(format!("[__str__] Num: {}, Debug: {}", self.num, self.debug))
    }

    fn __repr__(&self) -> PyResult<String> {
        Ok(format!("[__repr__] Num: {}, Debug: {}", self.num, self.debug))
    }
}

#[pyproto]
impl PyObjectProtocol for my_class {
    fn __str__(&self) -> PyResult<String>   {
        Ok(format!("[__str__] Num: {}, Debug: {}", self.num, self.debug))
    }

    fn __repr__(&self) -> PyResult<String> {
        Ok(format!("[__repr__] Num: {}, Debug: {}", self.num, self.debug))
    }
}

#[pymodule]
fn pymspdb(_py: Python, m: &PyModule) -> PyResult<()> {
    m.add_class::<my_class>()?;
    Ok(())
}
Mike Pennington
  • 41,899
  • 19
  • 136
  • 174