Skip to content

Commit 9aa515f

Browse files
committed
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.
1 parent 3d725f6 commit 9aa515f

File tree

5 files changed

+77
-16
lines changed

5 files changed

+77
-16
lines changed

lib/matplotlib/backend_bases.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2755,10 +2755,15 @@ def show(self):
27552755
warning in `.Figure.show`.
27562756
"""
27572757
# This should be overridden in GUI backends.
2758-
if cbook._get_running_interactive_framework() != "headless":
2759-
raise NonGuiException(
2760-
f"Matplotlib is currently using {get_backend()}, which is "
2761-
f"a non-GUI backend, so cannot show the figure.")
2758+
if sys.platform == "linux" and not os.environ.get("DISPLAY"):
2759+
# We cannot check _get_running_interactive_framework() ==
2760+
# "headless" because that would also suppress the warning when
2761+
# $DISPLAY exists but is invalid, which is more likely an error and
2762+
# thus warrants a warning.
2763+
return
2764+
raise NonGuiException(
2765+
f"Matplotlib is currently using {get_backend()}, which is a "
2766+
f"non-GUI backend, so cannot show the figure.")
27622767

27632768
def destroy(self):
27642769
pass

lib/matplotlib/backends/backend_qt5.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
import traceback
88

99
import matplotlib
10-
1110
from matplotlib import backend_tools, cbook
1211
from matplotlib._pylab_helpers import Gcf
1312
from matplotlib.backend_bases import (
@@ -108,15 +107,13 @@ def _create_qApp():
108107
importlib.import_module(
109108
# i.e. PyQt5.QtX11Extras or PySide2.QtX11Extras.
110109
f"{QtWidgets.__package__}.QtX11Extras")
111-
is_x11_build = True
110+
x11_build = True
112111
except ImportError:
113-
is_x11_build = False
112+
x11_build = False
114113
else:
115-
is_x11_build = hasattr(QtGui, "QX11Info")
116-
if is_x11_build:
117-
display = os.environ.get('DISPLAY')
118-
if display is None or not re.search(r':\d', display):
119-
raise RuntimeError('Invalid DISPLAY variable')
114+
x11_build = hasattr(QtGui, "QX11Info")
115+
if x11_build and matplotlib._c_internal_utils.display_is_invalid():
116+
raise RuntimeError('Invalid DISPLAY variable')
120117

121118
try:
122119
QtWidgets.QApplication.setAttribute(

lib/matplotlib/cbook/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ def _get_running_interactive_framework():
7373
if 'matplotlib.backends._macosx' in sys.modules:
7474
if sys.modules["matplotlib.backends._macosx"].event_loop_is_running():
7575
return "macosx"
76-
if sys.platform.startswith("linux") and not os.environ.get("DISPLAY"):
76+
if (sys.platform.startswith("linux")
77+
and _c_internal_utils.display_is_invalid()):
7778
return "headless"
7879
return None
7980

setupext.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -349,8 +349,10 @@ def get_extensions(self):
349349
# c_internal_utils
350350
ext = Extension(
351351
"matplotlib._c_internal_utils", ["src/_c_internal_utils.c"],
352-
libraries=({"win32": ["ole32", "shell32", "user32"]}
353-
.get(sys.platform, [])))
352+
libraries=({
353+
"linux": ["dl"],
354+
"win32": ["ole32", "shell32", "user32"],
355+
}.get(sys.platform, [])))
354356
yield ext
355357
# contour
356358
ext = Extension(

src/_c_internal_utils.c

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,61 @@
11
#define PY_SSIZE_T_CLEAN
22
#include <Python.h>
3+
#ifdef __linux__
4+
#include <dlfcn.h>
5+
#endif
36
#ifdef _WIN32
47
#include <Objbase.h>
58
#include <Shobjidl.h>
69
#include <Windows.h>
710
#endif
811

12+
static PyObject* mpl_display_is_invalid(PyObject* module)
13+
{
14+
#ifdef __linux__
15+
void* libX11;
16+
if ((libX11 = dlopen("libX11.so", RTLD_LAZY))) {
17+
struct Display* display = NULL;
18+
struct Display* (* XOpenDisplay)(char const*) =
19+
dlsym(libX11, "XOpenDisplay");
20+
int (* XCloseDisplay)(struct Display*) =
21+
dlsym(libX11, "XCloseDisplay");
22+
if (XOpenDisplay && XCloseDisplay
23+
&& (display = XOpenDisplay(NULL))) {
24+
XCloseDisplay(display);
25+
}
26+
if (dlclose(libX11)) {
27+
PyErr_SetString(PyExc_RuntimeError, dlerror());
28+
return NULL;
29+
}
30+
if (display) {
31+
Py_RETURN_FALSE;
32+
}
33+
}
34+
void* libwayland_client;
35+
if ((libwayland_client = dlopen("libwayland-client.so", RTLD_LAZY))) {
36+
struct wl_display* display = NULL;
37+
struct wl_display* (* wl_display_connect)(char const*) =
38+
dlsym(libwayland_client, "wl_display_connect");
39+
int (* wl_display_disconnect)(struct wl_display*) =
40+
dlsym(libwayland_client, "wl_display_disconnect");
41+
if (wl_display_connect && wl_display_disconnect
42+
&& (display = wl_display_connect(NULL))) {
43+
wl_display_disconnect(display);
44+
}
45+
if (dlclose(libwayland_client)) {
46+
PyErr_SetString(PyExc_RuntimeError, dlerror());
47+
return NULL;
48+
}
49+
if (display) {
50+
Py_RETURN_FALSE;
51+
}
52+
}
53+
Py_RETURN_TRUE;
54+
#else
55+
Py_RETURN_FALSE;
56+
#endif
57+
}
58+
959
static PyObject* mpl_GetCurrentProcessExplicitAppUserModelID(PyObject* module)
1060
{
1161
#ifdef _WIN32
@@ -66,6 +116,12 @@ static PyObject* mpl_SetForegroundWindow(PyObject* module, PyObject *arg)
66116
}
67117

68118
static PyMethodDef functions[] = {
119+
{"display_is_invalid", (PyCFunction)mpl_display_is_invalid, METH_NOARGS,
120+
"display_is_invalid()\n--\n\n"
121+
"Attempt to check whether the current X11 or Wayland display is invalid.\n\n"
122+
"Returns True if running on Linux and both XOpenDisplay(NULL) returns NULL\n"
123+
"(if libX11 can be loaded) and wl_display_connect(NULL) returns NULL\n"
124+
"(if libwayland-client can be loaded), False otherwise."},
69125
{"Win32_GetCurrentProcessExplicitAppUserModelID",
70126
(PyCFunction)mpl_GetCurrentProcessExplicitAppUserModelID, METH_NOARGS,
71127
"Win32_GetCurrentProcessExplicitAppUserModelID()\n--\n\n"
@@ -83,7 +139,7 @@ static PyMethodDef functions[] = {
83139
"always returns None."},
84140
{"Win32_SetForegroundWindow",
85141
(PyCFunction)mpl_SetForegroundWindow, METH_O,
86-
"Win32_SetForegroundWindow(hwnd)\n--\n\n"
142+
"Win32_SetForegroundWindow(hwnd, /)\n--\n\n"
87143
"Wrapper for Windows' SetForegroundWindow. On non-Windows platforms, \n"
88144
"a no-op."},
89145
{NULL, NULL}}; // sentinel.

0 commit comments

Comments
 (0)
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