Source code for kwplot.auto_backends

# -*- coding: utf-8 -*-
"""
This module handles automatically determening a "good" matplotlib backend to
use before importing pyplot.
"""
from __future__ import absolute_import, division, print_function, unicode_literals
import sys
import os
import ubelt as ub

__all__ = [
    'autompl', 'autoplt', 'autosns', 'set_mpl_backend', 'BackendContext',
]


_qtensured = False


def _current_ipython_session():
    """
    Returns a reference to the current IPython session, if one is running
    """
    try:
        __IPYTHON__
    except NameError:
        return None
    else:
        # if ipython is None we must have exited ipython at some point
        import IPython
        ipython = IPython.get_ipython()
        return ipython


def _qtensure():
    """
    If you are in an IPython session, ensures that your backend is Qt.
    """
    global _qtensured
    if not _qtensured:
        ipython = _current_ipython_session()
        if ipython:
            if 'PyQt4' in sys.modules:
                ipython.magic('pylab qt4 --no-import-all')
                _qtensured = True
            else:
                ipython.magic('pylab qt5 --no-import-all')
                _qtensured = True


def _aggensure():
    """
    Ensures that you are in agg mode as long as IPython is not running

    This might help prevent errors in tmux like:
        qt.qpa.screen: QXcbConnection: Could not connect to display localhost:10.0
        Could not connect to any X display.
    """
    import matplotlib as mpl
    current_backend = mpl.get_backend()
    if current_backend != 'agg':
        ipython = _current_ipython_session()
        if not ipython:
            set_mpl_backend('agg')


[docs]def set_mpl_backend(backend, verbose=None): """ Args: backend (str): name of backend to use (e.g. Agg, PyQt) """ import matplotlib as mpl if verbose: print('set_mpl_backend backend={}'.format(backend)) if backend.lower().startswith('qt'): # handle interactive qt case _qtensure() current_backend = mpl.get_backend() if verbose: print('* current_backend = {!r}'.format(current_backend)) if backend != current_backend: # If we have already imported pyplot, then we need to use experimental # behavior. Otherwise, we can just set the backend. if 'matplotlib.pyplot' in sys.modules: from matplotlib import pyplot as plt if verbose: print('plt.switch_backend({!r})'.format(current_backend)) plt.switch_backend(backend) else: if verbose: print('mpl.use({!r})'.format(backend)) mpl.use(backend) else: if verbose: print('not changing backends') if verbose: print('* new_backend = {!r}'.format(mpl.get_backend()))
_AUTOMPL_WAS_RUN = False
[docs]def autompl(verbose=0, recheck=False, force=None): """ Uses platform heuristics to automatically set the matplotlib backend. If no display is available it will be set to `agg`, otherwise we will try to use the cross-platform `Qt5Agg` backend. Args: verbose (int, default=0): verbosity level recheck (bool, default=False): if False, this function will not run if it has already been called (this can save a significant amount of time). force (str, default=None): backend to force to or "auto" CommandLine: # Checks export QT_DEBUG_PLUGINS=1 xdoctest -m kwplot.auto_backends autompl --check KWPLOT_UNSAFE=1 xdoctest -m kwplot.auto_backends autompl --check KWPLOT_UNSAFE=0 xdoctest -m kwplot.auto_backends autompl --check Example: >>> # xdoctest +REQUIRES(--check) >>> plt = autoplt(verbose=1) >>> plt.figure() References: https://stackoverflow.com/questions/637005/check-if-x-server-is-running """ global _AUTOMPL_WAS_RUN if force == 'auto': recheck = True force = None elif force is not None: set_mpl_backend(force) _AUTOMPL_WAS_RUN = True if recheck or not _AUTOMPL_WAS_RUN: if verbose: print('AUTOMPL') if sys.platform.startswith('win32'): # TODO: something reasonable pass else: DISPLAY = os.environ.get('DISPLAY', '') if DISPLAY: if sys.platform.startswith('linux') and ub.find_exe('xdpyinfo'): # On Linux, check if we can actually connect to X # NOTE: this call takes a significant amount of time info = ub.cmd('xdpyinfo', shell=True) if verbose: print('xdpyinfo-info = {}'.format(ub.repr2(info))) if info['ret'] != 0: DISPLAY = None if verbose: print(' * DISPLAY = {!r}'.format(DISPLAY)) if not DISPLAY: backend = 'agg' else: """ Note: May encounter error that crashes the program, not sure why this happens yet. The current workaround is to uninstall PyQt5, but that isn't sustainable. QObject::moveToThread: Current thread (0x7fe8d965d030) is not the object's thread (0x7fffb0f64340). Cannot move to target thread (0x7fe8d965d030) qt.qpa.plugin: Could not load the Qt platform plugin "xcb" in "" even though it was found. This application failed to start because no Qt platform plugin could be initialized. Reinstalling the application may fix this problem. Available platform plugins are: eglfs, linuxfb, minimal, minimalegl, offscreen, vnc, wayland-egl, wayland, wayland-xcomposite-egl, wayland-xcomposite-glx, webgl, xcb. UPDATE 2021-01-04: By setting export QT_DEBUG_PLUGINS=1 I was able to look at more debug information. It turns out that it was grabbing the xcb plugin from the opencv-python package. I uninstalled that package and then installed opencv-python-headless which does not include an xcb binary. However, now the it is missing "libxcb-xinerama". May be able to do something with: conda install -c conda-forge xorg-libxinerama # But that didnt work I had to pip uninstall PyQt5 # This seems to work correctly conda install -c anaconda pyqt """ if ub.modname_to_modpath('PyQt5'): try: import PyQt5 # NOQA from PyQt5 import QtCore # NOQA except ImportError: backend = 'agg' else: backend = 'Qt5Agg' KWPLOT_UNSAFE = os.environ.get('KWPLOT_UNSAFE', '') TRY_AVOID_CRASH = KWPLOT_UNSAFE.lower() not in ['1', 'true', 'yes'] if TRY_AVOID_CRASH and ub.LINUX: # HOLD UP. Lets try to avoid a crash. if 'cv2' in sys.modules: from os.path import dirname, join, exists cv2 = sys.modules['cv2'] cv2_mod_dpath = dirname(cv2.__file__) cv2_lib_dpath = join(cv2_mod_dpath, 'qt/plugins/platforms') cv2_qxcb_fpath = join(cv2_lib_dpath, 'libqxcb.so') qt_mod_dpath = dirname(QtCore.__file__) qt_lib_dpath = join(qt_mod_dpath, 'Qt/plugins/platforms') qt_qxcb_fpath = join(qt_lib_dpath, 'libqxcb.so') if exists(cv2_qxcb_fpath) and exists(qt_qxcb_fpath): # Can we use ldd to make the test better? import warnings warnings.warn(ub.paragraph( ''' Autompl has detected libqxcb in PyQt and cv2. Falling back to agg to avoid a potential crash. This can be worked around by installing opencv-python-headless instead of opencv-python. Disable this check by setting the environ KWPLOT_UNSAFE=1 ''' )) backend = 'agg' elif ub.modname_to_modpath('PyQt4'): try: import Qt4Agg # NOQA from PyQt4 import QtCore # NOQA except ImportError: backend = 'agg' else: backend = 'Qt4Agg' else: backend = 'agg' set_mpl_backend(backend, verbose=verbose) if 0: # TODO: # IF IN A NOTEBOOK, BE SURE TO SET INLINE BEHAVIOR # THIS EFFECTIVELY REPRODUCES THE %matplotlib inline behavior # BUT USING AN ACTUAL PYTHON FUNCTION shell = _current_ipython_session() if shell: shell.enable_matplotlib('inline') _AUTOMPL_WAS_RUN = True
[docs]def autoplt(verbose=0, recheck=False, force=None): """ Like autompl, but also returns the :mod:`matplotlib.pyplot` module for convenience. Returns: ModuleType """ autompl(verbose=verbose, recheck=recheck, force=force) from matplotlib import pyplot as plt return plt
[docs]def autosns(verbose=0, recheck=False, force=None): """ Like autompl, but also calls :func:`seaborn.set` and returns the :mod:`seaborn` module for convenience. Returns: ModuleType """ autompl(verbose=verbose, recheck=recheck, force=force) import seaborn as sns sns.set() return sns
[docs]class BackendContext(object): """ Context manager that ensures a specific backend, but then reverts after the context has ended. Because this changes the backend after pyplot has initialized, there is a chance for odd behavior to occur. Please submit and issue if you experience this and can document the environment that caused it. CommandLine: # Checks xdoctest -m kwplot.auto_backends BackendContext --check Example: >>> # xdoctest +REQUIRES(--check) >>> from kwplot.auto_backends import * # NOQA >>> import matplotlib as mpl >>> import kwplot >>> print(mpl.get_backend()) >>> #kwplot.autompl(force='auto') >>> #print(mpl.get_backend()) >>> #fig1 = kwplot.figure(fnum=3) >>> #print(mpl.get_backend()) >>> with BackendContext('agg'): >>> print(mpl.get_backend()) >>> fig2 = kwplot.figure(fnum=4) >>> print(mpl.get_backend()) """ def __init__(self, backend): self.backend = backend self.prev = None self._prev_backend_was_loaded = 'matplotlib.pyplot' in sys.modules def __enter__(self): import matplotlib as mpl self.prev = mpl.get_backend() if self.prev == 'Qt5Agg': # Hack for the case where our default matplotlib backend is Qt5Agg # but we don't have Qt bindings available. (I think this may be a # configuration error on my part). Either way, its easy to test for # and fix. If the default backend is Qt5Agg, but importing the # bindings causes an error, just set the default to agg, which will # supress the warnings. try: from matplotlib.backends.qt_compat import QtGui # NOQA except ImportError: # TODO: should we try this instead? # mpl.rcParams['backend_fallback'] self.prev = 'agg' set_mpl_backend(self.backend) def __exit__(self, *args): if self.prev is not None: """ Note: 2021-01-07 Running on in an ssh-session (where in this case ssh did not have an X server, but an X server was running elsewhere) we got this error. ImportError: Cannot load backend 'Qt5Agg' which requires the 'qt5' interactive framework, as 'headless' is currently running when using BackendContext('agg') This is likely because the default was Qt5Agg, but it was not loaded. We switched to agg just fine, but when we switched back it tried to load Qt5Agg, which was not available and thus it failed. """ try: # Note set_mpl_backend(self.prev) except Exception: if self._prev_backend_was_loaded: # Only propogate the error if we had explicitly used pyplot # beforehand. Note sure if this is the right thing to do. raise