ReluctantProgrammer
ReluctantProgrammer

Reputation: 77

How to get an error message as string in CPython

When I call PyRun_SimpleString(...) in C++ and my Python script contains invalid syntax or an error, I get an error message that gets printed to my console.

How can I stop that error message from getting flushed to the console and instead, get that error message as a string or a const char* and probably use that string in my own way (display it as a GUI text or something)?

Upvotes: 0

Views: 1223

Answers (1)

user2357112
user2357112

Reputation: 281486

PyRun_SimpleString is a simplified interface to PyRun_SimpleStringFlags, and quoting the docs for PyRun_SimpleStringFlags:

If there was an error, there is no way to get the exception information.

Also pretty important,

Note that if an otherwise unhandled SystemExit is raised, this function will not return -1, but exit the process, as long as Py_InspectFlag is not set.

You probably don't want that.


You should instead be using PyRun_String or PyRun_StringFlags. Probably PyRun_StringFlags, since it lets you remember the effects of __future__ imports.

PyRun_StringFlags has the following signature:

PyObject* PyRun_StringFlags(const char *str, int start, PyObject *globals, PyObject *locals, PyCompilerFlags *flags)

  • str is the source string to run.

  • start is either Py_eval_input, Py_file_input, or Py_single_input, depending on whether you want this to behave like eval, like running a script, or like running a statement interactively. Py_eval_input only allows a single expression, the value of which will be returned from the PyRun_StringFlags call. Py_single_input does stuff like expression auto-printing, and only allows one (possibly-compound) statement. You probably want Py_file_input or Py_single_input (and if you use Py_single_input, you might want a custom sys.displayhook).

  • globals and locals are the global and local dicts to use, just like when calling exec or eval. You probably want to fetch the __main__ module's __dict__, like this:

    m = PyImport_AddModule("__main__");
    if (m == NULL)
        /* bad stuff happened */
        do_something_about_that();
    Py_INCREF(m);
    d = PyModule_GetDict(m);
    ...
    /* later, when you're done with m and d */
    Py_DECREF(m);
    

    and use d for both of these arguments.

    PyImport_AddModule and PyModule_GetDict both return borrowed references, but I've used Py_INCREF to get a new reference for m, and Py_DECREF when we're done with that reference. That's because the reference PyImport_AddModule returns is borrowed from sys.modules['__main__'], and it's possible that something could remove __main__ from sys.modules. (There's no such worry with d, because that's borrowed from __main__.__dict__, which cannot be deleted or reassigned as long as __main__ itself survives. As long as our m reference is safe, d is safe.)

  • flags is a pointer to a struct with one relevant field, an int representing compiler flags. __future__ imports affect this field. To make sure __future__ statements from one PyRun_StringFlags call are still active in the next call, you should create a struct somewhere it won't be deallocated or wiped between runs:

    PyCompilerFlags flags = {0};
    

    and pass &flags as this argument. If you don't want to remember __future__ statement effects, you can pass NULL, or use PyRun_String, which doesn't have this argument.


PyRun_StringFlags will return a Python object on success (usually None, and remember to decref the reference) and NULL on an exception. If there's an exception, you can inspect it through the C-level API for exception handling, particularly PyErr_Fetch, PyErr_NormalizeException, and PyException_SetTraceback.

Running the following while an exception is set:

PyObject *type, *val, *tb;
PyErr_Fetch(&type, &val, &tb);

is sort of the C-level equivalent to entering an except block and fetching sys.exc_info(), except that val might be NULL or not an instance of type, and the exception's __traceback__ attribute might not be set. The following:

PyErr_NormalizeException(&type, &val, &tb);
if (tb != NULL) {
  PyException_SetTraceback(val, tb);
}

does that normalization, if you need it.

Upvotes: 7

Related Questions