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
2720# Minimal smoke-testing of the backends for which the dependencies are
@@ -95,8 +88,8 @@ def _test_interactive_impl():
9588 "webagg.open_in_browser" : False ,
9689 "webagg.port_retries" : 1 ,
9790 })
98- if len ( sys . argv ) >= 2 : # Second argument is json-encoded rcParams.
99- rcParams .update (json .loads (sys .argv [1 ]))
91+
92+ rcParams .update (json .loads (sys .argv [1 ]))
10093 backend = plt .rcParams ["backend" ].lower ()
10194 assert_equal = TestCase ().assertEqual
10295 assert_raises = TestCase ().assertRaises
@@ -171,36 +164,23 @@ def test_interactive_backend(env, toolbar):
171164 if env ["MPLBACKEND" ] == "macosx" :
172165 if toolbar == "toolmanager" :
173166 pytest .skip ("toolmanager is not implemented for macosx." )
167+ proc = _run_helper (_test_interactive_impl ,
168+ json .dumps ({"toolbar" : toolbar }),
169+ timeout = _test_timeout ,
170+ ** env )
174171
175- proc = subprocess .run (
176- [sys .executable , "-c" ,
177- inspect .getsource (_test_interactive_impl )
178- + "\n _test_interactive_impl()" ,
179- json .dumps ({"toolbar" : toolbar })],
180- env = {** os .environ , "SOURCE_DATE_EPOCH" : "0" , ** env },
181- timeout = _test_timeout ,
182- stdout = subprocess .PIPE , universal_newlines = True )
183- if proc .returncode :
184- pytest .fail ("The subprocess returned with non-zero exit status "
185- f"{ proc .returncode } ." )
186172 assert proc .stdout .count ("CloseEvent" ) == 1
187173
188174
189- # The source of this function gets extracted and run in another process, so it
190- # must be fully self-contained.
191175def _test_thread_impl ():
192176 from concurrent .futures import ThreadPoolExecutor
193- import json
194- import sys
195177
196178 from matplotlib import pyplot as plt , rcParams
197179
198180 rcParams .update ({
199181 "webagg.open_in_browser" : False ,
200182 "webagg.port_retries" : 1 ,
201183 })
202- if len (sys .argv ) >= 2 : # Second argument is json-encoded rcParams.
203- rcParams .update (json .loads (sys .argv [1 ]))
204184
205185 # Test artist creation and drawing does not crash from thread
206186 # No other guarantees!
@@ -254,40 +234,65 @@ def _test_thread_impl():
254234@pytest .mark .parametrize ("env" , _thread_safe_backends )
255235@pytest .mark .flaky (reruns = 3 )
256236def test_interactive_thread_safety (env ):
257- proc = subprocess .run (
258- [sys .executable , "-c" ,
259- inspect .getsource (_test_thread_impl ) + "\n _test_thread_impl()" ],
260- env = {** os .environ , "SOURCE_DATE_EPOCH" : "0" , ** env },
261- timeout = _test_timeout , check = True ,
262- stdout = subprocess .PIPE , universal_newlines = True )
237+ proc = _run_helper (_test_thread_impl ,
238+ timeout = _test_timeout , ** env )
263239 assert proc .stdout .count ("CloseEvent" ) == 1
264240
265241
242+ def _impl_test_lazy_auto_backend_selection ():
243+ import matplotlib
244+ import matplotlib .pyplot as plt
245+ # just importing pyplot should not be enough to trigger resolution
246+ bk = dict .__getitem__ (matplotlib .rcParams , 'backend' )
247+ assert not isinstance (bk , str )
248+ assert plt ._backend_mod is None
249+ # but actually plotting should
250+ plt .plot (5 )
251+ assert plt ._backend_mod is not None
252+ bk = dict .__getitem__ (matplotlib .rcParams , 'backend' )
253+ assert isinstance (bk , str )
254+
255+
266256def test_lazy_auto_backend_selection ():
257+ _run_helper (_impl_test_lazy_auto_backend_selection ,
258+ timeout = _test_timeout )
267259
268- @_run_function_in_subprocess
269- def _impl ():
270- import matplotlib
271- import matplotlib .pyplot as plt
272- # just importing pyplot should not be enough to trigger resolution
273- bk = dict .__getitem__ (matplotlib .rcParams , 'backend' )
274- assert not isinstance (bk , str )
275- assert plt ._backend_mod is None
276- # but actually plotting should
277- plt .plot (5 )
278- assert plt ._backend_mod is not None
279- bk = dict .__getitem__ (matplotlib .rcParams , 'backend' )
280- assert isinstance (bk , str )
281-
282- proc = subprocess .run (
283- [sys .executable , "-c" , _impl ],
284- env = {** os .environ , "SOURCE_DATE_EPOCH" : "0" },
285- timeout = _test_timeout , check = True ,
286- stdout = subprocess .PIPE , universal_newlines = True )
287260
261+ def _implqt5agg ():
262+ import matplotlib .backends .backend_qt5agg # noqa
263+ import sys
264+
265+ assert 'PyQt6' not in sys .modules
266+ assert 'pyside6' not in sys .modules
267+ assert 'PyQt5' in sys .modules or 'pyside2' in sys .modules
268+
269+ import matplotlib .backends .backend_qt5
270+ matplotlib .backends .backend_qt5 .qApp
271+
272+
273+ def _implcairo ():
274+ import matplotlib .backends .backend_qt5cairo # noqa
275+ import sys
276+
277+ assert 'PyQt6' not in sys .modules
278+ assert 'pyside6' not in sys .modules
279+ assert 'PyQt5' in sys .modules or 'pyside2' in sys .modules
280+
281+ import matplotlib .backends .backend_qt5
282+ matplotlib .backends .backend_qt5 .qApp
283+
284+
285+ def _implcore ():
286+ import matplotlib .backends .backend_qt5
287+ import sys
288+
289+ assert 'PyQt6' not in sys .modules
290+ assert 'pyside6' not in sys .modules
291+ assert 'PyQt5' in sys .modules or 'pyside2' in sys .modules
292+ matplotlib .backends .backend_qt5 .qApp
288293
289- def test_qt5backends_uses_qt5 ():
290294
295+ def test_qt5backends_uses_qt5 ():
291296 qt5_bindings = [
292297 dep for dep in ['PyQt5' , 'pyside2' ]
293298 if importlib .util .find_spec (dep ) is not None
@@ -298,51 +303,10 @@ def test_qt5backends_uses_qt5():
298303 ]
299304 if len (qt5_bindings ) == 0 or len (qt6_bindings ) == 0 :
300305 pytest .skip ('need both QT6 and QT5 bindings' )
301-
302- @_run_function_in_subprocess
303- def _implagg ():
304- import matplotlib .backends .backend_qt5agg # noqa
305- import sys
306-
307- assert 'PyQt6' not in sys .modules
308- assert 'pyside6' not in sys .modules
309- assert 'PyQt5' in sys .modules or 'pyside2' in sys .modules
310-
311- @_run_function_in_subprocess
312- def _implcairo ():
313- import matplotlib .backends .backend_qt5cairo # noqa
314- import sys
315-
316- assert 'PyQt6' not in sys .modules
317- assert 'pyside6' not in sys .modules
318- assert 'PyQt5' in sys .modules or 'pyside2' in sys .modules
319-
320- @_run_function_in_subprocess
321- def _implcore ():
322- import matplotlib .backends .backend_qt5 # noqa
323- import sys
324-
325- assert 'PyQt6' not in sys .modules
326- assert 'pyside6' not in sys .modules
327- assert 'PyQt5' in sys .modules or 'pyside2' in sys .modules
328-
329- subprocess .run (
330- [sys .executable , "-c" , _implagg ],
331- env = {** os .environ , "SOURCE_DATE_EPOCH" : "0" },
332- timeout = _test_timeout , check = True ,
333- stdout = subprocess .PIPE , universal_newlines = True )
334-
335- subprocess .run (
336- [sys .executable , "-c" , _implcairo ],
337- env = {** os .environ , "SOURCE_DATE_EPOCH" : "0" },
338- timeout = _test_timeout , check = True ,
339- stdout = subprocess .PIPE , universal_newlines = True )
340-
341- subprocess .run (
342- [sys .executable , "-c" , _implcore ],
343- env = {** os .environ , "SOURCE_DATE_EPOCH" : "0" },
344- timeout = _test_timeout , check = True ,
345- stdout = subprocess .PIPE , universal_newlines = True )
306+ _run_helper (_implqt5agg , timeout = _test_timeout )
307+ if importlib .util .find_spec ('pycairo' ) is not None :
308+ _run_helper (_implcairo , timeout = _test_timeout )
309+ _run_helper (_implcore , timeout = _test_timeout )
346310
347311
348312def _impl_test_cross_Qt_imports ():
@@ -378,11 +342,11 @@ def test_cross_Qt_imports():
378342 for qt6 in qt6_bindings :
379343 for pair in ([qt5 , qt6 ], [qt6 , qt5 ]):
380344 try :
381- _run_helper (__name__ , _impl_test_cross_Qt_imports ,
345+ _run_helper (_impl_test_cross_Qt_imports ,
382346 * pair ,
383347 timeout = _test_timeout )
384348 except subprocess .CalledProcessError as ex :
385- # if segfauldt , carry on. We do try to warn the user they
349+ # if segfault , carry on. We do try to warn the user they
386350 # are doing something that we do not expect to work
387351 if ex .returncode == - 11 :
388352 continue
@@ -397,7 +361,7 @@ def test_webagg():
397361 proc = subprocess .Popen (
398362 [sys .executable , "-c" ,
399363 inspect .getsource (_test_interactive_impl )
400- + "\n _test_interactive_impl()" ],
364+ + "\n _test_interactive_impl()" , "{}" ],
401365 env = {** os .environ , "MPLBACKEND" : "webagg" , "SOURCE_DATE_EPOCH" : "0" })
402366 url = "http://{}:{}" .format (
403367 mpl .rcParams ["webagg.address" ], mpl .rcParams ["webagg.port" ])
@@ -419,37 +383,33 @@ def test_webagg():
419383 assert proc .wait (timeout = _test_timeout ) == 0
420384
421385
386+ def _lazy_headless ():
387+ import os
388+ import sys
389+
390+ # make it look headless
391+ os .environ .pop ('DISPLAY' , None )
392+ os .environ .pop ('WAYLAND_DISPLAY' , None )
393+
394+ # we should fast-track to Agg
395+ import matplotlib .pyplot as plt
396+ plt .get_backend () == 'agg'
397+ assert 'PyQt5' not in sys .modules
398+
399+ # make sure we really have pyqt installed
400+ import PyQt5 # noqa
401+ assert 'PyQt5' in sys .modules
402+
403+ # try to switch and make sure we fail with ImportError
404+ try :
405+ plt .switch_backend ('qt5agg' )
406+ except ImportError :
407+ ...
408+ else :
409+ sys .exit (1 )
410+
411+
422412@pytest .mark .skipif (sys .platform != "linux" , reason = "this a linux-only test" )
423413@pytest .mark .backend ('QtAgg' , skip_on_importerror = True )
424414def test_lazy_linux_headless ():
425- test_script = """
426- import os
427- import sys
428-
429- # make it look headless
430- os.environ.pop('DISPLAY', None)
431- os.environ.pop('WAYLAND_DISPLAY', None)
432-
433- # we should fast-track to Agg
434- import matplotlib.pyplot as plt
435- plt.get_backend() == 'agg'
436- assert 'PyQt5' not in sys.modules
437-
438- # make sure we really have pyqt installed
439- import PyQt5
440- assert 'PyQt5' in sys.modules
441-
442- # try to switch and make sure we fail with ImportError
443- try:
444- plt.switch_backend('qt5agg')
445- except ImportError:
446- ...
447- else:
448- sys.exit(1)
449-
450- """
451- proc = subprocess .run ([sys .executable , "-c" , test_script ],
452- env = {** os .environ , "MPLBACKEND" : "" })
453- if proc .returncode :
454- pytest .fail ("The subprocess returned with non-zero exit status "
455- f"{ proc .returncode } ." )
415+ proc = _run_helper (_lazy_headless , timeout = _test_timeout , MPLBACKEND = "" )
0 commit comments