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

Skip to content

Commit 1d597cf

Browse files
authored
Merge pull request #15270 from anntzer/cbr-exceptions
When no gui event loop is running, propagate callback exceptions.
2 parents 2eb0023 + e9c7544 commit 1d597cf

File tree

7 files changed

+82
-67
lines changed

7 files changed

+82
-67
lines changed

doc/api/next_api_changes/behaviour.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,11 @@ Previously, it did not necessarily have such an attribute. A check for
1717
``hasattr(figure.canvas, "manager")`` should now be replaced by
1818
``figure.canvas.manager is not None`` (or ``getattr(figure.canvas, "manager", None) is not None``
1919
for back-compatibility).
20+
21+
`.cbook.CallbackRegistry` now propagates exceptions when no GUI event loop is running
22+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
23+
`.cbook.CallbackRegistry` now defaults to propagating exceptions thrown by
24+
callbacks when no interactive GUI event loop is running. If a GUI event loop
25+
*is* running, `.cbook.CallbackRegistry` still defaults to just printing a
26+
traceback, as unhandled exceptions can make the program completely ``abort()``
27+
in that case.

lib/matplotlib/backend_bases.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@
4545

4646
import numpy as np
4747

48-
import matplotlib as mpl
4948
from matplotlib import (
5049
backend_tools as tools, cbook, colors, textpath, tight_bbox, transforms,
5150
widgets, get_backend, is_interactive, rcParams)
@@ -2552,7 +2551,7 @@ def show(self):
25522551
warning in `.Figure.show`.
25532552
"""
25542553
# This should be overridden in GUI backends.
2555-
if mpl.backends._get_running_interactive_framework() != "headless":
2554+
if cbook._get_running_interactive_framework() != "headless":
25562555
raise NonGuiException(
25572556
f"Matplotlib is currently using {get_backend()}, which is "
25582557
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: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -233,22 +233,33 @@ 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
239251

240252
def raising_function():
241253
raise RuntimeError
242254

255+
def raising_function_VE():
256+
raise ValueError
257+
243258
def transformer(excp):
244259
if isinstance(excp, RuntimeError):
245260
raise TestException
246261
raise excp
247262

248-
# default behavior
249-
cb = cbook.CallbackRegistry()
250-
cb.connect('foo', raising_function)
251-
252263
# old default
253264
cb_old = cbook.CallbackRegistry(exception_handler=None)
254265
cb_old.connect('foo', raising_function)
@@ -257,18 +268,21 @@ def transformer(excp):
257268
cb_filt = cbook.CallbackRegistry(exception_handler=transformer)
258269
cb_filt.connect('foo', raising_function)
259270

271+
# filter
272+
cb_filt_pass = cbook.CallbackRegistry(exception_handler=transformer)
273+
cb_filt_pass.connect('foo', raising_function_VE)
274+
260275
return pytest.mark.parametrize('cb, excp',
261-
[[cb, None],
262-
[cb_old, RuntimeError],
263-
[cb_filt, TestException]])(func)
276+
[[cb_old, RuntimeError],
277+
[cb_filt, TestException],
278+
[cb_filt_pass, ValueError]])(func)
264279

265280

266281
@raising_cb_reg
267-
def test_callbackregistry_process_exception(cb, excp):
268-
if excp is not None:
269-
with pytest.raises(excp):
270-
cb.process('foo')
271-
else:
282+
def test_callbackregistry_custom_exception_handler(monkeypatch, cb, excp):
283+
monkeypatch.setattr(
284+
cbook, "_get_running_interactive_framework", lambda: None)
285+
with pytest.raises(excp):
272286
cb.process('foo')
273287

274288

0 commit comments

Comments
 (0)