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?

Alexandra B.
  • 263
  • 2
  • 10
  • See [this question](http://stackoverflow.com/questions/26497922/how-to-wrap-a-c-function-that-returns-boostoptionalt). Perhaps if it needs to be a function for that to work, then wrap it using setter/getter. – Dan Mašek Apr 08 '16 at 00:09

1 Answers1

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
cromod
  • 1,721
  • 13
  • 26
  • 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`? – Alexandra B. Aug 03 '16 at 11:43
  • `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](http://www.boost.org/doc/libs/1_61_0/boost/python/converter/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. – cromod Sep 02 '16 at 08:50
  • You can also take a look to [add_reference.hpp](http://www.boost.org/doc/libs/1_61_0/boost/type_traits/add_reference.hpp) and [referent_storage.hpp](http://www.boost.org/doc/libs/1_61_0/boost/python/detail/referent_storage.hpp). – cromod Sep 02 '16 at 08:54
  • Indeed, `return_value_policy` is not necessary to specify the setter (I've just checked it). I blindly followed the FAQ of Boost :P – cromod Sep 02 '16 at 11:39
  • In [this link](https://wiki.python.org/moin/boost.python/CallPolicy#return_value_policy.3CT.3E) you'll find the different return policies. I choose `return_by_value` because I understand you want to get a value. – cromod Sep 02 '16 at 11:39
  • 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`. – Mohan Nov 29 '17 at 12:47