From 70f6ea3722a7a52cd1e9cc36dbe24cc531fa5309 Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Thu, 8 Dec 2022 09:53:05 -0800 Subject: [PATCH 01/18] Initial notes --- ipykernel/eventloops.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/ipykernel/eventloops.py b/ipykernel/eventloops.py index b2a312d90..811e31985 100644 --- a/ipykernel/eventloops.py +++ b/ipykernel/eventloops.py @@ -33,7 +33,7 @@ def process_stream_events(): # if there were any, wake it up if kernel.shell_stream.flush(limit=1): kernel._qt_notifier.setEnabled(False) - kernel.app.quit() + print('ZMQ events to process; quitting Qt event loop.') if not hasattr(kernel, "_qt_notifier"): fd = kernel.shell_stream.getsockopt(zmq.FD) @@ -110,17 +110,19 @@ def _loop_qt(app): rather than if the eventloop is actually running. """ app._in_event_loop = True - app.exec_() + print('Qt loop exited.') app._in_event_loop = False @register_integration("qt4") def loop_qt4(kernel): """Start a kernel with PyQt4 event loop integration.""" + print(f'Starting Qt4 loop with {os.environ.get("QT_API", None)=}') - from IPython.external.qt_for_kernel import QtGui + from IPython.external.qt_for_kernel import QtGui, QtCore, QT_API from IPython.lib.guisupport import get_app_qt4 + print(f'`qt_for_kernel` says {QT_API=}') kernel.app = get_app_qt4([" "]) if isinstance(kernel.app, QtGui.QApplication): kernel.app.setQuitOnLastWindowClosed(False) @@ -131,6 +133,7 @@ def loop_qt4(kernel): @register_integration("qt", "qt5") def loop_qt5(kernel): + print(f'Starting Qt5 loop with {os.environ.get("QT_API", None)=}') """Start a kernel with PyQt5 event loop integration.""" if os.environ.get("QT_API", None) is None: try: @@ -151,6 +154,7 @@ def loop_qt5(kernel): @loop_qt4.exit @loop_qt5.exit def loop_qt_exit(kernel): + print('Request Qt event loop exit.') kernel.app.exit() From 4bbc59a41a03a179d5e0b48d5b8baf23510d4147 Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Thu, 8 Dec 2022 11:11:03 -0800 Subject: [PATCH 02/18] ENH: Support for `PyQt6` and `PySide6`. - Distinguish between specific version requests and the generic one. - Use a `QEventLoop` instance to properly keep windows open between event loop calls. This is "instrumented" with print statements to follow the flow --- ipykernel/eventloops.py | 109 +++++++++++++++++++++++++++++++++------- 1 file changed, 92 insertions(+), 17 deletions(-) diff --git a/ipykernel/eventloops.py b/ipykernel/eventloops.py index 811e31985..8555a5b34 100644 --- a/ipykernel/eventloops.py +++ b/ipykernel/eventloops.py @@ -34,6 +34,7 @@ def process_stream_events(): if kernel.shell_stream.flush(limit=1): kernel._qt_notifier.setEnabled(False) print('ZMQ events to process; quitting Qt event loop.') + kernel.app.qt_event_loop.quit() if not hasattr(kernel, "_qt_notifier"): fd = kernel.shell_stream.getsockopt(zmq.FD) @@ -102,36 +103,78 @@ def exit_decorator(exit_func): return decorator -def _loop_qt(app): +def _loop_qt(kernel): """Inner-loop for running the Qt eventloop Pulled from guisupport.start_event_loop in IPython < 5.2, since IPython 5.2 only checks `get_ipython().active_eventloop` is defined, rather than if the eventloop is actually running. """ - app._in_event_loop = True + if not hasattr(kernel, 'app'): + # First time through, get the `QApplication` instance, create a `QEventLoop`, and install + # hook to stop the event loop when there's ZMQ traffic to process. Subsequent attempts to + # change the version will fail at `enable_gui` below anyway. + + # IPython's Qt importing mechanism will use the `QT_API` environment variable to choose the + # version. + try: + from IPython.external.qt_for_kernel import QtGui, QtCore, QT_API + success = True + except ImportError: + success = False + if not success: + raise RuntimeError('boo') + from IPython.lib.guisupport import get_app_qt4 + + print(f'`qt_for_kernel` says {QT_API=}') + kernel.app = get_app_qt4([" "]) + if isinstance(kernel.app, QtGui.QApplication): + kernel.app.setQuitOnLastWindowClosed(False) + + kernel.app.qt_event_loop = QtCore.QEventLoop(kernel.app) + + _notify_stream_qt(kernel) # install hook to stop event loop. + # Start the event loop. + kernel.app._in_event_loop = True + # `exec` blocks until there's ZMQ activity. + el = kernel.app.qt_event_loop # for brevity + el.exec() if hasattr(el, 'exec') else el.exec_() print('Qt loop exited.') - app._in_event_loop = False + kernel.app._in_event_loop = False + +# The user can generically request `qt` or a specific Qt version, e.g. `qt6`. For a generic Qt +# request, we let the mechanism in IPython choose the best available version by leaving the `QT_API` +# environment variable blank. +# +# For specific versions, we check to see whether the PyQt or PySide implementations are present and +# set `QT_API` accordingly to indicate to IPython which version we want. If neither implementation +# is present, we leave the environment variable set so IPython will generate a helpful error +# message. +# +# NOTE: if the environment variable is already set, it will be used unchanged, regardless of what +# the user requested. @register_integration("qt4") def loop_qt4(kernel): - """Start a kernel with PyQt4 event loop integration.""" print(f'Starting Qt4 loop with {os.environ.get("QT_API", None)=}') + """Start a kernel with PyQt4 event loop integration.""" + if os.environ.get("QT_API", None) is None: + try: + import PyQt # noqa - from IPython.external.qt_for_kernel import QtGui, QtCore, QT_API - from IPython.lib.guisupport import get_app_qt4 - - print(f'`qt_for_kernel` says {QT_API=}') - kernel.app = get_app_qt4([" "]) - if isinstance(kernel.app, QtGui.QApplication): - kernel.app.setQuitOnLastWindowClosed(False) - _notify_stream_qt(kernel) + os.environ["QT_API"] = "pyqt" + except ImportError: + try: + import PySide # noqa - _loop_qt(kernel.app) + os.environ["QT_API"] = "pyside" + except ImportError: + os.environ["QT_API"] = "pyqt" # Set it back so IPython gives an error + _loop_qt(kernel) -@register_integration("qt", "qt5") +@register_integration("qt5") def loop_qt5(kernel): print(f'Starting Qt5 loop with {os.environ.get("QT_API", None)=}') """Start a kernel with PyQt5 event loop integration.""" @@ -146,15 +189,45 @@ def loop_qt5(kernel): os.environ["QT_API"] = "pyside2" except ImportError: - os.environ["QT_API"] = "pyqt5" - return loop_qt4(kernel) + os.environ["QT_API"] = "pyqt5" # Set it back so IPython gives an error + _loop_qt(kernel) + + +@register_integration("qt6") +def loop_qt6(kernel): + print(f'Starting Qt6 loop with {os.environ.get("QT_API", None)=}') + """Start a kernel with PyQt6 event loop integration.""" + if os.environ.get("QT_API", None) is None: + try: + print('Trying to import PyQt6...') + import PyQt6 # noqa + + os.environ["QT_API"] = "pyqt6" + except ImportError: + try: + print('Trying to import PySide6...') + import PySide6 # noqa + + os.environ["QT_API"] = "pyside6" + except ImportError: + os.environ["QT_API"] = None + _loop_qt(kernel) + + +@register_integration("qt") +def loop_qt(kernel): + print(f'Starting Qtx loop with {os.environ.get("QT_API", None)=}') + """Generic request to start event loop; lets IPython mechanism choose the version.""" + _loop_qt(kernel) # exit and watch are the same for qt 4 and 5 +@loop_qt.exit @loop_qt4.exit @loop_qt5.exit +@loop_qt6.exit def loop_qt_exit(kernel): - print('Request Qt event loop exit.') + print('Request Qt app exit (will close all open windows).') kernel.app.exit() @@ -465,3 +538,5 @@ def enable_gui(gui, kernel=None): if loop and kernel.eventloop is not None and kernel.eventloop is not loop: raise RuntimeError("Cannot activate multiple GUI eventloops") kernel.eventloop = loop + # We set `eventloop`; the function the user chose is executed in `Kernel.enter_eventloop`, thus + # any exceptions raised during the event loop will not be shown in the client. From 01049505076efc3a23c08db0f296b743de4a2147 Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Thu, 8 Dec 2022 13:45:32 -0800 Subject: [PATCH 03/18] Move `Qt` importing to client side This way import errors show up in the client, not the kernel. --- ipykernel/eventloops.py | 286 ++++++++++++++++++++-------------------- 1 file changed, 144 insertions(+), 142 deletions(-) diff --git a/ipykernel/eventloops.py b/ipykernel/eventloops.py index 8555a5b34..78a311ac7 100644 --- a/ipykernel/eventloops.py +++ b/ipykernel/eventloops.py @@ -21,42 +21,6 @@ def _use_appnope(): return sys.platform == "darwin" and V(platform.mac_ver()[0]) >= V("10.9") -def _notify_stream_qt(kernel): - - from IPython.external.qt_for_kernel import QtCore - - def process_stream_events(): - """fall back to main loop when there's a socket event""" - # call flush to ensure that the stream doesn't lose events - # due to our consuming of the edge-triggered FD - # flush returns the number of events consumed. - # if there were any, wake it up - if kernel.shell_stream.flush(limit=1): - kernel._qt_notifier.setEnabled(False) - print('ZMQ events to process; quitting Qt event loop.') - kernel.app.qt_event_loop.quit() - - if not hasattr(kernel, "_qt_notifier"): - fd = kernel.shell_stream.getsockopt(zmq.FD) - kernel._qt_notifier = QtCore.QSocketNotifier(fd, QtCore.QSocketNotifier.Read, kernel.app) - kernel._qt_notifier.activated.connect(process_stream_events) - else: - kernel._qt_notifier.setEnabled(True) - - # there may already be unprocessed events waiting. - # these events will not wake zmq's edge-triggered FD - # since edge-triggered notification only occurs on new i/o activity. - # process all the waiting events immediately - # so we start in a clean state ensuring that any new i/o events will notify. - # schedule first call on the eventloop as soon as it's running, - # so we don't block here processing events - if not hasattr(kernel, "_qt_timer"): - kernel._qt_timer = QtCore.QTimer(kernel.app) - kernel._qt_timer.setSingleShot(True) - kernel._qt_timer.timeout.connect(process_stream_events) - kernel._qt_timer.start(0) - - # mapping of keys to loop functions loop_map = { "inline": None, @@ -103,36 +67,46 @@ def exit_decorator(exit_func): return decorator -def _loop_qt(kernel): - """Inner-loop for running the Qt eventloop +def _notify_stream_qt(kernel): - Pulled from guisupport.start_event_loop in IPython < 5.2, - since IPython 5.2 only checks `get_ipython().active_eventloop` is defined, - rather than if the eventloop is actually running. - """ - if not hasattr(kernel, 'app'): - # First time through, get the `QApplication` instance, create a `QEventLoop`, and install - # hook to stop the event loop when there's ZMQ traffic to process. Subsequent attempts to - # change the version will fail at `enable_gui` below anyway. + from IPython.external.qt_for_kernel import QtCore - # IPython's Qt importing mechanism will use the `QT_API` environment variable to choose the - # version. - try: - from IPython.external.qt_for_kernel import QtGui, QtCore, QT_API - success = True - except ImportError: - success = False - if not success: - raise RuntimeError('boo') - from IPython.lib.guisupport import get_app_qt4 + def process_stream_events(): + """fall back to main loop when there's a socket event""" + # call flush to ensure that the stream doesn't lose events + # due to our consuming of the edge-triggered FD + # flush returns the number of events consumed. + # if there were any, wake it up + if kernel.shell_stream.flush(limit=1): + kernel._qt_notifier.setEnabled(False) + print('ZMQ events to process; quitting Qt event loop.') + kernel.app.qt_event_loop.quit() + + if not hasattr(kernel, "_qt_notifier"): + fd = kernel.shell_stream.getsockopt(zmq.FD) + kernel._qt_notifier = QtCore.QSocketNotifier(fd, QtCore.QSocketNotifier.Read, kernel.app) + kernel._qt_notifier.activated.connect(process_stream_events) + else: + kernel._qt_notifier.setEnabled(True) - print(f'`qt_for_kernel` says {QT_API=}') - kernel.app = get_app_qt4([" "]) - if isinstance(kernel.app, QtGui.QApplication): - kernel.app.setQuitOnLastWindowClosed(False) + # there may already be unprocessed events waiting. + # these events will not wake zmq's edge-triggered FD + # since edge-triggered notification only occurs on new i/o activity. + # process all the waiting events immediately + # so we start in a clean state ensuring that any new i/o events will notify. + # schedule first call on the eventloop as soon as it's running, + # so we don't block here processing events + if not hasattr(kernel, "_qt_timer"): + kernel._qt_timer = QtCore.QTimer(kernel.app) + kernel._qt_timer.setSingleShot(True) + kernel._qt_timer.timeout.connect(process_stream_events) + kernel._qt_timer.start(0) - kernel.app.qt_event_loop = QtCore.QEventLoop(kernel.app) +@register_integration("qt", "qt4", "qt5", "qt6") +def loop_qt(kernel): + """Event loop for all versions of Qt.""" + print(f'Starting Qt loop with {os.environ.get("QT_API", None)=}') _notify_stream_qt(kernel) # install hook to stop event loop. # Start the event loop. kernel.app._in_event_loop = True @@ -142,90 +116,9 @@ def _loop_qt(kernel): print('Qt loop exited.') kernel.app._in_event_loop = False -# The user can generically request `qt` or a specific Qt version, e.g. `qt6`. For a generic Qt -# request, we let the mechanism in IPython choose the best available version by leaving the `QT_API` -# environment variable blank. -# -# For specific versions, we check to see whether the PyQt or PySide implementations are present and -# set `QT_API` accordingly to indicate to IPython which version we want. If neither implementation -# is present, we leave the environment variable set so IPython will generate a helpful error -# message. -# -# NOTE: if the environment variable is already set, it will be used unchanged, regardless of what -# the user requested. - - -@register_integration("qt4") -def loop_qt4(kernel): - print(f'Starting Qt4 loop with {os.environ.get("QT_API", None)=}') - """Start a kernel with PyQt4 event loop integration.""" - if os.environ.get("QT_API", None) is None: - try: - import PyQt # noqa - - os.environ["QT_API"] = "pyqt" - except ImportError: - try: - import PySide # noqa - - os.environ["QT_API"] = "pyside" - except ImportError: - os.environ["QT_API"] = "pyqt" # Set it back so IPython gives an error - _loop_qt(kernel) - - -@register_integration("qt5") -def loop_qt5(kernel): - print(f'Starting Qt5 loop with {os.environ.get("QT_API", None)=}') - """Start a kernel with PyQt5 event loop integration.""" - if os.environ.get("QT_API", None) is None: - try: - import PyQt5 # noqa - - os.environ["QT_API"] = "pyqt5" - except ImportError: - try: - import PySide2 # noqa - - os.environ["QT_API"] = "pyside2" - except ImportError: - os.environ["QT_API"] = "pyqt5" # Set it back so IPython gives an error - _loop_qt(kernel) - - -@register_integration("qt6") -def loop_qt6(kernel): - print(f'Starting Qt6 loop with {os.environ.get("QT_API", None)=}') - """Start a kernel with PyQt6 event loop integration.""" - if os.environ.get("QT_API", None) is None: - try: - print('Trying to import PyQt6...') - import PyQt6 # noqa - - os.environ["QT_API"] = "pyqt6" - except ImportError: - try: - print('Trying to import PySide6...') - import PySide6 # noqa - - os.environ["QT_API"] = "pyside6" - except ImportError: - os.environ["QT_API"] = None - _loop_qt(kernel) - - -@register_integration("qt") -def loop_qt(kernel): - print(f'Starting Qtx loop with {os.environ.get("QT_API", None)=}') - """Generic request to start event loop; lets IPython mechanism choose the version.""" - _loop_qt(kernel) - # exit and watch are the same for qt 4 and 5 @loop_qt.exit -@loop_qt4.exit -@loop_qt5.exit -@loop_qt6.exit def loop_qt_exit(kernel): print('Request Qt app exit (will close all open windows).') kernel.app.exit() @@ -520,6 +413,106 @@ def close_loop(): loop.run_until_complete(close_loop) # type:ignore[call-overload] loop.close() +# The user can generically request `qt` or a specific Qt version, e.g. `qt6`. For a generic Qt +# request, we let the mechanism in IPython choose the best available version by leaving the `QT_API` +# environment variable blank. +# +# For specific versions, we check to see whether the PyQt or PySide implementations are present and +# set `QT_API` accordingly to indicate to IPython which version we want. If neither implementation +# is present, we leave the environment variable set so IPython will generate a helpful error +# message. +# +# NOTE: if the environment variable is already set, it will be used unchanged, regardless of what +# the user requested. +def set_qt_api(gui, kernel): + """Sets the `QT_API` environment variable if it isn't already set.""" + if hasattr(kernel, 'app'): + raise RuntimeError('Kernel already running a Qt event loop.') + qt_api = os.environ.get("QT_API", None) + if qt_api is not None and gui != 'qt': + env2gui = {'pyside': 'qt4', + 'pyqt': 'qt4', + 'pyside2': 'qt5', + 'pyqt5': 'qt5', + 'pyside6': 'qt6', + 'pyqt6': 'qt6', + } + if env2gui[qt_api] != gui: + print(f'Request for "{gui}" will be ignored because `QT_API` ' + f'environment variable is set to "{qt_api}"') + else: + if gui == 'qt4': + try: + import PyQt # noqa + + os.environ["QT_API"] = "pyqt" + except ImportError: + try: + import PySide # noqa + + os.environ["QT_API"] = "pyside" + except ImportError: + # Neither implementation installed; set it to something so IPython gives an error + os.environ["QT_API"] = "pyqt" + elif gui == 'qt5': + try: + import PyQt5 # noqa + + os.environ["QT_API"] = "pyqt5" + except ImportError: + try: + import PySide2 # noqa + + os.environ["QT_API"] = "pyside2" + except ImportError: + os.environ["QT_API"] = "pyqt5" + elif gui == 'qt6': + try: + print('Trying to import PyQt6...') + import PyQt6 # noqa + + os.environ["QT_API"] = "pyqt6" + except ImportError: + try: + print('Trying to import PySide6...') + import PySide6 # noqa + + os.environ["QT_API"] = "pyside6" + except ImportError: + os.environ["QT_API"] = "pyqt6" + elif gui == 'qt': + # Don't set QT_API; let IPython logic choose the version. + if 'QT_API' in os.environ.keys(): + del os.environ['QT_API'] + else: + raise ValueError(f'Unrecognized Qt version: {gui}. Should be "qt4", "qt5", "qt6", or "qt".') + + if gui!= 'qt' and hasattr(kernel, 'last_qt_version') and 'QT_API' in os.environ.keys(): + if kernel.last_qt_version != gui: + raise ValueError('Cannot switch Qt versions for this session; ' + f'must use {kernel.last_qt_version}.') + + # Do the actual import now that the environment variable is set. + try: + from IPython.external.qt_for_kernel import QtGui, QtCore, QT_API + except ImportError: + # Clear the environment variable for the next attempt. + if 'QT_API' in os.environ.keys(): + del os.environ["QT_API"] + raise + + from IPython.lib.guisupport import get_app_qt4 + + print(f'`qt_for_kernel` says {QT_API=}') + kernel.app = get_app_qt4([" "]) + if isinstance(kernel.app, QtGui.QApplication): + kernel.app.setQuitOnLastWindowClosed(False) + + kernel.app.qt_event_loop = QtCore.QEventLoop(kernel.app) + + # Due to the import mechanism, we can't change Qt versions once we've chosen one. So we tag the + # version so we can check for this and give an error. + kernel.last_qt_version = gui def enable_gui(gui, kernel=None): """Enable integration with a given GUI""" @@ -534,6 +527,15 @@ def enable_gui(gui, kernel=None): "You didn't specify a kernel," " and no IPython Application with a kernel appears to be running." ) + if gui is None: + # User wants to turn off integration; clear any evidence if Qt was the last one. + if hasattr(kernel, 'app'): + delattr(kernel, 'app') + else: + if gui.startswith('qt'): + # Prepare the kernel here so any exceptions are displayed in the client. + set_qt_api(gui, kernel) + loop = loop_map[gui] if loop and kernel.eventloop is not None and kernel.eventloop is not loop: raise RuntimeError("Cannot activate multiple GUI eventloops") From f8dd83a49de533820e8ad3c51d96d6dbb545a2b5 Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Thu, 8 Dec 2022 13:56:48 -0800 Subject: [PATCH 04/18] Bring in some changes by @tacaswell See https://github.com/ipython/ipykernel/pull/782/commits/d5d718bc08609568d39aa3fed28b68715b83001d --- ipykernel/eventloops.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/ipykernel/eventloops.py b/ipykernel/eventloops.py index 78a311ac7..793dbfb3f 100644 --- a/ipykernel/eventloops.py +++ b/ipykernel/eventloops.py @@ -68,9 +68,20 @@ def exit_decorator(exit_func): def _notify_stream_qt(kernel): - + import operator + from functools import lru_cache from IPython.external.qt_for_kernel import QtCore + try: + from IPython.external.qt_for_kernel import enum_helper + except ImportError: + @lru_cache(None) + def enum_helper(name): + return operator.attrgetter( + name.rpartition(".")[0] + )(sys.modules[QtCore.__package__]) + + def process_stream_events(): """fall back to main loop when there's a socket event""" # call flush to ensure that the stream doesn't lose events @@ -84,7 +95,8 @@ def process_stream_events(): if not hasattr(kernel, "_qt_notifier"): fd = kernel.shell_stream.getsockopt(zmq.FD) - kernel._qt_notifier = QtCore.QSocketNotifier(fd, QtCore.QSocketNotifier.Read, kernel.app) + kernel._qt_notifier = QtCore.QSocketNotifier( + fd, enum_helper('QtCore.QSocketNotifier.Type').Read, kernel.app.qt_event_loop) kernel._qt_notifier.activated.connect(process_stream_events) else: kernel._qt_notifier.setEnabled(True) From 7bc5aed19af15c1b90276391e902042fddba504d Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Thu, 8 Dec 2022 13:58:25 -0800 Subject: [PATCH 05/18] Remove diagnostic `print` statements --- ipykernel/eventloops.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/ipykernel/eventloops.py b/ipykernel/eventloops.py index 793dbfb3f..c96052187 100644 --- a/ipykernel/eventloops.py +++ b/ipykernel/eventloops.py @@ -90,7 +90,6 @@ def process_stream_events(): # if there were any, wake it up if kernel.shell_stream.flush(limit=1): kernel._qt_notifier.setEnabled(False) - print('ZMQ events to process; quitting Qt event loop.') kernel.app.qt_event_loop.quit() if not hasattr(kernel, "_qt_notifier"): @@ -118,21 +117,18 @@ def process_stream_events(): @register_integration("qt", "qt4", "qt5", "qt6") def loop_qt(kernel): """Event loop for all versions of Qt.""" - print(f'Starting Qt loop with {os.environ.get("QT_API", None)=}') _notify_stream_qt(kernel) # install hook to stop event loop. # Start the event loop. kernel.app._in_event_loop = True # `exec` blocks until there's ZMQ activity. el = kernel.app.qt_event_loop # for brevity el.exec() if hasattr(el, 'exec') else el.exec_() - print('Qt loop exited.') kernel.app._in_event_loop = False # exit and watch are the same for qt 4 and 5 @loop_qt.exit def loop_qt_exit(kernel): - print('Request Qt app exit (will close all open windows).') kernel.app.exit() @@ -480,13 +476,11 @@ def set_qt_api(gui, kernel): os.environ["QT_API"] = "pyqt5" elif gui == 'qt6': try: - print('Trying to import PyQt6...') import PyQt6 # noqa os.environ["QT_API"] = "pyqt6" except ImportError: try: - print('Trying to import PySide6...') import PySide6 # noqa os.environ["QT_API"] = "pyside6" @@ -515,7 +509,6 @@ def set_qt_api(gui, kernel): from IPython.lib.guisupport import get_app_qt4 - print(f'`qt_for_kernel` says {QT_API=}') kernel.app = get_app_qt4([" "]) if isinstance(kernel.app, QtGui.QApplication): kernel.app.setQuitOnLastWindowClosed(False) From f772d80ae65cf60c19ad50d40b5a3ebdf707a271 Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Thu, 8 Dec 2022 14:15:41 -0800 Subject: [PATCH 06/18] Move last version check up --- ipykernel/eventloops.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/ipykernel/eventloops.py b/ipykernel/eventloops.py index c96052187..9d9a9d240 100644 --- a/ipykernel/eventloops.py +++ b/ipykernel/eventloops.py @@ -436,6 +436,12 @@ def set_qt_api(gui, kernel): """Sets the `QT_API` environment variable if it isn't already set.""" if hasattr(kernel, 'app'): raise RuntimeError('Kernel already running a Qt event loop.') + + if gui!= 'qt' and hasattr(kernel, 'last_qt_version'): + if kernel.last_qt_version != gui: + raise ValueError('Cannot switch Qt versions for this session; ' + f'must use {kernel.last_qt_version}.') + qt_api = os.environ.get("QT_API", None) if qt_api is not None and gui != 'qt': env2gui = {'pyside': 'qt4', @@ -493,11 +499,6 @@ def set_qt_api(gui, kernel): else: raise ValueError(f'Unrecognized Qt version: {gui}. Should be "qt4", "qt5", "qt6", or "qt".') - if gui!= 'qt' and hasattr(kernel, 'last_qt_version') and 'QT_API' in os.environ.keys(): - if kernel.last_qt_version != gui: - raise ValueError('Cannot switch Qt versions for this session; ' - f'must use {kernel.last_qt_version}.') - # Do the actual import now that the environment variable is set. try: from IPython.external.qt_for_kernel import QtGui, QtCore, QT_API From 8a876584af24467995bd3b6e8b68a20cbae9e37e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 8 Dec 2022 22:19:46 +0000 Subject: [PATCH 07/18] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- ipykernel/eventloops.py | 48 ++++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/ipykernel/eventloops.py b/ipykernel/eventloops.py index 9d9a9d240..976bb3446 100644 --- a/ipykernel/eventloops.py +++ b/ipykernel/eventloops.py @@ -70,17 +70,16 @@ def exit_decorator(exit_func): def _notify_stream_qt(kernel): import operator from functools import lru_cache + from IPython.external.qt_for_kernel import QtCore try: from IPython.external.qt_for_kernel import enum_helper except ImportError: + @lru_cache(None) def enum_helper(name): - return operator.attrgetter( - name.rpartition(".")[0] - )(sys.modules[QtCore.__package__]) - + return operator.attrgetter(name.rpartition(".")[0])(sys.modules[QtCore.__package__]) def process_stream_events(): """fall back to main loop when there's a socket event""" @@ -95,7 +94,8 @@ def process_stream_events(): if not hasattr(kernel, "_qt_notifier"): fd = kernel.shell_stream.getsockopt(zmq.FD) kernel._qt_notifier = QtCore.QSocketNotifier( - fd, enum_helper('QtCore.QSocketNotifier.Type').Read, kernel.app.qt_event_loop) + fd, enum_helper('QtCore.QSocketNotifier.Type').Read, kernel.app.qt_event_loop + ) kernel._qt_notifier.activated.connect(process_stream_events) else: kernel._qt_notifier.setEnabled(True) @@ -117,7 +117,7 @@ def process_stream_events(): @register_integration("qt", "qt4", "qt5", "qt6") def loop_qt(kernel): """Event loop for all versions of Qt.""" - _notify_stream_qt(kernel) # install hook to stop event loop. + _notify_stream_qt(kernel) # install hook to stop event loop. # Start the event loop. kernel.app._in_event_loop = True # `exec` blocks until there's ZMQ activity. @@ -421,6 +421,7 @@ def close_loop(): loop.run_until_complete(close_loop) # type:ignore[call-overload] loop.close() + # The user can generically request `qt` or a specific Qt version, e.g. `qt6`. For a generic Qt # request, we let the mechanism in IPython choose the best available version by leaving the `QT_API` # environment variable blank. @@ -437,23 +438,27 @@ def set_qt_api(gui, kernel): if hasattr(kernel, 'app'): raise RuntimeError('Kernel already running a Qt event loop.') - if gui!= 'qt' and hasattr(kernel, 'last_qt_version'): + if gui != 'qt' and hasattr(kernel, 'last_qt_version'): if kernel.last_qt_version != gui: - raise ValueError('Cannot switch Qt versions for this session; ' - f'must use {kernel.last_qt_version}.') + raise ValueError( + 'Cannot switch Qt versions for this session; ' f'must use {kernel.last_qt_version}.' + ) qt_api = os.environ.get("QT_API", None) if qt_api is not None and gui != 'qt': - env2gui = {'pyside': 'qt4', - 'pyqt': 'qt4', - 'pyside2': 'qt5', - 'pyqt5': 'qt5', - 'pyside6': 'qt6', - 'pyqt6': 'qt6', - } + env2gui = { + 'pyside': 'qt4', + 'pyqt': 'qt4', + 'pyside2': 'qt5', + 'pyqt5': 'qt5', + 'pyside6': 'qt6', + 'pyqt6': 'qt6', + } if env2gui[qt_api] != gui: - print(f'Request for "{gui}" will be ignored because `QT_API` ' - f'environment variable is set to "{qt_api}"') + print( + f'Request for "{gui}" will be ignored because `QT_API` ' + f'environment variable is set to "{qt_api}"' + ) else: if gui == 'qt4': try: @@ -497,11 +502,13 @@ def set_qt_api(gui, kernel): if 'QT_API' in os.environ.keys(): del os.environ['QT_API'] else: - raise ValueError(f'Unrecognized Qt version: {gui}. Should be "qt4", "qt5", "qt6", or "qt".') + raise ValueError( + f'Unrecognized Qt version: {gui}. Should be "qt4", "qt5", "qt6", or "qt".' + ) # Do the actual import now that the environment variable is set. try: - from IPython.external.qt_for_kernel import QtGui, QtCore, QT_API + from IPython.external.qt_for_kernel import QT_API, QtCore, QtGui except ImportError: # Clear the environment variable for the next attempt. if 'QT_API' in os.environ.keys(): @@ -520,6 +527,7 @@ def set_qt_api(gui, kernel): # version so we can check for this and give an error. kernel.last_qt_version = gui + def enable_gui(gui, kernel=None): """Enable integration with a given GUI""" if gui not in loop_map: From a6e5bdba724ae9c74d659f173254c6438af33c04 Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Thu, 8 Dec 2022 14:23:47 -0800 Subject: [PATCH 08/18] Remove unused import --- ipykernel/eventloops.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ipykernel/eventloops.py b/ipykernel/eventloops.py index 976bb3446..61fd46b2e 100644 --- a/ipykernel/eventloops.py +++ b/ipykernel/eventloops.py @@ -508,7 +508,7 @@ def set_qt_api(gui, kernel): # Do the actual import now that the environment variable is set. try: - from IPython.external.qt_for_kernel import QT_API, QtCore, QtGui + from IPython.external.qt_for_kernel import QtCore, QtGui except ImportError: # Clear the environment variable for the next attempt. if 'QT_API' in os.environ.keys(): From 62ca4d20d42f5261db2fc3d307cd668fb9d9fef3 Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Mon, 19 Dec 2022 12:14:15 -0800 Subject: [PATCH 09/18] These seem to run fine in Windows. --- ipykernel/tests/test_eventloop.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/ipykernel/tests/test_eventloop.py b/ipykernel/tests/test_eventloop.py index 7d0063e61..adb8a8aee 100644 --- a/ipykernel/tests/test_eventloop.py +++ b/ipykernel/tests/test_eventloop.py @@ -58,7 +58,6 @@ def test_asyncio_interrupt(): windows_skip = pytest.mark.skipif(os.name == "nt", reason="causing failures on windows") -@windows_skip @pytest.mark.skipif(sys.platform == "darwin", reason="hangs on macos") def test_tk_loop(kernel): def do_thing(): @@ -79,7 +78,6 @@ def do_thing(): t.join() -@windows_skip def test_asyncio_loop(kernel): def do_thing(): loop.call_soon(loop.stop) @@ -89,7 +87,6 @@ def do_thing(): loop_asyncio(kernel) -@windows_skip def test_enable_gui(kernel): enable_gui("tk", kernel) From c04a1de6e15d22494c549e6e6de9a0c9ed343b2d Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Mon, 19 Dec 2022 12:15:04 -0800 Subject: [PATCH 10/18] TST: Qt event loop logic --- ipykernel/eventloops.py | 36 ++++++++++++------- ipykernel/tests/test_eventloop.py | 57 ++++++++++++++++++++++++++++++- 2 files changed, 80 insertions(+), 13 deletions(-) diff --git a/ipykernel/eventloops.py b/ipykernel/eventloops.py index 61fd46b2e..553fe52e0 100644 --- a/ipykernel/eventloops.py +++ b/ipykernel/eventloops.py @@ -433,17 +433,14 @@ def close_loop(): # # NOTE: if the environment variable is already set, it will be used unchanged, regardless of what # the user requested. -def set_qt_api(gui, kernel): - """Sets the `QT_API` environment variable if it isn't already set.""" - if hasattr(kernel, 'app'): - raise RuntimeError('Kernel already running a Qt event loop.') - if gui != 'qt' and hasattr(kernel, 'last_qt_version'): - if kernel.last_qt_version != gui: - raise ValueError( - 'Cannot switch Qt versions for this session; ' f'must use {kernel.last_qt_version}.' - ) +def set_qt_api_env_from_gui(gui): + """ + Sets the QT_API environment variable by trying to import PyQtx or PySidex. + + If QT_API is already set, ignore the request. + """ qt_api = os.environ.get("QT_API", None) if qt_api is not None and gui != 'qt': env2gui = { @@ -506,15 +503,30 @@ def set_qt_api(gui, kernel): f'Unrecognized Qt version: {gui}. Should be "qt4", "qt5", "qt6", or "qt".' ) - # Do the actual import now that the environment variable is set. + # Do the actual import now that the environment variable is set to make sure it works. try: - from IPython.external.qt_for_kernel import QtCore, QtGui + from IPython.external.qt_for_kernel import QtCore, QtGui # noqa except ImportError: # Clear the environment variable for the next attempt. if 'QT_API' in os.environ.keys(): del os.environ["QT_API"] raise + +def make_qt_app_for_kernel(gui, kernel): + """Sets the `QT_API` environment variable if it isn't already set.""" + if hasattr(kernel, 'app'): + raise RuntimeError('Kernel already running a Qt event loop.') + + if gui != 'qt' and hasattr(kernel, 'last_qt_version'): + if kernel.last_qt_version != gui: + raise ValueError( + 'Cannot switch Qt versions for this session; ' f'must use {kernel.last_qt_version}.' + ) + + set_qt_api_env_from_gui(gui) + # This import is guaranteed to work now: + from IPython.external.qt_for_kernel import QtCore, QtGui from IPython.lib.guisupport import get_app_qt4 kernel.app = get_app_qt4([" "]) @@ -548,7 +560,7 @@ def enable_gui(gui, kernel=None): else: if gui.startswith('qt'): # Prepare the kernel here so any exceptions are displayed in the client. - set_qt_api(gui, kernel) + make_qt_app_for_kernel(gui, kernel) loop = loop_map[gui] if loop and kernel.eventloop is not None and kernel.eventloop is not loop: diff --git a/ipykernel/tests/test_eventloop.py b/ipykernel/tests/test_eventloop.py index adb8a8aee..097bc6608 100644 --- a/ipykernel/tests/test_eventloop.py +++ b/ipykernel/tests/test_eventloop.py @@ -9,12 +9,34 @@ import pytest import tornado -from ipykernel.eventloops import enable_gui, loop_asyncio, loop_cocoa, loop_tk +from ipykernel.eventloops import ( + enable_gui, + loop_asyncio, + loop_cocoa, + loop_tk, + set_qt_api_env_from_gui, +) from .utils import execute, flush_channels, start_new_kernel KC = KM = None +guis_avail = [] + + +def _get_qt_vers(): + for gui in ['qt', 'qt6', 'qt5', 'qt4']: + try: + set_qt_api_env_from_gui(gui) + guis_avail.append(gui) + if 'QT_API' in os.environ.keys(): + del os.environ['QT_API'] + except ImportError: + pass # that version of Qt isn't available. + + +_get_qt_vers() + def setup(): """start the global kernel (if it isn't running) and return its client""" @@ -94,3 +116,36 @@ def test_enable_gui(kernel): @pytest.mark.skipif(sys.platform != "darwin", reason="MacOS-only") def test_cocoa_loop(kernel): loop_cocoa(kernel) + + +@pytest.mark.skipif(len(guis_avail) == 0, reason='No viable version of PyQt or PySide installed.') +def test_qt_enable_gui(kernel): + gui = guis_avail[0] + + enable_gui(gui, kernel) + + # This tags the kernel with the gui that was requested: + assert kernel.last_qt_version == gui + + # We store the `QApplication` instance in the kernel. + assert hasattr(kernel, 'app') + # And the `QEventLoop` is added to `app`:` + assert hasattr(kernel.app, 'qt_event_loop') + + # Can't start another event loop, even if `gui` is the same. + with pytest.raises(RuntimeError): + enable_gui(gui, kernel) + + # Turning off the event loop retains `last_qt_version`; now we're stuck importing that forever. + enable_gui(None, kernel) + assert kernel.last_qt_version == gui + assert not hasattr(kernel, 'app') + + if len(guis_avail) > 1: + for gui2 in guis_avail[1:]: + # Won't work; Qt version is different than the first one. + with pytest.raises(ValueError): + enable_gui(gui2, kernel) + + # A gui of 'qt' means "latest availble", or in this case, the last one that was used. + enable_gui('qt', kernel) From 0534c2117b14b4e42c895365968e914d4dc628da Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Mon, 19 Dec 2022 12:52:33 -0800 Subject: [PATCH 11/18] Fix "Test Minimum Versions" CI test --- ipykernel/tests/test_eventloop.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ipykernel/tests/test_eventloop.py b/ipykernel/tests/test_eventloop.py index 097bc6608..b734f0746 100644 --- a/ipykernel/tests/test_eventloop.py +++ b/ipykernel/tests/test_eventloop.py @@ -33,6 +33,8 @@ def _get_qt_vers(): del os.environ['QT_API'] except ImportError: pass # that version of Qt isn't available. + except RuntimeError: + pass # the version of IPython doesn't know what to do with this Qt version. _get_qt_vers() From 02c99c2f007edd834c68e99d111cf6a3b57097db Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Mon, 19 Dec 2022 14:05:28 -0800 Subject: [PATCH 12/18] Use `IPython` constants and version check. Importing a second version of Qt is not allowed. `IPython` silently ignores requests for different versions; we want `enable_gui` to raise an exception so the user can see it. --- ipykernel/eventloops.py | 48 +++++++++++++++++++------------ ipykernel/tests/test_eventloop.py | 23 ++++++++------- 2 files changed, 41 insertions(+), 30 deletions(-) diff --git a/ipykernel/eventloops.py b/ipykernel/eventloops.py index 553fe52e0..dde14c52c 100644 --- a/ipykernel/eventloops.py +++ b/ipykernel/eventloops.py @@ -442,20 +442,40 @@ def set_qt_api_env_from_gui(gui): If QT_API is already set, ignore the request. """ qt_api = os.environ.get("QT_API", None) + + from IPython.external.qt_loaders import ( + QT_API_PYQT, + QT_API_PYQT5, + QT_API_PYQT6, + QT_API_PYSIDE, + QT_API_PYSIDE2, + QT_API_PYSIDE6, + QT_API_PYQTv1, + loaded_api, + ) + + loaded = loaded_api() + + qt_env2gui = { + QT_API_PYSIDE: 'qt4', + QT_API_PYQTv1: 'qt4', + QT_API_PYQT: 'qt4', + QT_API_PYSIDE2: 'qt5', + QT_API_PYQT5: 'qt5', + QT_API_PYSIDE6: 'qt6', + QT_API_PYQT6: 'qt6', + } if qt_api is not None and gui != 'qt': - env2gui = { - 'pyside': 'qt4', - 'pyqt': 'qt4', - 'pyside2': 'qt5', - 'pyqt5': 'qt5', - 'pyside6': 'qt6', - 'pyqt6': 'qt6', - } - if env2gui[qt_api] != gui: + if qt_env2gui[qt_api] != gui: print( f'Request for "{gui}" will be ignored because `QT_API` ' f'environment variable is set to "{qt_api}"' ) + elif loaded is not None and gui != 'qt': + if qt_env2gui[loaded] != gui: + raise ImportError( + f'Cannot switch Qt versions for this session; must use {qt_env2gui[loaded]}.' + ) else: if gui == 'qt4': try: @@ -518,12 +538,6 @@ def make_qt_app_for_kernel(gui, kernel): if hasattr(kernel, 'app'): raise RuntimeError('Kernel already running a Qt event loop.') - if gui != 'qt' and hasattr(kernel, 'last_qt_version'): - if kernel.last_qt_version != gui: - raise ValueError( - 'Cannot switch Qt versions for this session; ' f'must use {kernel.last_qt_version}.' - ) - set_qt_api_env_from_gui(gui) # This import is guaranteed to work now: from IPython.external.qt_for_kernel import QtCore, QtGui @@ -535,10 +549,6 @@ def make_qt_app_for_kernel(gui, kernel): kernel.app.qt_event_loop = QtCore.QEventLoop(kernel.app) - # Due to the import mechanism, we can't change Qt versions once we've chosen one. So we tag the - # version so we can check for this and give an error. - kernel.last_qt_version = gui - def enable_gui(gui, kernel=None): """Enable integration with a given GUI""" diff --git a/ipykernel/tests/test_eventloop.py b/ipykernel/tests/test_eventloop.py index b734f0746..4b5979910 100644 --- a/ipykernel/tests/test_eventloop.py +++ b/ipykernel/tests/test_eventloop.py @@ -25,7 +25,10 @@ def _get_qt_vers(): + """If any version of Qt is available, this will populate `guis_avail` with 'qt' and 'qtx'. Due + to the import mechanism, we can't import multiple versions of Qt in one session.""" for gui in ['qt', 'qt6', 'qt5', 'qt4']: + print(f'Trying {gui}') try: set_qt_api_env_from_gui(gui) guis_avail.append(gui) @@ -126,9 +129,6 @@ def test_qt_enable_gui(kernel): enable_gui(gui, kernel) - # This tags the kernel with the gui that was requested: - assert kernel.last_qt_version == gui - # We store the `QApplication` instance in the kernel. assert hasattr(kernel, 'app') # And the `QEventLoop` is added to `app`:` @@ -138,16 +138,17 @@ def test_qt_enable_gui(kernel): with pytest.raises(RuntimeError): enable_gui(gui, kernel) - # Turning off the event loop retains `last_qt_version`; now we're stuck importing that forever. + # Event loop intergration can be turned off. enable_gui(None, kernel) - assert kernel.last_qt_version == gui assert not hasattr(kernel, 'app') - if len(guis_avail) > 1: - for gui2 in guis_avail[1:]: - # Won't work; Qt version is different than the first one. - with pytest.raises(ValueError): - enable_gui(gui2, kernel) + # But now we're stuck with this version of Qt for good; can't switch. + for not_gui in ['qt6', 'qt5', 'qt4']: + if not_gui not in guis_avail: + break + + with pytest.raises(ImportError): + enable_gui(not_gui, kernel) - # A gui of 'qt' means "latest availble", or in this case, the last one that was used. + # A gui of 'qt' means "best available", or in this case, the last one that was used. enable_gui('qt', kernel) From a424c0fca823280b6112c294c3088db3a0696b83 Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Mon, 19 Dec 2022 17:54:09 -0800 Subject: [PATCH 13/18] Add two Qt versions to test matrix --- pyproject.toml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 9904b54b9..41c93b73a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,6 +62,8 @@ cov = [ "curio", "trio", ] +pyqt5 = ["pyqt5"] +pyside6 = ["pyside6"] [tool.hatch.version] path = "ipykernel/_version.py" @@ -92,6 +94,15 @@ features = ["test", "cov"] test = "python -m pytest -vv --cov ipykernel --cov-branch --cov-report term-missing:skip-covered {args}" nowarn = "test -W default {args}" +[[tool.hatch.envs.cov.matrix]] +qt = ["qt5", "qt6"] + +[tool.hatch.envs.cov.overrides] +matrix.qt.features = [ + { value = "pyqt5", if = ["qt5"] }, + { value = "pyside6", if = ["qt6"] }, +] + [tool.hatch.envs.typing] features = ["test"] dependencies = ["mypy>=0.990"] From 9f47308c28db7ab55a755fba4ef3b7f2803ff65d Mon Sep 17 00:00:00 2001 From: Emilio Graff <1@emil.io> Date: Mon, 19 Dec 2022 18:18:10 -0800 Subject: [PATCH 14/18] Improved logic --- ipykernel/eventloops.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/ipykernel/eventloops.py b/ipykernel/eventloops.py index dde14c52c..bb86d3515 100644 --- a/ipykernel/eventloops.py +++ b/ipykernel/eventloops.py @@ -465,17 +465,18 @@ def set_qt_api_env_from_gui(gui): QT_API_PYSIDE6: 'qt6', QT_API_PYQT6: 'qt6', } + if loaded is not None and gui != 'qt': + if qt_env2gui[loaded] != gui: + raise ImportError( + f'Cannot switch Qt versions for this session; must use {qt_env2gui[loaded]}.' + ) + if qt_api is not None and gui != 'qt': if qt_env2gui[qt_api] != gui: print( f'Request for "{gui}" will be ignored because `QT_API` ' f'environment variable is set to "{qt_api}"' ) - elif loaded is not None and gui != 'qt': - if qt_env2gui[loaded] != gui: - raise ImportError( - f'Cannot switch Qt versions for this session; must use {qt_env2gui[loaded]}.' - ) else: if gui == 'qt4': try: From b7dff736d3ca077a5e897206761a3bd87f788684 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 21 Dec 2022 13:37:15 -0600 Subject: [PATCH 15/18] get coverage on windows back --- ipykernel/tests/test_eventloop.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ipykernel/tests/test_eventloop.py b/ipykernel/tests/test_eventloop.py index 4b5979910..ba57ec2d9 100644 --- a/ipykernel/tests/test_eventloop.py +++ b/ipykernel/tests/test_eventloop.py @@ -19,6 +19,9 @@ from .utils import execute, flush_channels, start_new_kernel +if os.name == 'nt': + pytest.skip("skipping eventloop tests on Windows", allow_module_level=True) + KC = KM = None guis_avail = [] From cf1a68667d0a3352c225bfbe80ea0f776c6778d8 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 21 Dec 2022 13:42:28 -0600 Subject: [PATCH 16/18] more targeted windows skip --- ipykernel/tests/test_eventloop.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ipykernel/tests/test_eventloop.py b/ipykernel/tests/test_eventloop.py index ba57ec2d9..d77ff0fc2 100644 --- a/ipykernel/tests/test_eventloop.py +++ b/ipykernel/tests/test_eventloop.py @@ -19,9 +19,6 @@ from .utils import execute, flush_channels, start_new_kernel -if os.name == 'nt': - pytest.skip("skipping eventloop tests on Windows", allow_module_level=True) - KC = KM = None guis_avail = [] @@ -88,6 +85,7 @@ def test_asyncio_interrupt(): windows_skip = pytest.mark.skipif(os.name == "nt", reason="causing failures on windows") +@windows_skip @pytest.mark.skipif(sys.platform == "darwin", reason="hangs on macos") def test_tk_loop(kernel): def do_thing(): @@ -108,6 +106,7 @@ def do_thing(): t.join() +@windows_skip def test_asyncio_loop(kernel): def do_thing(): loop.call_soon(loop.stop) @@ -117,6 +116,7 @@ def do_thing(): loop_asyncio(kernel) +@windows_skip def test_enable_gui(kernel): enable_gui("tk", kernel) From 86c95be630c764dcb392b155a9b536fe4f2b1005 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 21 Dec 2022 13:43:51 -0600 Subject: [PATCH 17/18] rename symbol in test --- ipykernel/tests/test_eventloop.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ipykernel/tests/test_eventloop.py b/ipykernel/tests/test_eventloop.py index d77ff0fc2..81f67eb56 100644 --- a/ipykernel/tests/test_eventloop.py +++ b/ipykernel/tests/test_eventloop.py @@ -21,7 +21,7 @@ KC = KM = None -guis_avail = [] +qt_guis_avail = [] def _get_qt_vers(): @@ -31,7 +31,7 @@ def _get_qt_vers(): print(f'Trying {gui}') try: set_qt_api_env_from_gui(gui) - guis_avail.append(gui) + qt_guis_avail.append(gui) if 'QT_API' in os.environ.keys(): del os.environ['QT_API'] except ImportError: @@ -126,9 +126,9 @@ def test_cocoa_loop(kernel): loop_cocoa(kernel) -@pytest.mark.skipif(len(guis_avail) == 0, reason='No viable version of PyQt or PySide installed.') +@pytest.mark.skipif(len(qt_guis_avail) == 0, reason='No viable version of PyQt or PySide installed.') def test_qt_enable_gui(kernel): - gui = guis_avail[0] + gui = qt_guis_avail[0] enable_gui(gui, kernel) @@ -147,7 +147,7 @@ def test_qt_enable_gui(kernel): # But now we're stuck with this version of Qt for good; can't switch. for not_gui in ['qt6', 'qt5', 'qt4']: - if not_gui not in guis_avail: + if not_gui not in qt_guis_avail: break with pytest.raises(ImportError): From 67135709b6c2cb3ceb1d43ef704af837aa213389 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 21 Dec 2022 19:44:02 +0000 Subject: [PATCH 18/18] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- ipykernel/tests/test_eventloop.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ipykernel/tests/test_eventloop.py b/ipykernel/tests/test_eventloop.py index 81f67eb56..3a684d625 100644 --- a/ipykernel/tests/test_eventloop.py +++ b/ipykernel/tests/test_eventloop.py @@ -126,7 +126,9 @@ def test_cocoa_loop(kernel): loop_cocoa(kernel) -@pytest.mark.skipif(len(qt_guis_avail) == 0, reason='No viable version of PyQt or PySide installed.') +@pytest.mark.skipif( + len(qt_guis_avail) == 0, reason='No viable version of PyQt or PySide installed.' +) def test_qt_enable_gui(kernel): gui = qt_guis_avail[0]