wrap boost::optional using boost::python
Asked Answered
A

1

7

Is there a way to wrap boost::optional<T> type object to expose it via boost::python::class_ (used from BOOST_PYTHON_MODULE)

struct Foo 
{
    boost::optional<int> bar;
};

BOOST_PYTHON_MODULE(module_name)
{
    class_<Foo>("Foo")
    .def_readwrite("bar", &Foo::bar);
}

What I expect in Python is AttributeError in this case

import module_name
f = module_name.Foo()
print f.bar

as the value of bar hasn't been set yet. And TypeError when

import module_name
f = module_name.Foo()
f.bar = "string"

bar is of int type.

The other related problem is to export, in the same fashion, classes' objects of boost::python::indexing_suite container types.

Is the problem solvable using boost::python api?

Anatole answered 7/4, 2016 at 19:48 Comment(1)
See this question. Perhaps if it needs to be a function for that to work, then wrap it using setter/getter.Tip
O
6

You need an exception translator and python converters.

Exception translator

namespace bp = boost::python;

// Custom exceptions
struct AttributeError: std::exception
{
  const char* what() const throw() { return "AttributeError exception"; }
};

struct TypeError: std::exception
{
  const char* what() const throw() { return "TypeError exception"; }
};

// Set python exceptions
void translate(const std::exception& e)
{
  if(dynamic_cast<const AttributeError*>(&e)) 
     PyErr_SetString(PyExc_AttributeError, e.what());
  if(dynamic_cast<const TypeError*>(&e)) 
     PyErr_SetString(PyExc_TypeError, e.what());
}

BOOST_PYTHON_MODULE(module_name)
{
  // Exception translator
  bp::register_exception_translator<AttributeError>(&translate);
  bp::register_exception_translator<TypeError>(&translate);
  ...
}

To-python converter

template <typename T>
struct to_python_optional
{
  static PyObject* convert(const boost::optional<T>& obj)
  {
    if(obj) return bp::incref(bp::object(*obj).ptr());
    // raise AttributeError if any value hasn't been set yet
    else throw AttributeError();
  }
};

BOOST_PYTHON_MODULE(module_name)
{
  ... 
  bp::to_python_converter<boost::optional<int>,
                          to_python_optional<int> >();
  ...
}

From-python converter

template<typename T>
struct from_python_optional
{ 
  static void* convertible(PyObject *obj_ptr)
    {
      try { return typename bp::extract<T>::extract(obj_ptr) ? obj_ptr : 0 ; }
      // Without try catch it still raises a TypeError exception
      // But this enables to custom your error message
      catch(...) { throw TypeError(); }
    }

  static void construct(
    PyObject *obj_ptr,
    boost::python::converter::rvalue_from_python_stage1_data* data)
    {
      const T value = typename bp::extract<T>::extract(obj_ptr);

      assert(value);

      void* storage = (
        (bp::converter::rvalue_from_python_storage<boost::optional<T> >*)
        data)->storage.bytes;

      new (storage) boost::optional<T>(value);

      data->convertible = storage;
    }

  from_python_optional()
  {
    bp::converter::registry::push_back(
      &convertible,
      &construct,
      bp::type_id<boost::optional<T> >());
  }
};

BOOST_PYTHON_MODULE(module_name)
{
  ... 
  from_python_optional<int>();
  ...
}

Moreover you can't use converters with def_readwrite (see this FAQ), you have to use add_property.

BOOST_PYTHON_MODULE(module_name)
{
  ...
  bp::class_<Foo>("Foo")
    .add_property("bar", bp::make_getter(&Foo::bar,
                         bp::return_value_policy<bp::return_by_value>()),
                         bp::make_setter(&Foo::bar,
                         bp::return_value_policy<bp::return_by_value>()));
}

Thus you'll get those outputs in your Python interpreter :

>>> import module_name
>>> f = module_name.Foo()
>>> print f.bar
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: AttributeError exception
>>> f.bar="string"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: TypeError exception
Onstage answered 28/4, 2016 at 13:1 Comment(6)
Thank you! Is data pointer argument in construct function the pointer to memory where member variable Foo::bar is actually stored?? If so, then why there is no check that it's not used yet before new? The other question is about return_value_policy: 1. why are we specifying any return policy for the setter which return void? 2. why is it return_by_value?Anatole
data pointer holds the adress of the conversion's result (python integer). The Boost documentation is really poor on this subject so we have to watch the source files. In rvalue_from_python_data.hpp, we can see that rvalue_from_python_storage uses boost::python::detail::referent_storage and boost::add_reference to get the memory chunk.Onstage
You can also take a look to add_reference.hpp and referent_storage.hpp.Onstage
Indeed, return_value_policy is not necessary to specify the setter (I've just checked it). I blindly followed the FAQ of Boost :POnstage
In this link you'll find the different return policies. I choose return_by_value because I understand you want to get a value.Onstage
I don't think this solution handles passing None into C++ as boost::none. You need to trap and handle obj_ptr == Py_None in both convertible and construct.Useful

© 2022 - 2024 — McMap. All rights reserved.