From 043c578ca68213a0cc229323f5e4d645d03ca568 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Wed, 13 May 2020 15:15:39 +0200 Subject: [PATCH] Improve headlessness detection for backend selection. We currently check the $DISPLAY environment variable to autodetect whether we should auto-pick a non-interactive backend on Linux, but that variable can be set to an "invalid" value. A realistic use case is for example a tmux session started interactively inheriting an initially valid $DISPLAY, but to which one later reconnects e.g. via ssh, at which point $DISPLAY becomes invalid. Before this PR, something like ``` DISPLAY=:123 MPLBACKEND= MATPLOTLIBRC=/dev/null python -c 'import pylab' ``` (where we unset matplotlibrc to force backend autoselection) would crash when we select qt and qt fails to initialize as $DISPLAY is invalid (qt unconditionally abort()s via qFatal() in that case). With this PR, we correctly autoselect a non-interactive backend. --- lib/matplotlib/backend_bases.py | 13 +++-- lib/matplotlib/backends/backend_qt5.py | 24 ++++----- lib/matplotlib/cbook/__init__.py | 2 +- setupext.py | 6 ++- src/_c_internal_utils.c | 75 ++++++++++++++++++++++++-- 5 files changed, 93 insertions(+), 27 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index fe8cc66913cc..29739b258b71 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -2755,10 +2755,15 @@ def show(self): warning in `.Figure.show`. """ # This should be overridden in GUI backends. - if cbook._get_running_interactive_framework() != "headless": - raise NonGuiException( - f"Matplotlib is currently using {get_backend()}, which is " - f"a non-GUI backend, so cannot show the figure.") + if sys.platform == "linux" and not os.environ.get("DISPLAY"): + # We cannot check _get_running_interactive_framework() == + # "headless" because that would also suppress the warning when + # $DISPLAY exists but is invalid, which is more likely an error and + # thus warrants a warning. + return + raise NonGuiException( + f"Matplotlib is currently using {get_backend()}, which is a " + f"non-GUI backend, so cannot show the figure.") def destroy(self): pass diff --git a/lib/matplotlib/backends/backend_qt5.py b/lib/matplotlib/backends/backend_qt5.py index 06a279fca24e..c25232f7942a 100644 --- a/lib/matplotlib/backends/backend_qt5.py +++ b/lib/matplotlib/backends/backend_qt5.py @@ -6,8 +6,7 @@ import sys import traceback -import matplotlib - +import matplotlib as mpl from matplotlib import backend_tools, cbook from matplotlib._pylab_helpers import Gcf from matplotlib.backend_bases import ( @@ -113,11 +112,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 mpl._c_internal_utils.display_is_valid(): + raise RuntimeError('Invalid DISPLAY variable') try: QtWidgets.QApplication.setAttribute( QtCore.Qt.AA_EnableHighDpiScaling) @@ -574,7 +570,7 @@ def __init__(self, canvas, num): self.window.setCentralWidget(self.canvas) - if matplotlib.is_interactive(): + if mpl.is_interactive(): self.window.show() self.canvas.draw_idle() @@ -601,9 +597,9 @@ def _widgetclosed(self): def _get_toolbar(self, canvas, parent): # must be inited after the window, drawingArea and figure # attrs are set - if matplotlib.rcParams['toolbar'] == 'toolbar2': + if mpl.rcParams['toolbar'] == 'toolbar2': toolbar = NavigationToolbar2QT(canvas, parent, True) - elif matplotlib.rcParams['toolbar'] == 'toolmanager': + elif mpl.rcParams['toolbar'] == 'toolmanager': toolbar = ToolbarQt(self.toolmanager, self.window) else: toolbar = None @@ -619,7 +615,7 @@ def resize(self, width, height): def show(self): self.window.show() - if matplotlib.rcParams['figure.raise_window']: + if mpl.rcParams['figure.raise_window']: self.window.activateWindow() self.window.raise_() @@ -792,8 +788,7 @@ def save_figure(self, *args): sorted_filetypes = sorted(filetypes.items()) default_filetype = self.canvas.get_default_filetype() - startpath = os.path.expanduser( - matplotlib.rcParams['savefig.directory']) + startpath = os.path.expanduser(mpl.rcParams['savefig.directory']) start = os.path.join(startpath, self.canvas.get_default_filename()) filters = [] selectedFilter = None @@ -811,8 +806,7 @@ def save_figure(self, *args): if fname: # Save dir for next time, unless empty str (i.e., use cwd). if startpath != "": - matplotlib.rcParams['savefig.directory'] = ( - os.path.dirname(fname)) + mpl.rcParams['savefig.directory'] = os.path.dirname(fname) try: self.canvas.figure.savefig(fname) except Exception as e: diff --git a/lib/matplotlib/cbook/__init__.py b/lib/matplotlib/cbook/__init__.py index f7dfa4c02b50..be12ac52407d 100644 --- a/lib/matplotlib/cbook/__init__.py +++ b/lib/matplotlib/cbook/__init__.py @@ -73,7 +73,7 @@ def _get_running_interactive_framework(): if 'matplotlib.backends._macosx' in sys.modules: if sys.modules["matplotlib.backends._macosx"].event_loop_is_running(): return "macosx" - if sys.platform.startswith("linux") and not os.environ.get("DISPLAY"): + if not _c_internal_utils.display_is_valid(): return "headless" return None diff --git a/setupext.py b/setupext.py index a8a0ae7cdde7..0742230844fc 100644 --- a/setupext.py +++ b/setupext.py @@ -349,8 +349,10 @@ def get_extensions(self): # c_internal_utils ext = Extension( "matplotlib._c_internal_utils", ["src/_c_internal_utils.c"], - libraries=({"win32": ["ole32", "shell32", "user32"]} - .get(sys.platform, []))) + libraries=({ + "linux": ["dl"], + "win32": ["ole32", "shell32", "user32"], + }.get(sys.platform, []))) yield ext # contour ext = Extension( diff --git a/src/_c_internal_utils.c b/src/_c_internal_utils.c index caca31585019..4a61fe5b6ee7 100644 --- a/src/_c_internal_utils.c +++ b/src/_c_internal_utils.c @@ -1,12 +1,68 @@ #define PY_SSIZE_T_CLEAN #include +#ifdef __linux__ +#include +#endif #ifdef _WIN32 #include #include #include #endif -static PyObject* mpl_GetCurrentProcessExplicitAppUserModelID(PyObject* module) +static PyObject* +mpl_display_is_valid(PyObject* module) +{ +#ifdef __linux__ + void* libX11; + // The getenv check is redundant but helps performance as it is much faster + // than dlopen(). + if (getenv("DISPLAY") + && (libX11 = dlopen("libX11.so.6", RTLD_LAZY))) { + struct Display* display = NULL; + struct Display* (* XOpenDisplay)(char const*) = + dlsym(libX11, "XOpenDisplay"); + int (* XCloseDisplay)(struct Display*) = + dlsym(libX11, "XCloseDisplay"); + if (XOpenDisplay && XCloseDisplay + && (display = XOpenDisplay(NULL))) { + XCloseDisplay(display); + } + if (dlclose(libX11)) { + PyErr_SetString(PyExc_RuntimeError, dlerror()); + return NULL; + } + if (display) { + Py_RETURN_TRUE; + } + } + void* libwayland_client; + if (getenv("WAYLAND_DISPLAY") + && (libwayland_client = dlopen("libwayland-client.so.0", RTLD_LAZY))) { + struct wl_display* display = NULL; + struct wl_display* (* wl_display_connect)(char const*) = + dlsym(libwayland_client, "wl_display_connect"); + void (* wl_display_disconnect)(struct wl_display*) = + dlsym(libwayland_client, "wl_display_disconnect"); + if (wl_display_connect && wl_display_disconnect + && (display = wl_display_connect(NULL))) { + wl_display_disconnect(display); + } + if (dlclose(libwayland_client)) { + PyErr_SetString(PyExc_RuntimeError, dlerror()); + return NULL; + } + if (display) { + Py_RETURN_TRUE; + } + } + Py_RETURN_FALSE; +#else + Py_RETURN_TRUE; +#endif +} + +static PyObject* +mpl_GetCurrentProcessExplicitAppUserModelID(PyObject* module) { #ifdef _WIN32 wchar_t* appid = NULL; @@ -22,7 +78,8 @@ static PyObject* mpl_GetCurrentProcessExplicitAppUserModelID(PyObject* module) #endif } -static PyObject* mpl_SetCurrentProcessExplicitAppUserModelID(PyObject* module, PyObject* arg) +static PyObject* +mpl_SetCurrentProcessExplicitAppUserModelID(PyObject* module, PyObject* arg) { #ifdef _WIN32 wchar_t* appid = PyUnicode_AsWideCharString(arg, NULL); @@ -40,7 +97,8 @@ static PyObject* mpl_SetCurrentProcessExplicitAppUserModelID(PyObject* module, P #endif } -static PyObject* mpl_GetForegroundWindow(PyObject* module) +static PyObject* +mpl_GetForegroundWindow(PyObject* module) { #ifdef _WIN32 return PyLong_FromVoidPtr(GetForegroundWindow()); @@ -49,7 +107,8 @@ static PyObject* mpl_GetForegroundWindow(PyObject* module) #endif } -static PyObject* mpl_SetForegroundWindow(PyObject* module, PyObject *arg) +static PyObject* +mpl_SetForegroundWindow(PyObject* module, PyObject *arg) { #ifdef _WIN32 HWND handle = PyLong_AsVoidPtr(arg); @@ -66,6 +125,12 @@ static PyObject* mpl_SetForegroundWindow(PyObject* module, PyObject *arg) } static PyMethodDef functions[] = { + {"display_is_valid", (PyCFunction)mpl_display_is_valid, METH_NOARGS, + "display_is_valid()\n--\n\n" + "Check whether the current X11 or Wayland display is valid.\n\n" + "On Linux, returns True if either $DISPLAY is set and XOpenDisplay(NULL)\n" + "succeeds, or $WAYLAND_DISPLAY is set and wl_display_connect(NULL)\n" + "succeeds. On other platforms, always returns True."}, {"Win32_GetCurrentProcessExplicitAppUserModelID", (PyCFunction)mpl_GetCurrentProcessExplicitAppUserModelID, METH_NOARGS, "Win32_GetCurrentProcessExplicitAppUserModelID()\n--\n\n" @@ -83,7 +148,7 @@ static PyMethodDef functions[] = { "always returns None."}, {"Win32_SetForegroundWindow", (PyCFunction)mpl_SetForegroundWindow, METH_O, - "Win32_SetForegroundWindow(hwnd)\n--\n\n" + "Win32_SetForegroundWindow(hwnd, /)\n--\n\n" "Wrapper for Windows' SetForegroundWindow. On non-Windows platforms, \n" "a no-op."}, {NULL, NULL}}; // sentinel. pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy