<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>