11import importlib
22import importlib .util
3+ import inspect
34import json
45import os
56import signal
@@ -61,92 +62,96 @@ def _get_testable_interactive_backends():
6162 return backends
6263
6364
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.
6470# Using a timer not only allows testing of timers (on other backends), but is
6571# also necessary on gtk3 and wx, where a direct call to key_press_event("q")
6672# from draw_event causes breakage due to the canvas widget being deleted too
6773# early. Also, gtk3 redefines key_press_event with a different signature, so
6874# 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 ())
150155
151156
152157@pytest .mark .parametrize ("backend" , _get_testable_interactive_backends ())
@@ -161,7 +166,9 @@ def test_interactive_backend(backend, toolbar):
161166 pytest .skip ("toolbar2 for macosx is buggy on Travis." )
162167
163168 proc = subprocess .run (
164- [sys .executable , "-c" , _test_script ,
169+ [sys .executable , "-c" ,
170+ inspect .getsource (_test_interactive_impl )
171+ + "\n _test_interactive_impl()" ,
165172 json .dumps ({"toolbar" : toolbar })],
166173 env = {** os .environ , "MPLBACKEND" : backend , "SOURCE_DATE_EPOCH" : "0" },
167174 timeout = _test_timeout ,
@@ -172,64 +179,38 @@ def test_interactive_backend(backend, toolbar):
172179 assert proc .stdout .count ("CloseEvent" ) == 1
173180
174181
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
179188
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
187190
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 ]))
193197
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
195213
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- """
233214
234215_thread_safe_backends = _get_testable_interactive_backends ()
235216# Known unsafe backends. Remove the xfails if they start to pass!
@@ -249,7 +230,8 @@ def thread_draw_work():
249230@pytest .mark .flaky (reruns = 3 )
250231def test_interactive_thread_safety (backend ):
251232 proc = subprocess .run (
252- [sys .executable , "-c" , _thread_test_script ],
233+ [sys .executable , "-c" ,
234+ inspect .getsource (_test_thread_impl ) + "\n _test_thread_impl()" ],
253235 env = {** os .environ , "MPLBACKEND" : backend , "SOURCE_DATE_EPOCH" : "0" },
254236 timeout = _test_timeout , check = True ,
255237 stdout = subprocess .PIPE , universal_newlines = True )
@@ -261,9 +243,11 @@ def test_interactive_thread_safety(backend):
261243@pytest .mark .skipif (os .name == "nt" , reason = "Cannot send SIGINT on Windows." )
262244def test_webagg ():
263245 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" })
267251 url = "http://{}:{}" .format (
268252 mpl .rcParams ["webagg.address" ], mpl .rcParams ["webagg.port" ])
269253 timeout = time .perf_counter () + _test_timeout
0 commit comments