Skip to content

Move show() to somewhere naturally inheritable / document what pyplot expects from a backend. #23101

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 55 additions & 3 deletions lib/matplotlib/backend_bases.py
Original file line number Diff line number Diff line change
Expand Up @@ -2840,6 +2840,53 @@ def create_with_canvas(cls, canvas_class, figure, num):
"""
return cls(canvas_class(figure), num)

@classmethod
def start_main_loop(cls):
"""
Start the main event loop.

This method is called by `.FigureManagerBase.pyplot_show`, which is the
implementation of `.pyplot.show`. To customize the behavior of
`.pyplot.show`, interactive backends should usually override
`~.FigureManagerBase.start_main_loop`; if more customized logic is
necessary, `~.FigureManagerBase.pyplot_show` can also be overridden.
"""

@classmethod
def pyplot_show(cls, *, block=None):
"""
Show all figures. This method is the implementation of `.pyplot.show`.

To customize the behavior of `.pyplot.show`, interactive backends
should usually override `~.FigureManagerBase.start_main_loop`; if more
customized logic is necessary, `~.FigureManagerBase.pyplot_show` can
also be overridden.

Parameters
----------
block : bool, optional
Whether to block by calling ``start_main_loop``. The default,
None, means to block if we are neither in IPython's ``%pylab`` mode
nor in ``interactive`` mode.
"""
managers = Gcf.get_all_fig_managers()
if not managers:
return
for manager in managers:
try:
manager.show() # Emits a warning for non-interactive backend.
except NonGuiException as exc:
_api.warn_external(str(exc))
if block is None:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we also want to check if pypplot is already imported?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure.

# Hack: Are we in IPython's %pylab mode? In pylab mode, IPython
# (>= 0.10) tacks a _needmain attribute onto pyplot.show (always
# set to False).
ipython_pylab = hasattr(
getattr(sys.modules.get("pyplot"), "show", None), "_needmain")
block = not ipython_pylab and not is_interactive()
if block:
cls.start_main_loop()

def show(self):
"""
For GUI backends, show the figure window and redraw.
Expand Down Expand Up @@ -3518,7 +3565,12 @@ def new_figure_manager_given_figure(cls, num, figure):

@classmethod
def draw_if_interactive(cls):
if cls.mainloop is not None and is_interactive():
manager_class = cls.FigureCanvas.manager_class
# Interactive backends reimplement start_main_loop or pyplot_show.
backend_is_interactive = (
manager_class.start_main_loop != FigureManagerBase.start_main_loop
or manager_class.pyplot_show != FigureManagerBase.pyplot_show)
if backend_is_interactive and is_interactive():
manager = Gcf.get_active()
if manager:
manager.canvas.draw_idle()
Expand Down Expand Up @@ -3546,8 +3598,8 @@ def show(cls, *, block=None):
# Hack: Are we in IPython's %pylab mode? In pylab mode, IPython
# (>= 0.10) tacks a _needmain attribute onto pyplot.show (always
# set to False).
from matplotlib import pyplot
ipython_pylab = hasattr(pyplot.show, "_needmain")
ipython_pylab = hasattr(
getattr(sys.modules.get("pyplot"), "show", None), "_needmain")
Comment on lines 3598 to +3602
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this to a helper function in cbook?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The duplication is only temporary, because the version in _Backend.show will go away as I plan to get rid of the _Backend class, which was an implementation detail anyways. (I'm not doing it immediately because the ipympl backend has started relying on it, but I plan to help them to get rid of that use, either going back to the old-style fully manual API if they want a long backcompat, or going to the new class-based API.)

block = not ipython_pylab and not is_interactive()
if block:
cls.mainloop()
Expand Down
46 changes: 26 additions & 20 deletions lib/matplotlib/backends/_backend_gtk.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
from matplotlib import _api, backend_tools, cbook
from matplotlib._pylab_helpers import Gcf
from matplotlib.backend_bases import (
_Backend, FigureManagerBase, NavigationToolbar2, TimerBase)
_Backend, FigureCanvasBase, FigureManagerBase, NavigationToolbar2,
TimerBase)
from matplotlib.backend_tools import Cursors

import gi
Expand Down Expand Up @@ -113,6 +114,10 @@ def _on_timer(self):
return False


class _FigureCanvasGTK(FigureCanvasBase):
_timer_cls = TimerGTK


class _FigureManagerGTK(FigureManagerBase):
"""
Attributes
Expand Down Expand Up @@ -192,6 +197,25 @@ def destroy(self, *args):
self.window.destroy()
self.canvas.destroy()

@classmethod
def start_main_loop(cls):
global _application
if _application is None:
return

try:
_application.run() # Quits when all added windows close.
except KeyboardInterrupt:
# Ensure all windows can process their close event from
# _shutdown_application.
context = GLib.MainContext.default()
while context.pending():
context.iteration(True)
raise
finally:
# Running after quit is undefined, so create a new one next time.
_application = None

def show(self):
# show the figure window
self.window.show()
Expand Down Expand Up @@ -305,22 +329,4 @@ class _BackendGTK(_Backend):
Gtk.get_minor_version(),
Gtk.get_micro_version(),
)

@staticmethod
def mainloop():
global _application
if _application is None:
return

try:
_application.run() # Quits when all added windows close.
except KeyboardInterrupt:
# Ensure all windows can process their close event from
# _shutdown_application.
context = GLib.MainContext.default()
while context.pending():
context.iteration(True)
raise
finally:
# Running after quit is undefined, so create a new one next time.
_application = None
mainloop = _FigureManagerGTK.start_main_loop
30 changes: 16 additions & 14 deletions lib/matplotlib/backends/_backend_tk.py
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,20 @@ def create_with_canvas(cls, canvas_class, figure, num):
canvas.draw_idle()
return manager

@classmethod
def start_main_loop(cls):
managers = Gcf.get_all_fig_managers()
if managers:
first_manager = managers[0]
manager_class = type(first_manager)
if manager_class._owns_mainloop:
return
manager_class._owns_mainloop = True
try:
first_manager.window.mainloop()
finally:
manager_class._owns_mainloop = False

def _update_window_dpi(self, *args):
newdpi = self._window_dpi.get()
self.window.call('tk', 'scaling', newdpi / 72)
Expand Down Expand Up @@ -1018,18 +1032,6 @@ def trigger(self, *args):
@_Backend.export
class _BackendTk(_Backend):
backend_version = tk.TkVersion
FigureCanvas = FigureCanvasTk
FigureManager = FigureManagerTk

@staticmethod
def mainloop():
managers = Gcf.get_all_fig_managers()
if managers:
first_manager = managers[0]
manager_class = type(first_manager)
if manager_class._owns_mainloop:
return
manager_class._owns_mainloop = True
try:
first_manager.window.mainloop()
finally:
manager_class._owns_mainloop = False
mainloop = FigureManagerTk.start_main_loop
11 changes: 5 additions & 6 deletions lib/matplotlib/backends/backend_gtk3.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
import matplotlib as mpl
from matplotlib import _api, backend_tools, cbook
from matplotlib.backend_bases import (
FigureCanvasBase, ToolContainerBase,
CloseEvent, KeyEvent, LocationEvent, MouseEvent, ResizeEvent)
ToolContainerBase, CloseEvent, KeyEvent, LocationEvent, MouseEvent,
ResizeEvent)

try:
import gi
Expand All @@ -26,8 +26,8 @@

from gi.repository import Gio, GLib, GObject, Gtk, Gdk
from . import _backend_gtk
from ._backend_gtk import (
_BackendGTK, _FigureManagerGTK, _NavigationToolbar2GTK,
from ._backend_gtk import ( # noqa: F401 # pylint: disable=W0611
_BackendGTK, _FigureCanvasGTK, _FigureManagerGTK, _NavigationToolbar2GTK,
TimerGTK as TimerGTK3,
)

Expand All @@ -52,9 +52,8 @@ def _mpl_to_gtk_cursor(mpl_cursor):
_backend_gtk.mpl_to_gtk_cursor_name(mpl_cursor))


class FigureCanvasGTK3(FigureCanvasBase, Gtk.DrawingArea):
class FigureCanvasGTK3(_FigureCanvasGTK, Gtk.DrawingArea):
required_interactive_framework = "gtk3"
_timer_cls = TimerGTK3
manager_class = _api.classproperty(lambda cls: FigureManagerGTK3)
# Setting this as a static constant prevents
# this resulting expression from leaking
Expand Down
10 changes: 4 additions & 6 deletions lib/matplotlib/backends/backend_gtk4.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
import matplotlib as mpl
from matplotlib import _api, backend_tools, cbook
from matplotlib.backend_bases import (
FigureCanvasBase, ToolContainerBase,
KeyEvent, LocationEvent, MouseEvent, ResizeEvent)
ToolContainerBase, KeyEvent, LocationEvent, MouseEvent, ResizeEvent)

try:
import gi
Expand All @@ -24,16 +23,15 @@

from gi.repository import Gio, GLib, Gtk, Gdk, GdkPixbuf
from . import _backend_gtk
from ._backend_gtk import (
_BackendGTK, _FigureManagerGTK, _NavigationToolbar2GTK,
from ._backend_gtk import ( # noqa: F401 # pylint: disable=W0611
_BackendGTK, _FigureCanvasGTK, _FigureManagerGTK, _NavigationToolbar2GTK,
TimerGTK as TimerGTK4,
)


class FigureCanvasGTK4(FigureCanvasBase, Gtk.DrawingArea):
class FigureCanvasGTK4(_FigureCanvasGTK, Gtk.DrawingArea):
required_interactive_framework = "gtk4"
supports_blit = False
_timer_cls = TimerGTK4
manager_class = _api.classproperty(lambda cls: FigureManagerGTK4)
_context_is_scaled = False

Expand Down
9 changes: 5 additions & 4 deletions lib/matplotlib/backends/backend_macosx.py
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,10 @@ def _close_button_pressed(self):
def close(self):
return self._close_button_pressed()

@classmethod
def start_main_loop(cls):
_macosx.show()

def show(self):
if not self._shown:
self._show()
Expand All @@ -177,7 +181,4 @@ def show(self):
class _BackendMac(_Backend):
FigureCanvas = FigureCanvasMac
FigureManager = FigureManagerMac

@staticmethod
def mainloop():
_macosx.show()
mainloop = FigureManagerMac.start_main_loop
14 changes: 8 additions & 6 deletions lib/matplotlib/backends/backend_qt.py
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,13 @@ def resize(self, width, height):
self.canvas.resize(width, height)
self.window.resize(width + extra_width, height + extra_height)

@classmethod
def start_main_loop(cls):
qapp = QtWidgets.QApplication.instance()
if qapp:
with _maybe_allow_interrupt(qapp):
qt_compat._exec(qapp)

def show(self):
self.window.show()
if mpl.rcParams['figure.raise_window']:
Expand Down Expand Up @@ -1007,9 +1014,4 @@ class _BackendQT(_Backend):
backend_version = __version__
FigureCanvas = FigureCanvasQT
FigureManager = FigureManagerQT

@staticmethod
def mainloop():
qapp = QtWidgets.QApplication.instance()
with _maybe_allow_interrupt(qapp):
qt_compat._exec(qapp)
mainloop = FigureManagerQT.start_main_loop
36 changes: 18 additions & 18 deletions lib/matplotlib/backends/backend_webagg.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,24 @@ def run(self):
class FigureManagerWebAgg(core.FigureManagerWebAgg):
_toolbar2_class = core.NavigationToolbar2WebAgg

@classmethod
def pyplot_show(cls, *, block=None):
WebAggApplication.initialize()

url = "http://{address}:{port}{prefix}".format(
address=WebAggApplication.address,
port=WebAggApplication.port,
prefix=WebAggApplication.url_prefix)

if mpl.rcParams['webagg.open_in_browser']:
import webbrowser
if not webbrowser.open(url):
print("To view figure, visit {0}".format(url))
else:
print("To view figure, visit {0}".format(url))

WebAggApplication.start()


class FigureCanvasWebAgg(core.FigureCanvasWebAggCore):
manager_class = FigureManagerWebAgg
Expand Down Expand Up @@ -307,21 +325,3 @@ def ipython_inline_display(figure):
class _BackendWebAgg(_Backend):
FigureCanvas = FigureCanvasWebAgg
FigureManager = FigureManagerWebAgg

@staticmethod
def show(*, block=None):
WebAggApplication.initialize()

url = "http://{address}:{port}{prefix}".format(
address=WebAggApplication.address,
port=WebAggApplication.port,
prefix=WebAggApplication.url_prefix)

if mpl.rcParams['webagg.open_in_browser']:
import webbrowser
if not webbrowser.open(url):
print("To view figure, visit {0}".format(url))
else:
print("To view figure, visit {0}".format(url))

WebAggApplication.start()
15 changes: 8 additions & 7 deletions lib/matplotlib/backends/backend_wx.py
Original file line number Diff line number Diff line change
Expand Up @@ -999,6 +999,13 @@ def create_with_canvas(cls, canvas_class, figure, num):
figure.canvas.draw_idle()
return manager

@classmethod
def start_main_loop(cls):
if not wx.App.IsMainLoopRunning():
wxapp = wx.GetApp()
if wxapp is not None:
wxapp.MainLoop()

def show(self):
# docstring inherited
self.frame.Show()
Expand Down Expand Up @@ -1365,10 +1372,4 @@ def trigger(self, *args, **kwargs):
class _BackendWx(_Backend):
FigureCanvas = FigureCanvasWx
FigureManager = FigureManagerWx

@staticmethod
def mainloop():
if not wx.App.IsMainLoopRunning():
wxapp = wx.GetApp()
if wxapp is not None:
wxapp.MainLoop()
mainloop = FigureManagerWx.start_main_loop
Loading