From 88d1fedc3852128236aec950164233f0710356d7 2023-03-13 10:37:33 From: Matthias Bussonnier Date: 2023-03-13 10:37:33 Subject: [PATCH] Shaperilio/qtgui fixes (#13957) I started using the released version of my `PySide6`-enabling changes and noted some problems. In this PR, I fix those, and also overall improve the feedback to the user when a GUI event loop is hooked in: - Report which event loop is running when using `%gui `; e.g. `%gui qt` will show `Installed qt6 event loop hook.` - Report when the event loop is disabled; i.e. `%gui` will show `GUI event loop hook disabled.` if an event loop hook was installed, or `No event loop hook running.` if nothing was installed. - Requesting a second event loop will give the message `Shell is already running a gui event loop for . Call with no arguments to disable current loop.` - Requesting a different version of Qt, i.e. `%gui qt6` followed by `%gui` followed by `%gui qt5` will show `Cannot switch Qt versions for this session; will use qt6.` followed by `Installed qt6 event loop hook.` (Fixes / improves #13864) --- diff --git a/IPython/external/qt_loaders.py b/IPython/external/qt_loaders.py index c900c8f..1486cf9 100644 --- a/IPython/external/qt_loaders.py +++ b/IPython/external/qt_loaders.py @@ -10,6 +10,7 @@ be accessed directly from the outside """ import importlib.abc import sys +import os import types from functools import partial, lru_cache import operator @@ -368,6 +369,10 @@ def load_qt(api_options): commit_api(api) return result else: + # Clear the environment variable since it doesn't work. + if "QT_API" in os.environ: + del os.environ["QT_API"] + raise ImportError( """ Could not load requested Qt binding. Please ensure that diff --git a/IPython/terminal/interactiveshell.py b/IPython/terminal/interactiveshell.py index c73acf9..96a9687 100644 --- a/IPython/terminal/interactiveshell.py +++ b/IPython/terminal/interactiveshell.py @@ -913,10 +913,19 @@ class TerminalInteractiveShell(InteractiveShell): active_eventloop = None def enable_gui(self, gui=None): + if self._inputhook is None and gui is None: + print("No event loop hook running.") + return + if self._inputhook is not None and gui is not None: - warn( - f"Shell was already running a gui event loop for {self.active_eventloop}; switching to {gui}." + print( + f"Shell is already running a gui event loop for {self.active_eventloop}. " + "Call with no arguments to disable the current loop." ) + return + if self._inputhook is not None and gui is None: + self.active_eventloop = self._inputhook = None + if gui and (gui not in {"inline", "webagg"}): # This hook runs with each cycle of the `prompt_toolkit`'s event loop. self.active_eventloop, self._inputhook = get_inputhook_name_and_func(gui) @@ -934,15 +943,18 @@ class TerminalInteractiveShell(InteractiveShell): # same event loop as the rest of the code. don't use an actual # input hook. (Asyncio is not made for nesting event loops.) self.pt_loop = get_asyncio_loop() + print("Installed asyncio event loop hook.") elif self._inputhook: # If an inputhook was set, create a new asyncio event loop with # this inputhook for the prompt. self.pt_loop = new_eventloop_with_inputhook(self._inputhook) + print(f"Installed {self.active_eventloop} event loop hook.") else: # When there's no inputhook, run the prompt in a separate # asyncio event loop. self.pt_loop = asyncio.new_event_loop() + print("GUI event loop hook disabled.") # Run !system commands directly, not through pipes, so terminal programs # work correctly. diff --git a/IPython/terminal/pt_inputhooks/__init__.py b/IPython/terminal/pt_inputhooks/__init__.py index 57960e4..9043f15 100644 --- a/IPython/terminal/pt_inputhooks/__init__.py +++ b/IPython/terminal/pt_inputhooks/__init__.py @@ -69,9 +69,9 @@ def set_qt_api(gui): if loaded is not None and gui != "qt": if qt_env2gui[loaded] != gui: print( - f"Cannot switch Qt versions for this session; must use {qt_env2gui[loaded]}." + f"Cannot switch Qt versions for this session; will use {qt_env2gui[loaded]}." ) - return + return qt_env2gui[loaded] if qt_api is not None and gui != "qt": if qt_env2gui[qt_api] != gui: @@ -79,6 +79,7 @@ def set_qt_api(gui): f'Request for "{gui}" will be ignored because `QT_API` ' f'environment variable is set to "{qt_api}"' ) + return qt_env2gui[qt_api] else: if gui == "qt5": try: @@ -112,6 +113,11 @@ def set_qt_api(gui): print(f'Unrecognized Qt version: {gui}. Should be "qt5", "qt6", or "qt".') return + # Import it now so we can figure out which version it is. + from IPython.external.qt_for_kernel import QT_API + + return qt_env2gui[QT_API] + def get_inputhook_name_and_func(gui): if gui in registered: @@ -125,7 +131,7 @@ def get_inputhook_name_and_func(gui): gui_mod = gui if gui.startswith("qt"): - set_qt_api(gui) + gui = set_qt_api(gui) gui_mod = "qt" mod = importlib.import_module("IPython.terminal.pt_inputhooks." + gui_mod) diff --git a/IPython/terminal/tests/test_pt_inputhooks.py b/IPython/terminal/tests/test_pt_inputhooks.py index bb4baaa..3f788c7 100644 --- a/IPython/terminal/tests/test_pt_inputhooks.py +++ b/IPython/terminal/tests/test_pt_inputhooks.py @@ -33,18 +33,18 @@ _get_qt_vers() len(guis_avail) == 0, reason="No viable version of PyQt or PySide installed." ) def test_inputhook_qt(): - gui = guis_avail[0] - - # Choose a qt version and get the input hook function. This will import Qt... - get_inputhook_name_and_func(gui) - - # ...and now we're stuck with this version of Qt for good; can't switch. - for not_gui in ["qt6", "qt5"]: - if not_gui not in guis_avail: - break - - with pytest.raises(ImportError): - get_inputhook_name_and_func(not_gui) - - # A gui of 'qt' means "best available", or in this case, the last one that was used. - get_inputhook_name_and_func("qt") + # Choose the "best" Qt version. + gui_ret, _ = get_inputhook_name_and_func("qt") + + assert gui_ret != "qt" # you get back the specific version that was loaded. + assert gui_ret in guis_avail + + if len(guis_avail) > 2: + # ...and now we're stuck with this version of Qt for good; can't switch. + for not_gui in ["qt6", "qt5"]: + if not_gui != gui_ret: + break + # Try to import the other gui; it won't work. + gui_ret2, _ = get_inputhook_name_and_func(not_gui) + assert gui_ret2 == gui_ret + assert gui_ret2 != not_gui