77import signal
88import subprocess
99import sys
10- import textwrap
1110import time
1211import urllib .request
1312
1413import pytest
1514
1615import matplotlib as mpl
1716from matplotlib import _c_internal_utils
18-
19-
20- def _run_function_in_subprocess (func ):
21- func_source = textwrap .dedent (inspect .getsource (func ))
22- func_source = func_source [func_source .index ('\n ' )+ 1 :] # Remove decorator
23- return f"{ func_source } \n { func .__name__ } ()"
24-
17+ from matplotlib .testing import subprocess_run_helper as _run_helper
2518
2619# Minimal smoke-testing of the backends for which the dependencies are
2720# PyPI-installable on CI. They are not available for all tested Python
@@ -94,8 +87,8 @@ def _test_interactive_impl():
9487 "webagg.open_in_browser" : False ,
9588 "webagg.port_retries" : 1 ,
9689 })
97- if len ( sys . argv ) >= 2 : # Second argument is json-encoded rcParams.
98- rcParams .update (json .loads (sys .argv [1 ]))
90+
91+ rcParams .update (json .loads (sys .argv [1 ]))
9992 backend = plt .rcParams ["backend" ].lower ()
10093 assert_equal = TestCase ().assertEqual
10194 assert_raises = TestCase ().assertRaises
@@ -170,36 +163,23 @@ def test_interactive_backend(env, toolbar):
170163 if env ["MPLBACKEND" ] == "macosx" :
171164 if toolbar == "toolmanager" :
172165 pytest .skip ("toolmanager is not implemented for macosx." )
166+ proc = _run_helper (__name__ , _test_interactive_impl ,
167+ json .dumps ({"toolbar" : toolbar }),
168+ timeout = _test_timeout ,
169+ ** env )
173170
174- proc = subprocess .run (
175- [sys .executable , "-c" ,
176- inspect .getsource (_test_interactive_impl )
177- + "\n _test_interactive_impl()" ,
178- json .dumps ({"toolbar" : toolbar })],
179- env = {** os .environ , "SOURCE_DATE_EPOCH" : "0" , ** env },
180- timeout = _test_timeout ,
181- stdout = subprocess .PIPE , universal_newlines = True )
182- if proc .returncode :
183- pytest .fail ("The subprocess returned with non-zero exit status "
184- f"{ proc .returncode } ." )
185171 assert proc .stdout .count ("CloseEvent" ) == 1
186172
187173
188- # The source of this function gets extracted and run in another process, so it
189- # must be fully self-contained.
190174def _test_thread_impl ():
191175 from concurrent .futures import ThreadPoolExecutor
192- import json
193- import sys
194176
195177 from matplotlib import pyplot as plt , rcParams
196178
197179 rcParams .update ({
198180 "webagg.open_in_browser" : False ,
199181 "webagg.port_retries" : 1 ,
200182 })
201- if len (sys .argv ) >= 2 : # Second argument is json-encoded rcParams.
202- rcParams .update (json .loads (sys .argv [1 ]))
203183
204184 # Test artist creation and drawing does not crash from thread
205185 # No other guarantees!
@@ -253,40 +233,65 @@ def _test_thread_impl():
253233@pytest .mark .parametrize ("env" , _thread_safe_backends )
254234@pytest .mark .flaky (reruns = 3 )
255235def test_interactive_thread_safety (env ):
256- proc = subprocess .run (
257- [sys .executable , "-c" ,
258- inspect .getsource (_test_thread_impl ) + "\n _test_thread_impl()" ],
259- env = {** os .environ , "SOURCE_DATE_EPOCH" : "0" , ** env },
260- timeout = _test_timeout , check = True ,
261- stdout = subprocess .PIPE , universal_newlines = True )
236+ proc = _run_helper (__name__ , _test_thread_impl ,
237+ timeout = _test_timeout , ** env )
262238 assert proc .stdout .count ("CloseEvent" ) == 1
263239
264240
241+ def _impl_test_lazy_auto_backend_selection ():
242+ import matplotlib
243+ import matplotlib .pyplot as plt
244+ # just importing pyplot should not be enough to trigger resolution
245+ bk = dict .__getitem__ (matplotlib .rcParams , 'backend' )
246+ assert not isinstance (bk , str )
247+ assert plt ._backend_mod is None
248+ # but actually plotting should
249+ plt .plot (5 )
250+ assert plt ._backend_mod is not None
251+ bk = dict .__getitem__ (matplotlib .rcParams , 'backend' )
252+ assert isinstance (bk , str )
253+
254+
265255def test_lazy_auto_backend_selection ():
256+ _run_helper (__name__ , _impl_test_lazy_auto_backend_selection ,
257+ timeout = _test_timeout )
266258
267- @_run_function_in_subprocess
268- def _impl ():
269- import matplotlib
270- import matplotlib .pyplot as plt
271- # just importing pyplot should not be enough to trigger resolution
272- bk = dict .__getitem__ (matplotlib .rcParams , 'backend' )
273- assert not isinstance (bk , str )
274- assert plt ._backend_mod is None
275- # but actually plotting should
276- plt .plot (5 )
277- assert plt ._backend_mod is not None
278- bk = dict .__getitem__ (matplotlib .rcParams , 'backend' )
279- assert isinstance (bk , str )
280-
281- proc = subprocess .run (
282- [sys .executable , "-c" , _impl ],
283- env = {** os .environ , "SOURCE_DATE_EPOCH" : "0" },
284- timeout = _test_timeout , check = True ,
285- stdout = subprocess .PIPE , universal_newlines = True )
286259
260+ def _implqt5agg ():
261+ import matplotlib .backends .backend_qt5agg # noqa
262+ import sys
263+
264+ assert 'PyQt6' not in sys .modules
265+ assert 'pyside6' not in sys .modules
266+ assert 'PyQt5' in sys .modules or 'pyside2' in sys .modules
267+
268+ import matplotlib .backends .backend_qt5
269+ matplotlib .backends .backend_qt5 .qApp
270+
271+
272+ def _implcairo ():
273+ import matplotlib .backends .backend_qt5cairo # noqa
274+ import sys
275+
276+ assert 'PyQt6' not in sys .modules
277+ assert 'pyside6' not in sys .modules
278+ assert 'PyQt5' in sys .modules or 'pyside2' in sys .modules
279+
280+ import matplotlib .backends .backend_qt5
281+ matplotlib .backends .backend_qt5 .qApp
282+
283+
284+ def _implcore ():
285+ import matplotlib .backends .backend_qt5
286+ import sys
287+
288+ assert 'PyQt6' not in sys .modules
289+ assert 'pyside6' not in sys .modules
290+ assert 'PyQt5' in sys .modules or 'pyside2' in sys .modules
291+ matplotlib .backends .backend_qt5 .qApp
287292
288- def test_qt5backends_uses_qt5 ():
289293
294+ def test_qt5backends_uses_qt5 ():
290295 qt5_bindings = [
291296 dep for dep in ['PyQt5' , 'pyside2' ]
292297 if importlib .util .find_spec (dep ) is not None
@@ -297,51 +302,9 @@ def test_qt5backends_uses_qt5():
297302 ]
298303 if len (qt5_bindings ) == 0 or len (qt6_bindings ) == 0 :
299304 pytest .skip ('need both QT6 and QT5 bindings' )
300-
301- @_run_function_in_subprocess
302- def _implagg ():
303- import matplotlib .backends .backend_qt5agg # noqa
304- import sys
305-
306- assert 'PyQt6' not in sys .modules
307- assert 'pyside6' not in sys .modules
308- assert 'PyQt5' in sys .modules or 'pyside2' in sys .modules
309-
310- @_run_function_in_subprocess
311- def _implcairo ():
312- import matplotlib .backends .backend_qt5cairo # noqa
313- import sys
314-
315- assert 'PyQt6' not in sys .modules
316- assert 'pyside6' not in sys .modules
317- assert 'PyQt5' in sys .modules or 'pyside2' in sys .modules
318-
319- @_run_function_in_subprocess
320- def _implcore ():
321- import matplotlib .backends .backend_qt5 # noqa
322- import sys
323-
324- assert 'PyQt6' not in sys .modules
325- assert 'pyside6' not in sys .modules
326- assert 'PyQt5' in sys .modules or 'pyside2' in sys .modules
327-
328- subprocess .run (
329- [sys .executable , "-c" , _implagg ],
330- env = {** os .environ , "SOURCE_DATE_EPOCH" : "0" },
331- timeout = _test_timeout , check = True ,
332- stdout = subprocess .PIPE , universal_newlines = True )
333-
334- subprocess .run (
335- [sys .executable , "-c" , _implcairo ],
336- env = {** os .environ , "SOURCE_DATE_EPOCH" : "0" },
337- timeout = _test_timeout , check = True ,
338- stdout = subprocess .PIPE , universal_newlines = True )
339-
340- subprocess .run (
341- [sys .executable , "-c" , _implcore ],
342- env = {** os .environ , "SOURCE_DATE_EPOCH" : "0" },
343- timeout = _test_timeout , check = True ,
344- stdout = subprocess .PIPE , universal_newlines = True )
305+ _run_helper (__name__ , _implqt5agg , timeout = _test_timeout )
306+ _run_helper (__name__ , _implcairo , timeout = _test_timeout )
307+ _run_helper (__name__ , _implcore , timeout = _test_timeout )
345308
346309
347310@pytest .mark .skipif ('TF_BUILD' in os .environ ,
@@ -352,7 +315,7 @@ def test_webagg():
352315 proc = subprocess .Popen (
353316 [sys .executable , "-c" ,
354317 inspect .getsource (_test_interactive_impl )
355- + "\n _test_interactive_impl()" ],
318+ + "\n _test_interactive_impl()" , "{}" ],
356319 env = {** os .environ , "MPLBACKEND" : "webagg" , "SOURCE_DATE_EPOCH" : "0" })
357320 url = "http://{}:{}" .format (
358321 mpl .rcParams ["webagg.address" ], mpl .rcParams ["webagg.port" ])
@@ -374,37 +337,35 @@ def test_webagg():
374337 assert proc .wait (timeout = _test_timeout ) == 0
375338
376339
340+ def _lazy_headless ():
341+ import os
342+ import sys
343+
344+ # make it look headless
345+ os .environ .pop ('DISPLAY' , None )
346+ os .environ .pop ('WAYLAND_DISPLAY' , None )
347+
348+ # we should fast-track to Agg
349+ import matplotlib .pyplot as plt
350+ plt .get_backend () == 'agg'
351+ assert 'PyQt5' not in sys .modules
352+
353+ # make sure we really have pyqt installed
354+ import PyQt5 # noqa
355+ assert 'PyQt5' in sys .modules
356+
357+ # try to switch and make sure we fail with ImportError
358+ try :
359+ plt .switch_backend ('qt5agg' )
360+ except ImportError :
361+ ...
362+ else :
363+ sys .exit (1 )
364+
365+
377366@pytest .mark .skipif (sys .platform != "linux" , reason = "this a linux-only test" )
378367@pytest .mark .backend ('QtAgg' , skip_on_importerror = True )
379368def test_lazy_linux_headless ():
380- test_script = """
381- import os
382- import sys
383-
384- # make it look headless
385- os.environ.pop('DISPLAY', None)
386- os.environ.pop('WAYLAND_DISPLAY', None)
387-
388- # we should fast-track to Agg
389- import matplotlib.pyplot as plt
390- plt.get_backend() == 'agg'
391- assert 'PyQt5' not in sys.modules
392-
393- # make sure we really have pyqt installed
394- import PyQt5
395- assert 'PyQt5' in sys.modules
396-
397- # try to switch and make sure we fail with ImportError
398- try:
399- plt.switch_backend('qt5agg')
400- except ImportError:
401- ...
402- else:
403- sys.exit(1)
404-
405- """
406- proc = subprocess .run ([sys .executable , "-c" , test_script ],
407- env = {** os .environ , "MPLBACKEND" : "" })
408- if proc .returncode :
409- pytest .fail ("The subprocess returned with non-zero exit status "
410- f"{ proc .returncode } ." )
369+ proc = _run_helper (__name__ , _lazy_headless ,
370+ timeout = _test_timeout ,
371+ MPLBACKEND = "" )
0 commit comments