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/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 8a7861c06bc2..ed38dfcf3c95 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,28 @@ 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} -""" - - 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. + 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. - .. 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`. + To find out which backend is currently set, see `matplotlib.get_backend`. + Parameters + ---------- + arg : str + The name of the backend to use. """ - # 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 + 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(): @@ -1508,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/backend_bases.py b/lib/matplotlib/backend_bases.py index 6625ac7dc14e..f023c36ca9d0 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. @@ -2651,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 @@ -3366,3 +3254,130 @@ 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. + + # 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. + 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 [ + "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. + + 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..e8aabac04b9c 100644 --- a/lib/matplotlib/backends/__init__.py +++ b/lib/matplotlib/backends/__init__.py @@ -3,19 +3,64 @@ import six -import matplotlib -import inspect -import traceback -import warnings +import importlib import logging +import os +import sys +import traceback + +import matplotlib +from matplotlib import rcParams +from matplotlib.backend_bases import _Backend + _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 "', ''): - 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)) + name = matplotlib.rcParams["backend"] + + 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 as exc: + _log.info("Loading backend %s failed: %s", n, exc) + else: + raise ValueError("No suitable backend among {}".format(name)) + + 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("Loaded backend %s version %s.", name, Backend.backend_version) + + required_event_loop = Backend.required_event_loop + current_event_loop = _get_current_event_loop() + if (current_event_loop and required_event_loop + and current_event_loop != required_event_loop): + raise ImportError( + "Cannot load backend {!r} which requires the {!r} event loop, as " + "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 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_gdk.py b/lib/matplotlib/backends/backend_gdk.py index ced57a881a3b..8c962c581410 100644 --- a/lib/matplotlib/backends/backend_gdk.py +++ b/lib/matplotlib/backends/backend_gdk.py @@ -434,5 +434,6 @@ def _print_image(self, filename, format, *args, **kwargs): @_Backend.export class _BackendGDK(_Backend): + required_event_loop = "gtk2" FigureCanvas = FigureCanvasGDK FigureManager = FigureManagerBase diff --git a/lib/matplotlib/backends/backend_gtk.py b/lib/matplotlib/backends/backend_gtk.py index 0ffe6ec01f3e..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) @@ -1025,6 +1026,7 @@ def error_msg_gtk(msg, parent=None): @_Backend.export class _BackendGTK(_Backend): + required_event_loop = "gtk2" FigureCanvas = FigureCanvasGTK FigureManager = FigureManagerGTK diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 070056c090e1..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): @@ -907,6 +912,7 @@ def error_msg_gtk(msg, parent=None): @_Backend.export class _BackendGTK3(_Backend): + required_event_loop = "gtk3" FigureCanvas = FigureCanvasGTK3 FigureManager = FigureManagerGTK3 diff --git a/lib/matplotlib/backends/backend_macosx.py b/lib/matplotlib/backends/backend_macosx.py index 4ab5d0c90772..d20ed0febdbc 100644 --- a/lib/matplotlib/backends/backend_macosx.py +++ b/lib/matplotlib/backends/backend_macosx.py @@ -193,6 +193,7 @@ def set_message(self, message): @_Backend.export class _BackendMac(_Backend): + required_event_loop = "macosx" FigureCanvas = FigureCanvasMac FigureManager = FigureManagerMac 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_qt4.py b/lib/matplotlib/backends/backend_qt4.py index 92463a6573a9..8b2d0c574fea 100644 --- a/lib/matplotlib/backends/backend_qt4.py +++ b/lib/matplotlib/backends/backend_qt4.py @@ -12,4 +12,4 @@ @_BackendQT5.export class _BackendQT4(_BackendQT5): - pass + required_event_loop = "qt4" diff --git a/lib/matplotlib/backends/backend_qt5.py b/lib/matplotlib/backends/backend_qt5.py index 6b5e8bbfb54f..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) @@ -857,6 +854,7 @@ def exception_handler(type, value, tb): @_Backend.export class _BackendQT5(_Backend): + required_event_loop = "qt5" FigureCanvas = FigureCanvasQT FigureManager = FigureManagerQT 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 diff --git a/lib/matplotlib/backends/backend_tkagg.py b/lib/matplotlib/backends/backend_tkagg.py index fc2f4291be6a..20897149a7d3 100644 --- a/lib/matplotlib/backends/backend_tkagg.py +++ b/lib/matplotlib/backends/backend_tkagg.py @@ -1040,6 +1040,7 @@ def destroy(self, *args, **kwargs): @_Backend.export class _BackendTkAgg(_Backend): + required_event_loop = "tk" FigureCanvas = FigureCanvasTkAgg FigureManager = FigureManagerTkAgg diff --git a/lib/matplotlib/backends/backend_wx.py b/lib/matplotlib/backends/backend_wx.py index 1d9b33c79267..ccdc4f4794d0 100644 --- a/lib/matplotlib/backends/backend_wx.py +++ b/lib/matplotlib/backends/backend_wx.py @@ -1814,6 +1814,7 @@ def OnPrintPage(self, page): @_Backend.export class _BackendWx(_Backend): + required_event_loop = "wx" FigureCanvas = FigureCanvasWx FigureManager = FigureManagerWx _frame_class = FigureFrameWx 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/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/pyplot.py b/lib/matplotlib/pyplot.py index 0a67b24f4a54..1ed92117048a 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -29,7 +29,7 @@ from cycler import cycler import matplotlib import matplotlib.colorbar -from matplotlib import style +from matplotlib import backends, style from matplotlib import _pylab_helpers, interactive from matplotlib.cbook import dedent, silent_list, is_numlike from matplotlib.cbook import _string_to_bool @@ -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 @@ -79,40 +125,44 @@ def _backend_selection(): backend = rcParams['backend'] if not rcParams['backend_fallback'] or backend not in _interactive_bk: return - is_agg_backend = rcParams['backend'].endswith('Agg') - if 'wx' in sys.modules and not backend in ('WX', 'WXAgg'): - import wx - if wx.App.IsMainLoopRunning(): - rcParams['backend'] = 'wx' + 'Agg' * is_agg_backend - elif 'PyQt4.QtCore' in sys.modules and not backend == 'Qt4Agg': - import PyQt4.QtGui - if not PyQt4.QtGui.qApp.startingUp(): - # The mainloop is running. - rcParams['backend'] = 'qt4Agg' - elif 'PyQt5.QtCore' in sys.modules and not backend == 'Qt5Agg': - import PyQt5.QtWidgets - if not PyQt5.QtWidgets.qApp.startingUp(): - # The mainloop is running. - rcParams['backend'] = 'qt5Agg' - elif ('gtk' in sys.modules and - backend not in ('GTK', 'GTKAgg', 'GTKCairo')): - if 'gi' in sys.modules: - from gi.repository import GObject - ml = GObject.MainLoop - else: - import gobject - ml = gobject.MainLoop - if ml().is_running(): - rcParams['backend'] = 'gtk' + 'Agg' * is_agg_backend - elif 'Tkinter' in sys.modules and not backend == 'TkAgg': - # import Tkinter - pass # what if anything do we need to do for tkinter? + current_event_loop = backends._get_current_event_loop() + if current_event_loop == "qt5": + rcParams["backend"] = "qt5agg" + elif current_event_loop == "qt4": + rcParams["backend"] = "qt4agg" + elif current_event_loop == "gtk3": + rcParams["backend"] = ("gtk3agg" if rcParams["backend"].endswith("agg") + else "gtk3cairo") + elif current_event_loop == "gtk2": + rcParams["backend"] = "gtkagg" + elif current_event_loop == "tk": + rcParams["backend"] = "tkagg" _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. -_backend_mod, new_figure_manager, draw_if_interactive, _show = pylab_setup() + 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) + +switch_backend(rcParams["backend"]) + + +## Global ## _IP_REGISTERED = None _INSTALL_FIG_OBSERVER = False @@ -214,25 +264,6 @@ def findobj(o=None, match=None, include_self=True): return o.findobj(match, include_self=include_self) -def switch_backend(newbackend): - """ - Switch the default backend. This feature is **experimental**, and - is only expected to work switching to an image backend. e.g., if - you have a bunch of PostScript scripts that you want to run from - an interactive ipython session, you may want to switch to the PS - backend before running them to avoid having a bunch of GUI windows - popup. If you try to interactively switch from one GUI backend to - another, you will explode. - - Calling this command will close all open windows. - """ - close('all') - global _backend_mod, new_figure_manager, draw_if_interactive, _show - matplotlib.use(newbackend, warn=False, force=True) - from matplotlib.backends import pylab_setup - _backend_mod, new_figure_manager, draw_if_interactive, _show = pylab_setup() - - def show(*args, **kw): """ Display a figure. @@ -635,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..66fdccb67153 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,36 @@ 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) + pyplot = sys.modules.get("matplotlib.pyplot") + if len(candidates) == 1: + 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: - return _validate_standard_backends(s) + if pyplot: + from matplotlib import rcParams + pyplot.switch_backend(candidates) # Actually resolves the backend. + return rcParams["backend"] + else: + return candidates validate_qt4 = ValidateInStrings('backend.qt4', ['PyQt4', 'PySide', 'PyQt4v2']) @@ -934,9 +956,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/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_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 diff --git a/lib/matplotlib/tests/test_rcparams.py b/lib/matplotlib/tests/test_rcparams.py index 7e142a8e0bdf..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,11 +60,8 @@ def test_rcparams(): assert mpl.rcParams['lines.linewidth'] == linewidth # test rc_file - try: - mpl.rc_file(fname) - assert mpl.rcParams['lines.linewidth'] == 33 - finally: - mpl.rcParams['lines.linewidth'] = linewidth + mpl.rc_file(fname) + assert mpl.rcParams['lines.linewidth'] == 33 def test_RcParams_class(): @@ -137,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 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 diff --git a/src/_macosx.m b/src/_macosx.m index 50556c017b49..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) { @@ -3059,23 +3069,30 @@ 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; } 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