From 617c5d6899598e11d2566f3efbc19ee8a4bbbff2 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Thu, 24 May 2018 22:52:02 -0700 Subject: [PATCH] Revival of the backend loading refactor. --- doc/api/next_api_changes/2018-05-25-AL.rst | 7 + lib/matplotlib/backend_bases.py | 234 +++++++++++---------- lib/matplotlib/backends/__init__.py | 76 +++---- lib/matplotlib/backends/backend_pdf.py | 4 +- lib/matplotlib/backends/backend_ps.py | 4 +- lib/matplotlib/backends/backend_svg.py | 5 +- 6 files changed, 154 insertions(+), 176 deletions(-) create mode 100644 doc/api/next_api_changes/2018-05-25-AL.rst diff --git a/doc/api/next_api_changes/2018-05-25-AL.rst b/doc/api/next_api_changes/2018-05-25-AL.rst new file mode 100644 index 000000000000..cc461c69c61a --- /dev/null +++ b/doc/api/next_api_changes/2018-05-25-AL.rst @@ -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`. diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 61b2e3b79333..d828a7b0a16b 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -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. @@ -3328,3 +3213,122 @@ def set_message(self, s): Message text """ pass + + +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) diff --git a/lib/matplotlib/backends/__init__.py b/lib/matplotlib/backends/__init__.py index 74a866365e4b..e506d3100927 100644 --- a/lib/matplotlib/backends/__init__.py +++ b/lib/matplotlib/backends/__init__.py @@ -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__) @@ -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 ---------- @@ -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 ('', ''): - 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)) + + # 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) diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index b2973117e0c5..43d03aae7c8d 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -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 diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index 62699400cac8..3209acb4a59b 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -1695,8 +1695,7 @@ def pstoeps(tmpfile, bbox=None, rotated=False): shutil.move(epsfile, tmpfile) -class FigureManagerPS(FigureManagerBase): - pass +FigureManagerPS = FigureManagerBase # The following Python dictionary psDefs contains the entries for the @@ -1742,4 +1741,3 @@ class FigureManagerPS(FigureManagerBase): @_Backend.export class _BackendPS(_Backend): FigureCanvas = FigureCanvasPS - FigureManager = FigureManagerPS diff --git a/lib/matplotlib/backends/backend_svg.py b/lib/matplotlib/backends/backend_svg.py index 06ea019b5fce..17b913fbd4b7 100644 --- a/lib/matplotlib/backends/backend_svg.py +++ b/lib/matplotlib/backends/backend_svg.py @@ -1213,8 +1213,8 @@ def _print_svg( def get_default_filetype(self): return 'svg' -class FigureManagerSVG(FigureManagerBase): - pass + +FigureManagerSVG = FigureManagerBase svgProlog = """\ @@ -1228,4 +1228,3 @@ class FigureManagerSVG(FigureManagerBase): @_Backend.export class _BackendSVG(_Backend): FigureCanvas = FigureCanvasSVG - FigureManager = FigureManagerSVG