Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit f3ecbd0

Browse files
committed
When no gui event loop is running, propagate callback exceptions.
CallbackRegistry currently defaults to suppressing exceptions that occur in callbacks because they cause PyQt5 to abort() the program with no chance of even catching the exception, but such a behavior is annoying when writing tests involving callbacks (e.g., mplcursors), because in that case we actually want to see whether an exception occurred, and typically fail the test in that case. Instead we can detect whether a GUI event loop is currently running and propagate the exception if none is running. Part of the patch is just moving `_get_running_interactive_framework` from `backends` to `cbook`, so that `cbook` keeps its property of being importable independent of the rest of mpl.
1 parent bb7e441 commit f3ecbd0

File tree

6 files changed

+75
-68
lines changed

6 files changed

+75
-68
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
API changes
2+
```````````
3+
4+
`.cbook.CallbackRegistry` now defaults to propagating exceptions thrown by
5+
callbacks when no interactive GUI event loop is running. If a GUI event loop
6+
*is* running, `.cbook.CallbackRegistry` still defaults to just printing a
7+
traceback, as unhandled exceptions can make the program completely ``abort()``
8+
in that case.

lib/matplotlib/backends/__init__.py

Lines changed: 0 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,2 @@
1-
import importlib
2-
import logging
3-
import os
4-
import sys
5-
6-
import matplotlib
7-
from matplotlib import cbook
8-
from matplotlib.backend_bases import _Backend
9-
10-
_log = logging.getLogger(__name__)
11-
12-
131
# NOTE: plt.switch_backend() (called at import time) will add a "backend"
142
# attribute here for backcompat.
15-
16-
17-
def _get_running_interactive_framework():
18-
"""
19-
Return the interactive framework whose event loop is currently running, if
20-
any, or "headless" if no event loop can be started, or None.
21-
22-
Returns
23-
-------
24-
Optional[str]
25-
One of the following values: "qt5", "qt4", "gtk3", "wx", "tk",
26-
"macosx", "headless", ``None``.
27-
"""
28-
QtWidgets = (sys.modules.get("PyQt5.QtWidgets")
29-
or sys.modules.get("PySide2.QtWidgets"))
30-
if QtWidgets and QtWidgets.QApplication.instance():
31-
return "qt5"
32-
QtGui = (sys.modules.get("PyQt4.QtGui")
33-
or sys.modules.get("PySide.QtGui"))
34-
if QtGui and QtGui.QApplication.instance():
35-
return "qt4"
36-
Gtk = sys.modules.get("gi.repository.Gtk")
37-
if Gtk and Gtk.main_level():
38-
return "gtk3"
39-
wx = sys.modules.get("wx")
40-
if wx and wx.GetApp():
41-
return "wx"
42-
tkinter = sys.modules.get("tkinter")
43-
if tkinter:
44-
for frame in sys._current_frames().values():
45-
while frame:
46-
if frame.f_code == tkinter.mainloop.__code__:
47-
return "tk"
48-
frame = frame.f_back
49-
if 'matplotlib.backends._macosx' in sys.modules:
50-
if sys.modules["matplotlib.backends._macosx"].event_loop_is_running():
51-
return "macosx"
52-
if sys.platform.startswith("linux") and not os.environ.get("DISPLAY"):
53-
return "headless"
54-
return None

lib/matplotlib/cbook/__init__.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,17 @@
2222
import shlex
2323
import subprocess
2424
import sys
25+
import threading
2526
import time
2627
import traceback
2728
import types
2829
import warnings
2930
import weakref
3031
from weakref import WeakMethod
3132

33+
import matplotlib
34+
from matplotlib import cbook
35+
3236
import numpy as np
3337

3438
import matplotlib
@@ -39,8 +43,51 @@
3943
MatplotlibDeprecationWarning, mplDeprecation)
4044

4145

46+
def _get_running_interactive_framework():
47+
"""
48+
Return the interactive framework whose event loop is currently running, if
49+
any, or "headless" if no event loop can be started, or None.
50+
51+
Returns
52+
-------
53+
Optional[str]
54+
One of the following values: "qt5", "qt4", "gtk3", "wx", "tk",
55+
"macosx", "headless", ``None``.
56+
"""
57+
QtWidgets = (sys.modules.get("PyQt5.QtWidgets")
58+
or sys.modules.get("PySide2.QtWidgets"))
59+
if QtWidgets and QtWidgets.QApplication.instance():
60+
return "qt5"
61+
QtGui = (sys.modules.get("PyQt4.QtGui")
62+
or sys.modules.get("PySide.QtGui"))
63+
if QtGui and QtGui.QApplication.instance():
64+
return "qt4"
65+
Gtk = sys.modules.get("gi.repository.Gtk")
66+
if Gtk and Gtk.main_level():
67+
return "gtk3"
68+
wx = sys.modules.get("wx")
69+
if wx and wx.GetApp():
70+
return "wx"
71+
tkinter = sys.modules.get("tkinter")
72+
if tkinter:
73+
for frame in sys._current_frames().values():
74+
while frame:
75+
if frame.f_code == tkinter.mainloop.__code__:
76+
return "tk"
77+
frame = frame.f_back
78+
if 'matplotlib.backends._macosx' in sys.modules:
79+
if sys.modules["matplotlib.backends._macosx"].event_loop_is_running():
80+
return "macosx"
81+
if sys.platform.startswith("linux") and not os.environ.get("DISPLAY"):
82+
return "headless"
83+
return None
84+
85+
4286
def _exception_printer(exc):
43-
traceback.print_exc()
87+
if _get_running_interactive_framework() in ["headless", None]:
88+
raise exc
89+
else:
90+
traceback.print_exc()
4491

4592

4693
class _StrongRef:

lib/matplotlib/figure.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import numpy as np
1818

1919
from matplotlib import rcParams
20-
from matplotlib import backends, docstring, projections
20+
from matplotlib import docstring, projections
2121
from matplotlib import __version__ as _mpl_version
2222
from matplotlib import get_backend
2323

@@ -444,8 +444,7 @@ def show(self, warn=True):
444444
return
445445
except NonGuiException:
446446
pass
447-
if (backends._get_running_interactive_framework() != "headless"
448-
and warn):
447+
if cbook._get_running_interactive_framework() != "headless" and warn:
449448
cbook._warn_external('Matplotlib is currently using %s, which is '
450449
'a non-GUI backend, so cannot show the '
451450
'figure.' % get_backend())

lib/matplotlib/pyplot.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,6 @@
6767
Locator, IndexLocator, FixedLocator, NullLocator,\
6868
LinearLocator, LogLocator, AutoLocator, MultipleLocator,\
6969
MaxNLocator
70-
from matplotlib.backends import _get_running_interactive_framework
7170

7271
_log = logging.getLogger(__name__)
7372

@@ -220,15 +219,14 @@ def switch_backend(newbackend):
220219

221220
backend_mod = importlib.import_module(backend_name)
222221
Backend = type(
223-
"Backend", (matplotlib.backends._Backend,), vars(backend_mod))
222+
"Backend", (matplotlib.backend_bases._Backend,), vars(backend_mod))
224223
_log.debug("Loaded backend %s version %s.",
225224
newbackend, Backend.backend_version)
226225

227226
required_framework = getattr(
228227
Backend.FigureCanvas, "required_interactive_framework", None)
229228
if required_framework is not None:
230-
current_framework = \
231-
matplotlib.backends._get_running_interactive_framework()
229+
current_framework = cbook._get_running_interactive_framework()
232230
if (current_framework and required_framework
233231
and current_framework != required_framework):
234232
raise ImportError(
@@ -2304,7 +2302,7 @@ def getname_val(identifier):
23042302
# is compatible with the current running interactive framework.
23052303
if (rcParams["backend_fallback"]
23062304
and dict.__getitem__(rcParams, "backend") in _interactive_bk
2307-
and _get_running_interactive_framework()):
2305+
and cbook._get_running_interactive_framework()):
23082306
dict.__setitem__(rcParams, "backend", rcsetup._auto_backend_sentinel)
23092307
# Set up the backend.
23102308
switch_backend(rcParams["backend"])

lib/matplotlib/tests/test_cbook.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,16 @@ def test_pickling(self):
233233
"callbacks")
234234

235235

236+
def test_callbackregistry_default_exception_handler(monkeypatch):
237+
cb = cbook.CallbackRegistry()
238+
cb.connect("foo", lambda: None)
239+
with pytest.raises(TypeError):
240+
cb.process("foo", "argument mismatch")
241+
monkeypatch.setattr(
242+
cbook, "_get_running_interactive_framework", lambda: "not-none")
243+
cb.process("foo", "argument mismatch") # No error in that case.
244+
245+
236246
def raising_cb_reg(func):
237247
class TestException(Exception):
238248
pass
@@ -245,10 +255,6 @@ def transformer(excp):
245255
raise TestException
246256
raise excp
247257

248-
# default behavior
249-
cb = cbook.CallbackRegistry()
250-
cb.connect('foo', raising_function)
251-
252258
# old default
253259
cb_old = cbook.CallbackRegistry(exception_handler=None)
254260
cb_old.connect('foo', raising_function)
@@ -258,13 +264,14 @@ def transformer(excp):
258264
cb_filt.connect('foo', raising_function)
259265

260266
return pytest.mark.parametrize('cb, excp',
261-
[[cb, None],
262-
[cb_old, RuntimeError],
267+
[[cb_old, RuntimeError],
263268
[cb_filt, TestException]])(func)
264269

265270

266271
@raising_cb_reg
267-
def test_callbackregistry_process_exception(cb, excp):
272+
def test_callbackregistry_custom_exception_handler(monkeypatch, cb, excp):
273+
monkeypatch.setattr(
274+
cbook, "_get_running_interactive_framework", lambda: None)
268275
if excp is not None:
269276
with pytest.raises(excp):
270277
cb.process('foo')

0 commit comments

Comments
 (0)