1
1
import importlib
2
2
import importlib .util
3
+ import inspect
3
4
import json
4
5
import os
5
6
import signal
@@ -61,92 +62,96 @@ def _get_testable_interactive_backends():
61
62
return backends
62
63
63
64
65
+ _test_timeout = 10 # Empirically, 1s is not enough on Travis.
66
+
67
+
68
+ # The source of this function gets extracted and run in another process, so it
69
+ # must be fully self-contained.
64
70
# Using a timer not only allows testing of timers (on other backends), but is
65
71
# also necessary on gtk3 and wx, where a direct call to key_press_event("q")
66
72
# from draw_event causes breakage due to the canvas widget being deleted too
67
73
# early. Also, gtk3 redefines key_press_event with a different signature, so
68
74
# we directly invoke it from the superclass instead.
69
- _test_script = """\
70
- import importlib.util
71
- import io
72
- import json
73
- import sys
74
- from unittest import TestCase
75
-
76
- import matplotlib as mpl
77
- from matplotlib import pyplot as plt, rcParams
78
- from matplotlib.backend_bases import FigureCanvasBase
79
- rcParams.update({
80
- "webagg.open_in_browser": False,
81
- "webagg.port_retries": 1,
82
- })
83
- if len(sys.argv) >= 2: # Second argument is json-encoded rcParams.
84
- rcParams.update(json.loads(sys.argv[1]))
85
- backend = plt.rcParams["backend"].lower()
86
- assert_equal = TestCase().assertEqual
87
- assert_raises = TestCase().assertRaises
88
-
89
- if backend.endswith("agg") and not backend.startswith(("gtk3", "web")):
90
- # Force interactive framework setup.
91
- plt.figure()
92
-
93
- # Check that we cannot switch to a backend using another interactive
94
- # framework, but can switch to a backend using cairo instead of agg, or a
95
- # non-interactive backend. In the first case, we use tkagg as the "other"
96
- # interactive backend as it is (essentially) guaranteed to be present.
97
- # Moreover, don't test switching away from gtk3 (as Gtk.main_level() is
98
- # not set up at this point yet) and webagg (which uses no interactive
99
- # framework).
100
-
101
- if backend != "tkagg":
102
- with assert_raises(ImportError):
103
- mpl.use("tkagg", force=True)
104
-
105
- def check_alt_backend(alt_backend):
106
- mpl.use(alt_backend, force=True)
107
- fig = plt.figure()
108
- assert_equal(
109
- type(fig.canvas).__module__,
110
- "matplotlib.backends.backend_{}".format(alt_backend))
111
-
112
- if importlib.util.find_spec("cairocffi"):
113
- check_alt_backend(backend[:-3] + "cairo")
114
- check_alt_backend("svg")
115
-
116
- mpl.use(backend, force=True)
117
-
118
- fig, ax = plt.subplots()
119
- assert_equal(
120
- type(fig.canvas).__module__,
121
- "matplotlib.backends.backend_{}".format(backend))
122
-
123
- ax.plot([0, 1], [2, 3])
124
-
125
- timer = fig.canvas.new_timer(1.) # Test that floats are cast to int as needed.
126
- timer.add_callback(FigureCanvasBase.key_press_event, fig.canvas, "q")
127
- # Trigger quitting upon draw.
128
- fig.canvas.mpl_connect("draw_event", lambda event: timer.start())
129
- fig.canvas.mpl_connect("close_event", print)
130
-
131
- result = io.BytesIO()
132
- fig.savefig(result, format='png')
133
-
134
- plt.show()
135
-
136
- # Ensure that the window is really closed.
137
- plt.pause(0.5)
138
-
139
- # Test that saving works after interactive window is closed, but the figure is
140
- # not deleted.
141
- result_after = io.BytesIO()
142
- fig.savefig(result_after, format='png')
143
-
144
- if not backend.startswith('qt5') and sys.platform == 'darwin':
145
- # FIXME: This should be enabled everywhere once Qt5 is fixed on macOS to
146
- # not resize incorrectly.
147
- assert_equal(result.getvalue(), result_after.getvalue())
148
- """
149
- _test_timeout = 10 # Empirically, 1s is not enough on Travis.
75
+ def _test_interactive_impl ():
76
+ import importlib .util
77
+ import io
78
+ import json
79
+ import sys
80
+ from unittest import TestCase
81
+
82
+ import matplotlib as mpl
83
+ from matplotlib import pyplot as plt , rcParams
84
+ from matplotlib .backend_bases import FigureCanvasBase
85
+
86
+ rcParams .update ({
87
+ "webagg.open_in_browser" : False ,
88
+ "webagg.port_retries" : 1 ,
89
+ })
90
+ if len (sys .argv ) >= 2 : # Second argument is json-encoded rcParams.
91
+ rcParams .update (json .loads (sys .argv [1 ]))
92
+ backend = plt .rcParams ["backend" ].lower ()
93
+ assert_equal = TestCase ().assertEqual
94
+ assert_raises = TestCase ().assertRaises
95
+
96
+ if backend .endswith ("agg" ) and not backend .startswith (("gtk3" , "web" )):
97
+ # Force interactive framework setup.
98
+ plt .figure ()
99
+
100
+ # Check that we cannot switch to a backend using another interactive
101
+ # framework, but can switch to a backend using cairo instead of agg,
102
+ # or a non-interactive backend. In the first case, we use tkagg as
103
+ # the "other" interactive backend as it is (essentially) guaranteed
104
+ # to be present. Moreover, don't test switching away from gtk3 (as
105
+ # Gtk.main_level() is not set up at this point yet) and webagg (which
106
+ # uses no interactive framework).
107
+
108
+ if backend != "tkagg" :
109
+ with assert_raises (ImportError ):
110
+ mpl .use ("tkagg" , force = True )
111
+
112
+ def check_alt_backend (alt_backend ):
113
+ mpl .use (alt_backend , force = True )
114
+ fig = plt .figure ()
115
+ assert_equal (
116
+ type (fig .canvas ).__module__ ,
117
+ "matplotlib.backends.backend_{}" .format (alt_backend ))
118
+
119
+ if importlib .util .find_spec ("cairocffi" ):
120
+ check_alt_backend (backend [:- 3 ] + "cairo" )
121
+ check_alt_backend ("svg" )
122
+
123
+ mpl .use (backend , force = True )
124
+
125
+ fig , ax = plt .subplots ()
126
+ assert_equal (
127
+ type (fig .canvas ).__module__ ,
128
+ "matplotlib.backends.backend_{}" .format (backend ))
129
+
130
+ ax .plot ([0 , 1 ], [2 , 3 ])
131
+
132
+ timer = fig .canvas .new_timer (1. ) # Test floats casting to int as needed.
133
+ timer .add_callback (FigureCanvasBase .key_press_event , fig .canvas , "q" )
134
+ # Trigger quitting upon draw.
135
+ fig .canvas .mpl_connect ("draw_event" , lambda event : timer .start ())
136
+ fig .canvas .mpl_connect ("close_event" , print )
137
+
138
+ result = io .BytesIO ()
139
+ fig .savefig (result , format = 'png' )
140
+
141
+ plt .show ()
142
+
143
+ # Ensure that the window is really closed.
144
+ plt .pause (0.5 )
145
+
146
+ # Test that saving works after interactive window is closed, but the figure
147
+ # is not deleted.
148
+ result_after = io .BytesIO ()
149
+ fig .savefig (result_after , format = 'png' )
150
+
151
+ if not backend .startswith ('qt5' ) and sys .platform == 'darwin' :
152
+ # FIXME: This should be enabled everywhere once Qt5 is fixed on macOS
153
+ # to not resize incorrectly.
154
+ assert_equal (result .getvalue (), result_after .getvalue ())
150
155
151
156
152
157
@pytest .mark .parametrize ("backend" , _get_testable_interactive_backends ())
@@ -161,7 +166,9 @@ def test_interactive_backend(backend, toolbar):
161
166
pytest .skip ("toolbar2 for macosx is buggy on Travis." )
162
167
163
168
proc = subprocess .run (
164
- [sys .executable , "-c" , _test_script ,
169
+ [sys .executable , "-c" ,
170
+ inspect .getsource (_test_interactive_impl )
171
+ + "\n _test_interactive_impl()" ,
165
172
json .dumps ({"toolbar" : toolbar })],
166
173
env = {** os .environ , "MPLBACKEND" : backend , "SOURCE_DATE_EPOCH" : "0" },
167
174
timeout = _test_timeout ,
@@ -172,64 +179,38 @@ def test_interactive_backend(backend, toolbar):
172
179
assert proc .stdout .count ("CloseEvent" ) == 1
173
180
174
181
175
- _thread_test_script = """\
176
- import json
177
- import sys
178
- import threading
182
+ # The source of this function gets extracted and run in another process, so it
183
+ # must be fully self-contained.
184
+ def _test_thread_impl ():
185
+ from concurrent .futures import ThreadPoolExecutor
186
+ import json
187
+ import sys
179
188
180
- from matplotlib import pyplot as plt, rcParams
181
- rcParams.update({
182
- "webagg.open_in_browser": False,
183
- "webagg.port_retries": 1,
184
- })
185
- if len(sys.argv) >= 2: # Second argument is json-encoded rcParams.
186
- rcParams.update(json.loads(sys.argv[1]))
189
+ from matplotlib import pyplot as plt , rcParams
187
190
188
- # Test artist creation and drawing does not crash from thread
189
- # No other guarantees!
190
- fig, ax = plt.subplots()
191
- # plt.pause needed vs plt.show(block=False) at least on toolbar2-tkagg
192
- plt.pause(0.5)
191
+ rcParams .update ({
192
+ "webagg.open_in_browser" : False ,
193
+ "webagg.port_retries" : 1 ,
194
+ })
195
+ if len (sys .argv ) >= 2 : # Second argument is json-encoded rcParams.
196
+ rcParams .update (json .loads (sys .argv [1 ]))
193
197
194
- exc_info = None
198
+ # Test artist creation and drawing does not crash from thread
199
+ # No other guarantees!
200
+ fig , ax = plt .subplots ()
201
+ # plt.pause needed vs plt.show(block=False) at least on toolbar2-tkagg
202
+ plt .pause (0.5 )
203
+
204
+ future = ThreadPoolExecutor ().submit (ax .plot , [1 , 3 , 6 ])
205
+ future .result () # Joins the thread; rethrows any exception.
206
+
207
+ fig .canvas .mpl_connect ("close_event" , print )
208
+ future = ThreadPoolExecutor ().submit (fig .canvas .draw )
209
+ plt .pause (0.5 ) # flush_events fails here on at least Tkagg (bpo-41176)
210
+ future .result () # Joins the thread; rethrows any exception.
211
+ plt .close ()
212
+ fig .canvas .flush_events () # pause doesn't process events after close
195
213
196
- def thread_artist_work():
197
- try:
198
- ax.plot([1,3,6])
199
- except:
200
- # Propagate error to main thread
201
- import sys
202
- global exc_info
203
- exc_info = sys.exc_info()
204
-
205
- def thread_draw_work():
206
- try:
207
- fig.canvas.draw()
208
- except:
209
- # Propagate error to main thread
210
- import sys
211
- global exc_info
212
- exc_info = sys.exc_info()
213
-
214
- t = threading.Thread(target=thread_artist_work)
215
- t.start()
216
- # artists never wait for the event loop to run, so just join
217
- t.join()
218
-
219
- if exc_info: # Raise thread error
220
- raise exc_info[1].with_traceback(exc_info[2])
221
-
222
- t = threading.Thread(target=thread_draw_work)
223
- fig.canvas.mpl_connect("close_event", print)
224
- t.start()
225
- plt.pause(0.5) # flush_events fails here on at least Tkagg (bpo-41176)
226
- t.join()
227
- plt.close()
228
- fig.canvas.flush_events() # pause doesn't process events after close
229
-
230
- if exc_info: # Raise thread error
231
- raise exc_info[1].with_traceback(exc_info[2])
232
- """
233
214
234
215
_thread_safe_backends = _get_testable_interactive_backends ()
235
216
# Known unsafe backends. Remove the xfails if they start to pass!
@@ -249,7 +230,8 @@ def thread_draw_work():
249
230
@pytest .mark .flaky (reruns = 3 )
250
231
def test_interactive_thread_safety (backend ):
251
232
proc = subprocess .run (
252
- [sys .executable , "-c" , _thread_test_script ],
233
+ [sys .executable , "-c" ,
234
+ inspect .getsource (_test_thread_impl ) + "\n _test_thread_impl()" ],
253
235
env = {** os .environ , "MPLBACKEND" : backend , "SOURCE_DATE_EPOCH" : "0" },
254
236
timeout = _test_timeout , check = True ,
255
237
stdout = subprocess .PIPE , universal_newlines = True )
@@ -261,9 +243,11 @@ def test_interactive_thread_safety(backend):
261
243
@pytest .mark .skipif (os .name == "nt" , reason = "Cannot send SIGINT on Windows." )
262
244
def test_webagg ():
263
245
pytest .importorskip ("tornado" )
264
- proc = subprocess .Popen ([sys .executable , "-c" , _test_script ],
265
- env = {** os .environ , "MPLBACKEND" : "webagg" ,
266
- "SOURCE_DATE_EPOCH" : "0" })
246
+ proc = subprocess .Popen (
247
+ [sys .executable , "-c" ,
248
+ inspect .getsource (_test_interactive_impl )
249
+ + "\n _test_interactive_impl()" ],
250
+ env = {** os .environ , "MPLBACKEND" : "webagg" , "SOURCE_DATE_EPOCH" : "0" })
267
251
url = "http://{}:{}" .format (
268
252
mpl .rcParams ["webagg.address" ], mpl .rcParams ["webagg.port" ])
269
253
timeout = time .perf_counter () + _test_timeout
0 commit comments