Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 00dcbf6

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. Note that we were *already* relying on $DISPLAY being correctly set before, and this PR also doesn't return "invalid display" if we can't load X11, so this should not make anything worse on Wayland (at worst we'll just fail to detect headlessness like before).
1 parent 8589fed commit 00dcbf6

File tree

5 files changed

+53
-11
lines changed

5 files changed

+53
-11
lines changed

lib/matplotlib/backend_bases.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2717,10 +2717,15 @@ def show(self):
27172717
warning in `.Figure.show`.
27182718
"""
27192719
# This should be overridden in GUI backends.
2720-
if cbook._get_running_interactive_framework() != "headless":
2721-
raise NonGuiException(
2722-
f"Matplotlib is currently using {get_backend()}, which is "
2723-
f"a non-GUI backend, so cannot show the figure.")
2720+
if sys.platform == "linux" and not os.environ.get("DISPLAY"):
2721+
# We cannot check _get_running_interactive_framework() ==
2722+
# "headless" because that would also suppress the warning when
2723+
# $DISPLAY exists but is invalid, which is more likely an error and
2724+
# thus warrants a warning.
2725+
return
2726+
raise NonGuiException(
2727+
f"Matplotlib is currently using {get_backend()}, which is a "
2728+
f"non-GUI backend, so cannot show the figure.")
27242729

27252730
def destroy(self):
27262731
pass

lib/matplotlib/backends/backend_qt5.py

Lines changed: 2 additions & 5 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 (
@@ -113,10 +112,8 @@ def _create_qApp():
113112
is_x11_build = False
114113
else:
115114
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')
115+
if is_x11_build and matplotlib._c_internal_utils.invalid_display():
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
@@ -72,7 +72,8 @@ def _get_running_interactive_framework():
7272
if 'matplotlib.backends._macosx' in sys.modules:
7373
if sys.modules["matplotlib.backends._macosx"].event_loop_is_running():
7474
return "macosx"
75-
if sys.platform.startswith("linux") and not os.environ.get("DISPLAY"):
75+
if (sys.platform.startswith("linux")
76+
and _c_internal_utils.invalid_display()):
7677
return "headless"
7778
return None
7879

setupext.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,8 @@ def get_extensions(self):
341341
# c_internal_utils
342342
ext = Extension(
343343
"matplotlib._c_internal_utils", ["src/_c_internal_utils.c"],
344-
libraries={"win32": ["ole32", "shell32"]}.get(sys.platform, []))
344+
libraries={"linux": ["dl"], "win32": ["ole32", "shell32"]}.get(
345+
sys.platform, []))
345346
yield ext
346347
# contour
347348
ext = Extension(

src/_c_internal_utils.c

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,43 @@
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
#endif
710

11+
static PyObject* mpl_invalid_display(PyObject* module)
12+
{
13+
#ifdef __linux__
14+
void* libX11;
15+
typedef struct Display Display;
16+
Display* (* XOpenDisplay)(char* display_name);
17+
int (* XCloseDisplay)(Display* display);
18+
if (!(libX11 = dlopen("libX11.so", RTLD_LAZY)) ||
19+
!(XOpenDisplay = dlsym(libX11, "XOpenDisplay")) ||
20+
!(XCloseDisplay = dlsym(libX11, "XCloseDisplay"))) {
21+
Py_RETURN_FALSE;
22+
}
23+
Display* display = XOpenDisplay(NULL);
24+
if (display) {
25+
XCloseDisplay(display);
26+
}
27+
if (dlclose(libX11)) {
28+
PyErr_SetString(PyExc_RuntimeError, dlerror());
29+
return NULL;
30+
}
31+
if (display) {
32+
Py_RETURN_FALSE;
33+
} else {
34+
Py_RETURN_TRUE;
35+
}
36+
#else
37+
Py_RETURN_FALSE;
38+
#endif
39+
}
40+
841
static PyObject* mpl_GetCurrentProcessExplicitAppUserModelID(PyObject* module)
942
{
1043
#ifdef _WIN32
@@ -40,6 +73,11 @@ static PyObject* mpl_SetCurrentProcessExplicitAppUserModelID(PyObject* module, P
4073
}
4174

4275
static PyMethodDef functions[] = {
76+
{"invalid_display", (PyCFunction)mpl_invalid_display, METH_NOARGS,
77+
"invalid_display()\n--\n\n"
78+
"Attempt to check whether the current X11 display is invalid.\n\n"
79+
"Returns True if running on Linux and libX11 can be loaded and \n"
80+
"XOpenDisplay(NULL) returns NULL, False otherwise."},
4381
{"Win32_GetCurrentProcessExplicitAppUserModelID",
4482
(PyCFunction)mpl_GetCurrentProcessExplicitAppUserModelID, METH_NOARGS,
4583
"Win32_GetCurrentProcessExplicitAppUserModelID()\n--\n\n"

0 commit comments

Comments
 (0)