Reputation: 1235
I have a C++ library with a custom exception that's derived from std::runtime_error. It represents a type of assertion failure, so I would like to map this to AssertionError in python. So far, this is easy:
%exception {
try {
$action
} catch(MyNamespace::Assertion& e) {
PyErr_SetString(PyExc_AssertionError, e.what() );
return nullptr;
} catch(...) {
PyErr_SetString(PyExc_RuntimeError, "Unknown Exception");
return nullptr;
}
}
and this works just fine.
The issue is that I just added a data member to MyNamespace::Assertion in C++, and I would like to propagate this to the python exception. The catch (and what makes this different than the similar questions that have been answered before) is that I would like the exception type in python to be derived from AssertionError. If the client cares about the new data member, they can catch it explicitly, but if the client doesn't care, I want them to be able to catch AssertionError and have it work.
I can add my own exception type derived from AssertionError and map the C++ assertion to it
%{
static PyObject* p_MyAssertionError;
%}
%init %{
p_MyAssertionError = PyErr_NewException("_MyModule.MyAssertionError",PyExc_AssertionError, NULL);
Py_INCREF(p_MyAssertionError);
PyModule_AddObject(m, "MyAssertionError", p_MyAssertionError);
%}
%pythoncode %{
MyAssertionError = _MyModule.MyAssertionError
%}
%exception {
try {
$action
} catch(MyNamespace::Assertion& e) {
PyErr_SetString(p_MyAssertionError, e.what() );
return nullptr;
} catch(...) {
PyErr_SetString(PyExc_RuntimeError, "Unknown Exception");
return nullptr;
}
}
and this works fine as well. The problem is that now I want to add the new data member to the python exception, and I don't know how to do that (I'm not terribly familiar with the python API). From what I see in the documentation for PyErr_NewException, the third argument can be a dictionary of attributes to add, so I would imagine that would be part of the solution, but I don't see any examples of how exactly to do that. The documentation just says that this parameter is usually NULL.
The other issue is that if I create my own derived exception type using PyErr_NewException, how do I construct one on the %exception block? I assume that I need to use PyErr_SetObject() instead of PyErr_SetString(), and although I see examples of how to use PyErr_SetObject() to create an exception of a fully custom type, I don't see any that create one that's derived from a standard exception type but has additional attributes.
Upvotes: 2
Views: 341
Reputation: 88711
As you've noticed you'll need to help SWIG/Python out a little here to make sure that the wrapped form of the C++ exception has a Python exception type as a base class. The easiest way to make this work is to use SWIG's pyabc to help us here. You'll also need to do some contortions to catch by reference, but have an object that can be owned by Python after exiting the C++ try/catch. I used the copy constructor for this in my code, which maybe came from this answer originally:
%module numbers
%include <std_except.i>
%include <exception.i>
%include <pyabc.i>
// This is important to hook our C++ exception to a Python exception type.
%pythonabc(TooBigException, Exception)
%{
#include <iostream>
#include <typeinfo>
%}
%exception {
try {
$action
}
catch (const TooBigException& e) {
// Copy construct and wrap a new instance of TooBigException here for Python
SWIG_Python_Raise(SWIG_NewPointerObj(
(new TooBigException(e)),
SWIGTYPE_p_TooBigException,SWIG_POINTER_OWN),
"TooBigException", SWIGTYPE_p_TooBigException);
SWIG_fail;
}
catch(const std::exception& ex) {
std::cerr << typeid(ex).name() << "\n";
}
}
%inline %{
struct TooBigException: std::exception {
void blahblah() {
std::cerr << "blah\n";
}
virtual const char* what() const noexcept {return "Your messasge here";}
};
int fact(int n) {
if (n > 10) throw TooBigException();
else if (n <= 1) return 1;
else return n*fact(n-1);
}
%extend TooBigException {
// Always nice to have:
const char * __str__() const {
return $self->what();
}
}
You can actually use exception specifiers to make SWIG generate the try/catch/new object block for us automatically:
%module numbers
%include <std_except.i>
%include <exception.i>
%include <pyabc.i>
// It seems this isn't actually needed with the exception specifiers either
//%pythonabc(TooBigException, Exception)
%{
#include <iostream>
#include <typeinfo>
%}
%inline %{
struct TooBigException: std::exception {
void blahblah() {
std::cerr << "blah\n";
}
virtual const char* what() const noexcept {return "Your messasge here";}
};
// This is the main change here - an exception specifier:
int fact(int n) throw(TooBigException) {
if (n > 10) throw TooBigException();
else if (n <= 1) return 1;
else return n*fact(n-1);
}
%extend TooBigException {
const char * __str__() const {
return $self->what();
}
}
If you inspect the generated C++ wrapper code you'll see that SWIG had generated something every similar to our catch block for us using the exception specifier.
Either of these is enough to work with this test though:
import numbers
try:
numbers.fact(11)
except numbers.TooBigException as e:
print(e)
print(type(e))
e.blahblah()
Upvotes: 1