Skip to content

Commit 799bc95

Browse files
authored
Merge pull request matplotlib#30711 from ngoldbaum/v3.10.x
Backport PR matplotlib#30697 on branch v3.10.x (BUG: raise when creating a MacOS FigureManager outside the main thread)
2 parents 878e71a + 134000b commit 799bc95

File tree

2 files changed

+65
-13
lines changed

2 files changed

+65
-13
lines changed
Lines changed: 55 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
import os
2+
import threading
3+
from pathlib import Path
24

35
import pytest
46
from unittest import mock
57

68
import matplotlib as mpl
79
import matplotlib.pyplot as plt
8-
try:
9-
from matplotlib.backends import _macosx
10-
except ImportError:
11-
pytest.skip("These are mac only tests", allow_module_level=True)
10+
from matplotlib.testing import subprocess_run_helper
1211

1312

14-
@pytest.mark.backend('macosx')
15-
def test_cached_renderer():
13+
_test_timeout = 60
14+
15+
16+
def _test_cached_renderer():
1617
# Make sure that figures have an associated renderer after
1718
# a fig.canvas.draw() call
1819
fig = plt.figure(1)
@@ -24,8 +25,14 @@ def test_cached_renderer():
2425
assert fig.canvas.get_renderer()._renderer is not None
2526

2627

27-
@pytest.mark.backend('macosx')
28-
def test_savefig_rcparam(monkeypatch, tmp_path):
28+
@pytest.mark.backend('macosx', skip_on_importerror=True)
29+
def test_cached_renderer():
30+
subprocess_run_helper(_test_cached_renderer, timeout=_test_timeout,
31+
extra_env={"MPLBACKEND": "macosx"})
32+
33+
34+
def _test_savefig_rcparam():
35+
tmp_path = Path(os.environ["TEST_SAVEFIG_PATH"])
2936

3037
def new_choose_save_file(title, directory, filename):
3138
# Replacement function instead of opening a GUI window
@@ -34,9 +41,10 @@ def new_choose_save_file(title, directory, filename):
3441
os.makedirs(f"{directory}/test")
3542
return f"{directory}/test/{filename}"
3643

37-
monkeypatch.setattr(_macosx, "choose_save_file", new_choose_save_file)
3844
fig = plt.figure()
39-
with mpl.rc_context({"savefig.directory": tmp_path}):
45+
with (mock.patch("matplotlib.backends._macosx.choose_save_file",
46+
new_choose_save_file),
47+
mpl.rc_context({"savefig.directory": tmp_path})):
4048
fig.canvas.toolbar.save_figure()
4149
# Check the saved location got created
4250
save_file = f"{tmp_path}/test/{fig.canvas.get_default_filename()}"
@@ -47,14 +55,20 @@ def new_choose_save_file(title, directory, filename):
4755
assert mpl.rcParams["savefig.directory"] == f"{tmp_path}/test"
4856

4957

50-
@pytest.mark.backend('macosx')
58+
@pytest.mark.backend('macosx', skip_on_importerror=True)
59+
def test_savefig_rcparam(tmp_path):
60+
subprocess_run_helper(
61+
_test_savefig_rcparam, timeout=_test_timeout,
62+
extra_env={"MPLBACKEND": "macosx", "TEST_SAVEFIG_PATH": tmp_path})
63+
64+
65+
@pytest.mark.backend('macosx', skip_on_importerror=True)
5166
def test_ipython():
5267
from matplotlib.testing import ipython_in_subprocess
5368
ipython_in_subprocess("osx", {(8, 24): "macosx", (7, 0): "MacOSX"})
5469

5570

56-
@pytest.mark.backend('macosx')
57-
def test_save_figure_return():
71+
def _test_save_figure_return():
5872
fig, ax = plt.subplots()
5973
ax.imshow([[1]])
6074
prop = "matplotlib.backends._macosx.choose_save_file"
@@ -65,3 +79,31 @@ def test_save_figure_return():
6579
with mock.patch(prop, return_value=None):
6680
fname = fig.canvas.manager.toolbar.save_figure()
6781
assert fname is None
82+
83+
84+
@pytest.mark.backend('macosx', skip_on_importerror=True)
85+
def test_save_figure_return():
86+
subprocess_run_helper(_test_save_figure_return, timeout=_test_timeout,
87+
extra_env={"MPLBACKEND": "macosx"})
88+
89+
90+
def _test_create_figure_on_worker_thread_fails():
91+
def create_figure():
92+
warn_msg = "Matplotlib GUI outside of the main thread will likely fail."
93+
err_msg = "Cannot create a GUI FigureManager outside the main thread"
94+
with pytest.warns(UserWarning, match=warn_msg):
95+
with pytest.raises(RuntimeError, match=err_msg):
96+
plt.gcf()
97+
98+
worker = threading.Thread(target=create_figure)
99+
worker.start()
100+
worker.join()
101+
102+
103+
@pytest.mark.backend('macosx', skip_on_importerror=True)
104+
def test_create_figure_on_worker_thread_fails():
105+
subprocess_run_helper(
106+
_test_create_figure_on_worker_thread_fails,
107+
timeout=_test_timeout,
108+
extra_env={"MPLBACKEND": "macosx"}
109+
)

src/_macosx.m

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -580,6 +580,16 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name)
580580
static PyObject*
581581
FigureManager_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
582582
{
583+
if (![NSThread isMainThread]) {
584+
PyErr_SetString(
585+
PyExc_RuntimeError,
586+
"Cannot create a GUI FigureManager outside the main thread "
587+
"using the MacOS backend. Use a non-interactive "
588+
"backend like 'agg' to make plots on worker threads."
589+
);
590+
return NULL;
591+
}
592+
583593
lazy_init();
584594
Window* window = [Window alloc];
585595
if (!window) { return NULL; }

0 commit comments

Comments
 (0)