Skip to content

Commit a192ced

Browse files
Don't raise error when trying to create another Qt app for Qt eventloop (#1071)
* 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). * Remove support to set a Qt4 eventloop because it's no longer supported * Move big comment outside a function to be part of a docstring * 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. * Restore loop_qt5 function because it was public API * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * 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. * Fix test_qt_enable_gui with the new changes Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 4f0e252 commit a192ced

File tree

2 files changed

+52
-57
lines changed

2 files changed

+52
-57
lines changed

ipykernel/eventloops.py

Lines changed: 35 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -115,18 +115,24 @@ def process_stream_events():
115115
kernel._qt_timer.start(0)
116116

117117

118-
@register_integration("qt", "qt4", "qt5", "qt6")
118+
@register_integration("qt", "qt5", "qt6")
119119
def loop_qt(kernel):
120-
"""Event loop for all versions of Qt."""
120+
"""Event loop for all supported versions of Qt."""
121121
_notify_stream_qt(kernel) # install hook to stop event loop.
122+
122123
# Start the event loop.
123124
kernel.app._in_event_loop = True
125+
124126
# `exec` blocks until there's ZMQ activity.
125127
el = kernel.app.qt_event_loop # for brevity
126128
el.exec() if hasattr(el, 'exec') else el.exec_()
127129
kernel.app._in_event_loop = False
128130

129131

132+
# NOTE: To be removed in version 7
133+
loop_qt5 = loop_qt
134+
135+
130136
# exit and watch are the same for qt 4 and 5
131137
@loop_qt.exit
132138
def loop_qt_exit(kernel):
@@ -428,75 +434,57 @@ def close_loop():
428434
loop.close()
429435

430436

431-
# The user can generically request `qt` or a specific Qt version, e.g. `qt6`. For a generic Qt
432-
# request, we let the mechanism in IPython choose the best available version by leaving the `QT_API`
433-
# environment variable blank.
434-
#
435-
# For specific versions, we check to see whether the PyQt or PySide implementations are present and
436-
# set `QT_API` accordingly to indicate to IPython which version we want. If neither implementation
437-
# is present, we leave the environment variable set so IPython will generate a helpful error
438-
# message.
439-
#
440-
# NOTE: if the environment variable is already set, it will be used unchanged, regardless of what
441-
# the user requested.
442-
443-
444437
def set_qt_api_env_from_gui(gui):
445438
"""
446439
Sets the QT_API environment variable by trying to import PyQtx or PySidex.
447440
448-
If QT_API is already set, ignore the request.
441+
The user can generically request `qt` or a specific Qt version, e.g. `qt6`.
442+
For a generic Qt request, we let the mechanism in IPython choose the best
443+
available version by leaving the `QT_API` environment variable blank.
444+
445+
For specific versions, we check to see whether the PyQt or PySide
446+
implementations are present and set `QT_API` accordingly to indicate to
447+
IPython which version we want. If neither implementation is present, we
448+
leave the environment variable set so IPython will generate a helpful error
449+
message.
450+
451+
Notes
452+
-----
453+
- If the environment variable is already set, it will be used unchanged,
454+
regardless of what the user requested.
449455
"""
450456
qt_api = os.environ.get("QT_API", None)
451457

452458
from IPython.external.qt_loaders import (
453-
QT_API_PYQT,
454459
QT_API_PYQT5,
455460
QT_API_PYQT6,
456-
QT_API_PYSIDE,
457461
QT_API_PYSIDE2,
458462
QT_API_PYSIDE6,
459-
QT_API_PYQTv1,
460463
loaded_api,
461464
)
462465

463466
loaded = loaded_api()
464467

465468
qt_env2gui = {
466-
QT_API_PYSIDE: 'qt4',
467-
QT_API_PYQTv1: 'qt4',
468-
QT_API_PYQT: 'qt4',
469469
QT_API_PYSIDE2: 'qt5',
470470
QT_API_PYQT5: 'qt5',
471471
QT_API_PYSIDE6: 'qt6',
472472
QT_API_PYQT6: 'qt6',
473473
}
474474
if loaded is not None and gui != 'qt':
475475
if qt_env2gui[loaded] != gui:
476-
msg = f'Cannot switch Qt versions for this session; must use {qt_env2gui[loaded]}.'
477-
raise ImportError(msg)
476+
print(f'Cannot switch Qt versions for this session; you must use {qt_env2gui[loaded]}.')
477+
return
478478

479479
if qt_api is not None and gui != 'qt':
480480
if qt_env2gui[qt_api] != gui:
481481
print(
482482
f'Request for "{gui}" will be ignored because `QT_API` '
483483
f'environment variable is set to "{qt_api}"'
484484
)
485+
return
485486
else:
486-
if gui == 'qt4':
487-
try:
488-
import PyQt # noqa
489-
490-
os.environ["QT_API"] = "pyqt"
491-
except ImportError:
492-
try:
493-
import PySide # noqa
494-
495-
os.environ["QT_API"] = "pyside"
496-
except ImportError:
497-
# Neither implementation installed; set it to something so IPython gives an error
498-
os.environ["QT_API"] = "pyqt"
499-
elif gui == 'qt5':
487+
if gui == 'qt5':
500488
try:
501489
import PyQt5 # noqa
502490

@@ -525,26 +513,29 @@ def set_qt_api_env_from_gui(gui):
525513
if 'QT_API' in os.environ.keys():
526514
del os.environ['QT_API']
527515
else:
528-
msg = f'Unrecognized Qt version: {gui}. Should be "qt4", "qt5", "qt6", or "qt".'
529-
raise ValueError(msg)
516+
print(f'Unrecognized Qt version: {gui}. Should be "qt5", "qt6", or "qt".')
517+
return
530518

531519
# Do the actual import now that the environment variable is set to make sure it works.
532520
try:
533521
from IPython.external.qt_for_kernel import QtCore, QtGui # noqa
534-
except ImportError:
522+
except Exception as e:
535523
# Clear the environment variable for the next attempt.
536524
if 'QT_API' in os.environ.keys():
537525
del os.environ["QT_API"]
538-
raise
526+
print(f"QT_API couldn't be set due to error {e}")
527+
return
539528

540529

541530
def make_qt_app_for_kernel(gui, kernel):
542531
"""Sets the `QT_API` environment variable if it isn't already set."""
543532
if hasattr(kernel, 'app'):
544-
msg = 'Kernel already running a Qt event loop.'
545-
raise RuntimeError(msg)
533+
# Kernel is already running a Qt event loop, so there's no need to
534+
# create another app for it.
535+
return
546536

547537
set_qt_api_env_from_gui(gui)
538+
548539
# This import is guaranteed to work now:
549540
from IPython.external.qt_for_kernel import QtCore, QtGui
550541
from IPython.lib.guisupport import get_app_qt4

ipykernel/tests/test_eventloop.py

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
loop_asyncio,
1515
loop_cocoa,
1616
loop_tk,
17-
set_qt_api_env_from_gui,
1817
)
1918

2019
from .utils import execute, flush_channels, start_new_kernel
@@ -23,21 +22,21 @@
2322

2423
qt_guis_avail = []
2524

25+
gui_to_module = {'qt6': 'PySide6', 'qt5': 'PyQt5'}
26+
2627

2728
def _get_qt_vers():
2829
"""If any version of Qt is available, this will populate `guis_avail` with 'qt' and 'qtx'. Due
2930
to the import mechanism, we can't import multiple versions of Qt in one session."""
30-
for gui in ['qt', 'qt6', 'qt5', 'qt4']:
31+
for gui in ['qt6', 'qt5']:
3132
print(f'Trying {gui}')
3233
try:
33-
set_qt_api_env_from_gui(gui)
34+
__import__(gui_to_module[gui])
3435
qt_guis_avail.append(gui)
3536
if 'QT_API' in os.environ.keys():
3637
del os.environ['QT_API']
3738
except ImportError:
3839
pass # that version of Qt isn't available.
39-
except RuntimeError:
40-
pass # the version of IPython doesn't know what to do with this Qt version.
4140

4241

4342
_get_qt_vers()
@@ -129,31 +128,36 @@ def test_cocoa_loop(kernel):
129128
@pytest.mark.skipif(
130129
len(qt_guis_avail) == 0, reason='No viable version of PyQt or PySide installed.'
131130
)
132-
def test_qt_enable_gui(kernel):
131+
def test_qt_enable_gui(kernel, capsys):
133132
gui = qt_guis_avail[0]
134133

135134
enable_gui(gui, kernel)
136135

137136
# We store the `QApplication` instance in the kernel.
138137
assert hasattr(kernel, 'app')
138+
139139
# And the `QEventLoop` is added to `app`:`
140140
assert hasattr(kernel.app, 'qt_event_loop')
141141

142-
# Can't start another event loop, even if `gui` is the same.
143-
with pytest.raises(RuntimeError):
144-
enable_gui(gui, kernel)
142+
# Don't create another app even if `gui` is the same.
143+
app = kernel.app
144+
enable_gui(gui, kernel)
145+
assert app == kernel.app
145146

146147
# Event loop intergration can be turned off.
147148
enable_gui(None, kernel)
148149
assert not hasattr(kernel, 'app')
149150

150151
# But now we're stuck with this version of Qt for good; can't switch.
151-
for not_gui in ['qt6', 'qt5', 'qt4']:
152+
for not_gui in ['qt6', 'qt5']:
152153
if not_gui not in qt_guis_avail:
153154
break
154155

155-
with pytest.raises(ImportError):
156-
enable_gui(not_gui, kernel)
156+
enable_gui(not_gui, kernel)
157+
captured = capsys.readouterr()
158+
assert captured.out == f'Cannot switch Qt versions for this session; you must use {gui}.\n'
157159

158-
# A gui of 'qt' means "best available", or in this case, the last one that was used.
160+
# Check 'qt' gui, which means "the best available"
161+
enable_gui(None, kernel)
159162
enable_gui('qt', kernel)
163+
assert gui_to_module[gui] in str(kernel.app)

0 commit comments

Comments
 (0)