Skip to content

Commit 4d83622

Browse files
committed
Move show() to somewhere naturally inheritable.
It's actually not clear whether to move it to FigureCanvas or FigureManager. FigureCanvas already has start_event_loop (cf. start_main_loop), but pyplot_show/start_main_loop is more a global/pyplot-only concept, so perhaps it belongs to FigureManager instead? OTOH, being on canvas_class makes it easier for switch_backend to access it (it doesn't need to go through `canvas_class.manager_class.pyplot_show`, which is relevant considering that some designs of inheritable backends didn't even have a manager_class attribute).
1 parent 8e34e70 commit 4d83622

File tree

11 files changed

+145
-80
lines changed

11 files changed

+145
-80
lines changed

lib/matplotlib/backend_bases.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2494,6 +2494,42 @@ def stop_event_loop(self):
24942494
"""
24952495
self._looping = False
24962496

2497+
@classmethod
2498+
def start_main_loop(cls):
2499+
"""
2500+
Start the main event loop.
2501+
2502+
Interactive backends need to reimplement this method or `pyplot_show`.
2503+
"""
2504+
2505+
@classmethod
2506+
def pyplot_show(cls, *, block=None):
2507+
"""
2508+
Show all figures. This method is the implementation of `.pyplot.show`.
2509+
2510+
Interactive backends need to reimplement this method or
2511+
`start_main_loop`.
2512+
2513+
`show` blocks by calling `mainloop` if *block* is ``True``, or if it
2514+
is ``None`` and we are neither in IPython's ``%pylab`` mode, nor in
2515+
`interactive` mode.
2516+
"""
2517+
managers = Gcf.get_all_fig_managers()
2518+
if not managers:
2519+
return
2520+
for manager in managers:
2521+
try:
2522+
manager.show() # Emits a warning for non-interactive backend.
2523+
except NonGuiException as exc:
2524+
_api.warn_external(str(exc))
2525+
if block is None:
2526+
# Hack: Are we in IPython's pylab mode?
2527+
from matplotlib import pyplot
2528+
ipython_pylab = hasattr(pyplot.show, "_needmain")
2529+
block = not ipython_pylab and not is_interactive()
2530+
if block:
2531+
cls.start_main_loop()
2532+
24972533

24982534
def key_press_handler(event, canvas=None, toolbar=None):
24992535
"""
@@ -3473,7 +3509,12 @@ def new_figure_manager_given_figure(cls, num, figure):
34733509

34743510
@classmethod
34753511
def draw_if_interactive(cls):
3476-
if cls.mainloop is not None and is_interactive():
3512+
canvas_class = cls.FigureCanvas
3513+
interactive_backend = (
3514+
canvas_class.start_main_loop != FigureCanvasBase.start_main_loop
3515+
or canvas_class.show != FigureCanvasBase.show
3516+
)
3517+
if interactive_backend and is_interactive():
34773518
manager = Gcf.get_active()
34783519
if manager:
34793520
manager.canvas.draw_idle()

lib/matplotlib/backends/_backend_gtk.py

Lines changed: 26 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
from matplotlib import _api, backend_tools, cbook
1010
from matplotlib._pylab_helpers import Gcf
1111
from matplotlib.backend_bases import (
12-
_Backend, FigureManagerBase, NavigationToolbar2, TimerBase)
12+
_Backend, FigureCanvasBase, FigureManagerBase, NavigationToolbar2,
13+
TimerBase)
1314
from matplotlib.backend_tools import Cursors
1415

1516
# The GTK3/GTK4 backends will have already called `gi.require_version` to set
@@ -112,6 +113,29 @@ def _on_timer(self):
112113
return False
113114

114115

116+
class _FigureCanvasGTK(FigureCanvasBase):
117+
_timer_cls = TimerGTK
118+
119+
@classmethod
120+
def start_main_loop(cls):
121+
global _application
122+
if _application is None:
123+
return
124+
125+
try:
126+
_application.run() # Quits when all added windows close.
127+
except KeyboardInterrupt:
128+
# Ensure all windows can process their close event from
129+
# _shutdown_application.
130+
context = GLib.MainContext.default()
131+
while context.pending():
132+
context.iteration(True)
133+
raise
134+
finally:
135+
# Running after quit is undefined, so create a new one next time.
136+
_application = None
137+
138+
115139
class _FigureManagerGTK(FigureManagerBase):
116140
"""
117141
Attributes
@@ -299,21 +323,4 @@ def trigger(self, *args):
299323

300324

301325
class _BackendGTK(_Backend):
302-
@staticmethod
303-
def mainloop():
304-
global _application
305-
if _application is None:
306-
return
307-
308-
try:
309-
_application.run() # Quits when all added windows close.
310-
except KeyboardInterrupt:
311-
# Ensure all windows can process their close event from
312-
# _shutdown_application.
313-
context = GLib.MainContext.default()
314-
while context.pending():
315-
context.iteration(True)
316-
raise
317-
finally:
318-
# Running after quit is undefined, so create a new one next time.
319-
_application = None
326+
mainloop = _FigureCanvasGTK.start_main_loop

lib/matplotlib/backends/_backend_tk.py

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,20 @@ def set_cursor(self, cursor):
396396
except tkinter.TclError:
397397
pass
398398

399+
@classmethod
400+
def start_main_loop(cls):
401+
managers = Gcf.get_all_fig_managers()
402+
if managers:
403+
first_manager = managers[0]
404+
manager_class = type(first_manager)
405+
if manager_class._owns_mainloop:
406+
return
407+
manager_class._owns_mainloop = True
408+
try:
409+
first_manager.window.mainloop()
410+
finally:
411+
manager_class._owns_mainloop = False
412+
399413

400414
class FigureManagerTk(FigureManagerBase):
401415
"""
@@ -1002,18 +1016,6 @@ def trigger(self, *args):
10021016

10031017
@_Backend.export
10041018
class _BackendTk(_Backend):
1019+
FigureCanvas = FigureCanvasTk
10051020
FigureManager = FigureManagerTk
1006-
1007-
@staticmethod
1008-
def mainloop():
1009-
managers = Gcf.get_all_fig_managers()
1010-
if managers:
1011-
first_manager = managers[0]
1012-
manager_class = type(first_manager)
1013-
if manager_class._owns_mainloop:
1014-
return
1015-
manager_class._owns_mainloop = True
1016-
try:
1017-
first_manager.window.mainloop()
1018-
finally:
1019-
manager_class._owns_mainloop = False
1021+
mainloop = FigureCanvas.start_main_loop

lib/matplotlib/backends/backend_gtk3.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from gi.repository import Gio, GLib, GObject, Gtk, Gdk
2727
from . import _backend_gtk
2828
from ._backend_gtk import (
29-
_BackendGTK, _FigureManagerGTK, _NavigationToolbar2GTK,
29+
_BackendGTK, _FigureCanvasGTK, _FigureManagerGTK, _NavigationToolbar2GTK,
3030
TimerGTK as TimerGTK3,
3131
)
3232
from ._backend_gtk import backend_version # noqa: F401 # pylint: disable=W0611
@@ -68,9 +68,8 @@ def _mpl_to_gtk_cursor(mpl_cursor):
6868
_backend_gtk.mpl_to_gtk_cursor_name(mpl_cursor))
6969

7070

71-
class FigureCanvasGTK3(Gtk.DrawingArea, FigureCanvasBase):
71+
class FigureCanvasGTK3(Gtk.DrawingArea, _FigureCanvasGTK):
7272
required_interactive_framework = "gtk3"
73-
_timer_cls = TimerGTK3
7473
manager_class = _api.classproperty(lambda cls: FigureManagerGTK3)
7574
# Setting this as a static constant prevents
7675
# this resulting expression from leaking

lib/matplotlib/backends/backend_gtk4.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,15 @@
2323
from gi.repository import Gio, GLib, GObject, Gtk, Gdk, GdkPixbuf
2424
from . import _backend_gtk
2525
from ._backend_gtk import (
26-
_BackendGTK, _FigureManagerGTK, _NavigationToolbar2GTK,
26+
_BackendGTK, _FigureCanvasGTK, _FigureManagerGTK, _NavigationToolbar2GTK,
2727
TimerGTK as TimerGTK4,
2828
)
2929
from ._backend_gtk import backend_version # noqa: F401 # pylint: disable=W0611
3030

3131

32-
class FigureCanvasGTK4(Gtk.DrawingArea, FigureCanvasBase):
32+
class FigureCanvasGTK4(Gtk.DrawingArea, _FigureCanvasGTK):
3333
required_interactive_framework = "gtk4"
3434
supports_blit = False
35-
_timer_cls = TimerGTK4
3635
manager_class = _api.classproperty(lambda cls: FigureManagerGTK4)
3736
_context_is_scaled = False
3837

lib/matplotlib/backends/backend_macosx.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@ def resize(self, width, height):
9797
FigureCanvasBase.resize_event(self)
9898
self.draw_idle()
9999

100+
@classmethod
101+
def start_main_loop(cls):
102+
_macosx.show()
103+
100104

101105
class NavigationToolbar2Mac(_macosx.NavigationToolbar2, NavigationToolbar2):
102106

@@ -171,7 +175,4 @@ def show(self):
171175
class _BackendMac(_Backend):
172176
FigureCanvas = FigureCanvasMac
173177
FigureManager = FigureManagerMac
174-
175-
@staticmethod
176-
def mainloop():
177-
_macosx.show()
178+
mainloop = FigureCanvas.start_main_loop

lib/matplotlib/backends/backend_qt.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,12 @@ def _draw_rect_callback(painter):
518518
self._draw_rect_callback = _draw_rect_callback
519519
self.update()
520520

521+
@classmethod
522+
def start_main_loop(cls):
523+
qapp = QtWidgets.QApplication.instance()
524+
with _maybe_allow_interrupt(qapp):
525+
qt_compat._exec(qapp)
526+
521527

522528
class MainWindow(QtWidgets.QMainWindow):
523529
closing = QtCore.Signal()
@@ -1040,9 +1046,4 @@ def trigger(self, *args, **kwargs):
10401046
class _BackendQT(_Backend):
10411047
FigureCanvas = FigureCanvasQT
10421048
FigureManager = FigureManagerQT
1043-
1044-
@staticmethod
1045-
def mainloop():
1046-
qapp = QtWidgets.QApplication.instance()
1047-
with _maybe_allow_interrupt(qapp):
1048-
qt_compat._exec(qapp)
1049+
mainloop = FigureCanvas.start_main_loop

lib/matplotlib/backends/backend_webagg.py

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,24 @@ class FigureManagerWebAgg(core.FigureManagerWebAgg):
5555
class FigureCanvasWebAgg(core.FigureCanvasWebAggCore):
5656
manager_class = FigureManagerWebAgg
5757

58+
@classmethod
59+
def pyplot_show(cls, *, block=None):
60+
WebAggApplication.initialize()
61+
62+
url = "http://{address}:{port}{prefix}".format(
63+
address=WebAggApplication.address,
64+
port=WebAggApplication.port,
65+
prefix=WebAggApplication.url_prefix)
66+
67+
if mpl.rcParams['webagg.open_in_browser']:
68+
import webbrowser
69+
if not webbrowser.open(url):
70+
print("To view figure, visit {0}".format(url))
71+
else:
72+
print("To view figure, visit {0}".format(url))
73+
74+
WebAggApplication.start()
75+
5876

5977
class WebAggApplication(tornado.web.Application):
6078
initialized = False
@@ -305,21 +323,3 @@ def ipython_inline_display(figure):
305323
class _BackendWebAgg(_Backend):
306324
FigureCanvas = FigureCanvasWebAgg
307325
FigureManager = FigureManagerWebAgg
308-
309-
@staticmethod
310-
def show(*, block=None):
311-
WebAggApplication.initialize()
312-
313-
url = "http://{address}:{port}{prefix}".format(
314-
address=WebAggApplication.address,
315-
port=WebAggApplication.port,
316-
prefix=WebAggApplication.url_prefix)
317-
318-
if mpl.rcParams['webagg.open_in_browser']:
319-
import webbrowser
320-
if not webbrowser.open(url):
321-
print("To view figure, visit {0}".format(url))
322-
else:
323-
print("To view figure, visit {0}".format(url))
324-
325-
WebAggApplication.start()

lib/matplotlib/backends/backend_wx.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -831,6 +831,13 @@ def _on_enter(self, event):
831831
event.Skip()
832832
FigureCanvasBase.enter_notify_event(self, guiEvent=event, xy=(x, y))
833833

834+
@classmethod
835+
def start_main_loop(cls):
836+
if not wx.App.IsMainLoopRunning():
837+
wxapp = wx.GetApp()
838+
if wxapp is not None:
839+
wxapp.MainLoop()
840+
834841

835842
class FigureCanvasWx(_FigureCanvasWxBase):
836843
# Rendering to a Wx canvas using the deprecated Wx renderer.
@@ -1364,10 +1371,4 @@ def trigger(self, *args, **kwargs):
13641371
class _BackendWx(_Backend):
13651372
FigureCanvas = FigureCanvasWx
13661373
FigureManager = FigureManagerWx
1367-
1368-
@staticmethod
1369-
def mainloop():
1370-
if not wx.App.IsMainLoopRunning():
1371-
wxapp = wx.GetApp()
1372-
if wxapp is not None:
1373-
wxapp.MainLoop()
1374+
mainloop = FigureCanvas.start_main_loop

lib/matplotlib/pyplot.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -286,18 +286,18 @@ def switch_backend(newbackend):
286286
# Classically, backends can directly export these functions. This should
287287
# keep working for backcompat.
288288
new_figure_manager = getattr(backend_mod, "new_figure_manager", None)
289-
# draw_if_interactive = getattr(backend_mod, "draw_if_interactive", None)
290-
# show = getattr(backend_mod, "show", None)
289+
show = getattr(backend_mod, "show", None)
291290
# In that classical approach, backends are implemented as modules, but
292291
# "inherit" default method implementations from backend_bases._Backend.
293292
# This is achieved by creating a "class" that inherits from
294293
# backend_bases._Backend and whose body is filled with the module globals.
295294
class backend_mod(matplotlib.backend_bases._Backend):
296295
locals().update(vars(backend_mod))
297296

298-
# However, the newer approach for defining new_figure_manager (and, in
299-
# the future, draw_if_interactive and show) is to derive them from canvas
300-
# methods. In that case, also update backend_mod accordingly.
297+
# However, the newer approach for defining new_figure_manager and show
298+
# is to derive them from canvas methods. In that case, also update
299+
# backend_mod accordingly.
300+
301301
if new_figure_manager is None:
302302
def new_figure_manager_given_figure(num, figure):
303303
return canvas_class.new_manager(figure, num)
@@ -310,6 +310,9 @@ def new_figure_manager(num, *args, FigureClass=Figure, **kwargs):
310310
new_figure_manager_given_figure
311311
backend_mod.new_figure_manager = new_figure_manager
312312

313+
if show is None:
314+
backend_mod.show = canvas_class.pyplot_show
315+
313316
_log.debug("Loaded backend %s version %s.",
314317
newbackend, backend_mod.backend_version)
315318

lib/matplotlib/tests/test_backend_template.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import sys
66
from types import SimpleNamespace
7+
from unittest.mock import MagicMock
78

89
import matplotlib as mpl
910
from matplotlib import pyplot as plt
@@ -21,3 +22,13 @@ def test_new_manager(monkeypatch):
2122
monkeypatch.setitem(sys.modules, "mpl_test_backend", mpl_test_backend)
2223
mpl.use("module://mpl_test_backend")
2324
assert type(plt.figure().canvas) == backend_template.FigureCanvasTemplate
25+
26+
27+
def test_show(monkeypatch):
28+
mpl_test_backend = SimpleNamespace(**vars(backend_template))
29+
mock_show = backend_template.FigureCanvasTemplate.pyplot_show = MagicMock()
30+
del mpl_test_backend.show
31+
monkeypatch.setitem(sys.modules, "mpl_test_backend", mpl_test_backend)
32+
mpl.use("module://mpl_test_backend")
33+
plt.show()
34+
mock_show.assert_called_with()

0 commit comments

Comments
 (0)