I am currently writing a C++ extension for Python using Boost.Python. A function in this extension may generate an exception containing information about the error (beyond
The answer given by Jack Edmonds defines a Python "exception" class that does not inherit Exception
(or any other built-in Python exception class). So although it can be caught with
except my_cpp_extension.MyCPPException as e:
...
it can not be caught with the usual catch all
except Exception as e:
...
Here is how to create a custom Python exception class that does inherit Exception
.
Thanks to variadic templates and generalized lambda capture, we can collapse Jack Edmond's answer into something much more manageable and hide all of the cruft from the user:
template <class E, class... Policies, class... Args>
py::class_<E, Policies...> exception_(Args&&... args) {
py::class_<E, Policies...> cls(std::forward<Args>(args)...);
py::register_exception_translator<E>([ptr=cls.ptr()](E const& e){
PyErr_SetObject(ptr, py::object(e).ptr());
});
return cls;
}
To expose MyCPPException
as an exception, you just need to change py::class_
in the bindings to exception_
:
exception_<MyCPPException>("MyCPPException", py::init<std::string, std::string>())
.add_property("message", &MyCPPException::getMessage)
.add_property("extra_data", &MyCPPException::getExtraData)
;
And now we're back to the niceties of Boost.Python: don't need to name the class_
instance, don't need this extra PyObject*
, and don't need an extra function somewhere.
Here's the solution from Jack Edmonds, ported to Python 3, using advice from here which itself uses code from here. Assembling it all together (and modernizing the C++ code a little bit) gives:
#include <boost/python.hpp>
#include <assert.h>
#include <iostream>
class MyCPPException : public std::exception
{
public:
MyCPPException(const std::string &message, const std::string &extraData)
: message(message), extraData(extraData)
{
}
const char *what() const noexcept override
{
return message.c_str();
}
std::string getMessage() const
{
return message;
}
std::string getExtraData() const
{
return extraData;
}
private:
std::string message;
std::string extraData;
};
void my_cpp_function(bool throwException)
{
std::cout << "Called a C++ function." << std::endl;
if (throwException) {
throw MyCPPException("Throwing an exception as requested.",
"This is the extra data.");
}
}
static PyObject* createExceptionClass(const char* name, PyObject* baseTypeObj = PyExc_Exception)
{
using std::string;
namespace bp = boost::python;
const string scopeName = bp::extract<string>(bp::scope().attr("__name__"));
const string qualifiedName0 = scopeName + "." + name;
PyObject* typeObj = PyErr_NewException(qualifiedName0.c_str(), baseTypeObj, 0);
if (!typeObj) bp::throw_error_already_set();
bp::scope().attr(name) = bp::handle<>(bp::borrowed(typeObj));
return typeObj;
}
static PyObject *pythonExceptionType = NULL;
static void translateMyCPPException(MyCPPException const &e)
{
using namespace boost;
python::object exc_t(python::handle<>(python::borrowed(pythonExceptionType)));
exc_t.attr("cause") = python::object(e); // add the wrapped exception to the Python exception
exc_t.attr("what") = python::object(e.what()); // for convenience
PyErr_SetString(pythonExceptionType, e.what()); // the string is used by print(exception) in python
}
BOOST_PYTHON_MODULE(my_cpp_extension)
{
using namespace boost;
python::class_<MyCPPException>
myCPPExceptionClass("MyCPPException",
python::init<std::string, std::string>());
myCPPExceptionClass.add_property("message", &MyCPPException::getMessage)
.add_property("extra_data", &MyCPPException::getExtraData);
pythonExceptionType = createExceptionClass("MyPythonException");
python::register_exception_translator<MyCPPException>(&translateMyCPPException);
python::def("my_cpp_function", &my_cpp_function);
}
and the python file to test it:
#!/usr/bin/env python3
import my_cpp_extension
try:
my_cpp_extension.my_cpp_function(False)
print('This line should be reached as no exception should be thrown.')
except my_cpp_extension.MyPythonException as e:
print('Message:', e.what)
print('Extra data:',e.cause.extra_data)
try:
my_cpp_extension.my_cpp_function(True)
print ('This line should not be reached as an exception should have been' +
'thrown by now.')
except my_cpp_extension.MyPythonException as e:
print('Message:', e.what)
print('Extra data:',e.cause.extra_data)
And catching it as a standard python Exception works too:
except Exception as e:
print('Exception: ',e)
The solution is to create your exception class like any normal C++ class
class MyCPPException : public std::exception {...}
The trick is that all boost::python::class_ instances hold a reference to the object's type which is accessible through their ptr() function. You can get this as you register the class with boost::python like so:
class_<MyCPPException> myCPPExceptionClass("MyCPPException"...);
PyObject *myCPPExceptionType=myCPPExceptionClass.ptr();
register_exception_translator<MyCPPException>(&translateFunc);
Finally, when you are translating the C++ exception to a Python exception, you do so as follows:
void translate(MyCPPException const &e)
{
PyErr_SetObject(myCPPExceptionType, boost::python::object(e).ptr());
}
Here is a full working example:
#include <boost/python.hpp>
#include <assert.h>
#include <iostream>
class MyCPPException : public std::exception
{
private:
std::string message;
std::string extraData;
public:
MyCPPException(std::string message, std::string extraData)
{
this->message = message;
this->extraData = extraData;
}
const char *what() const throw()
{
return this->message.c_str();
}
~MyCPPException() throw()
{
}
std::string getMessage()
{
return this->message;
}
std::string getExtraData()
{
return this->extraData;
}
};
void my_cpp_function(bool throwException)
{
std::cout << "Called a C++ function." << std::endl;
if (throwException)
{
throw MyCPPException("Throwing an exception as requested.",
"This is the extra data.");
}
}
PyObject *myCPPExceptionType = NULL;
void translateMyCPPException(MyCPPException const &e)
{
assert(myCPPExceptionType != NULL);
boost::python::object pythonExceptionInstance(e);
PyErr_SetObject(myCPPExceptionType, pythonExceptionInstance.ptr());
}
BOOST_PYTHON_MODULE(my_cpp_extension)
{
boost::python::class_<MyCPPException>
myCPPExceptionClass("MyCPPException",
boost::python::init<std::string, std::string>());
myCPPExceptionClass.add_property("message", &MyCPPException::getMessage)
.add_property("extra_data", &MyCPPException::getExtraData);
myCPPExceptionType = myCPPExceptionClass.ptr();
boost::python::register_exception_translator<MyCPPException>
(&translateMyCPPException);
boost::python::def("my_cpp_function", &my_cpp_function);
}
Here is the Python code that calls the extension:
import my_cpp_extension
try:
my_cpp_extension.my_cpp_function(False)
print 'This line should be reached as no exception should be thrown.'
except my_cpp_extension.MyCPPException, e:
print 'Message:', e.message
print 'Extra data:',e.extra_data
try:
my_cpp_extension.my_cpp_function(True)
print ('This line should not be reached as an exception should have been' +
'thrown by now.')
except my_cpp_extension.MyCPPException, e:
print 'Message:', e.message
print 'Extra data:',e.extra_data