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

Skip to content

Commit 7ae415a

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 74ed294 commit 7ae415a

File tree

7 files changed

+72
-61
lines changed

7 files changed

+72
-61
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/backend_bases.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2552,7 +2552,7 @@ def show(self):
25522552
warning in `.Figure.show`.
25532553
"""
25542554
# This should be overridden in GUI backends.
2555-
if mpl.backends._get_running_interactive_framework() != "headless":
2555+
if cbook._get_running_interactive_framework() != "headless":
25562556
raise NonGuiException(
25572557
f"Matplotlib is currently using {get_backend()}, which is "
25582558
f"a non-GUI backend, so cannot show the figure.")

lib/matplotlib/backends/__init__.py

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

lib/matplotlib/cbook/__init__.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,51 @@
3636
MatplotlibDeprecationWarning, mplDeprecation)
3737

3838

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

4285

4386
class _StrongRef:

lib/matplotlib/figure.py

Lines changed: 1 addition & 1 deletion
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

lib/matplotlib/pyplot.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,6 @@
6666
FormatStrFormatter, ScalarFormatter, LogFormatter, LogFormatterExponent,
6767
LogFormatterMathtext, Locator, IndexLocator, FixedLocator, NullLocator,
6868
LinearLocator, LogLocator, AutoLocator, MultipleLocator, MaxNLocator)
69-
from matplotlib.backends import _get_running_interactive_framework
7069

7170
_log = logging.getLogger(__name__)
7271

@@ -226,8 +225,7 @@ def switch_backend(newbackend):
226225
required_framework = getattr(
227226
Backend.FigureCanvas, "required_interactive_framework", None)
228227
if required_framework is not None:
229-
current_framework = \
230-
matplotlib.backends._get_running_interactive_framework()
228+
current_framework = cbook._get_running_interactive_framework()
231229
if (current_framework and required_framework
232230
and current_framework != required_framework):
233231
raise ImportError(
@@ -2250,7 +2248,7 @@ def getname_val(identifier):
22502248
# is compatible with the current running interactive framework.
22512249
if (rcParams["backend_fallback"]
22522250
and dict.__getitem__(rcParams, "backend") in _interactive_bk
2253-
and _get_running_interactive_framework()):
2251+
and cbook._get_running_interactive_framework()):
22542252
dict.__setitem__(rcParams, "backend", rcsetup._auto_backend_sentinel)
22552253
# Set up the backend.
22562254
switch_backend(rcParams["backend"])

lib/matplotlib/tests/test_cbook.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,18 @@ 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+
monkeypatch.setattr(
240+
cbook, "_get_running_interactive_framework", lambda: None)
241+
with pytest.raises(TypeError):
242+
cb.process("foo", "argument mismatch")
243+
monkeypatch.setattr(
244+
cbook, "_get_running_interactive_framework", lambda: "not-none")
245+
cb.process("foo", "argument mismatch") # No error in that case.
246+
247+
236248
def raising_cb_reg(func):
237249
class TestException(Exception):
238250
pass
@@ -245,10 +257,6 @@ def transformer(excp):
245257
raise TestException
246258
raise excp
247259

248-
# default behavior
249-
cb = cbook.CallbackRegistry()
250-
cb.connect('foo', raising_function)
251-
252260
# old default
253261
cb_old = cbook.CallbackRegistry(exception_handler=None)
254262
cb_old.connect('foo', raising_function)
@@ -258,13 +266,14 @@ def transformer(excp):
258266
cb_filt.connect('foo', raising_function)
259267

260268
return pytest.mark.parametrize('cb, excp',
261-
[[cb, None],
262-
[cb_old, RuntimeError],
269+
[[cb_old, RuntimeError],
263270
[cb_filt, TestException]])(func)
264271

265272

266273
@raising_cb_reg
267-
def test_callbackregistry_process_exception(cb, excp):
274+
def test_callbackregistry_custom_exception_handler(monkeypatch, cb, excp):
275+
monkeypatch.setattr(
276+
cbook, "_get_running_interactive_framework", lambda: None)
268277
if excp is not None:
269278
with pytest.raises(excp):
270279
cb.process('foo')

0 commit comments

Comments
 (0)