Skip to content

Refactor backend loading #9551

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
May 29, 2018
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
7 changes: 7 additions & 0 deletions doc/api/next_api_changes/2018-05-25-AL.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Non-interactive FigureManager classes are now aliases of FigureManagerBase
``````````````````````````````````````````````````````````````````````````

The `FigureManagerPdf`, `FigureManagerPS`, and `FigureManagerSVG` classes,
which were previously empty subclasses of `FigureManagerBase` (i.e., not
adding or overriding any attribute or method), are now direct aliases for
`FigureManagerBase`.
234 changes: 119 additions & 115 deletions lib/matplotlib/backend_bases.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,121 +124,6 @@ def get_registered_canvas_class(format):
return backend_class


class _Backend(object):
# A backend can be defined by using the following pattern:
#
# @_Backend.export
# class FooBackend(_Backend):
# # override the attributes and methods documented below.

# The following attributes and methods must be overridden by subclasses.

# The `FigureCanvas` and `FigureManager` classes must be defined.
FigureCanvas = None
FigureManager = None

# The following methods must be left as None for non-interactive backends.
# For interactive backends, `trigger_manager_draw` should be a function
# taking a manager as argument and triggering a canvas draw, and `mainloop`
# should be a function taking no argument and starting the backend main
# loop.
trigger_manager_draw = None
mainloop = None

# The following methods will be automatically defined and exported, but
# can be overridden.

@classmethod
def new_figure_manager(cls, num, *args, **kwargs):
"""Create a new figure manager instance.
"""
# This import needs to happen here due to circular imports.
from matplotlib.figure import Figure
fig_cls = kwargs.pop('FigureClass', Figure)
fig = fig_cls(*args, **kwargs)
return cls.new_figure_manager_given_figure(num, fig)

@classmethod
def new_figure_manager_given_figure(cls, num, figure):
"""Create a new figure manager instance for the given figure.
"""
canvas = cls.FigureCanvas(figure)
manager = cls.FigureManager(canvas, num)
return manager

@classmethod
def draw_if_interactive(cls):
if cls.trigger_manager_draw is not None and is_interactive():
manager = Gcf.get_active()
if manager:
cls.trigger_manager_draw(manager)

@classmethod
def show(cls, block=None):
"""Show all figures.

`show` blocks by calling `mainloop` if *block* is ``True``, or if it
is ``None`` and 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:
# Emits a warning if the backend is non-interactive.
manager.canvas.figure.show()
if cls.mainloop is None:
return
if block is None:
# Hack: Are we in IPython's pylab mode?
from matplotlib import pyplot
try:
# IPython versions >= 0.10 tack the _needmain attribute onto
# pyplot.show, and always set it to False, when in %pylab mode.
ipython_pylab = not pyplot.show._needmain
except AttributeError:
ipython_pylab = False
block = not ipython_pylab and not is_interactive()
# TODO: The above is a hack to get the WebAgg backend working with
# ipython's `%pylab` mode until proper integration is implemented.
if get_backend() == "WebAgg":
block = True
if block:
cls.mainloop()

# This method is the one actually exporting the required methods.

@staticmethod
def export(cls):
for name in ["FigureCanvas",
"FigureManager",
"new_figure_manager",
"new_figure_manager_given_figure",
"draw_if_interactive",
"show"]:
setattr(sys.modules[cls.__module__], name, getattr(cls, name))

# For back-compatibility, generate a shim `Show` class.

class Show(ShowBase):
def mainloop(self):
return cls.mainloop()

setattr(sys.modules[cls.__module__], "Show", Show)
return cls


class ShowBase(_Backend):
"""
Simple base class to generate a show() callable in backends.

Subclass must override mainloop() method.
"""

def __call__(self, block=None):
return self.show(block=block)


class RendererBase(object):
"""An abstract base class to handle drawing/rendering operations.

Expand Down Expand Up @@ -3328,3 +3213,122 @@ def set_message(self, s):
Message text
"""
pass

Copy link
Member

Choose a reason for hiding this comment

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

Is this block just a big copy/paste? (so we don't have to bother reviewing the code if it is)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is, except for the fact that the FigureManager attribute now defaults to FigureManagerBase.


class _Backend(object):
# A backend can be defined by using the following pattern:
#
# @_Backend.export
# class FooBackend(_Backend):
# # override the attributes and methods documented below.

# `backend_version` may be overridden by the subclass.
backend_version = "unknown"

# The `FigureCanvas` class must be defined.
FigureCanvas = None

# For interactive backends, the `FigureManager` class must be overridden.
FigureManager = FigureManagerBase

# The following methods must be left as None for non-interactive backends.
# For interactive backends, `trigger_manager_draw` should be a function
# taking a manager as argument and triggering a canvas draw, and `mainloop`
# should be a function taking no argument and starting the backend main
# loop.
trigger_manager_draw = None
mainloop = None

# The following methods will be automatically defined and exported, but
# can be overridden.

@classmethod
def new_figure_manager(cls, num, *args, **kwargs):
"""Create a new figure manager instance.
"""
# This import needs to happen here due to circular imports.
from matplotlib.figure import Figure
fig_cls = kwargs.pop('FigureClass', Figure)
fig = fig_cls(*args, **kwargs)
return cls.new_figure_manager_given_figure(num, fig)

@classmethod
def new_figure_manager_given_figure(cls, num, figure):
"""Create a new figure manager instance for the given figure.
"""
canvas = cls.FigureCanvas(figure)
manager = cls.FigureManager(canvas, num)
return manager

@classmethod
def draw_if_interactive(cls):
if cls.trigger_manager_draw is not None and is_interactive():
manager = Gcf.get_active()
if manager:
cls.trigger_manager_draw(manager)

@classmethod
def show(cls, block=None):
"""Show all figures.

`show` blocks by calling `mainloop` if *block* is ``True``, or if it
is ``None`` and 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:
# Emits a warning if the backend is non-interactive.
manager.canvas.figure.show()
if cls.mainloop is None:
return
if block is None:
# Hack: Are we in IPython's pylab mode?
from matplotlib import pyplot
try:
# IPython versions >= 0.10 tack the _needmain attribute onto
# pyplot.show, and always set it to False, when in %pylab mode.
ipython_pylab = not pyplot.show._needmain
except AttributeError:
ipython_pylab = False
block = not ipython_pylab and not is_interactive()
# TODO: The above is a hack to get the WebAgg backend working with
# ipython's `%pylab` mode until proper integration is implemented.
if get_backend() == "WebAgg":
block = True
if block:
cls.mainloop()

# This method is the one actually exporting the required methods.

@staticmethod
def export(cls):
for name in ["backend_version",
"FigureCanvas",
"FigureManager",
"new_figure_manager",
"new_figure_manager_given_figure",
"draw_if_interactive",
"show"]:
setattr(sys.modules[cls.__module__], name, getattr(cls, name))

# For back-compatibility, generate a shim `Show` class.

class Show(ShowBase):
def mainloop(self):
return cls.mainloop()

setattr(sys.modules[cls.__module__], "Show", Show)
return cls


class ShowBase(_Backend):
"""
Simple base class to generate a show() callable in backends.

Subclass must override mainloop() method.
"""

def __call__(self, block=None):
return self.show(block=block)
76 changes: 24 additions & 52 deletions lib/matplotlib/backends/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import inspect
import importlib
import logging
import traceback
import warnings

import matplotlib
from matplotlib.backend_bases import _Backend

_log = logging.getLogger(__name__)

Expand All @@ -15,10 +15,11 @@


def pylab_setup(name=None):
'''return new_figure_manager, draw_if_interactive and show for pyplot
"""
Return new_figure_manager, draw_if_interactive and show for pyplot.

This provides the backend-specific functions that are used by
pyplot to abstract away the difference between interactive backends.
This provides the backend-specific functions that are used by pyplot to
abstract away the difference between backends.

Parameters
----------
Expand All @@ -39,54 +40,25 @@ def pylab_setup(name=None):

show : function
Show (and possibly block) any unshown figures.

'''
# Import the requested backend into a generic module object
"""
# Import the requested backend into a generic module object.
if name is None:
# validates, to match all_backends
name = matplotlib.get_backend()
if name.startswith('module://'):
backend_name = name[9:]
else:
backend_name = 'backend_' + name
backend_name = backend_name.lower() # until we banish mixed case
backend_name = 'matplotlib.backends.%s' % backend_name.lower()

# the last argument is specifies whether to use absolute or relative
# imports. 0 means only perform absolute imports.
backend_mod = __import__(backend_name, globals(), locals(),
[backend_name], 0)

# Things we pull in from all backends
new_figure_manager = backend_mod.new_figure_manager

# image backends like pdf, agg or svg do not need to do anything
# for "show" or "draw_if_interactive", so if they are not defined
# by the backend, just do nothing
def do_nothing_show(*args, **kwargs):
frame = inspect.currentframe()
fname = frame.f_back.f_code.co_filename
if fname in ('<stdin>', '<ipython console>'):
warnings.warn("""
Your currently selected backend, '%s' does not support show().
Please select a GUI backend in your matplotlibrc file ('%s')
or with matplotlib.use()""" %
(name, matplotlib.matplotlib_fname()), stacklevel=2)

def do_nothing(*args, **kwargs):
pass

backend_version = getattr(backend_mod, 'backend_version', 'unknown')

show = getattr(backend_mod, 'show', do_nothing_show)

draw_if_interactive = getattr(backend_mod, 'draw_if_interactive',
do_nothing)

_log.debug('backend %s version %s', name, backend_version)

# need to keep a global reference to the backend for compatibility
# reasons. See https://github.com/matplotlib/matplotlib/issues/6092
backend_name = (name[9:] if name.startswith("module://")
else "matplotlib.backends.backend_{}".format(name.lower()))
backend_mod = importlib.import_module(backend_name)
# Create a local Backend class whose body corresponds to the contents of
# the backend module. This allows the Backend class to fill in the missing
# methods through inheritance.
Backend = type("Backend", (_Backend,), vars(backend_mod))
Copy link
Member

Choose a reason for hiding this comment

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

So we create the class, use the inheritance to fill in the missing functions and then return class level functions (which 'forget' they were part of the class)?

That is quite clever.

To check, if a backend is already using @_Backend.export then there should be nothing extra we need to provide?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Indeed, backends that were using @_Backend.export don't really need that anymore (the contents of the local _Backend class could just be dumped at the module level (or alternatively they could just do without the temporary creation of a new Backend class))... except for one thing: the _Backend class still allows inheritance between backends, so that e.g. _BackendQt5Agg can inherit from _BackendQt5.


# Need to keep a global reference to the backend for compatibility reasons.
# See https://github.com/matplotlib/matplotlib/issues/6092
global backend
backend = name
return backend_mod, new_figure_manager, draw_if_interactive, show

_log.debug('backend %s version %s', name, Backend.backend_version)
return (backend_mod,
Backend.new_figure_manager,
Backend.draw_if_interactive,
Backend.show)
4 changes: 1 addition & 3 deletions lib/matplotlib/backends/backend_pdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -2575,11 +2575,9 @@ def print_pdf(self, filename, *,
file.close()


class FigureManagerPdf(FigureManagerBase):
pass
FigureManagerPdf = FigureManagerBase


@_Backend.export
class _BackendPdf(_Backend):
FigureCanvas = FigureCanvasPdf
FigureManager = FigureManagerPdf
4 changes: 1 addition & 3 deletions lib/matplotlib/backends/backend_ps.py
Original file line number Diff line number Diff line change
Expand Up @@ -1695,8 +1695,7 @@ def pstoeps(tmpfile, bbox=None, rotated=False):
shutil.move(epsfile, tmpfile)


class FigureManagerPS(FigureManagerBase):
pass
FigureManagerPS = FigureManagerBase
Copy link
Member

Choose a reason for hiding this comment

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

There is a very subtle API change in here if people are looking at __name__, __module__, or __file__ of these classes. I am not sure if that is worth an API change note or not.

Copy link
Contributor Author

@anntzer anntzer May 26, 2018

Choose a reason for hiding this comment

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

I added an API note. The real intent here is the following:

I think the "correct" way to refer to a backend's FigureCanvas, FigureManager, etc. class is under the unsuffixed names (FigureCanvas instead of FigureCanvasPS, etc.) The reason is that 1) these unsuffixed names need to be defined anyways for the backend machinery to pick them up, and 2) some of these classes (FigureManager, Toolbar, though not FigureCanvas of course) can be shared between multiple backends (for example, the xxxcairo backends and mplcairo just reuses the FigureManager classes for each of the GUI toolkits).

So sure, I can define (e.g.) FigureManagerQt5 and FigureManagerQt5Agg and FigureManagerQt5Cairo and FigureManagerMplCairoQt to all be aliases (or trivial subclasses) of one another, but that's just muddying the picture IMO; promoting the use of the same FigureManager name everywhere seems clearer.



# The following Python dictionary psDefs contains the entries for the
Expand Down Expand Up @@ -1742,4 +1741,3 @@ class FigureManagerPS(FigureManagerBase):
@_Backend.export
class _BackendPS(_Backend):
FigureCanvas = FigureCanvasPS
FigureManager = FigureManagerPS
5 changes: 2 additions & 3 deletions lib/matplotlib/backends/backend_svg.py
Original file line number Diff line number Diff line change
Expand Up @@ -1213,8 +1213,8 @@ def _print_svg(
def get_default_filetype(self):
return 'svg'

class FigureManagerSVG(FigureManagerBase):
pass

FigureManagerSVG = FigureManagerBase


svgProlog = """\
Expand All @@ -1228,4 +1228,3 @@ class FigureManagerSVG(FigureManagerBase):
@_Backend.export
class _BackendSVG(_Backend):
FigureCanvas = FigureCanvasSVG
FigureManager = FigureManagerSVG