diff --git a/.appveyor.yml b/.appveyor.yml index 0e8e0a553fd4..c637dae4d869 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -60,6 +60,8 @@ install: # pull pywin32 from conda because on py38 there is something wrong with finding # the dlls when insalled from pip - conda install -c conda-forge pywin32 + # install pyqt from conda-forge + - conda install -c conda-forge pyqt - echo %PYTHON_VERSION% %TARGET_ARCH% # Install dependencies from PyPI. - python -m pip install --upgrade -r requirements/testing/all.txt %EXTRAREQS% %PINNEDVERS% diff --git a/lib/matplotlib/backends/qt_compat.py b/lib/matplotlib/backends/qt_compat.py index 6fbe103ce03b..9e320e341c4c 100644 --- a/lib/matplotlib/backends/qt_compat.py +++ b/lib/matplotlib/backends/qt_compat.py @@ -228,8 +228,20 @@ def _maybe_allow_interrupt(qapp): rsock.fileno(), _enum('QtCore.QSocketNotifier.Type').Read ) + # We do not actually care about this value other than running some + # Python code to ensure that the interpreter has a chance to handle the + # signal in Python land. We also need to drain the socket because it + # will be written to as part of the wakeup! There are some cases where + # this may fire too soon / more than once on Windows so we should be + # forgiving about reading an empty socket. + rsock.setblocking(False) # Clear the socket to re-arm the notifier. - sn.activated.connect(lambda *args: rsock.recv(1)) + @sn.activated.connect + def _may_clear_sock(*args): + try: + rsock.recv(1) + except BlockingIOError: + pass def handle(*args): nonlocal handler_args diff --git a/lib/matplotlib/tests/test_backend_qt.py b/lib/matplotlib/tests/test_backend_qt.py index b22772c35f78..7bdc01fe0223 100644 --- a/lib/matplotlib/tests/test_backend_qt.py +++ b/lib/matplotlib/tests/test_backend_qt.py @@ -24,6 +24,9 @@ pytestmark = pytest.mark.skip('No usable Qt bindings') +_test_timeout = 60 # A reasonably safe value for slower architectures. + + @pytest.fixture def qt_core(request): backend, = request.node.get_closest_marker('backend').args @@ -33,19 +36,6 @@ def qt_core(request): return QtCore -@pytest.fixture -def platform_simulate_ctrl_c(request): - import signal - from functools import partial - - if hasattr(signal, "CTRL_C_EVENT"): - win32api = pytest.importorskip('win32api') - return partial(win32api.GenerateConsoleCtrlEvent, 0, 0) - else: - # we're not on windows - return partial(os.kill, os.getpid(), signal.SIGINT) - - @pytest.mark.backend('QtAgg', skip_on_importerror=True) def test_fig_close(): @@ -64,50 +54,143 @@ def test_fig_close(): assert init_figs == Gcf.figs -@pytest.mark.backend('QtAgg', skip_on_importerror=True) -@pytest.mark.parametrize("target, kwargs", [ - (plt.show, {"block": True}), - (plt.pause, {"interval": 10}) -]) -def test_sigint(qt_core, platform_simulate_ctrl_c, target, - kwargs): - plt.figure() - def fire_signal(): - platform_simulate_ctrl_c() +class WaitForStringPopen(subprocess.Popen): + """ + A Popen that passes flags that allow triggering KeyboardInterrupt. + """ - qt_core.QTimer.singleShot(100, fire_signal) - with pytest.raises(KeyboardInterrupt): + def __init__(self, *args, **kwargs): + if sys.platform == 'win32': + kwargs['creationflags'] = subprocess.CREATE_NEW_CONSOLE + super().__init__( + *args, **kwargs, + # Force Agg so that each test can switch to its desired Qt backend. + env={**os.environ, "MPLBACKEND": "Agg", "SOURCE_DATE_EPOCH": "0"}, + stdout=subprocess.PIPE, universal_newlines=True) + + def wait_for(self, terminator): + """Read until the terminator is reached.""" + buf = '' + while True: + c = self.stdout.read(1) + if not c: + raise RuntimeError( + f'Subprocess died before emitting expected {terminator!r}') + buf += c + if buf.endswith(terminator): + return + + +def _test_sigint_impl(backend, target_name, kwargs): + import sys + import matplotlib.pyplot as plt + import os + import threading + + plt.switch_backend(backend) + from matplotlib.backends.qt_compat import QtCore + + def interupter(): + if sys.platform == 'win32': + import win32api + win32api.GenerateConsoleCtrlEvent(0, 0) + else: + import signal + os.kill(os.getpid(), signal.SIGINT) + + target = getattr(plt, target_name) + timer = threading.Timer(1, interupter) + fig = plt.figure() + fig.canvas.mpl_connect( + 'draw_event', + lambda *args: print('DRAW', flush=True) + ) + fig.canvas.mpl_connect( + 'draw_event', + lambda *args: timer.start() + ) + try: target(**kwargs) + except KeyboardInterrupt: + print('SUCCESS', flush=True) @pytest.mark.backend('QtAgg', skip_on_importerror=True) @pytest.mark.parametrize("target, kwargs", [ - (plt.show, {"block": True}), - (plt.pause, {"interval": 10}) + ('show', {'block': True}), + ('pause', {'interval': 10}) ]) -def test_other_signal_before_sigint(qt_core, platform_simulate_ctrl_c, - target, kwargs): - plt.figure() +def test_sigint(target, kwargs): + backend = plt.get_backend() + proc = WaitForStringPopen( + [sys.executable, "-c", + inspect.getsource(_test_sigint_impl) + + f"\n_test_sigint_impl({backend!r}, {target!r}, {kwargs!r})"]) + try: + proc.wait_for('DRAW') + stdout, _ = proc.communicate(timeout=_test_timeout) + except: + proc.kill() + stdout, _ = proc.communicate() + raise + print(stdout) + assert 'SUCCESS' in stdout + + +def _test_other_signal_before_sigint_impl(backend, target_name, kwargs): + import signal + import sys + import matplotlib.pyplot as plt + plt.switch_backend(backend) + from matplotlib.backends.qt_compat import QtCore - sigcld_caught = False - def custom_sigpipe_handler(signum, frame): - nonlocal sigcld_caught - sigcld_caught = True - signal.signal(signal.SIGCHLD, custom_sigpipe_handler) + target = getattr(plt, target_name) - def fire_other_signal(): - os.kill(os.getpid(), signal.SIGCHLD) + fig = plt.figure() + fig.canvas.mpl_connect('draw_event', + lambda *args: print('DRAW', flush=True)) - def fire_sigint(): - platform_simulate_ctrl_c() + timer = fig.canvas.new_timer(interval=1) + timer.single_shot = True + timer.add_callback(print, 'SIGUSR1', flush=True) - qt_core.QTimer.singleShot(50, fire_other_signal) - qt_core.QTimer.singleShot(100, fire_sigint) + def custom_signal_handler(signum, frame): + timer.start() + signal.signal(signal.SIGUSR1, custom_signal_handler) - with pytest.raises(KeyboardInterrupt): + try: target(**kwargs) + except KeyboardInterrupt: + print('SUCCESS', flush=True) - assert sigcld_caught + +@pytest.mark.skipif(sys.platform == 'win32', + reason='No other signal available to send on Windows') +@pytest.mark.backend('QtAgg', skip_on_importerror=True) +@pytest.mark.parametrize("target, kwargs", [ + ('show', {'block': True}), + ('pause', {'interval': 10}) +]) +def test_other_signal_before_sigint(target, kwargs): + backend = plt.get_backend() + proc = WaitForStringPopen( + [sys.executable, "-c", + inspect.getsource(_test_other_signal_before_sigint_impl) + + "\n_test_other_signal_before_sigint_impl(" + f"{backend!r}, {target!r}, {kwargs!r})"]) + try: + proc.wait_for('DRAW') + os.kill(proc.pid, signal.SIGUSR1) + proc.wait_for('SIGUSR1') + os.kill(proc.pid, signal.SIGINT) + stdout, _ = proc.communicate(timeout=_test_timeout) + except: + proc.kill() + stdout, _ = proc.communicate() + raise + print(stdout) + assert 'SUCCESS' in stdout + plt.figure() @pytest.mark.backend('Qt5Agg') @@ -140,29 +223,31 @@ def custom_handler(signum, frame): signal.signal(signal.SIGINT, custom_handler) - # mainloop() sets SIGINT, starts Qt event loop (which triggers timer and - # exits) and then mainloop() resets SIGINT - matplotlib.backends.backend_qt._BackendQT.mainloop() + try: + # mainloop() sets SIGINT, starts Qt event loop (which triggers timer + # and exits) and then mainloop() resets SIGINT + matplotlib.backends.backend_qt._BackendQT.mainloop() - # Assert: signal handler during loop execution is changed - # (can't test equality with func) - assert event_loop_handler != custom_handler + # Assert: signal handler during loop execution is changed + # (can't test equality with func) + assert event_loop_handler != custom_handler - # Assert: current signal handler is the same as the one we set before - assert signal.getsignal(signal.SIGINT) == custom_handler + # Assert: current signal handler is the same as the one we set before + assert signal.getsignal(signal.SIGINT) == custom_handler - # Repeat again to test that SIG_DFL and SIG_IGN will not be overridden - for custom_handler in (signal.SIG_DFL, signal.SIG_IGN): - qt_core.QTimer.singleShot(0, fire_signal_and_quit) - signal.signal(signal.SIGINT, custom_handler) + # Repeat again to test that SIG_DFL and SIG_IGN will not be overridden + for custom_handler in (signal.SIG_DFL, signal.SIG_IGN): + qt_core.QTimer.singleShot(0, fire_signal_and_quit) + signal.signal(signal.SIGINT, custom_handler) - _BackendQT5.mainloop() + _BackendQT5.mainloop() - assert event_loop_handler == custom_handler - assert signal.getsignal(signal.SIGINT) == custom_handler + assert event_loop_handler == custom_handler + assert signal.getsignal(signal.SIGINT) == custom_handler - # Reset SIGINT handler to what it was before the test - signal.signal(signal.SIGINT, original_handler) + finally: + # Reset SIGINT handler to what it was before the test + signal.signal(signal.SIGINT, original_handler) @pytest.mark.parametrize( @@ -548,8 +633,6 @@ def _get_testable_qt_backends(): envs.append(pytest.param(env, marks=marks, id=str(env))) return envs -_test_timeout = 60 # A reasonably safe value for slower architectures. - @pytest.mark.parametrize("env", _get_testable_qt_backends()) def test_enums_available(env): pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy