Skip to content

Catch exceptions that occur in callbacks. #7197

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 36 additions & 35 deletions lib/matplotlib/backend_bases.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
import os
import sys
import time
import traceback
import warnings

import numpy as np
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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):
"""
Expand All @@ -1808,24 +1807,25 @@ 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):
"""
This method will be called by all functions connected to the
'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):
"""
Expand All @@ -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):
"""
Expand All @@ -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):
Expand All @@ -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):
"""
Expand All @@ -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):
"""
Expand All @@ -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):
"""
Expand All @@ -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):
Expand All @@ -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):
"""
Expand All @@ -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

Expand All @@ -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):
"""
Expand Down
11 changes: 2 additions & 9 deletions lib/matplotlib/backends/backend_qt5agg.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down
15 changes: 15 additions & 0 deletions lib/matplotlib/cbook.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any changes you will be calling with an on_error different from traceback.print_exc?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, close_event completely silences out specific exception types.

"""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):
"""
Expand Down