Skip to content

Api callback exceptions #9063

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

Merged
merged 2 commits into from
Aug 24, 2017
Merged
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
30 changes: 30 additions & 0 deletions doc/api/api_changes/2017-08_TAC.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
:meth:`matpltolib.cbook.CallbackRegistry.process` suppresses exceptions by default
``````````````````````````````````````````````````````````````````````````````````

Matplotlib uses instances of :obj:`~matplotlib.cbook.CallbackRegistry`
as a bridge between user input event from the GUI and user callbacks.
Previously, any exceptions raised in a user call back would bubble out
of of the ``process`` method, which is typically in the GUI event
loop. Most GUI frameworks simple print the traceback to the screen
and continue as there is not always a clear method of getting the
exception back to the user. However PyQt5 now exits the process when
it receives and un-handled python exception in the event loop. Thus,
:meth:`~matplotlib.cbook.CallbackRegistry.process` now suppresses and
prints tracebacks to stderr by default.

What :meth:`~matplotlib.cbook.CallbackRegistry.process` does with exceptions
is now user configurable via the ``exception_handler`` attribute and kwarg. To
restore the previous behavior pass ``None`` ::

cb = CallbackRegistry(exception_handler=None)


A function which take and ``Exception`` as its only argument may also be passed ::

def maybe_reraise(exc):
if isinstance(exc, RuntimeError):
pass
else:
raise exc

cb = CallbackRegistry(exception_handler=maybe_reraise)
41 changes: 36 additions & 5 deletions lib/matplotlib/cbook/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,9 +249,12 @@ def __hash__(self):
return self._hash


def _exception_printer(exc):
traceback.print_exc()


class CallbackRegistry(object):
"""
Handle registering and disconnecting for a set of signals and
"""Handle registering and disconnecting for a set of signals and
callbacks:

>>> def oneat(x):
Expand Down Expand Up @@ -286,8 +289,29 @@ class CallbackRegistry(object):
functions). This technique was shared by Peter Parente on his
`"Mindtrove" blog
<http://mindtrove.info/python-weak-references/>`_.


Parameters
----------
exception_handler : callable, optional
If provided must have signature ::

def handler(exc: Exception) -> None:

If not None this function will be called with any `Exception`
subclass raised by the callbacks in `CallbackRegistry.process`.
The handler may either consume the exception or re-raise.

The callable must be pickle-able.

The default handler is ::

def h(exc):
traceback.print_exc()

"""
def __init__(self):
def __init__(self, exception_handler=_exception_printer):
self.exception_handler = exception_handler
self.callbacks = dict()
self._cid = 0
self._func_cid_map = {}
Expand All @@ -301,10 +325,10 @@ def __init__(self):
# http://bugs.python.org/issue12290).

def __getstate__(self):
return True
return {'exception_handler': self.exception_handler}

def __setstate__(self, state):
self.__init__()
self.__init__(**state)

def connect(self, s, func):
"""
Expand Down Expand Up @@ -365,6 +389,13 @@ def process(self, s, *args, **kwargs):
proxy(*args, **kwargs)
except ReferenceError:
self._remove_proxy(proxy)
# this does not capture KeyboardInterrupt, SystemExit,
# and GeneratorExit
except Exception as exc:
if self.exception_handler is not None:
self.exception_handler(exc)
else:
raise


class silent_list(list):
Expand Down
39 changes: 39 additions & 0 deletions lib/matplotlib/tests/test_cbook.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,45 @@ def test_pickling(self):
"callbacks")


def raising_cb_reg(func):
class TestException(Exception):
pass

def raising_function():
raise RuntimeError

def transformer(excp):
if isinstance(excp, RuntimeError):
raise TestException
raise excp

# default behavior
cb = cbook.CallbackRegistry()
cb.connect('foo', raising_function)

# old default
cb_old = cbook.CallbackRegistry(exception_handler=None)
cb_old.connect('foo', raising_function)

# filter
cb_filt = cbook.CallbackRegistry(exception_handler=transformer)
cb_filt.connect('foo', raising_function)

return pytest.mark.parametrize('cb, excp',
[[cb, None],
[cb_old, RuntimeError],
[cb_filt, TestException]])(func)


@raising_cb_reg
def test_callbackregistry_process_exception(cb, excp):
if excp is not None:
with pytest.raises(excp):
cb.process('foo')
else:
cb.process('foo')


def test_sanitize_sequence():
d = {'a': 1, 'b': 2, 'c': 3}
k = ['a', 'b', 'c']
Expand Down