Skip to content

Commit 461ba8e

Browse files
committed
API/ENH: add optional exception_handler to CallbackRegisty
1 parent 225a4a0 commit 461ba8e

File tree

3 files changed

+105
-5
lines changed

3 files changed

+105
-5
lines changed

doc/api/api_changes/2017-08_TAC.rst

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
:meth:`matpltolib.cbook.CallbackRegistry.process` suppresses exceptions by default
2+
``````````````````````````````````````````````````````````````````````````````````
3+
4+
Matplotlib uses instances of :obj:`~matplotlib.cbook.CallbackRegistry`
5+
as a bridge between user input event from the GUI and user callbacks.
6+
Previously, any exceptions raised in a user call back would bubble out
7+
of of the ``process`` method, which is typically in the GUI event
8+
loop. Most GUI frameworks simple print the traceback to the screen
9+
and continue as there is not always a clear method of getting the
10+
exception back to the user. However PyQt5 now exits the process when
11+
it receives and un-handled python exception in the event loop. Thus,
12+
:meth:`~matplotlib.cbook.CallbackRegistry.process` now suppresses and
13+
prints tracebacks to stderr by default.
14+
15+
What :meth:`~matplotlib.cbook.CallbackRegistry.process` does with exceptions
16+
is now user configurable via the ``exception_handler`` attribute and kwarg. To
17+
restore the previous behavior pass ``None`` ::
18+
19+
cb = CallbackRegistry(exception_handler=None)
20+
21+
22+
A function which take and ``Exception`` as it's only argument may also be passed ::
23+
24+
def maybe_reraise(excp):
25+
if isinstance(excp, RuntimeError):
26+
pass
27+
else:
28+
raise excp
29+
30+
cb = CallbackRegistry(exception_handler=maybe_reraise)

lib/matplotlib/cbook/__init__.py

+36-5
Original file line numberDiff line numberDiff line change
@@ -249,9 +249,12 @@ def __hash__(self):
249249
return self._hash
250250

251251

252+
def _excption_printer(exp):
253+
traceback.print_exc()
254+
255+
252256
class CallbackRegistry(object):
253-
"""
254-
Handle registering and disconnecting for a set of signals and
257+
"""Handle registering and disconnecting for a set of signals and
255258
callbacks:
256259
257260
>>> def oneat(x):
@@ -286,8 +289,29 @@ class CallbackRegistry(object):
286289
functions). This technique was shared by Peter Parente on his
287290
`"Mindtrove" blog
288291
<http://mindtrove.info/python-weak-references/>`_.
292+
293+
294+
Parameters
295+
----------
296+
exception_handler : callable, optional
297+
If provided must have signature ::
298+
299+
def handler(exception: Exception) -> None:
300+
301+
If not None this function will be called with any `Exception`
302+
subclass raised by the callbacks in `CallbackRegistry.process`.
303+
The handler may either consume the exception or re-raise.
304+
305+
The callable must be pickle-able.
306+
307+
The default handler is ::
308+
309+
def h(exc):
310+
traceback.print_exc()
311+
289312
"""
290-
def __init__(self):
313+
def __init__(self, exception_handler=_excption_printer):
314+
self.exception_handler = exception_handler
291315
self.callbacks = dict()
292316
self._cid = 0
293317
self._func_cid_map = {}
@@ -301,10 +325,10 @@ def __init__(self):
301325
# http://bugs.python.org/issue12290).
302326

303327
def __getstate__(self):
304-
return True
328+
return {'exception_handler': self.exception_handler}
305329

306330
def __setstate__(self, state):
307-
self.__init__()
331+
self.__init__(**state)
308332

309333
def connect(self, s, func):
310334
"""
@@ -365,6 +389,13 @@ def process(self, s, *args, **kwargs):
365389
proxy(*args, **kwargs)
366390
except ReferenceError:
367391
self._remove_proxy(proxy)
392+
# this does not capture KeyboardInterrupt, SystemExit,
393+
# and GeneratorExit
394+
except Exception as e:
395+
if self.exception_handler is not None:
396+
self.exception_handler(e)
397+
else:
398+
raise
368399

369400

370401
class silent_list(list):

lib/matplotlib/tests/test_cbook.py

+39
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,45 @@ def test_pickling(self):
262262
"callbacks")
263263

264264

265+
def raising_cb_reg(func):
266+
class TestException(Exception):
267+
pass
268+
269+
def raising_function():
270+
raise RuntimeError
271+
272+
def transformer(excp):
273+
if isinstance(excp, RuntimeError):
274+
raise TestException
275+
raise excp
276+
277+
# default behavior
278+
cb = cbook.CallbackRegistry()
279+
cb.connect('foo', raising_function)
280+
281+
# old default
282+
cb_old = cbook.CallbackRegistry(exception_handler=None)
283+
cb_old.connect('foo', raising_function)
284+
285+
# filter
286+
cb_filt = cbook.CallbackRegistry(exception_handler=transformer)
287+
cb_filt.connect('foo', raising_function)
288+
289+
return pytest.mark.parametrize('cb, excp',
290+
[[cb, None],
291+
[cb_old, RuntimeError],
292+
[cb_filt, TestException]])(func)
293+
294+
295+
@raising_cb_reg
296+
def test_callbackregistry_process_exception(cb, excp):
297+
if excp is not None:
298+
with pytest.raises(excp):
299+
cb.process('foo')
300+
else:
301+
cb.process('foo')
302+
303+
265304
def test_sanitize_sequence():
266305
d = {'a': 1, 'b': 2, 'c': 3}
267306
k = ['a', 'b', 'c']

0 commit comments

Comments
 (0)