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.