<div dir="ltr"><div class="gmail_quote"><div dir="ltr">On Wed, Aug 8, 2018 at 4:07 PM Richard W.M. Jones <<a href="mailto:rjones@redhat.com">rjones@redhat.com</a>> wrote:<br></div><blockquote class="gmail_quote" style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex">The tracebacks are compressed into a single line because we're using<br>
PyObject_Str, but they are just about usable if not very readable.<br>
For example you would see an error like this:<br>
<br>
nbdkit: error: ./python-exception.py: config_complete: error: ['Traceback (most recent call last):\n', ' File "./python-exception.py", line 54, in config_complete\n raise_error1()\n', ' File "./python-exception.py", line 48, in raise_error1\n raise_error2()\n', ' File "./python-exception.py", line 45, in raise_error2\n raise RuntimeError("this is the test string")\n', 'RuntimeError: this is the test string\n']<br>
<br>
which can be read by manually unfolding the exception in an editor as:<br>
<br>
nbdkit: error: ./python-exception.py: config_complete: error:<br>
Traceback (most recent call last):<br>
File "./python-exception.py", line 54, in config_complete<br>
raise_error1()<br>
File "./python-exception.py", line 48, in raise_error1<br>
raise_error2()<br>
File "./python-exception.py", line 45, in raise_error2<br>
raise RuntimeError("this is the test string")<br>
RuntimeError: this is the test string<br></blockquote><div><br></div><div>It would be nicer to join the lines before logging, but having the info in</div><div>the log is good enough.</div><div> </div><blockquote class="gmail_quote" style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex">
<br>
This also fixes the Python exception test which, it turned out, was<br>
not testing anything. It now tests both simple exceptions and<br>
tracebacks.<br>
<br>
Tested with Python 2.7.15 & 3.6.6.<br>
---<br>
plugins/python/python.c | 92 ++++++++++++++++++++++++++++------<br>
tests/python-exception.py | 20 +++++++-<br>
tests/test-python-exception.sh | 13 ++++-<br>
3 files changed, 109 insertions(+), 16 deletions(-)<br>
<br>
diff --git a/plugins/python/python.c b/plugins/python/python.c<br>
index 7eb91d7..3450b9b 100644<br>
--- a/plugins/python/python.c<br>
+++ b/plugins/python/python.c<br>
@@ -129,27 +129,91 @@ python_to_string (PyObject *str)<br>
return NULL;<br>
}<br>
<br>
+/* This is the fallback in case we cannot get the full traceback. */<br>
+static void<br>
+print_python_error (const char *callback, PyObject *error)<br>
+{<br>
+ PyObject *error_str;<br>
+ char *error_cstr = NULL;<br>
+<br>
+ error_str = PyObject_Str (error);<br>
+ error_cstr = python_to_string (error_str); </blockquote><blockquote class="gmail_quote" style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex">
+ nbdkit_error ("%s: %s: error: %s",<br>
+ script, callback,<br>
+ error_cstr ? error_cstr : "<unknown>");<br>
+ Py_DECREF (error_str);<br>
+ free (error_cstr);<br>
+}<br>
+<br>
+/* Convert the Python traceback to a string and call nbdkit_error.<br>
+ * <a href="https://stackoverflow.com/a/15907460/7126113" rel="noreferrer" target="_blank">https://stackoverflow.com/a/15907460/7126113</a><br>
+ */<br>
+static int<br>
+print_python_traceback (const char *callback,<br>
+ PyObject *type, PyObject *error, PyObject *traceback)<br>
+{<br>
+ PyObject *module_name, *traceback_module, *format_exception_fn, *rv,<br>
+ *traceback_str;<br>
+ char *traceback_cstr;<br>
+<br>
+#ifdef HAVE_PYSTRING_FROMSTRING<br>
+ module_name = PyString_FromString ("traceback");<br>
+#else<br>
+ module_name = PyUnicode_FromString ("traceback");<br>
+#endif<br>
+ traceback_module = PyImport_Import (module_name);<br>
+ Py_DECREF (module_name);<br>
+<br>
+ /* couldn't 'import traceback' */<br>
+ if (traceback_module == NULL)<br>
+ return -1;<br>
+<br>
+ format_exception_fn = PyObject_GetAttrString (traceback_module,<br>
+ "format_exception");<br>
+ if (format_exception_fn == NULL)<br>
+ return -1;<br>
+ if (!PyCallable_Check (format_exception_fn))<br>
+ return -1;<br>
+<br>
+ rv = PyObject_CallFunctionObjArgs (format_exception_fn,<br>
+ type, error, traceback, NULL);<br>
+ traceback_str = PyObject_Str (rv);<br>
+ traceback_cstr = python_to_string (traceback_str);<br>
+ if (traceback_cstr == NULL) {<br>
+ Py_DECREF (rv);<br>
+ return -1;<br>
+ }<br>
+<br>
+ nbdkit_error ("%s: %s: error: %s",<br>
+ script, callback,<br>
+ traceback_cstr);<br></blockquote><div><br></div><div>Can we simplify this these 2 calls?</div><div><br></div><div> nbdkit_error ("%s: %s: error", script, callback);</div><div> PyErr_PrintEx (0);</div><div><br></div><div>See <a href="https://docs.python.org/3.6/c-api/exceptions.html#c.PyErr_PrintEx">https://docs.python.org/3.6/c-api/exceptions.html#c.PyErr_PrintEx</a></div><div><br></div><div>In this case we don't to fallback to the simple error print.</div><div> </div><blockquote class="gmail_quote" style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex">
+ Py_DECREF (rv);<br>
+ free (traceback_cstr);<br>
+<br>
+ /* This means we succeeded in calling nbdkit_error. */<br>
+ return 0;<br>
+}<br>
+<br>
static int<br>
check_python_failure (const char *callback)<br>
{<br>
if (PyErr_Occurred ()) {<br>
- PyObject *type, *error, *traceback, *error_str;<br>
- char *error_cstr;<br>
+ PyObject *type, *error, *traceback;<br>
<br>
- /* Convert the Python exception to a string.<br>
- * <a href="https://stackoverflow.com/a/1418703" rel="noreferrer" target="_blank">https://stackoverflow.com/a/1418703</a><br>
- * But forget about the traceback, it's very hard to print.<br>
- * <a href="https://stackoverflow.com/q/1796510" rel="noreferrer" target="_blank">https://stackoverflow.com/q/1796510</a><br>
- */<br>
PyErr_Fetch (&type, &error, &traceback);<br>
PyErr_NormalizeException (&type, &error, &traceback);<br>
- error_str = PyObject_Str (error);<br>
- error_cstr = python_to_string (error_str);<br>
- nbdkit_error ("%s: %s: error: %s",<br>
- script, callback,<br>
- error_cstr ? error_cstr : "<unknown>");<br>
- Py_DECREF (error_str);<br>
- free (error_cstr);<br>
+<br>
+ /* Try to print the full traceback. */<br>
+ if (print_python_traceback (callback, type, error, traceback) == -1) {<br>
+ /* Couldn't do that, so fall back to converting the Python error<br>
+ * to a string.<br>
+ */<br>
+ print_python_error (callback, error);<br>
+ }<br>
+<br>
+ /* In all cases this returns -1 to indicate that a Python error<br>
+ * occurred.<br>
+ */<br>
return -1;<br>
}<br>
return 0;<br>
diff --git a/tests/python-exception.py b/tests/python-exception.py<br>
index 1debf51..739057f 100644<br>
--- a/tests/python-exception.py<br>
+++ b/tests/python-exception.py<br>
@@ -32,10 +32,28 @@<br>
<br>
# A dummy python plugin which just raises an exception in config_complete.<br>
<br>
+test = "simple"<br>
<br>
-def config_complete():<br>
+def config(k, v):<br>
+ global test<br>
+ if k == "test":<br>
+ test = v<br>
+ else:<br>
+ raise RuntimeError("unknown config parameter")<br>
+<br>
+def raise_error2():<br>
raise RuntimeError("this is the test string")<br>
<br>
+def raise_error1():<br>
+ raise_error2()<br>
+<br>
+def config_complete():<br>
+ if test == "simple":<br>
+ raise RuntimeError("this is the test string")<br>
+ elif test == "traceback":<br>
+ raise_error1()<br>
+ else:<br>
+ raise RuntimeError("unknown test")<br>
<br>
def open(readonly):<br>
return 1<br>
diff --git a/tests/test-python-exception.sh b/tests/test-python-exception.sh<br>
index 83999af..7120132 100755<br>
--- a/tests/test-python-exception.sh<br>
+++ b/tests/test-python-exception.sh<br>
@@ -31,12 +31,23 @@<br>
# OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF<br>
# SUCH DAMAGE.<br>
<br>
+set -e<br>
+set -x<br>
+<br>
output=test-python-exception.out<br>
<br>
rm -f $output<br>
<br>
-nbdkit -f -v python ./python-exception.py > $output 2>&1 ||:<br>
+nbdkit -f -v python ./python-exception.py test=simple > $output 2>&1 ||:<br>
+cat $output<br>
<br>
grep 'this is the test string' $output<br>
<br>
+nbdkit -f -v python ./python-exception.py test=traceback > $output 2>&1 ||:<br>
+cat $output<br>
+<br>
+grep 'raise_error1' $output<br>
+grep 'raise_error2' $output<br>
+grep 'this is the test string' $output<br>
+<br>
rm $output<br>
-- <br>
2.18.0<br>
<br>
</blockquote></div></div>