From 1e3245b4d459579471c6a810c3b8fb84c05f966d Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Mon, 2 Jan 2023 19:10:55 -0500 Subject: [PATCH 1/8] Don't raise error when trying to set another app for Qt eventloop - This was making the `%matplotlib qt` fail in Spyder. - It was also generating a long traceback when trying to set another interactive backend (e.g. tk). --- ipykernel/eventloops.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ipykernel/eventloops.py b/ipykernel/eventloops.py index d73fbe18e..ee19de18b 100644 --- a/ipykernel/eventloops.py +++ b/ipykernel/eventloops.py @@ -543,7 +543,9 @@ def set_qt_api_env_from_gui(gui): 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.') + # Kernel is already running a Qt event loop, so there's no need to + # create another app for it. + return set_qt_api_env_from_gui(gui) # This import is guaranteed to work now: From e573ad264363fe68b8938a7a2e4ebccf96170de6 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Mon, 2 Jan 2023 19:16:07 -0500 Subject: [PATCH 2/8] Remove support to set a Qt4 eventloop because it's no longer supported --- ipykernel/eventloops.py | 25 +++---------------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/ipykernel/eventloops.py b/ipykernel/eventloops.py index ee19de18b..674f5bf53 100644 --- a/ipykernel/eventloops.py +++ b/ipykernel/eventloops.py @@ -115,7 +115,7 @@ def process_stream_events(): kernel._qt_timer.start(0) -@register_integration("qt", "qt4", "qt5", "qt6") +@register_integration("qt", "qt5", "qt6") def loop_qt(kernel): """Event loop for all versions of Qt.""" _notify_stream_qt(kernel) # install hook to stop event loop. @@ -450,22 +450,16 @@ def set_qt_api_env_from_gui(gui): 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', @@ -484,20 +478,7 @@ def set_qt_api_env_from_gui(gui): 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': + if gui == 'qt5': try: import PyQt5 # noqa @@ -527,7 +508,7 @@ def set_qt_api_env_from_gui(gui): del os.environ['QT_API'] else: raise ValueError( - f'Unrecognized Qt version: {gui}. Should be "qt4", "qt5", "qt6", or "qt".' + f'Unrecognized Qt version: {gui}. Should be "qt5", "qt6", or "qt".' ) # Do the actual import now that the environment variable is set to make sure it works. From b26833b510a8fdd5601c8420cabe141a120ef62a Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Mon, 2 Jan 2023 19:56:21 -0500 Subject: [PATCH 3/8] Move big comment outside a function to be part of a docstring --- ipykernel/eventloops.py | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/ipykernel/eventloops.py b/ipykernel/eventloops.py index 674f5bf53..13b0ecb14 100644 --- a/ipykernel/eventloops.py +++ b/ipykernel/eventloops.py @@ -117,10 +117,12 @@ def process_stream_events(): @register_integration("qt", "qt5", "qt6") def loop_qt(kernel): - """Event loop for all versions of Qt.""" + """Event loop for all supported versions of Qt.""" _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_() @@ -428,24 +430,24 @@ def close_loop(): 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_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. + 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. + + Notes + ----- + - If the environment variable is already set, it will be used unchanged, + regardless of what the user requested. """ qt_api = os.environ.get("QT_API", None) From eb53c54394d21f45b0299ac7d4c04bb7d910cef6 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Mon, 2 Jan 2023 20:00:46 -0500 Subject: [PATCH 4/8] Don't raise errors in set_qt_api_env_from_gui Use prints instead because these errors are not displayed to users but contain valuable information for them. --- ipykernel/eventloops.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/ipykernel/eventloops.py b/ipykernel/eventloops.py index 13b0ecb14..9c47c74b9 100644 --- a/ipykernel/eventloops.py +++ b/ipykernel/eventloops.py @@ -469,8 +469,8 @@ def set_qt_api_env_from_gui(gui): } 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]}.' + print( + f'Cannot switch Qt versions for this session; you must use {qt_env2gui[loaded]}.' ) if qt_api is not None and gui != 'qt': @@ -483,24 +483,20 @@ def set_qt_api_env_from_gui(gui): if 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: import PyQt6 # noqa - os.environ["QT_API"] = "pyqt6" except ImportError: try: import PySide6 # noqa - os.environ["QT_API"] = "pyside6" except ImportError: os.environ["QT_API"] = "pyqt6" @@ -509,18 +505,19 @@ def set_qt_api_env_from_gui(gui): if 'QT_API' in os.environ.keys(): del os.environ['QT_API'] else: - raise ValueError( + print( f'Unrecognized Qt version: {gui}. Should be "qt5", "qt6", or "qt".' ) # 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 # noqa - except ImportError: + except Exception as e: # Clear the environment variable for the next attempt. if 'QT_API' in os.environ.keys(): del os.environ["QT_API"] - raise + print(f"QT_API couldn't be set due to error {e}") + return def make_qt_app_for_kernel(gui, kernel): @@ -531,6 +528,7 @@ def make_qt_app_for_kernel(gui, kernel): return 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 From ba4e76d07a001b7625199e375afae06fde340165 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Tue, 3 Jan 2023 16:03:51 -0500 Subject: [PATCH 5/8] Restore loop_qt5 function because it was public API --- ipykernel/eventloops.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ipykernel/eventloops.py b/ipykernel/eventloops.py index 9c47c74b9..de4196ce4 100644 --- a/ipykernel/eventloops.py +++ b/ipykernel/eventloops.py @@ -129,6 +129,10 @@ def loop_qt(kernel): kernel.app._in_event_loop = False +# NOTE: To be removed in version 7 +loop_qt5 = loop_qt + + # exit and watch are the same for qt 4 and 5 @loop_qt.exit def loop_qt_exit(kernel): From b0aac75b1bef54ae6591aaa2e123b35377e8ca91 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 3 Jan 2023 21:05:59 +0000 Subject: [PATCH 6/8] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- ipykernel/eventloops.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ipykernel/eventloops.py b/ipykernel/eventloops.py index de4196ce4..927c135e8 100644 --- a/ipykernel/eventloops.py +++ b/ipykernel/eventloops.py @@ -473,9 +473,7 @@ def set_qt_api_env_from_gui(gui): } if loaded is not None and gui != 'qt': if qt_env2gui[loaded] != gui: - print( - f'Cannot switch Qt versions for this session; you must use {qt_env2gui[loaded]}.' - ) + print(f'Cannot switch Qt versions for this session; you must use {qt_env2gui[loaded]}.') if qt_api is not None and gui != 'qt': if qt_env2gui[qt_api] != gui: @@ -487,20 +485,24 @@ def set_qt_api_env_from_gui(gui): if 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: import PyQt6 # noqa + os.environ["QT_API"] = "pyqt6" except ImportError: try: import PySide6 # noqa + os.environ["QT_API"] = "pyside6" except ImportError: os.environ["QT_API"] = "pyqt6" @@ -509,9 +511,7 @@ def set_qt_api_env_from_gui(gui): if 'QT_API' in os.environ.keys(): del os.environ['QT_API'] else: - print( - f'Unrecognized Qt version: {gui}. Should be "qt5", "qt6", or "qt".' - ) + print(f'Unrecognized Qt version: {gui}. Should be "qt5", "qt6", or "qt".') # Do the actual import now that the environment variable is set to make sure it works. try: From fde86f07290b74d81c01a83bfafde44918aeda7d Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sat, 7 Jan 2023 13:17:53 -0500 Subject: [PATCH 7/8] Add missing return's in set_qt_api_env_from_gui This avoids making that function run beyond the point where a certain message is printed. --- ipykernel/eventloops.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ipykernel/eventloops.py b/ipykernel/eventloops.py index 927c135e8..262c92569 100644 --- a/ipykernel/eventloops.py +++ b/ipykernel/eventloops.py @@ -474,6 +474,7 @@ def set_qt_api_env_from_gui(gui): if loaded is not None and gui != 'qt': if qt_env2gui[loaded] != gui: print(f'Cannot switch Qt versions for this session; you must use {qt_env2gui[loaded]}.') + return if qt_api is not None and gui != 'qt': if qt_env2gui[qt_api] != gui: @@ -481,6 +482,7 @@ def set_qt_api_env_from_gui(gui): f'Request for "{gui}" will be ignored because `QT_API` ' f'environment variable is set to "{qt_api}"' ) + return else: if gui == 'qt5': try: @@ -512,6 +514,7 @@ def set_qt_api_env_from_gui(gui): del os.environ['QT_API'] else: print(f'Unrecognized Qt version: {gui}. Should be "qt5", "qt6", or "qt".') + return # Do the actual import now that the environment variable is set to make sure it works. try: From 9bc06be4cbaf4894ff5851dd90b45c7b6583cbf7 Mon Sep 17 00:00:00 2001 From: Carlos Cordoba Date: Sat, 7 Jan 2023 13:18:20 -0500 Subject: [PATCH 8/8] Fix test_qt_enable_gui with the new changes --- ipykernel/tests/test_eventloop.py | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/ipykernel/tests/test_eventloop.py b/ipykernel/tests/test_eventloop.py index 3a684d625..c8bd95407 100644 --- a/ipykernel/tests/test_eventloop.py +++ b/ipykernel/tests/test_eventloop.py @@ -14,7 +14,6 @@ loop_asyncio, loop_cocoa, loop_tk, - set_qt_api_env_from_gui, ) from .utils import execute, flush_channels, start_new_kernel @@ -23,21 +22,21 @@ qt_guis_avail = [] +gui_to_module = {'qt6': 'PySide6', 'qt5': 'PyQt5'} + 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']: + for gui in ['qt6', 'qt5']: print(f'Trying {gui}') try: - set_qt_api_env_from_gui(gui) + __import__(gui_to_module[gui]) qt_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. - except RuntimeError: - pass # the version of IPython doesn't know what to do with this Qt version. _get_qt_vers() @@ -129,31 +128,36 @@ def test_cocoa_loop(kernel): @pytest.mark.skipif( len(qt_guis_avail) == 0, reason='No viable version of PyQt or PySide installed.' ) -def test_qt_enable_gui(kernel): +def test_qt_enable_gui(kernel, capsys): gui = qt_guis_avail[0] enable_gui(gui, kernel) # 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) + # Don't create another app even if `gui` is the same. + app = kernel.app + enable_gui(gui, kernel) + assert app == kernel.app # Event loop intergration can be turned off. enable_gui(None, kernel) assert not hasattr(kernel, 'app') # But now we're stuck with this version of Qt for good; can't switch. - for not_gui in ['qt6', 'qt5', 'qt4']: + for not_gui in ['qt6', 'qt5']: if not_gui not in qt_guis_avail: break - with pytest.raises(ImportError): - enable_gui(not_gui, kernel) + enable_gui(not_gui, kernel) + captured = capsys.readouterr() + assert captured.out == f'Cannot switch Qt versions for this session; you must use {gui}.\n' - # A gui of 'qt' means "best available", or in this case, the last one that was used. + # Check 'qt' gui, which means "the best available" + enable_gui(None, kernel) enable_gui('qt', kernel) + assert gui_to_module[gui] in str(kernel.app)