Exporting HashMap of HashMap to Python

Question:

I have a text parser written in Rust and want to provide a Python interface to it using pyo3.

The parser returns a HashMap within a HashMap and the values of the inner HashMap are of type serde_json::Value. When I try to return this as a PyObject I get an error that I am unable to solve.

This is a minimal exampel of my problem:

use std::collections::HashMap;

use pyo3::prelude::*;
use serde_json::Value;

#[pyfunction]
pub fn parse() -> PyResult<PyObject> {
    let mapping: HashMap<i64, HashMap<String, Value>> = HashMap::from( [
        ( 1, HashMap::from( [
            ( "test11".to_string(), "Foo".into() ),
            ( "test12".to_string(), 123.into() ),
        ] ) ),
        ( 2, HashMap::from( [
            ( "test21".to_string(), "Bar".into() ),
            ( "test22".to_string(), 123.45.into() ),
        ] ) ),
    ] );

    return pyo3::Python::with_gil( |py| {
        Ok( mapping.to_object( py ) )
    } );
}

#[pymodule]
fn parser( _py: Python, m: &PyModule ) -> PyResult<()> {
    m.add_function( wrap_pyfunction!( parse, m )? )?;

    return Ok( () );
}

Running this results in the error

error[E0599]: the method `to_object` exists for struct `HashMap<i64, HashMap<std::string::String, Value>>`, but its trait bounds were not satisfied
   --> src/lib.rs:22:15
    |
22  |         Ok( mapping.to_object( py ) )
    |                     ^^^^^^^^^ method cannot be called on `HashMap<i64, HashMap<std::string::String, Value>>` due to unsatisfied trait bounds
    |
   ::: /home/user/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/collections/hash/map.rs:209:1
    |
209 | pub struct HashMap<K, V, S = RandomState> {
    | ----------------------------------------- doesn't satisfy `_: pyo3::ToPyObject`
    |
    = note: the following trait bounds were not satisfied:
            `HashMap<std::string::String, Value>: pyo3::ToPyObject`
            which is required by `HashMap<i64, HashMap<std::string::String, Value>>: pyo3::ToPyObject`

error[E0277]: the trait bound `Result<PyDict, PyErr>: IntoPyCallbackOutput<_>` is not satisfied
   --> src/lib.rs:8:1
    |
8   | #[pyfunction]
    | ^^^^^^^^^^^^^ the trait `IntoPyCallbackOutput<_>` is not implemented for `Result<PyDict, PyErr>`
    |
    = help: the following implementations were found:
              <Result<T, E> as IntoPyCallbackOutput<U>>
note: required by a bound in `pyo3::callback::convert`
   --> /home/user/.cargo/registry/src/github.com-1ecc6299db9ec823/pyo3-0.14.5/src/callback.rs:182:8
    |
182 |     T: IntoPyCallbackOutput<U>,
    |        ^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `pyo3::callback::convert`
    = note: this error originates in the attribute macro `pyfunction` (in Nightly builds, run with -Z macro-backtrace for more info)

The goal is to call this function from Python and it returns a dict like this:

{ 
    1: {
        "test11": "Foo",
        "test12": 123,
    },
    2: {
        "test21": "Bar",
        "test22": 123.45,
    },
}

Edit: Implemented Solution

(based on the answer of @orlp)

use std::collections::HashMap;

use pyo3::prelude::*;
use serde_json::Value;

fn value_to_object( val: &Value, py: Python<'_> ) -> PyObject {
    match val {
        Value::Null => py.None(),
        Value::Bool( x ) => x.to_object( py ),
        Value::Number( x ) => {
            let oi64 = x.as_i64().map( |i| i.to_object( py ) );
            let ou64 = x.as_u64().map( |i| i.to_object( py ) );
            let of64 = x.as_f64().map( |i| i.to_object( py ) );
            oi64.or( ou64 ).or( of64 ).expect( "number too large" )
        },
        Value::String( x ) => x.to_object( py ),
        Value::Array( x ) => {
            let inner: Vec<_> = x.iter().map(|x| value_to_object(x, py)).collect();
            inner.to_object( py )
        },
        Value::Object( x ) => {
            let inner: HashMap<_, _> =
                x.iter()
                    .map( |( k, v )| ( k, value_to_object( v, py ) ) ).collect();
            inner.to_object( py )
        },
    }
}

#[repr(transparent)]
#[derive( Clone, Debug )]
struct ParsedValue( Value );

impl ToPyObject for ParsedValue {
    fn to_object( &self, py: Python<'_> ) -> PyObject {
        value_to_object( &self.0, py )
    }
}

#[pyfunction]
pub fn parse() -> PyResult<PyObject> {
    let mapping: HashMap<i64, HashMap<String, ParsedValue>> = HashMap::from( [
        ( 1, HashMap::from( [
            ( "test11".to_string(), ParsedValue( "Foo".into() ) ),
            ( "test12".to_string(), ParsedValue( 123.into() ) ),
        ] ) ),
        ( 2, HashMap::from( [
            ( "test21".to_string(), ParsedValue( "Bar".into() ) ),
            ( "test22".to_string(), ParsedValue( 123.45.into() ) ),
        ] ) ),
    ] );

    Ok( pyo3::Python::with_gil( |py| {
        mapping.to_object( py )
    } ) )
}

#[pymodule]
fn parser( _py: Python, m: &PyModule ) -> PyResult<()> {
    m.add_function( wrap_pyfunction!( parse, m )? )?;

    return Ok( () );
}
Asked By: leviathan

||

Answers:

The problem is that serde_json::Value doesn’t implement the pyo3::conversion::ToPyObject trait. You can’t implement that yourself either, since you can’t implement a foreign trait on a foreign object.

What you can do is wrap your serde_json::Value and implement the trait on that. Something like this should work (untested):

use serde_json::Value;
use pyo3::conversion::ToPyObject;

fn value_to_object(val: &Value, py: Python<'_>) -> PyObject {
    match val {
        Value::Null => py.None(),
        Value::Bool(b) => b.to_object(py),
        Value::Number(n) => {
            let oi64 = n.as_i64().map(|i| i.to_object(py));
            let ou64 = n.as_u64().map(|i| i.to_object(py));
            let of64 = n.as_f64().map(|i| i.to_object(py));
            oi64.or(ou64).or(of64).expect("number too large")
        },
        Value::String(s) => s.to_object(py),
        Value::Array(v) => {
            let inner: Vec<_> = v.iter().map(|x| value_to_object(x, py)).collect();
            inner.to_object(py)
        },
        Value::Object(m) => {
            let inner: HashMap<_, _> =
                m.iter().map(|(k, v)| (k, value_to_object(v, py))).collect();
            inner.to_object(py)
        },
    }
}

#[repr(transparent)]
#[derive(Clone, Debug)]
struct MyValue(Value);

impl ToPyObject for MyValue {
    fn to_object(&self, py: Python<'_>) -> PyObject {
        value_to_object(&self.0, py)
    }
}

Then you should store MyValues instead.

Answered By: orlp
Categories: questions Tags: , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.