From 129bee9e7710f80223171334e53ccfc532041be3 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Wed, 28 Sep 2016 19:53:22 -0700 Subject: [PATCH] Catch exceptions that occur in callbacks. Uncaught exceptions are fatal to PyQt5 so we catch anything that occurs in a callback of the canvas class. e.g. ``` from matplotlib import pyplot as plt plt.gca().figure.canvas.mpl_connect( "axes_enter_event", lambda event: 1 / 0) plt.show() ``` used to crash matplotlib upon entering the axes. --- lib/matplotlib/backend_bases.py | 71 ++++++++++++----------- lib/matplotlib/backends/backend_qt5agg.py | 11 +--- lib/matplotlib/cbook.py | 15 +++++ 3 files changed, 53 insertions(+), 44 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 30f606eb625f..039f4aaa28fc 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -44,6 +44,7 @@ import os import sys import time +import traceback import warnings import numpy as np @@ -1465,23 +1466,21 @@ def _update_enter_leave(self): last = LocationEvent.lastevent if last.inaxes != self.inaxes: # process axes enter/leave events - try: - if last.inaxes is not None: - last.canvas.callbacks.process('axes_leave_event', last) - except: - pass - # See ticket 2901582. - # I think this is a valid exception to the rule - # against catching all exceptions; if anything goes - # wrong, we simply want to move on and process the - # current event. + if last.inaxes is not None: + # The previous implementation *completely* suppressed any + # exception that occured here. It seems reasonable to + # print them instead. + last.canvas.callbacks.safe_process( + 'axes_leave_event', traceback.print_exc, last) if self.inaxes is not None: - self.canvas.callbacks.process('axes_enter_event', self) + self.canvas.callbacks.safe_process( + 'axes_enter_event', traceback.print_exc, self) else: # process a figure enter event if self.inaxes is not None: - self.canvas.callbacks.process('axes_enter_event', self) + self.canvas.callbacks.safe_process( + 'axes_enter_event', traceback.print_exc, self) LocationEvent.lastevent = self @@ -1798,7 +1797,7 @@ def draw_event(self, renderer): s = 'draw_event' event = DrawEvent(s, self, renderer) - self.callbacks.process(s, event) + self.callbacks.safe_process(s, traceback.print_exc, event) def resize_event(self): """ @@ -1808,7 +1807,7 @@ def resize_event(self): s = 'resize_event' event = ResizeEvent(s, self) - self.callbacks.process(s, event) + self.callbacks.safe_process(s, traceback.print_exc, event) def close_event(self, guiEvent=None): """ @@ -1816,16 +1815,17 @@ def close_event(self, guiEvent=None): 'close_event' with a :class:`CloseEvent` """ s = 'close_event' - try: - event = CloseEvent(s, self, guiEvent=guiEvent) - self.callbacks.process(s, event) - except (TypeError, AttributeError): - pass + def on_error(): # Suppress the TypeError when the python session is being killed. # It may be that a better solution would be a mechanism to # disconnect all callbacks upon shutdown. # AttributeError occurs on OSX with qt4agg upon exiting # with an open window; 'callbacks' attribute no longer exists. + tp, value, traceback = sys.exc_info() + if not isinstance(value, (TypeError, AttributeError)): + traceback.print_exc() + event = CloseEvent(s, self, guiEvent=guiEvent) + self.callbacks.safe_process(s, on_error, event) def key_press_event(self, key, guiEvent=None): """ @@ -1836,7 +1836,7 @@ def key_press_event(self, key, guiEvent=None): s = 'key_press_event' event = KeyEvent( s, self, key, self._lastx, self._lasty, guiEvent=guiEvent) - self.callbacks.process(s, event) + self.callbacks.safe_process(s, traceback.print_exc, event) def key_release_event(self, key, guiEvent=None): """ @@ -1846,7 +1846,7 @@ def key_release_event(self, key, guiEvent=None): s = 'key_release_event' event = KeyEvent( s, self, key, self._lastx, self._lasty, guiEvent=guiEvent) - self.callbacks.process(s, event) + self.callbacks.safe_process(s, traceback.print_exc, event) self._key = None def pick_event(self, mouseevent, artist, **kwargs): @@ -1858,7 +1858,7 @@ def pick_event(self, mouseevent, artist, **kwargs): event = PickEvent(s, self, mouseevent, artist, guiEvent=mouseevent.guiEvent, **kwargs) - self.callbacks.process(s, event) + self.callbacks.safe_process(s, traceback.print_exc, event) def scroll_event(self, x, y, step, guiEvent=None): """ @@ -1874,9 +1874,9 @@ def scroll_event(self, x, y, step, guiEvent=None): else: self._button = 'down' s = 'scroll_event' - mouseevent = MouseEvent(s, self, x, y, self._button, self._key, - step=step, guiEvent=guiEvent) - self.callbacks.process(s, mouseevent) + event = MouseEvent(s, self, x, y, self._button, self._key, + step=step, guiEvent=guiEvent) + self.callbacks.safe_process(s, traceback.print_exc, event) def button_press_event(self, x, y, button, dblclick=False, guiEvent=None): """ @@ -1890,9 +1890,9 @@ def button_press_event(self, x, y, button, dblclick=False, guiEvent=None): """ self._button = button s = 'button_press_event' - mouseevent = MouseEvent(s, self, x, y, button, self._key, - dblclick=dblclick, guiEvent=guiEvent) - self.callbacks.process(s, mouseevent) + event = MouseEvent(s, self, x, y, button, self._key, + dblclick=dblclick, guiEvent=guiEvent) + self.callbacks.safe_process(s, traceback.print_exc, event) def button_release_event(self, x, y, button, guiEvent=None): """ @@ -1915,7 +1915,7 @@ def button_release_event(self, x, y, button, guiEvent=None): """ s = 'button_release_event' event = MouseEvent(s, self, x, y, button, self._key, guiEvent=guiEvent) - self.callbacks.process(s, event) + self.callbacks.safe_process(s, traceback.print_exc, event) self._button = None def motion_notify_event(self, x, y, guiEvent=None): @@ -1941,7 +1941,7 @@ def motion_notify_event(self, x, y, guiEvent=None): s = 'motion_notify_event' event = MouseEvent(s, self, x, y, self._button, self._key, guiEvent=guiEvent) - self.callbacks.process(s, event) + self.callbacks.safe_process(s, traceback.print_exc, event) def leave_notify_event(self, guiEvent=None): """ @@ -1953,7 +1953,8 @@ def leave_notify_event(self, guiEvent=None): """ - self.callbacks.process('figure_leave_event', LocationEvent.lastevent) + self.callbacks.safe_process( + 'figure_leave_event', traceback.print_exc, LocationEvent.lastevent) LocationEvent.lastevent = None self._lastx, self._lasty = None, None @@ -1972,15 +1973,15 @@ def enter_notify_event(self, guiEvent=None, xy=None): if xy is not None: x, y = xy self._lastx, self._lasty = x, y - - event = Event('figure_enter_event', self, guiEvent) - self.callbacks.process('figure_enter_event', event) + s = 'figure_enter_event' + event = Event(s, self, guiEvent) + self.callbacks.safe_process(s, traceback.print_exc, event) def idle_event(self, guiEvent=None): """Called when GUI is idle.""" s = 'idle_event' event = IdleEvent(s, self, guiEvent=guiEvent) - self.callbacks.process(s, event) + self.callbacks.safe_process(s, traceback.print_exc, event) def grab_mouse(self, ax): """ diff --git a/lib/matplotlib/backends/backend_qt5agg.py b/lib/matplotlib/backends/backend_qt5agg.py index ecc49840b19a..06c638d78c15 100644 --- a/lib/matplotlib/backends/backend_qt5agg.py +++ b/lib/matplotlib/backends/backend_qt5agg.py @@ -176,17 +176,10 @@ def draw_idle(self): QtCore.QTimer.singleShot(0, self.__draw_idle_agg) def __draw_idle_agg(self, *args): - if self.height() < 0 or self.width() < 0: - self._agg_draw_pending = False - return - try: + if self.height() >= 0 and self.width() >= 0: FigureCanvasAgg.draw(self) self.update() - except Exception: - # Uncaught exceptions are fatal for PyQt5, so catch them instead. - traceback.print_exc() - finally: - self._agg_draw_pending = False + self._agg_draw_pending = False def blit(self, bbox=None): """ diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index 0e8c0f50e150..b3430de9cf50 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -542,6 +542,21 @@ def process(self, s, *args, **kwargs): except ReferenceError: self._remove_proxy(proxy) + def safe_process(self, s, on_error, *args, **kwargs): + """Call all callbacks registered for signal `s` with `*args, **kwargs`. + + If a callback raises an exception, `on_error` will be called with no + argument. + """ + if s in self.callbacks: + for cid, proxy in list(six.iteritems(self.callbacks[s])): + try: + proxy(*args, **kwargs) + except ReferenceError: + self._remove_proxy(proxy) + except Exception: + on_error() + class silent_list(list): """