5

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]
6
  • Could you define the class MyLibraryError so that MyLibraryError.code is a property that returns args[1]? Commented Mar 15, 2019 at 8:42
  • @DavidW Currently MyLibraryError in this example is an exception defined in C. Do you suggest creating a Python Exception which subclasses this C exception, like class MyPyLibraryError(LibraryError) @property def code() return getattr(self, 'code', -1) ? Commented Mar 15, 2019 at 16:43
  • I was suggesting class MyPyLibraryError(LibraryError) @property def code(self): return self.args[1] if len(self.args)>=2 else -1. If MyLibraryError is a class you've made then can create the property in C instead of making a Python subclass. Commented Mar 15, 2019 at 16:57
  • @DavidW Great, and then in Python I would do try: call_my_library_func() except MyLibraryError as ex: raise MyPyLibraryError(*ex.args) right -- and better yet add it as a decorator and apply it to all my my functions? Commented Mar 15, 2019 at 17:59
  • Not quite - in C you raise the MyPyLibraryError (using the tuple, like you're doing). In Python you just do try: call_my_library_func() except MyPyLibraryError as ex: code = ex.code # ... and whatever else you want Commented Mar 15, 2019 at 19:19

1 Answer 1

5

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
Sign up to request clarification or add additional context in comments.

9 Comments

Very cool. I did not know about PyRun_String or setting the dict during PyErr_NewException. Thanks.
I've added an edit that shows how you can directly do what you original asked about (set an attribute on the current exception) an therefore avoid messing around with custom classes. I'd personally still use the custom class though
Regarding your addendum, I had to do PyObject_SetAttrString(type, ...), not value. Using value raised an AttributeError: 'str' object has no attribute 'err'. Additionally, the docs have this warning about PyErr_Restore: "If you don’t understand this, don’t use this function. I warned you." - so I'll think I'll implement your first suggestion :)
In your first suggestion, why are you doing the PyDict_DelItemString(dict, "__builtins__") ? I noticed that I cannot use len for example in my code block, but if I comment out the PyDict_DelItemString line, I can then use len.
You're right - I don't seem to be able to get the addendum to work. I can't remember at this stage if I actually had it working, or was just "certain" it would work... I've removed it anyway. Sorry about that!
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.