Search code examples
c++boostboost-python

Boost Python: exposing std::list of enums


I have the following struct:

enum class Tag
{
    QR,
    April,
    Chili
}

struct Options
{
   std::list<Tag> tags;
}

that I want to expose in a Python package using boost python.

Here's my code for converting list from/to python:

// Convert std list to python list
template<typename T>
struct std_list_to_python
{
    static PyObject* convert(std::list<T> const& l)
    {
        boost::python::list result;
        for (auto const& value : l)
        {
            result.append(value);
        }
        return boost::python::incref(result.ptr());
    }
};

// Convert Python list of enum to std::list of enum
template<typename E>
struct from_python_list_enum
{
    static void* convertible(PyObject* obj_ptr)
    {
        if (!PyList_Check(obj_ptr)) {
            return nullptr;
        }
        size_t sz = PySequence_Size(obj_ptr);
        for (size_t i = 0; i < sz; ++i)
        {
            if (!boost::python::extract<int>::extract(PyList_GetItem(obj_ptr, i)))
                return nullptr;
        }
        return obj_ptr;
    }

    static void construct(PyObject* obj_ptr, boost::python::converter::rvalue_from_python_stage1_data* data)
    {
        typedef boost::python::converter::rvalue_from_python_storage<std::list<E>> storage_type;
        void* storage = reinterpret_cast<storage_type*>(data)->storage.bytes;

        data->convertible = new (storage) std::list<E>();
        std::list<E>* l = (std::list<E>*)(storage);
        size_t sz = PySequence_Size(obj_ptr);
        for (size_t i = 0; i < sz; ++i)
        {
            int v = typename boost::python::extract<int>::extract(PyList_GetItem(obj_ptr, i));
            l->push_back(static_cast<E>(v));
        }
    }

    from_python_list_enum()
    {
        boost::python::converter::registry::push_back(
            &convertible,
            &construct,
            boost::python::type_id<std::list<E>>());
    }
};

Here's the code for exporting the classes:

void export_test()
{
    enum_<Tag>("Tag", "Values for Tag")
        .value("QR", Tag::QR)
        .value("April", Tag::April)
        .value("Chili", Tag::Chili)
        ;

    boost::python::to_python_converter<std::list<Tag>, std_list_to_python<Tag>>();
    from_python_list_enum<Tag>();

    class_<Options>("Options", "Struct with a list of enum")
        .add_property("tags", make_getter(&Options::tags, return_value_policy<return_by_value>()), make_setter(&Options::tags, return_value_policy<return_by_value>()), "Tags")
        ;
}

Compilation works fine. However, upon running the following Python code:

import my_package
options = my_package.Options()
options.tags = [my_package.Tag.April, my_package.Tag.QR]

I have the following exception on the last line:

Boost.Python.ArgumentError
Python argument types in
    None.None(Options, list)
did not match C++ signature:
    None(struct Options{lvalue}, class std::list<enum Tag,class std::allocator<enum Tag> >)

What can I do to make this work?

Note: I'm able to wrap list of integers without issues with a very similar code. I have also tested that the cast from int to enum works in another scenario (boost::optional).


Solution

  • Your convertible check has a bug:

        if (!BP::extract<int>(PyList_GetItem(obj_ptr, i)))
            return nullptr;
    

    does not check that an int can be extracted, but rather that the integer value returned is true-is when contextually converted to bool. You can witness this by omitting the QR value from the list, e.g.

    print([my_package.Tag.April, my_package.Tag.QR])
    options.tags = [my_package.Tag.April, my_package.Tag.Chili]
    print(options.tags)
    print("\nNow the trouble starts:\n--")
    options.tags = [my_package.Tag.April, my_package.Tag.QR]
    

    Gives the following output:

    [my_package.Tag.April, my_package.Tag.QR]
    [my_package.Tag.April, my_package.Tag.Chili]
    
    Now the trouble starts:
    --
    Traceback (most recent call last):
      File "/home/sehe/Projects/stackoverflow/./test.py", line 12, in <module>
        options.tags = [my_package.Tag.April, my_package.Tag.QR]
    Boost.Python.ArgumentError: Python argument types in
        None.None(Options, list)
    did not match C++ signature:
        None(Options {lvalue}, std::__cxx11::list<Tag, std::allocator<Tag> >)
    

    Fix it e.g. by doing

        BP::extract<int> n(PyList_GetItem(obj_ptr, i));
        if (!n.check())
            return nullptr;
    

    Now the output is

    [my_package.Tag.April, my_package.Tag.QR]
    [my_package.Tag.April, my_package.Tag.Chili]
    
    Now the trouble starts:
    --
    [my_package.Tag.April, my_package.Tag.QR]