From 181cb7001032d956d5ca251241f9f9a2a8df025c Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Mon, 23 Oct 2017 13:00:40 -0700 Subject: [PATCH 1/9] Rework backend loading. This is preliminary work towards fixing the runtime detection of default backend problem (the first step being to make it easier to check whether a backend *can* be loaded). No publically visible API is changed. Backend loading now defines default versions of `backend_version`, `draw_if_interactive`, and `show` using the same inheritance strategy as builtin backends do. For non-interactive backends (which don't override `mainloop`), restore the default implementation of `show()` that prints a warning when run from a console (the backend refactor accidentally removed this error message as it provided a silent default `show()`). The `_Backend` class had to be moved to the end of the module as `FigureManagerBase` needs to be defined first. The `ShowBase` class had to be moved even after that as it depends on the `_Backend` class. --- lib/matplotlib/backend_bases.py | 241 +++++++++++++------------ lib/matplotlib/backends/__init__.py | 72 +++----- lib/matplotlib/backends/backend_pdf.py | 4 +- lib/matplotlib/backends/backend_ps.py | 4 +- lib/matplotlib/backends/backend_svg.py | 5 +- 5 files changed, 158 insertions(+), 168 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 6625ac7dc14e..c50291b7f7e6 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -41,6 +41,7 @@ from contextlib import contextmanager from functools import partial import importlib +import inspect import io import os import sys @@ -126,120 +127,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. - """ - if cls.mainloop is None: - return - managers = Gcf.get_all_fig_managers() - if not managers: - return - for manager in managers: - manager.show() - 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. @@ -3366,3 +3253,129 @@ 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. + + # May be overridden by the subclass. + backend_version = "unknown" + # The `FigureCanvas` class must be overridden. + 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. + """ + if cls.mainloop is None: + frame = inspect.currentframe() + while frame: + if frame.f_code.co_filename in [ + "", ""]: + warnings.warn("""\ +Your currently selected backend does not support show(). +Please select a GUI backend in your matplotlibrc file ('{}') +or with matplotlib.use()""".format(matplotlib.matplotlib_fname())) + break + else: + frame = frame.f_back + return + managers = Gcf.get_all_fig_managers() + if not managers: + return + for manager in managers: + manager.show() + 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 f74eabb95cbc..408fdafcd812 100644 --- a/lib/matplotlib/backends/__init__.py +++ b/lib/matplotlib/backends/__init__.py @@ -3,11 +3,13 @@ import six -import matplotlib -import inspect -import traceback -import warnings +import importlib import logging +import traceback + +import matplotlib +from matplotlib.backend_bases import _Backend + _log = logging.getLogger(__name__) @@ -47,50 +49,30 @@ def pylab_setup(name=None): ''' # 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())) - - 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.info('backend %s version %s' % (name, backend_version)) + backend_name = (name[9:] if name.startswith("module://") + else "matplotlib.backends.backend_{}".format(name.lower())) + + backend_mod = importlib.import_module(backend_name) + Backend = type(str("Backend"), (_Backend,), vars(backend_mod)) + _log.info('backend %s version %s', name, Backend.backend_version) # 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 + + # We want to get functions out of a class namespace and call them *without + # the first argument being an instance of the class*. This works directly + # on Py3. On Py2, we need to remove the check that the first argument be + # an instance of the class. The only relevant case is if `.im_self` is + # None, in which case we need to use `.im_func` (if we have a bound method + # (e.g. a classmethod), everything is fine). + def _dont_check_first_arg(func): + return (func.im_func if getattr(func, "im_self", 0) is None + else func) + + return (backend_mod, + _dont_check_first_arg(Backend.new_figure_manager), + _dont_check_first_arg(Backend.draw_if_interactive), + _dont_check_first_arg(Backend.show)) diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index 4e2669b5b861..dc9b67c7a90a 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -2594,11 +2594,9 @@ def print_pdf(self, filename, **kwargs): 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 0c8281b899bc..e56afd4b44c6 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -1723,8 +1723,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 @@ -1770,4 +1769,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 85d43f65e645..5a194a11611e 100644 --- a/lib/matplotlib/backends/backend_svg.py +++ b/lib/matplotlib/backends/backend_svg.py @@ -1252,8 +1252,8 @@ def _print_svg(self, filename, svgwriter, **kwargs): def get_default_filetype(self): return 'svg' -class FigureManagerSVG(FigureManagerBase): - pass + +FigureManagerSVG = FigureManagerBase svgProlog = """\ @@ -1267,4 +1267,3 @@ class FigureManagerSVG(FigureManagerBase): @_Backend.export class _BackendSVG(_Backend): FigureCanvas = FigureCanvasSVG - FigureManager = FigureManagerSVG From 5013d9204dd7827140fcac6352beaec4f2e9da4e Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Tue, 24 Oct 2017 13:00:38 -0700 Subject: [PATCH 2/9] Backend switching "just works". The matplotlib.backends.backend attribute has been removed as it is too difficult to fully keep in sync (especially during startup, when the backend may not have been selected yet). Instead, end users should consult ``rcParams["backend"]``, which may hold either a single value (if pyplot has been imported) or a list of candidate values (if pyplot has not been imported yet). If the latter is not acceptable, import pyplot first to force backend resolution. --- lib/matplotlib/__init__.py | 80 +++-------------------- lib/matplotlib/backend_bases.py | 16 +++-- lib/matplotlib/backends/__init__.py | 51 +++++++++++++-- lib/matplotlib/backends/backend_gdk.py | 1 + lib/matplotlib/backends/backend_gtk.py | 1 + lib/matplotlib/backends/backend_gtk3.py | 1 + lib/matplotlib/backends/backend_macosx.py | 1 + lib/matplotlib/backends/backend_qt4.py | 2 +- lib/matplotlib/backends/backend_qt5.py | 1 + lib/matplotlib/backends/backend_tkagg.py | 1 + lib/matplotlib/backends/backend_wx.py | 1 + lib/matplotlib/pyplot.py | 64 +++++++----------- 12 files changed, 95 insertions(+), 125 deletions(-) diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index 8a7861c06bc2..51280f7600c6 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -131,7 +131,6 @@ import numpy from six.moves.urllib.request import urlopen -from six.moves import reload_module as reload # Get the version from the _version.py versioneer file. For a git checkout, # this is computed based on the number of commits since the last tag. @@ -1134,6 +1133,8 @@ def rc_params_from_file(fname, fail_on_error=False, use_default_template=True): # this is the instance used by the matplotlib classes rcParams = rc_params() +if "MPLBACKEND" in os.environ: + rcParams["backend"] = os.environ["MPLBACKEND"] if rcParams['examples.directory']: # paths that are intended to be relative to matplotlib_fname() @@ -1311,80 +1312,19 @@ def rc_context(rc=None, fname=None): dict.update(rcParams, orig) -_use_error_msg = """ -This call to matplotlib.use() has no effect because the backend has already -been chosen; matplotlib.use() must be called *before* pylab, matplotlib.pyplot, -or matplotlib.backends is imported for the first time. - -The backend was *originally* set to {backend!r} by the following code: -{tb} -""" - - +# Could be entirely replaced by pyplot.switch_backends. def use(arg, warn=True, force=False): """ - Set the matplotlib backend to one of the known backends. + Set the Matplotlib backend. - The argument is case-insensitive. *warn* specifies whether a - warning should be issued if a backend has already been set up. - *force* is an **experimental** flag that tells matplotlib to - attempt to initialize a new backend by reloading the backend - module. - - .. note:: - - This function must be called *before* importing pyplot for - the first time; or, if you are not using pyplot, it must be called - before importing matplotlib.backends. If warn is True, a warning - is issued if you try and call this after pylab or pyplot have been - loaded. In certain black magic use cases, e.g. - :func:`pyplot.switch_backend`, we are doing the reloading necessary to - make the backend switch work (in some cases, e.g., pure image - backends) so one can set warn=False to suppress the warnings. - - To find out which backend is currently set, see - :func:`matplotlib.get_backend`. + The argument is case-insensitive. Switching to an interactive backend is + only safe if no event loop for another interactive backend has started. + Switching to and from non-interactive backends is safe. + To find out which backend is currently set, see `matplotlib.get_backend`. """ - # Lets determine the proper backend name first - if arg.startswith('module://'): - name = arg - else: - # Lowercase only non-module backend names (modules are case-sensitive) - arg = arg.lower() - name = validate_backend(arg) - - # Check if we've already set up a backend - if 'matplotlib.backends' in sys.modules: - # Warn only if called with a different name - if (rcParams['backend'] != name) and warn: - import matplotlib.backends - warnings.warn( - _use_error_msg.format( - backend=rcParams['backend'], - tb=matplotlib.backends._backend_loading_tb), - stacklevel=2) - - # Unless we've been told to force it, just return - if not force: - return - need_reload = True - else: - need_reload = False - - # Store the backend name - rcParams['backend'] = name - - # If needed we reload here because a lot of setup code is triggered on - # module import. See backends/__init__.py for more detail. - if need_reload: - reload(sys.modules['matplotlib.backends']) - - -try: - use(os.environ['MPLBACKEND']) -except KeyError: - pass + import matplotlib.pyplot + pyplot.switch_backend(arg) def get_backend(): diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index c50291b7f7e6..f023c36ca9d0 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -2538,6 +2538,7 @@ class FigureManagerBase(object): figure.canvas.mpl_disconnect( figure.canvas.manager.key_press_handler_id) """ + def __init__(self, canvas, num): self.canvas = canvas canvas.manager = self # store a pointer to parent @@ -3262,6 +3263,9 @@ class _Backend(object): # class FooBackend(_Backend): # # override the attributes and methods documented below. + # Set to one of {"qt5", "qt4", "gtk3", "gtk2", "tk"} if an event loop is + # required, or None otherwise. + required_event_loop = None # May be overridden by the subclass. backend_version = "unknown" # The `FigureCanvas` class must be overridden. @@ -3351,13 +3355,11 @@ def show(cls, block=None): @staticmethod def export(cls): - for name in ["backend_version", - "FigureCanvas", - "FigureManager", - "new_figure_manager", - "new_figure_manager_given_figure", - "draw_if_interactive", - "show"]: + for name in [ + "required_event_loop", "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. diff --git a/lib/matplotlib/backends/__init__.py b/lib/matplotlib/backends/__init__.py index 408fdafcd812..eff90e9c779f 100644 --- a/lib/matplotlib/backends/__init__.py +++ b/lib/matplotlib/backends/__init__.py @@ -5,6 +5,7 @@ import importlib import logging +import sys import traceback import matplotlib @@ -13,11 +14,36 @@ _log = logging.getLogger(__name__) -backend = matplotlib.get_backend() -_backend_loading_tb = "".join( - line for line in traceback.format_stack() - # Filter out line noise from importlib line. - if not line.startswith(' File " Date: Wed, 25 Oct 2017 01:22:42 -0700 Subject: [PATCH 3/9] Setting multiple candidate backends in rcParams. It is now possible to set `rcParams["backend"]` to a *list* of candidate backends. If `.pyplot` has already been imported, Matplotlib will try to load each candidate backend in the given order until one of them can be loaded successfully. `rcParams["backend"]` will then be set to the value of the successfully loaded backend. (If `.pyplot` has already been imported and `rcParams["backend"]` is set to a single value, then the backend will likewise be updated.) If `.pyplot` has not been imported yet, then `rcParams["backend"]` will maintain the value as a list, and the loading attempt will occur when `.pyplot` is imported. If you rely on `rcParams["backend"]` (or its synonym, `matplotlib.get_backend()` always being a string, import `.pyplot` to trigger backend resolution. `matplotlib.use`, `pyplot.switch_backends`, and `matplotlib.backends.pylab_setup` have likewise gained the ability to accept a list of candidate backends. Note, however, that the first two functions have become redundant with directly setting `rcParams["backend"]`. --- .../20171025-AL-rcParams-backend.rst | 23 ++++ lib/matplotlib/__init__.py | 9 +- lib/matplotlib/backends/__init__.py | 23 +++- lib/matplotlib/pyplot.py | 129 +++++++++--------- lib/matplotlib/rcsetup.py | 23 ++-- 5 files changed, 131 insertions(+), 76 deletions(-) create mode 100644 doc/api/api_changes/20171025-AL-rcParams-backend.rst diff --git a/doc/api/api_changes/20171025-AL-rcParams-backend.rst b/doc/api/api_changes/20171025-AL-rcParams-backend.rst new file mode 100644 index 000000000000..a9a6f151f927 --- /dev/null +++ b/doc/api/api_changes/20171025-AL-rcParams-backend.rst @@ -0,0 +1,23 @@ +Testing multiple candidate backends in rcParams +``````````````````````````````````````````````` + +It is now possible to set ``rcParams["backend"]`` to a *list* of candidate +backends. + +If `.pyplot` has already been imported, Matplotlib will try to load each +candidate backend in the given order until one of them can be loaded +successfully. ``rcParams["backend"]`` will then be set to the value of the +successfully loaded backend. (If `.pyplot` has already been imported and +``rcParams["backend"]`` is set to a single value, then the backend will +likewise be updated.) + +If `.pyplot` has not been imported yet, then ``rcParams["backend"]`` will +maintain the value as a list, and the loading attempt will occur when `.pyplot` +is imported. If you rely on ``rcParams["backend"]`` (or its synonym, +``matplotlib.get_backend()`` always being a string, import `.pyplot` to trigger +backend resolution. + +`matplotlib.use`, `pyplot.switch_backends`, and +`matplotlib.backends.pylab_setup` have likewise gained the ability to accept a +list of candidate backends. Note, however, that the first two functions have +become redundant with directly setting ``rcParams["backend"]``. diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index 51280f7600c6..9a64bb29d2da 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -1322,9 +1322,14 @@ def use(arg, warn=True, force=False): Switching to and from non-interactive backends is safe. To find out which backend is currently set, see `matplotlib.get_backend`. + + Parameters + ---------- + arg : str or List[str] + The name of the backend to use. If a list of backends, they will be + tried in order until one successfully loads. """ - import matplotlib.pyplot - pyplot.switch_backend(arg) + rcParams["backend"] = arg def get_backend(): diff --git a/lib/matplotlib/backends/__init__.py b/lib/matplotlib/backends/__init__.py index eff90e9c779f..b2ca86663a4d 100644 --- a/lib/matplotlib/backends/__init__.py +++ b/lib/matplotlib/backends/__init__.py @@ -9,6 +9,7 @@ import traceback import matplotlib +from matplotlib import rcParams from matplotlib.backend_bases import _Backend @@ -54,9 +55,10 @@ def pylab_setup(name=None): Parameters ---------- - name : str, optional - The name of the backend to use. If `None`, falls back to - ``matplotlib.get_backend()`` (which return ``rcParams['backend']``) + name : str or List[str], optional + The name of the backend to use. If a list of backends, they will be + tried in order until one successfully loads. If ``None``, use + ``rcParams['backend']``. Returns ------- @@ -79,9 +81,18 @@ def pylab_setup(name=None): already started, or if a third-party backend fails to import. ''' - # Import the requested backend into a generic module object if name is None: - name = matplotlib.get_backend() + name = matplotlib.rcParams["backend"] + + if not isinstance(name, six.string_types): + for n in name: + try: + return pylab_setup(n) + except ImportError: + pass + else: + raise ValueError("No suitable backend among {}".format(name)) + backend_name = (name[9:] if name.startswith("module://") else "matplotlib.backends.backend_{}".format(name.lower())) @@ -98,6 +109,8 @@ def pylab_setup(name=None): "the {!r} event loop is currently running".format( name, required_event_loop, current_event_loop)) + rcParams["backend"] = name + # need to keep a global reference to the backend for compatibility # reasons. See https://github.com/matplotlib/matplotlib/issues/6092 global backend diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index c52210f9d8bb..1ed92117048a 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -70,7 +70,53 @@ MaxNLocator from matplotlib.backends import pylab_setup + +def close(*args): + """ + Close a figure window. + + ``close()`` by itself closes the current figure + + ``close(fig)`` closes the `~.Figure` instance *fig* + + ``close(num)`` closes the figure number *num* + + ``close(name)`` where *name* is a string, closes figure with that label + + ``close('all')`` closes all the figure windows + """ + + if len(args) == 0: + figManager = _pylab_helpers.Gcf.get_active() + if figManager is None: + return + else: + _pylab_helpers.Gcf.destroy(figManager.num) + elif len(args) == 1: + arg = args[0] + if arg == 'all': + _pylab_helpers.Gcf.destroy_all() + elif isinstance(arg, six.integer_types): + _pylab_helpers.Gcf.destroy(arg) + elif hasattr(arg, 'int'): + # if we are dealing with a type UUID, we + # can use its integer representation + _pylab_helpers.Gcf.destroy(arg.int) + elif isinstance(arg, six.string_types): + allLabels = get_figlabels() + if arg in allLabels: + num = get_fignums()[allLabels.index(arg)] + _pylab_helpers.Gcf.destroy(num) + elif isinstance(arg, Figure): + _pylab_helpers.Gcf.destroy_fig(arg) + else: + raise TypeError('Unrecognized argument type %s to close' % type(arg)) + else: + raise TypeError('close takes 0 or 1 arguments') + + ## Backend detection ## + def _backend_selection(): """ If rcParams['backend_fallback'] is true, check to see if the current backend is compatible with the current running event @@ -94,9 +140,29 @@ def _backend_selection(): _backend_selection() -## Global ## +def switch_backend(newbackend): + """ + Close all open figures and set the Matplotlib backend. + + The argument is case-insensitive. Switching to an interactive backend is + only safe if no event loop for another interactive backend has started. + Switching to and from non-interactive backends is safe. + + Parameters + ---------- + newbackend : str or List[str] + The name of the backend to use. If a list of backends, they will be + tried in order until one successfully loads. + """ + close("all") + global _backend_mod, new_figure_manager, draw_if_interactive, _show + _backend_mod, new_figure_manager, draw_if_interactive, _show = \ + backends.pylab_setup(newbackend) -_backend_mod, new_figure_manager, draw_if_interactive, _show = pylab_setup() +switch_backend(rcParams["backend"]) + + +## Global ## _IP_REGISTERED = None _INSTALL_FIG_OBSERVER = False @@ -198,21 +264,6 @@ def findobj(o=None, match=None, include_self=True): return o.findobj(match, include_self=include_self) -def switch_backend(newbackend): - """ - Close all open figures and set the Matplotlib backend. - - The argument is case-insensitive. Switching to an interactive backend is - only safe if no event loop for another interactive backend has started. - Switching to and from non-interactive backends is safe. - """ - close("all") - global _backend_mod, new_figure_manager, draw_if_interactive, _show - rcParams["backend"] = newbackend - _backend_mod, new_figure_manager, draw_if_interactive, _show = \ - backends.pylab_setup() - - def show(*args, **kw): """ Display a figure. @@ -615,50 +666,6 @@ def disconnect(cid): return get_current_fig_manager().canvas.mpl_disconnect(cid) -def close(*args): - """ - Close a figure window. - - ``close()`` by itself closes the current figure - - ``close(fig)`` closes the `~.Figure` instance *fig* - - ``close(num)`` closes the figure number *num* - - ``close(name)`` where *name* is a string, closes figure with that label - - ``close('all')`` closes all the figure windows - """ - - if len(args) == 0: - figManager = _pylab_helpers.Gcf.get_active() - if figManager is None: - return - else: - _pylab_helpers.Gcf.destroy(figManager.num) - elif len(args) == 1: - arg = args[0] - if arg == 'all': - _pylab_helpers.Gcf.destroy_all() - elif isinstance(arg, six.integer_types): - _pylab_helpers.Gcf.destroy(arg) - elif hasattr(arg, 'int'): - # if we are dealing with a type UUID, we - # can use its integer representation - _pylab_helpers.Gcf.destroy(arg.int) - elif isinstance(arg, six.string_types): - allLabels = get_figlabels() - if arg in allLabels: - num = get_fignums()[allLabels.index(arg)] - _pylab_helpers.Gcf.destroy(num) - elif isinstance(arg, Figure): - _pylab_helpers.Gcf.destroy_fig(arg) - else: - raise TypeError('Unrecognized argument type %s to close' % type(arg)) - else: - raise TypeError('close takes 0 or 1 arguments') - - def clf(): """ Clear the current figure. diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index eafc8d4eecf7..b7bfe2ec3011 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -21,8 +21,9 @@ from functools import reduce import operator import os -import warnings import re +import sys +import warnings from matplotlib import cbook from matplotlib.cbook import mplDeprecation, deprecated, ls_mapper @@ -255,15 +256,21 @@ def validate_fonttype(s): return fonttype -_validate_standard_backends = ValidateInStrings( - 'backend', all_backends, ignorecase=True) - - def validate_backend(s): - if s.startswith('module://'): - return s + candidates = _listify_validator( + lambda s: + s if s.startswith("module://") + else ValidateInStrings('backend', all_backends, ignorecase=True)(s))(s) + if len(candidates) == 1: + return candidates[0] else: - return _validate_standard_backends(s) + pyplot = sys.modules.get("matplotlib.pyplot") + if pyplot: + pyplot.switch_backend(candidates) # Actually resolves the backend. + from matplotlib import rcParams + return rcParams["backend"] + else: + return candidates validate_qt4 = ValidateInStrings('backend.qt4', ['PyQt4', 'PySide', 'PyQt4v2']) From 75856e36cdb137225216c06b6d4b687b0276b1e8 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Wed, 25 Oct 2017 01:54:30 -0700 Subject: [PATCH 4/9] Don't bother with checking backends at setup time. We can just default to trying all backends in order until one loads. Only backends actually requiring a compiled component need to be handled in setupext.py now. Move the `mpl-data/*.glade` entry into a recursive glob for the Matplotlib SetupPackage (as there are no more entries for Gtk3). As a side benefit, we don't need any multiprocessing craziness in setupext.py anymore... --- .gitignore | 5 - lib/matplotlib/mpl-data/matplotlibrc | 1 + lib/matplotlib/rcsetup.py | 10 +- matplotlibrc.template | 5 +- setup.py | 35 +-- setupext.py | 379 +-------------------------- 6 files changed, 19 insertions(+), 416 deletions(-) create mode 100644 lib/matplotlib/mpl-data/matplotlibrc diff --git a/.gitignore b/.gitignore index 0473729069d6..e45e31cfc18a 100644 --- a/.gitignore +++ b/.gitignore @@ -50,11 +50,6 @@ ehthumbs.db Icon? Thumbs.db -# Things specific to this project # -################################### -lib/matplotlib/mpl-data/matplotlib.conf -lib/matplotlib/mpl-data/matplotlibrc - # Documentation generated files # ################################# # sphinx build directory diff --git a/lib/matplotlib/mpl-data/matplotlibrc b/lib/matplotlib/mpl-data/matplotlibrc new file mode 100644 index 000000000000..8b137891791f --- /dev/null +++ b/lib/matplotlib/mpl-data/matplotlibrc @@ -0,0 +1 @@ + diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index b7bfe2ec3011..bc3b84e89fc3 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -941,9 +941,13 @@ def _validate_linestyle(ls): # a map from key -> value, converter defaultParams = { - 'backend': ['Agg', validate_backend], # agg is certainly - # present - 'backend_fallback': [True, validate_bool], # agg is certainly present + 'backend': [["macosx", + "qt5agg", "qt4agg", + "gtk3agg", "gtk3cairo", "gtkagg", + "tkagg", + "wxagg", + "agg", "cairo"], validate_backend], + 'backend_fallback': [True, validate_bool], 'backend.qt4': ['PyQt4', validate_qt4], 'backend.qt5': ['PyQt5', validate_qt5], 'webagg.port': [8988, validate_int], diff --git a/matplotlibrc.template b/matplotlibrc.template index 514bee3e3755..574bb74a1e8c 100644 --- a/matplotlibrc.template +++ b/matplotlibrc.template @@ -36,9 +36,8 @@ # referring to the module name (which must be in the PYTHONPATH) as # 'module://my_backend'. # -# If you omit this parameter, it will always default to "Agg", which is a -# non-interactive backend. -backend : $TEMPLATE_BACKEND +# Try the backends in the given order until one successfully loads. +# backend : macosx, qt5agg, qt4agg, gtk3agg, gtk3cairo, gtkagg, tkagg, wxagg, agg, cairo # If you are using the Qt4Agg backend, you can choose here # to use the PyQt4 bindings or the newer PySide bindings to diff --git a/setup.py b/setup.py index fb2134d19856..7a69c1fe5698 100644 --- a/setup.py +++ b/setup.py @@ -75,20 +75,11 @@ setupext.Tests(), setupext.Toolkits_Tests(), 'Optional backend extensions', - # These backends are listed in order of preference, the first - # being the most preferred. The first one that looks like it will - # work will be selected as the default backend. setupext.BackendMacOSX(), - setupext.BackendQt5(), - setupext.BackendQt4(), - setupext.BackendGtk3Agg(), - setupext.BackendGtk3Cairo(), setupext.BackendGtkAgg(), setupext.BackendTkAgg(), - setupext.BackendWxAgg(), setupext.BackendGtk(), setupext.BackendAgg(), - setupext.BackendCairo(), setupext.Windowing(), 'Optional LaTeX dependencies', setupext.DviPng(), @@ -132,9 +123,7 @@ def run(self): cmdclass['test'] = NoopTestCommand cmdclass['build_ext'] = BuildExtraLibraries -# One doesn't normally see `if __name__ == '__main__'` blocks in a setup.py, -# however, this is needed on Windows to avoid creating infinite subprocesses -# when using multiprocessing. + if __name__ == '__main__': # These are distutils.setup parameters that the various packages add # things to. @@ -146,7 +135,6 @@ def run(self): package_dir = {'': 'lib'} install_requires = [] setup_requires = [] - default_backend = None # If the user just queries for information, don't bother figuring out which # packages to build or install. @@ -182,10 +170,6 @@ def run(self): required_failed.append(package) else: good_packages.append(package) - if (isinstance(package, setupext.OptionalBackendPackage) - and package.runtime_check() - and default_backend is None): - default_backend = package.name print_raw('') # Abort if any of the required packages can not be built. @@ -215,18 +199,6 @@ def run(self): install_requires.extend(package.get_install_requires()) setup_requires.extend(package.get_setup_requires()) - # Write the default matplotlibrc file - if default_backend is None: - default_backend = 'svg' - if setupext.options['backend']: - default_backend = setupext.options['backend'] - with open('matplotlibrc.template') as fd: - template = fd.read() - template = Template(template) - with open('lib/matplotlib/mpl-data/matplotlibrc', 'w') as fd: - fd.write( - template.safe_substitute(TEMPLATE_BACKEND=default_backend)) - # Build in verbose mode if requested if setupext.options['verbose']: for mod in ext_modules: @@ -237,10 +209,8 @@ def run(self): for mod in ext_modules: mod.finalize() - extra_args = {} - # Finally, pass this all along to distutils to do the heavy lifting. - distrib = setup( + setup( name="matplotlib", version=__version__, description="Python plotting package", @@ -274,5 +244,4 @@ def run(self): # check for zip safety. zip_safe=False, cmdclass=cmdclass, - **extra_args ) diff --git a/setupext.py b/setupext.py index 508f2b825d45..3ad56a1d20cd 100644 --- a/setupext.py +++ b/setupext.py @@ -7,7 +7,6 @@ from distutils.core import Extension import distutils.command.build_ext import glob -import multiprocessing import os import platform import re @@ -746,34 +745,13 @@ def get_py_modules(self): return ['pylab'] def get_package_data(self): - return { - 'matplotlib': - [ - 'mpl-data/fonts/afm/*.afm', - 'mpl-data/fonts/pdfcorefonts/*.afm', - 'mpl-data/fonts/pdfcorefonts/*.txt', - 'mpl-data/fonts/ttf/*.ttf', - 'mpl-data/fonts/ttf/LICENSE_STIX', - 'mpl-data/fonts/ttf/COPYRIGHT.TXT', - 'mpl-data/fonts/ttf/README.TXT', - 'mpl-data/fonts/ttf/RELEASENOTES.TXT', - 'mpl-data/images/*.xpm', - 'mpl-data/images/*.svg', - 'mpl-data/images/*.gif', - 'mpl-data/images/*.pdf', - 'mpl-data/images/*.png', - 'mpl-data/images/*.ppm', - 'mpl-data/example/*.npy', - 'mpl-data/matplotlibrc', - 'backends/web_backend/*.*', - 'backends/web_backend/js/*.*', - 'backends/web_backend/jquery/js/*.min.js', - 'backends/web_backend/jquery/css/themes/base/*.min.css', - 'backends/web_backend/jquery/css/themes/base/images/*', - 'backends/web_backend/css/*.*', - 'backends/Matplotlib.nib/*', - 'mpl-data/stylelib/*.mplstyle', - ]} + return {'matplotlib': [ + # Work around lack of rglob on Py2. + os.path.relpath(os.path.join(dirpath, filename), "lib/matplotlib") + for data_dir in ["lib/matplotlib/mpl-data", + "lib/matplotlib/backends/web_backend"] + for dirpath, _, filenames in os.walk(data_dir) + for filename in filenames]} class SampleData(OptionalPackage): @@ -1552,9 +1530,6 @@ def check_requirements(self): ".".join(str(x) for x in gtk.gtk_version), ".".join(str(x) for x in gtk.pygtk_version)) - def get_package_data(self): - return {'matplotlib': ['mpl-data/*.glade']} - def get_extension(self): sources = [ 'src/_backend_gdk.c' @@ -1644,178 +1619,6 @@ def get_extension(self): return ext -def backend_gtk3agg_internal_check(x): - try: - import gi - except ImportError: - return (False, "Requires pygobject to be installed.") - - try: - gi.require_version("Gtk", "3.0") - except ValueError: - return (False, "Requires gtk3 development files to be installed.") - except AttributeError: - return (False, "pygobject version too old.") - - try: - from gi.repository import Gtk, Gdk, GObject - except (ImportError, RuntimeError): - return (False, "Requires pygobject to be installed.") - - return (True, "version %s.%s.%s" % ( - Gtk.get_major_version(), - Gtk.get_micro_version(), - Gtk.get_minor_version())) - - -class BackendGtk3Agg(OptionalBackendPackage): - name = "gtk3agg" - - def check_requirements(self): - if 'TRAVIS' in os.environ: - raise CheckFailed("Can't build with Travis") - - # This check needs to be performed out-of-process, because - # importing gi and then importing regular old pygtk afterward - # segfaults the interpreter. - try: - p = multiprocessing.Pool() - except: - return "unknown (can not use multiprocessing to determine)" - try: - res = p.map_async(backend_gtk3agg_internal_check, [0]) - success, msg = res.get(timeout=10)[0] - except multiprocessing.TimeoutError: - p.terminate() - # No result returned. Probaly hanging, terminate the process. - success = False - raise CheckFailed("Check timed out") - except: - p.close() - # Some other error. - success = False - msg = "Could not determine" - raise - else: - p.close() - finally: - p.join() - - if success: - return msg - else: - raise CheckFailed(msg) - - def get_package_data(self): - return {'matplotlib': ['mpl-data/*.glade']} - - -def backend_gtk3cairo_internal_check(x): - try: - import cairocffi - except ImportError: - try: - import cairo - except ImportError: - return (False, "Requires cairocffi or pycairo to be installed.") - - try: - import gi - except ImportError: - return (False, "Requires pygobject to be installed.") - - try: - gi.require_version("Gtk", "3.0") - except ValueError: - return (False, "Requires gtk3 development files to be installed.") - except AttributeError: - return (False, "pygobject version too old.") - - try: - from gi.repository import Gtk, Gdk, GObject - except (RuntimeError, ImportError): - return (False, "Requires pygobject to be installed.") - - return (True, "version %s.%s.%s" % ( - Gtk.get_major_version(), - Gtk.get_micro_version(), - Gtk.get_minor_version())) - - -class BackendGtk3Cairo(OptionalBackendPackage): - name = "gtk3cairo" - - def check_requirements(self): - if 'TRAVIS' in os.environ: - raise CheckFailed("Can't build with Travis") - - # This check needs to be performed out-of-process, because - # importing gi and then importing regular old pygtk afterward - # segfaults the interpreter. - try: - p = multiprocessing.Pool() - except: - return "unknown (can not use multiprocessing to determine)" - try: - res = p.map_async(backend_gtk3cairo_internal_check, [0]) - success, msg = res.get(timeout=10)[0] - except multiprocessing.TimeoutError: - p.terminate() - # No result returned. Probaly hanging, terminate the process. - success = False - raise CheckFailed("Check timed out") - except: - p.close() - success = False - raise - else: - p.close() - finally: - p.join() - - if success: - return msg - else: - raise CheckFailed(msg) - - def get_package_data(self): - return {'matplotlib': ['mpl-data/*.glade']} - - -class BackendWxAgg(OptionalBackendPackage): - name = "wxagg" - - def check_requirements(self): - wxversioninstalled = True - try: - import wxversion - except ImportError: - wxversioninstalled = False - - if wxversioninstalled: - try: - _wx_ensure_failed = wxversion.AlreadyImportedError - except AttributeError: - _wx_ensure_failed = wxversion.VersionError - - try: - wxversion.ensureMinimal('2.9') - except _wx_ensure_failed: - pass - - try: - import wx - backend_version = wx.VERSION_STRING - except ImportError: - raise CheckFailed("requires wxPython") - - if not is_min_version(backend_version, "2.9"): - raise CheckFailed( - "Requires wxPython 2.9, found %s" % backend_version) - - return "version %s" % backend_version - - class BackendMacOSX(OptionalBackendPackage): name = 'macosx' @@ -1861,174 +1664,6 @@ def get_extension(self): return ext -class BackendQtBase(OptionalBackendPackage): - - def convert_qt_version(self, version): - version = '%x' % version - temp = [] - while len(version) > 0: - version, chunk = version[:-2], version[-2:] - temp.insert(0, str(int(chunk, 16))) - return '.'.join(temp) - - def check_requirements(self): - ''' - If PyQt4/PyQt5 is already imported, importing PyQt5/PyQt4 will fail - so we need to test in a subprocess (as for Gtk3). - ''' - try: - p = multiprocessing.Pool() - - except: - # Can't do multiprocessing, fall back to normal approach - # (this will fail if importing both PyQt4 and PyQt5). - try: - # Try in-process - msg = self.callback(self) - except RuntimeError: - raise CheckFailed( - "Could not import: are PyQt4 & PyQt5 both installed?") - - else: - # Multiprocessing OK - try: - res = p.map_async(self.callback, [self]) - msg = res.get(timeout=10)[0] - except multiprocessing.TimeoutError: - p.terminate() - # No result returned. Probaly hanging, terminate the process. - raise CheckFailed("Check timed out") - except: - # Some other error. - p.close() - raise - else: - # Clean exit - p.close() - finally: - # Tidy up multiprocessing - p.join() - - return msg - - -def backend_pyside_internal_check(self): - try: - from PySide import __version__ - from PySide import QtCore - except ImportError: - raise CheckFailed("PySide not found") - else: - return ("Qt: %s, PySide: %s" % - (QtCore.__version__, __version__)) - - -def backend_pyqt4_internal_check(self): - try: - from PyQt4 import QtCore - except ImportError: - raise CheckFailed("PyQt4 not found") - - try: - qt_version = QtCore.QT_VERSION - pyqt_version_str = QtCore.PYQT_VERSION_STR - except AttributeError: - raise CheckFailed('PyQt4 not correctly imported') - else: - return ("Qt: %s, PyQt: %s" % (self.convert_qt_version(qt_version), pyqt_version_str)) - - -def backend_qt4_internal_check(self): - successes = [] - failures = [] - try: - successes.append(backend_pyside_internal_check(self)) - except CheckFailed as e: - failures.append(str(e)) - - try: - successes.append(backend_pyqt4_internal_check(self)) - except CheckFailed as e: - failures.append(str(e)) - - if len(successes) == 0: - raise CheckFailed('; '.join(failures)) - return '; '.join(successes + failures) - - -class BackendQt4(BackendQtBase): - name = "qt4agg" - - def __init__(self, *args, **kwargs): - BackendQtBase.__init__(self, *args, **kwargs) - self.callback = backend_qt4_internal_check - -def backend_pyside2_internal_check(self): - try: - from PySide2 import __version__ - from PySide2 import QtCore - except ImportError: - raise CheckFailed("PySide2 not found") - else: - return ("Qt: %s, PySide2: %s" % - (QtCore.__version__, __version__)) - -def backend_pyqt5_internal_check(self): - try: - from PyQt5 import QtCore - except ImportError: - raise CheckFailed("PyQt5 not found") - - try: - qt_version = QtCore.QT_VERSION - pyqt_version_str = QtCore.PYQT_VERSION_STR - except AttributeError: - raise CheckFailed('PyQt5 not correctly imported') - else: - return ("Qt: %s, PyQt: %s" % (self.convert_qt_version(qt_version), pyqt_version_str)) - -def backend_qt5_internal_check(self): - successes = [] - failures = [] - try: - successes.append(backend_pyside2_internal_check(self)) - except CheckFailed as e: - failures.append(str(e)) - - try: - successes.append(backend_pyqt5_internal_check(self)) - except CheckFailed as e: - failures.append(str(e)) - - if len(successes) == 0: - raise CheckFailed('; '.join(failures)) - return '; '.join(successes + failures) - -class BackendQt5(BackendQtBase): - name = "qt5agg" - - def __init__(self, *args, **kwargs): - BackendQtBase.__init__(self, *args, **kwargs) - self.callback = backend_qt5_internal_check - - -class BackendCairo(OptionalBackendPackage): - name = "cairo" - - def check_requirements(self): - try: - import cairocffi - except ImportError: - try: - import cairo - except ImportError: - raise CheckFailed("cairocffi or pycairo not found") - else: - return "pycairo version %s" % cairo.version - else: - return "cairocffi version %s" % cairocffi.version - - class DviPng(SetupPackage): name = "dvipng" optional = True From 2cdcc05b4f9b00e99b1653117de801fcdc2062c6 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Thu, 16 Nov 2017 18:44:56 -0800 Subject: [PATCH 5/9] Fix backend detection in headless mode. --- lib/matplotlib/backends/__init__.py | 16 +++++++++++----- lib/matplotlib/backends/backend_gtk.py | 3 ++- lib/matplotlib/backends/backend_gtk3.py | 19 ++++++++++++------- lib/matplotlib/backends/backend_qt5.py | 7 ++----- lib/matplotlib/backends/qt_compat.py | 10 +++++----- src/_macosx.m | 8 ++++---- 6 files changed, 36 insertions(+), 27 deletions(-) diff --git a/lib/matplotlib/backends/__init__.py b/lib/matplotlib/backends/__init__.py index b2ca86663a4d..e7dee06f0c01 100644 --- a/lib/matplotlib/backends/__init__.py +++ b/lib/matplotlib/backends/__init__.py @@ -5,6 +5,7 @@ import importlib import logging +import os import sys import traceback @@ -17,12 +18,14 @@ def _get_current_event_loop(): - """Return the currently running event loop if any, or None. + """Return the currently running event loop if any, or "headless", or None. + + "headless" indicates that no event loop can be started. Returns ------- Optional[str] - A value in {"qt5", "qt4", "gtk3", "gtk2", "tk", None} + A value in {"qt5", "qt4", "gtk3", "gtk2", "tk", "headless", None} """ QtWidgets = (sys.modules.get("PyQt5.QtWidgets") or sys.modules.get("PySide2.QtWidgets")) @@ -45,6 +48,8 @@ def _get_current_event_loop(): and frame.f_code.co_name == "mainloop" for frame in sys._current_frames().values()): return "tk" + if sys.platform.startswith("linux") and not os.environ.get("DISPLAY"): + return "headless" def pylab_setup(name=None): @@ -87,9 +92,10 @@ def pylab_setup(name=None): if not isinstance(name, six.string_types): for n in name: try: + _log.info("Trying to load backend %s.", n) return pylab_setup(n) - except ImportError: - pass + except ImportError as exc: + _log.info("Loading backend %s failed: %s", n, exc) else: raise ValueError("No suitable backend among {}".format(name)) @@ -98,7 +104,7 @@ def pylab_setup(name=None): backend_mod = importlib.import_module(backend_name) Backend = type(str("Backend"), (_Backend,), vars(backend_mod)) - _log.info('backend %s version %s', name, Backend.backend_version) + _log.info("Loaded backend %s version %s.", name, Backend.backend_version) required_event_loop = Backend.required_event_loop current_event_loop = _get_current_event_loop() diff --git a/lib/matplotlib/backends/backend_gtk.py b/lib/matplotlib/backends/backend_gtk.py index 92b655c69333..827b5f51b9ae 100644 --- a/lib/matplotlib/backends/backend_gtk.py +++ b/lib/matplotlib/backends/backend_gtk.py @@ -17,7 +17,8 @@ import gobject import gtk; gdk = gtk.gdk import pango -except ImportError: +except (ImportError, AttributeError): + # AttributeError occurs when getting gtk.gdk if gi is already imported. raise ImportError("Gtk* backend requires pygtk to be installed.") pygtk_version_required = (2,4,0) diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 724a2cd2386d..ec68ba595fc4 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -28,13 +28,18 @@ # see http://groups.google.com/groups?q=screen+dpi+x11&hl=en&lr=&ie=UTF-8&oe=UTF-8&safe=off&selm=7077.26e81ad5%40swift.cs.tcd.ie&rnum=5 for some info about screen dpi PIXELS_PER_INCH = 96 -cursord = { - cursors.MOVE : Gdk.Cursor.new(Gdk.CursorType.FLEUR), - cursors.HAND : Gdk.Cursor.new(Gdk.CursorType.HAND2), - cursors.POINTER : Gdk.Cursor.new(Gdk.CursorType.LEFT_PTR), - cursors.SELECT_REGION : Gdk.Cursor.new(Gdk.CursorType.TCROSS), - cursors.WAIT : Gdk.Cursor.new(Gdk.CursorType.WATCH), - } +try: + cursord = { + cursors.MOVE : Gdk.Cursor.new(Gdk.CursorType.FLEUR), + cursors.HAND : Gdk.Cursor.new(Gdk.CursorType.HAND2), + cursors.POINTER : Gdk.Cursor.new(Gdk.CursorType.LEFT_PTR), + cursors.SELECT_REGION : Gdk.Cursor.new(Gdk.CursorType.TCROSS), + cursors.WAIT : Gdk.Cursor.new(Gdk.CursorType.WATCH), + } +except TypeError as exc: + # Happens when running headless. Convert to ImportError to cooperate with + # backend switching. + raise ImportError(exc) class TimerGTK3(TimerBase): diff --git a/lib/matplotlib/backends/backend_qt5.py b/lib/matplotlib/backends/backend_qt5.py index a71cb49f61b5..d1be44685c19 100644 --- a/lib/matplotlib/backends/backend_qt5.py +++ b/lib/matplotlib/backends/backend_qt5.py @@ -4,7 +4,6 @@ import functools import os -import re import signal import sys from six import unichr @@ -116,10 +115,8 @@ def _create_qApp(): is_x11_build = False else: is_x11_build = hasattr(QtGui, "QX11Info") - if is_x11_build: - display = os.environ.get('DISPLAY') - if display is None or not re.search(r':\d', display): - raise RuntimeError('Invalid DISPLAY variable') + if is_x11_build and not os.environ.get("DISPLAY"): + raise RuntimeError("No DISPLAY variable") qApp = QtWidgets.QApplication([b"matplotlib"]) qApp.lastWindowClosed.connect(qApp.quit) diff --git a/lib/matplotlib/backends/qt_compat.py b/lib/matplotlib/backends/qt_compat.py index 3b8d4ecf3478..c32dc367c0af 100644 --- a/lib/matplotlib/backends/qt_compat.py +++ b/lib/matplotlib/backends/qt_compat.py @@ -112,17 +112,17 @@ QT_API = QT_API_PYSIDE2 else: QT_API = QT_API_PYSIDE - cond = ("Could not import sip; falling back on PySide\n" - "in place of PyQt4 or PyQt5.\n") + cond = ("Could not import sip; falling back on PySide in place of " + "PyQt4 or PyQt5.") _log.info(cond) if _sip_imported: if QT_API == QT_API_PYQTv2: if QT_API_ENV == 'pyqt': cond = ("Found 'QT_API=pyqt' environment variable. " - "Setting PyQt4 API accordingly.\n") + "Setting PyQt4 API accordingly. ") else: - cond = "PyQt API v2 specified." + cond = "PyQt API v2 specified. " try: sip.setapi('QString', 2) except: @@ -201,7 +201,7 @@ def _getSaveFileName(*args, **kwargs): from PySide import QtCore, QtGui, __version__, __version_info__ except ImportError: raise ImportError( - "Matplotlib qt-based backends require an external PyQt4, PyQt5,\n" + "Matplotlib qt-based backends require an external PyQt4, PyQt5, " "PySide or PySide2 package to be installed, but it was not found.") if __version_info__ < (1, 0, 3): diff --git a/src/_macosx.m b/src/_macosx.m index 50556c017b49..6ac728ae57d1 100644 --- a/src/_macosx.m +++ b/src/_macosx.m @@ -3059,15 +3059,15 @@ static bool verify_framework(void) && GetCurrentProcess(&psn)==noErr && SetFrontProcess(&psn)==noErr) return true; #endif - PyErr_SetString(PyExc_RuntimeError, + PyErr_SetString(PyExc_ImportError, "Python is not installed as a framework. The Mac OS X backend will " "not be able to function correctly if Python is not installed as a " "framework. See the Python documentation for more information on " "installing Python as a framework on Mac OS X. Please either reinstall " "Python as a framework, or try one of the other backends. If you are " - "using (Ana)Conda please install python.app and replace the use of 'python' " - "with 'pythonw'. See 'Working with Matplotlib on OSX' " - "in the Matplotlib FAQ for more information."); + "using (Ana)Conda please install python.app and replace the use of " + "'python' with 'pythonw'. See 'Working with Matplotlib on OSX' in the " + "Matplotlib FAQ for more information."); return false; } From 4542ebf3c2bbb9768e8d13ab5734ef0aef14d788 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Thu, 16 Nov 2017 23:07:10 -0800 Subject: [PATCH 6/9] Various test fixes. --- lib/matplotlib/__init__.py | 17 ++++++++++------- lib/matplotlib/sphinxext/plot_directive.py | 14 +++----------- lib/matplotlib/testing/__init__.py | 14 +++++--------- lib/matplotlib/tests/test_rcparams.py | 4 +--- 4 files changed, 19 insertions(+), 30 deletions(-) diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index 9a64bb29d2da..ed38dfcf3c95 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -1312,7 +1312,6 @@ def rc_context(rc=None, fname=None): dict.update(rcParams, orig) -# Could be entirely replaced by pyplot.switch_backends. def use(arg, warn=True, force=False): """ Set the Matplotlib backend. @@ -1325,11 +1324,16 @@ def use(arg, warn=True, force=False): Parameters ---------- - arg : str or List[str] - The name of the backend to use. If a list of backends, they will be - tried in order until one successfully loads. + arg : str + The name of the backend to use. """ - rcParams["backend"] = arg + if not isinstance(arg, six.string_types): + # We want to keep 'use(...); rcdefaults()' working, which means that + # use(...) needs to force the default backend, and thus be a single + # string. + raise TypeError("matplotlib.use takes a single string as argument") + rcParams["backend"] = \ + rcParamsDefault["backend"] = rcParamsOrig["backend"] = arg def get_backend(): @@ -1453,8 +1457,7 @@ def test(verbosity=None, coverage=False, switch_backend_warn=True, retcode = pytest.main(args, **kwargs) finally: - if old_backend.lower() != 'agg': - use(old_backend, warn=switch_backend_warn) + rcParams['backend'] = old_backend if recursionlimit: sys.setrecursionlimit(old_recursionlimit) diff --git a/lib/matplotlib/sphinxext/plot_directive.py b/lib/matplotlib/sphinxext/plot_directive.py index d7f03b881d9f..0fc33c0a1c94 100644 --- a/lib/matplotlib/sphinxext/plot_directive.py +++ b/lib/matplotlib/sphinxext/plot_directive.py @@ -154,24 +154,16 @@ import sphinx sphinx_version = sphinx.__version__.split(".") -# The split is necessary for sphinx beta versions where the string is -# '6b1' +# The split is necessary for sphinx beta versions where the string is '6b1'. sphinx_version = tuple([int(re.split('[^0-9]', x)[0]) for x in sphinx_version[:2]]) import jinja2 # Sphinx dependency. import matplotlib +matplotlib.use("agg") import matplotlib.cbook as cbook -try: - with warnings.catch_warnings(record=True): - warnings.simplefilter("error", UserWarning) - matplotlib.use('Agg') -except UserWarning: - import matplotlib.pyplot as plt - plt.switch_backend("Agg") -else: - import matplotlib.pyplot as plt +import matplotlib.pyplot as plt from matplotlib import _pylab_helpers __version__ = 2 diff --git a/lib/matplotlib/testing/__init__.py b/lib/matplotlib/testing/__init__.py index 36a7403698f2..28505b9e7cea 100644 --- a/lib/matplotlib/testing/__init__.py +++ b/lib/matplotlib/testing/__init__.py @@ -2,6 +2,7 @@ unicode_literals) import functools +import locale import warnings import matplotlib @@ -38,9 +39,6 @@ def set_reproducibility_for_testing(): def setup(): # The baseline images are created in this locale, so we should use # it during all of the tests. - import locale - from matplotlib.backends import backend_agg, backend_pdf, backend_svg - try: locale.setlocale(locale.LC_ALL, str('en_US.UTF-8')) except locale.Error: @@ -49,14 +47,12 @@ def setup(): except locale.Error: warnings.warn( "Could not set locale to English/United States. " - "Some date-related tests may fail") - - use('Agg', warn=False) # use Agg backend for these tests + "Some date-related tests may fail.") - # These settings *must* be hardcoded for running the comparison - # tests and are not necessarily the default values as specified in - # rcsetup.py + matplotlib.use("agg") rcdefaults() # Start with all defaults + # These settings *must* be hardcoded for running the comparison tests and + # are not necessarily the default values as specified in rcsetup.py. set_font_settings_for_testing() set_reproducibility_for_testing() diff --git a/lib/matplotlib/tests/test_rcparams.py b/lib/matplotlib/tests/test_rcparams.py index 7e142a8e0bdf..e418e4f6d0b8 100644 --- a/lib/matplotlib/tests/test_rcparams.py +++ b/lib/matplotlib/tests/test_rcparams.py @@ -57,11 +57,9 @@ def test_rcparams(): assert mpl.rcParams['lines.linewidth'] == linewidth # test rc_file - try: + with mpl.rc_context(): mpl.rc_file(fname) assert mpl.rcParams['lines.linewidth'] == 33 - finally: - mpl.rcParams['lines.linewidth'] = linewidth def test_RcParams_class(): From b8b826e9126b977c18123ce98ee348ff8966d56c Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Fri, 17 Nov 2017 18:48:25 -0800 Subject: [PATCH 7/9] Investigate test failures. --- lib/matplotlib/tests/test_rcparams.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/lib/matplotlib/tests/test_rcparams.py b/lib/matplotlib/tests/test_rcparams.py index e418e4f6d0b8..735b3d42b7f4 100644 --- a/lib/matplotlib/tests/test_rcparams.py +++ b/lib/matplotlib/tests/test_rcparams.py @@ -2,9 +2,11 @@ import six +from collections import OrderedDict +import copy +from itertools import chain import os import warnings -from collections import OrderedDict from cycler import cycler, Cycler import pytest @@ -16,7 +18,6 @@ import matplotlib as mpl import matplotlib.pyplot as plt import matplotlib.colors as mcolors -from itertools import chain import numpy as np from matplotlib.rcsetup import (validate_bool_maybe_none, validate_stringlist, @@ -31,15 +32,17 @@ _validate_linestyle) -mpl.rc('text', usetex=False) -mpl.rc('lines', linewidth=22) - -fname = os.path.join(os.path.dirname(__file__), 'test_rcparams.rc') +@pytest.fixture(autouse=True) +def setup_module(): + with mpl.rc_context(): + mpl.rc('text', usetex=False) + mpl.rc('lines', linewidth=22) def test_rcparams(): usetex = mpl.rcParams['text.usetex'] linewidth = mpl.rcParams['lines.linewidth'] + fname = os.path.join(os.path.dirname(__file__), 'test_rcparams.rc') # test context given dictionary with mpl.rc_context(rc={'text.usetex': not usetex}): @@ -57,9 +60,8 @@ def test_rcparams(): assert mpl.rcParams['lines.linewidth'] == linewidth # test rc_file - with mpl.rc_context(): - mpl.rc_file(fname) - assert mpl.rcParams['lines.linewidth'] == 33 + mpl.rc_file(fname) + assert mpl.rcParams['lines.linewidth'] == 33 def test_RcParams_class(): @@ -135,8 +137,7 @@ def test_Bug_2543(): mpl.rcParams[key] = _copy[key] mpl.rcParams['text.dvipnghack'] = None with mpl.rc_context(): - from copy import deepcopy - _deep_copy = deepcopy(mpl.rcParams) + _deep_copy = copy.deepcopy(mpl.rcParams) # real test is that this does not raise assert validate_bool_maybe_none(None) is None assert validate_bool_maybe_none("none") is None From fe82135a2df7e818ebf1bc62f0a24ab24e77dae5 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Mon, 20 Nov 2017 16:04:01 -0800 Subject: [PATCH 8/9] Also update the backend when doing rcParams["backend"] = "foo". --- lib/matplotlib/rcsetup.py | 21 ++++++++++++++++++--- lib/matplotlib/tests/test_backend_qt4.py | 6 ++---- lib/matplotlib/tests/test_backend_qt5.py | 7 ++++--- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index bc3b84e89fc3..66fdccb67153 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -261,13 +261,28 @@ def validate_backend(s): lambda s: s if s.startswith("module://") else ValidateInStrings('backend', all_backends, ignorecase=True)(s))(s) + pyplot = sys.modules.get("matplotlib.pyplot") if len(candidates) == 1: - return candidates[0] + backend, = candidates + if pyplot: + # This import needs to be delayed (below too) because it is not + # available at first import. + from matplotlib import rcParams + # Don't recurse. + old_backend = rcParams["backend"] + if old_backend == backend: + return backend + dict.__setitem__(rcParams, "backend", backend) + try: + pyplot.switch_backend(backend) + except Exception: + dict.__setitem__(rcParams, "backend", old_backend) + raise + return backend else: - pyplot = sys.modules.get("matplotlib.pyplot") if pyplot: - pyplot.switch_backend(candidates) # Actually resolves the backend. from matplotlib import rcParams + pyplot.switch_backend(candidates) # Actually resolves the backend. return rcParams["backend"] else: return candidates diff --git a/lib/matplotlib/tests/test_backend_qt4.py b/lib/matplotlib/tests/test_backend_qt4.py index a621329772ed..69dcf1f7b580 100644 --- a/lib/matplotlib/tests/test_backend_qt4.py +++ b/lib/matplotlib/tests/test_backend_qt4.py @@ -12,10 +12,8 @@ except ImportError: import mock -with matplotlib.rc_context(rc={'backend': 'Qt4Agg'}): - qt_compat = pytest.importorskip('matplotlib.backends.qt_compat') -from matplotlib.backends.backend_qt4 import ( - MODIFIER_KEYS, SUPER, ALT, CTRL, SHIFT) # noqa +pytest.importorskip('PyQt4') +qt_compat = pytest.importorskip('matplotlib.backends.qt_compat') QtCore = qt_compat.QtCore _, ControlModifier, ControlKey = MODIFIER_KEYS[CTRL] diff --git a/lib/matplotlib/tests/test_backend_qt5.py b/lib/matplotlib/tests/test_backend_qt5.py index 81a23081ddbd..63c17254dddf 100644 --- a/lib/matplotlib/tests/test_backend_qt5.py +++ b/lib/matplotlib/tests/test_backend_qt5.py @@ -15,9 +15,10 @@ except ImportError: import mock -with matplotlib.rc_context(rc={'backend': 'Qt5Agg'}): - qt_compat = pytest.importorskip('matplotlib.backends.qt_compat', - minversion='5') +pytest.importorskip('PyQt5') +qt_compat = pytest.importorskip('matplotlib.backends.qt_compat', + minversion='5') + from matplotlib.backends.backend_qt5 import ( MODIFIER_KEYS, SUPER, ALT, CTRL, SHIFT) # noqa From 1083c3c079048e5679536c3c0e50081f9af60045 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Fri, 29 Dec 2017 15:52:00 -0800 Subject: [PATCH 9/9] Add detection of OSX NSApp event loop. --- lib/matplotlib/backends/__init__.py | 13 ++++++++++++- src/_macosx.m | 21 +++++++++++++++++++-- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/backends/__init__.py b/lib/matplotlib/backends/__init__.py index e7dee06f0c01..e8aabac04b9c 100644 --- a/lib/matplotlib/backends/__init__.py +++ b/lib/matplotlib/backends/__init__.py @@ -25,7 +25,8 @@ def _get_current_event_loop(): Returns ------- Optional[str] - A value in {"qt5", "qt4", "gtk3", "gtk2", "tk", "headless", None} + One of the following values: "qt5", "qt4", "gtk3", "gtk2", "tk", + "macosx", "headless", ``None``. """ QtWidgets = (sys.modules.get("PyQt5.QtWidgets") or sys.modules.get("PySide2.QtWidgets")) @@ -48,6 +49,16 @@ def _get_current_event_loop(): and frame.f_code.co_name == "mainloop" for frame in sys._current_frames().values()): return "tk" + try: + from matplotlib.backends import _macosx + except ImportError: + pass + else: + # Note that the NSApp event loop is also running when a non-native + # toolkit (e.g. Qt5) is active, but in that case we want to report the + # other toolkit; thus, this check comes after the other toolkits. + if _macosx.event_loop_is_running(): + return "macosx" if sys.platform.startswith("linux") and not os.environ.get("DISPLAY"): return "headless" diff --git a/src/_macosx.m b/src/_macosx.m index 6ac728ae57d1..57762f2b2226 100644 --- a/src/_macosx.m +++ b/src/_macosx.m @@ -2818,6 +2818,16 @@ - (int)index } @end +static PyObject* +event_loop_is_running(PyObject* self) +{ + if ([NSApp isRunning]) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } +} + static PyObject* show(PyObject* self) { @@ -3072,10 +3082,17 @@ static bool verify_framework(void) } static struct PyMethodDef methods[] = { + {"event_loop_is_running", + (PyCFunction)event_loop_is_running, + METH_NOARGS, + "Return whether the NSApp main event loop is currently running." + }, {"show", (PyCFunction)show, METH_NOARGS, - "Show all the figures and enter the main loop.\nThis function does not return until all Matplotlib windows are closed,\nand is normally not needed in interactive sessions." + "Show all the figures and enter the main loop.\n" + "This function does not return until all Matplotlib windows are closed,\n" + "and is normally not needed in interactive sessions." }, {"choose_save_file", (PyCFunction)choose_save_file, @@ -3087,7 +3104,7 @@ static bool verify_framework(void) METH_VARARGS, "Sets the active cursor." }, - {NULL, NULL, 0, NULL}/* sentinel */ + {NULL, NULL, 0, NULL} /* sentinel */ }; #if PY3K