Matthew Moisen
Matthew Moisen

Reputation: 18299

Python C Extensions adding Attributes to an Exception

I'm wrapping a C library which returns one of a finite number of error codes upon failure. When the error happens, I would like to add the error code as an attribute on the C exception, such that the Python code can retrieve it and map the error code to a human readable exception. Is this possible?

For example I want to do this in the Python layer:

try:
    call_my_library_func()
except MyLibraryError as ex:
    print("Error code was %s" % ex.code)

The closest I can get to, which I don't like, is by using PyErr_SetObject

PyObject *tuple = PyTuple_New(2);
PyTuple_SetItem(tuple, 0, PyUnicode_FromString("Helpful error message"));
PyTuple_SetItem(tuple, 1, PyLong_FromLong(257));
//PyErr_SetString(MyLibraryError, "Helpful error message\n");
PyErr_SetObject(MyLibraryError, tuple);

Then I can do this:

try:
    call_my_library_func()
except MyLibraryError as ex:
    message, code = ex.args[0], -1
    if len(ex.args > 1):
        code = ex.args[1]

Upvotes: 5

Views: 1443

Answers (1)

DavidW
DavidW

Reputation: 30917

The C API exception handling is largely written in terms of raising an exception by its class, its arguments (passed to the constructor) and its traceback, and therefore those it's probably best to follow that scheme. Your basic approach of passing a tuple as the arguments is probably the best option.

However there are two options to make your exception class slightly more user-friendly on the Python side:

  1. You process the arguments in a custom __init__ method to set a code attribute on the class.
  2. You define code as a property of your exception class that accesses args[1].

I've illustrated option 2, but I don't think there a huge reason to prefer one or the other.


To briefly explain the example code below: to define an exception using the C API you use PyErr_NewException which takes an optional base class and dictionary as its second and third arguments. The functions used (either __init__ or the property definitions) should be part of the dictionary.

To define the property definitions I've written the code in Python and used PyRun_String since it's easier to write in Python than C and because I doubt this code will be performance critical. The functions end up injected into the global dictionary passed to PyRun_String.

C code:

#include <Python.h>

PyObject* make_getter_code() {
    const char* code = 
    "def code(self):\n"
    "  try:\n"
    "    return self.args[1]\n"
    "  except IndexError:\n"
    "    return -1\n"
    "code = property(code)\n"
    "def message(self):\n"
    "  try:\n"
    "    return self.args[0]\n"
    "  except IndexError:\n"
    "    return ''\n"
    "\n";

    PyObject* d = PyDict_New();
    PyObject* dict_globals = PyDict_New();
    PyDict_SetItemString(dict_globals, "__builtins__", PyEval_GetBuiltins());
    PyObject* output = PyRun_String(code,Py_file_input,dict_globals,d);
    if (output==NULL) {
        Py_DECREF(d);
        return NULL;
    }
    Py_DECREF(output);
    Py_DECREF(dict_globals);
    return d;
}

static PyObject* MyLibraryError;

static PyObject* my_library_function(PyObject* self) {
    /* something's gone wrong */
    PyObject *tuple = PyTuple_New(2);
    PyTuple_SetItem(tuple, 0, PyUnicode_FromString("Helpful error message"));
    PyTuple_SetItem(tuple, 1, PyLong_FromLong(257));
    PyErr_SetObject(MyLibraryError, tuple);
    return NULL;
}

static PyMethodDef methods[] = {
    {"my_library_function",  my_library_function,  METH_NOARGS,
     "raise an error."},
    {NULL, NULL, 0, NULL}        /* Sentinel */
};

static struct PyModuleDef librarymodule = {
    PyModuleDef_HEAD_INIT,
    "library",   /* name of module */
    NULL, /* module documentation, may be NULL */
    -1,       /* size of per-interpreter state of the module,
                 or -1 if the module keeps state in global variables. */
    methods
};

PyMODINIT_FUNC
PyInit_library(void) {
    PyObject *m;
    m = PyModule_Create(&librarymodule);
    if (m == NULL)
        return NULL;

    PyObject* exc_dict = make_getter_code();
    if (exc_dict == NULL) {
        return NULL;
    }

    MyLibraryError = PyErr_NewException("library.MyLibraryError", 
                                        NULL, // use to pick base class
                                        exc_dict);
    PyModule_AddObject(m,"MyLibraryError",MyLibraryError);
    return m;
}

As an example of the more elegant Python interface, your Python code changes to:

try:
    my_library_func()
except MyLibraryError as ex:
    message, code = ex.message, ex.code

Upvotes: 5

Related Questions