Repository: ipython/ipython

Issue:
We should be less aggressive in de-registering post-execution functions
I'm marking this as a blocker so we make a decision on it in the next few days.  If we decide to make a change, the implementation is easy.

Currently, if a post-execution function fails we immediately de-register it.  The rationale for this was originally that if the user had a bad function, there was no point in failing every time.

But in more extensive use, I'm realizing this is a bad policy in practice.  To see the issue, try in a pylab notebook just this one cell:

```
plt.title("Naïve")
```

note that this is unicode _without_ the proper `u` qualifier.  That triggers a matplotlib exception (as it should) but we immediately go ahead and disable further figure display:

```
Disabling failed post-execution function: <function flush_figures at 0x7fe574a20a28>
ERROR: An unexpected error occurred while tokenizing input
The following traceback may be corrupted or invalid
The error message is: ('EOF in multi-line statement', (316, 0))
```

So even though in this case the user did pass invalid input, from this point on their experience will simply be "the notebook doesn't work anymore".  They can recover functionality by calling `%pylab` which re-registers the figure display function, but most people will just be puzzled by the behavior and likely just do a full session restart (possibly losing costly computations).

I think what we should do instead is to store in a set all functions that have ever failed at this stage, and change the error message to something like:

```
Function <....> produced an error.  If this problem persists, you can disable all problematic post-execution functions by calling:

get_ipython().disable_failing_postfuncs()
```

The user can then make the call manually if they really need it.  But in a case like this example, they would simply realize their mpl error, fix their call and be able to continue.



Discussion:
I think that's fine to do, though I would actually consider it a bug that we allow user errors to raise an exception in our callback.  It seems to me that post-execute functions should be extremely safe, given their relatively low-level position.  Especially ones that we write ourselves.

Well, but in the example above, it's not really our callback that causes an error, the traceback comes from matplotlib itself.  You can see it by typing the same thing in a plain `ipython --pylab` session.  And if we masked all exceptions in our callback, it would make it much harder for users to sort out by themselves situations like this one: in this example, the mpl traceback is actually pretty informative:

```
...
  File "/home/fperez/usr/opt/lib/python2.7/site-packages/matplotlib/text.py", line 985, in is_math_text
    if cbook.is_math_text(s):
  File "/home/fperez/usr/opt/lib/python2.7/site-packages/matplotlib/cbook.py", line 1868, in is_math_text
    "matplotlib display text must have all code points < 128 or use Unicode strings")
ValueError: matplotlib display text must have all code points < 128 or use Unicode strings
```

and seeing that would immediately tell a user what's going on.  That's why I think we should actually let exceptions of this kind bubble through, and just make it very easy/obvious for the user to disable a callback if it's really a buggy one.

It's not that our callback caused an error, it's that our callback allowed a _user_ error to raise in itself, rather than calling showtraceback().  In general, IPython does not allow user errors to raise in IPython calls, but here we are allowing exactly that.

I still think your idea is a sensible one.

Looking at the error output, I am reminded that we should put messages about side effects _after_ tracebacks, not before.  The end of a traceback is much more visible in interactive use than the beginning.

Ah yes, I misunderstood your original message then.  We could certainly wrap those calls with a `try/except showtraceback()`.

BTW, if you want to take a crack at this one, go for it; I have to clean up some materials for the p4s lecture this afternoon.


Resolving patch:
diff --git a/IPython/core/interactiveshell.py b/IPython/core/interactiveshell.py
--- a/IPython/core/interactiveshell.py
+++ b/IPython/core/interactiveshell.py
@@ -261,6 +261,9 @@ class InteractiveShell(SingletonConfigurable, Magic):
         deep_reload will still be available as dreload().
         """
     )
+    disable_failing_post_execute = CBool(False, config=True,
+        help="Don't call post-execute functions that have failed in the past."""
+    )
     display_formatter = Instance(DisplayFormatter)
     displayhook_class = Type(DisplayHook)
     display_pub_class = Type(DisplayPublisher)
@@ -749,7 +752,7 @@ def register_post_execute(self, func):
         if not callable(func):
             raise ValueError('argument %s must be callable' % func)
         self._post_execute[func] = True
-
+    
     #-------------------------------------------------------------------------
     # Things related to the "main" module
     #-------------------------------------------------------------------------
@@ -2426,17 +2429,22 @@ def run_cell(self, raw_cell, store_history=False):
 
                     # Execute any registered post-execution functions.
                     for func, status in self._post_execute.iteritems():
-                        if not status:
+                        if self.disable_failing_post_execute and not status:
                             continue
                         try:
                             func()
                         except KeyboardInterrupt:
                             print >> io.stderr, "\nKeyboardInterrupt"
                         except Exception:
-                            print >> io.stderr, "Disabling failed post-execution function: %s" % func
-                            self.showtraceback()
-                            # Deactivate failing function
+                            # register as failing:
                             self._post_execute[func] = False
+                            self.showtraceback()
+                            print >> io.stderr, '\n'.join([
+                                "post-execution function %r produced an error." % func,
+                                "If this problem persists, you can disable failing post-exec functions with:",
+                                "",
+                                "    get_ipython().disable_failing_post_execute = True"
+                            ])
 
         if store_history:
             # Write output to the database. Does nothing unless
diff --git a/IPython/zmq/pylab/backend_inline.py b/IPython/zmq/pylab/backend_inline.py
--- a/IPython/zmq/pylab/backend_inline.py
+++ b/IPython/zmq/pylab/backend_inline.py
@@ -140,19 +140,43 @@ def flush_figures():
 
     This is meant to be called automatically and will call show() if, during
     prior code execution, there had been any calls to draw_if_interactive.
+    
+    This function is meant to be used as a post_execute callback in IPython,
+    so user-caused errors are handled with showtraceback() instead of being
+    allowed to raise.  If this function is not called from within IPython,
+    then these exceptions will raise.
     """
     if not show._draw_called:
         return
     
     if InlineBackend.instance().close_figures:
         # ignore the tracking, just draw and close all figures
-        return show(True)
-    
+        try:
+            return show(True)
+        except Exception as e:
+            # safely show traceback if in IPython, else raise
+            try:
+                get_ipython
+            except NameError:
+                raise e
+            else:
+                get_ipython().showtraceback()
+                return
     try:
         # exclude any figures that were closed:
         active = set([fm.canvas.figure for fm in Gcf.get_all_fig_managers()])
         for fig in [ fig for fig in show._to_draw if fig in active ]:
-            send_figure(fig)
+            try:
+                send_figure(fig)
+            except Exception as e:
+                # safely show traceback if in IPython, else raise
+                try:
+                    get_ipython
+                except NameError:
+                    raise e
+                else:
+                    get_ipython().showtraceback()
+                    break
     finally:
         # clear flags for next round
         show._to_draw = []
@@ -162,12 +186,11 @@ def flush_figures():
 def send_figure(fig):
     """Draw the current figure and send it as a PNG payload.
     """
-    # For an empty figure, don't even bother calling figure_to_svg, to avoid
-    # big blank spaces in the qt console
-    if not fig.axes:
-        return
     fmt = InlineBackend.instance().figure_format
     data = print_figure(fig, fmt)
+    # print_figure will return None if there's nothing to draw:
+    if data is None:
+        return
     mimetypes = { 'png' : 'image/png', 'svg' : 'image/svg+xml' }
     mime = mimetypes[fmt]
     # flush text streams before sending figures, helps a little with output


Test patch: