Boost Python : exposing optional list of enums

57 Views Asked by At

I have the following structures:

enum class Tag
{
    QR,
    April,
    Chili
}

enum class Constraint
{
    Tag,
    GPS,
    User
}

struct Options
{
   boost::optional<std::list<Tag>> tags;
   boost::optional<std::list<Constraint>> constraints;
}

I want to expose them in a Python package, using boost python. I wrote custom code to convert the std::list and boost::optionalstd::list to/from Python:

template<typename T>
struct pylist_converter
{
    static void* convertible(PyObject* object)
    {
        if (!PyList_Check(object)) {
            return nullptr;
        }

        size_t sz = PySequence_Size(object);
        for (size_t i = 0; i < sz; ++i)
        {
            if (!boost::python::extract<T>::extract(PyList_GetItem(object, i)))
                return nullptr;
        }

        return object;
    }

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

        data->convertible = new (storage) std::list<T>();

        std::list<T>* l = (std::list<T>*)(storage);

        size_t sz = PySequence_Size(object);
        for (size_t i = 0; i < sz; ++i)
        {
            l->push_back(boost::python::extract<T>(PyList_GetItem(object, i)));
        }
    }

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

template<typename T>
struct from_python_optional_list
{
    static void* convertible(PyObject* obj_ptr)
    {
        if (obj_ptr == object().ptr()) // = None
            return 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<T>::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)
    {
        if (obj_ptr == object().ptr()) // = None
        {
            void* storage = (
                (boost::python::converter::rvalue_from_python_storage<boost::optional<std::list<T>> >*)
                data)->storage.bytes;
            new (storage) boost::optional<std::list<T>>(boost::none);
            data->convertible = storage;
            return;
        }

        std::list<T> l;
        size_t sz = PySequence_Size(obj_ptr);
        for (size_t i = 0; i < sz; ++i)
        {
            l.push_back(boost::python::extract<T>(PyList_GetItem(obj_ptr, i)));
        }

        assert(value);
        void* storage = (
            (boost::python::converter::rvalue_from_python_storage<boost::optional<std::list<T>> >*)
            data)->storage.bytes;
        new (storage) boost::optional<std::list<T>>(l);
        data->convertible = storage;
    }

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

#define STD_LIST_PYTHON(type) \
    boost::python::to_python_converter<std::list<type>, std_list_to_python<type>>(); \
    pylist_converter<type>(); \
    boost::python::to_python_converter<boost::optional<std::list<type>>, to_python_optional<std::list<type>> >(); \
    from_python_optional_list<type>();

I've tested the above code with "regular" type (like int), it works fine.

Here's what's failing:

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

enum_<Constraint>("Constraint", "Values for Constraint")
    .value("Tag", Constraint::Tag)
    .value("GPS", Constraint::GPS)
    .value("User", Constraint::User)
    ;

STD_LIST_PYTHON(Tag);
STD_LIST_PYTHON(Constraints);

This is the output I have from VS2022:

Build started...
1>------ Build started: Project: PyPackage, Configuration: Release x64 ------
1>Automatic MOC and UIC for target PyPackage
1>pypack.cpp
1>D:\dev\pypack.cpp(378,56): error C2678: binary '!': no operator found which takes a left-hand operand of type 'boost::python::extract<T>' (or there is no acceptable conversion)
1>        with
1>        [
1>            T=Tag
1>        ]
1>D:\dev\pypack.cpp(378,56): message : could be 'built-in C++ operator!(bool)'
1>D:\dev\pypack.cpp(378,56): message : while trying to match the argument list '(boost::python::extract<T>)'
1>        with
1>        [
1>            T=Tag
1>        ]
1>D:\dev\pypack.cpp(365,1): message : while compiling class template member function 'void *from_python_optional_list<Tag>::convertible(PyObject *)'
1>D:\dev\pypack.cpp(420,54): message : see reference to function template instantiation 'void *from_python_optional_list<Tag>::convertible(PyObject *)' being compiled
1>D:\dev\pypack.cpp(504,5): message : see reference to class template instantiation 'from_python_optional_list<Tag>' being compiled
1>D:\dev\pypack.cpp(378,21): error C2088: '!': illegal for struct
1>Done building project "PyPackage.vcxproj" -- FAILED.
========== Build: 0 succeeded, 1 failed, 42 up-to-date, 0 skipped ==========
========== Build started at 10:22 AM and took 09.334 seconds ==========

How can I extract the enums correctly?

Edit: I tried to read int from Python, then cast them as enum. I simplified my problem for now: forget about the optional, I want a list of enum.

template<typename E>
struct from_python_list_enum
{
    static void* convertible(PyObject* obj_ptr)
    {
        if (!PyList_Check(obj_ptr)) { // Check with have a list
            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))) // Check we can extract item as int
                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)); // Cast the int as Enum
        }
    }

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

I'm registering/exposing what's needed. It compiles, but when running this Python code:

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

I have the following exception:

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> >)
0

There are 0 best solutions below