@@ -21,41 +21,6 @@ def _use_appnope():
2121 return sys .platform == "darwin" and V (platform .mac_ver ()[0 ]) >= V ("10.9" )
2222
2323
24- def _notify_stream_qt (kernel ):
25-
26- from IPython .external .qt_for_kernel import QtCore
27-
28- def process_stream_events ():
29- """fall back to main loop when there's a socket event"""
30- # call flush to ensure that the stream doesn't lose events
31- # due to our consuming of the edge-triggered FD
32- # flush returns the number of events consumed.
33- # if there were any, wake it up
34- if kernel .shell_stream .flush (limit = 1 ):
35- kernel ._qt_notifier .setEnabled (False )
36- kernel .app .quit ()
37-
38- if not hasattr (kernel , "_qt_notifier" ):
39- fd = kernel .shell_stream .getsockopt (zmq .FD )
40- kernel ._qt_notifier = QtCore .QSocketNotifier (fd , QtCore .QSocketNotifier .Read , kernel .app )
41- kernel ._qt_notifier .activated .connect (process_stream_events )
42- else :
43- kernel ._qt_notifier .setEnabled (True )
44-
45- # there may already be unprocessed events waiting.
46- # these events will not wake zmq's edge-triggered FD
47- # since edge-triggered notification only occurs on new i/o activity.
48- # process all the waiting events immediately
49- # so we start in a clean state ensuring that any new i/o events will notify.
50- # schedule first call on the eventloop as soon as it's running,
51- # so we don't block here processing events
52- if not hasattr (kernel , "_qt_timer" ):
53- kernel ._qt_timer = QtCore .QTimer (kernel .app )
54- kernel ._qt_timer .setSingleShot (True )
55- kernel ._qt_timer .timeout .connect (process_stream_events )
56- kernel ._qt_timer .start (0 )
57-
58-
5924# mapping of keys to loop functions
6025loop_map = {
6126 "inline" : None ,
@@ -103,54 +68,67 @@ def exit_decorator(exit_func):
10368 return decorator
10469
10570
106- def _loop_qt (app ):
107- """Inner-loop for running the Qt eventloop
108-
109- Pulled from guisupport.start_event_loop in IPython < 5.2,
110- since IPython 5.2 only checks `get_ipython().active_eventloop` is defined,
111- rather than if the eventloop is actually running.
112- """
113- app ._in_event_loop = True
114- app .exec_ ()
115- app ._in_event_loop = False
116-
71+ def _notify_stream_qt (kernel ):
72+ import operator
73+ from functools import lru_cache
11774
118- @register_integration ("qt4" )
119- def loop_qt4 (kernel ):
120- """Start a kernel with PyQt4 event loop integration."""
75+ from IPython .external .qt_for_kernel import QtCore
12176
122- from IPython .external .qt_for_kernel import QtGui
123- from IPython .lib .guisupport import get_app_qt4
77+ try :
78+ from IPython .external .qt_for_kernel import enum_helper
79+ except ImportError :
12480
125- kernel .app = get_app_qt4 ([" " ])
126- if isinstance (kernel .app , QtGui .QApplication ):
127- kernel .app .setQuitOnLastWindowClosed (False )
128- _notify_stream_qt (kernel )
81+ @lru_cache (None )
82+ def enum_helper (name ):
83+ return operator .attrgetter (name .rpartition ("." )[0 ])(sys .modules [QtCore .__package__ ])
12984
130- _loop_qt (kernel .app )
85+ def process_stream_events ():
86+ """fall back to main loop when there's a socket event"""
87+ # call flush to ensure that the stream doesn't lose events
88+ # due to our consuming of the edge-triggered FD
89+ # flush returns the number of events consumed.
90+ # if there were any, wake it up
91+ if kernel .shell_stream .flush (limit = 1 ):
92+ kernel ._qt_notifier .setEnabled (False )
93+ kernel .app .qt_event_loop .quit ()
13194
95+ if not hasattr (kernel , "_qt_notifier" ):
96+ fd = kernel .shell_stream .getsockopt (zmq .FD )
97+ kernel ._qt_notifier = QtCore .QSocketNotifier (
98+ fd , enum_helper ('QtCore.QSocketNotifier.Type' ).Read , kernel .app .qt_event_loop
99+ )
100+ kernel ._qt_notifier .activated .connect (process_stream_events )
101+ else :
102+ kernel ._qt_notifier .setEnabled (True )
132103
133- @register_integration ("qt" , "qt5" )
134- def loop_qt5 (kernel ):
135- """Start a kernel with PyQt5 event loop integration."""
136- if os .environ .get ("QT_API" , None ) is None :
137- try :
138- import PyQt5 # noqa
104+ # there may already be unprocessed events waiting.
105+ # these events will not wake zmq's edge-triggered FD
106+ # since edge-triggered notification only occurs on new i/o activity.
107+ # process all the waiting events immediately
108+ # so we start in a clean state ensuring that any new i/o events will notify.
109+ # schedule first call on the eventloop as soon as it's running,
110+ # so we don't block here processing events
111+ if not hasattr (kernel , "_qt_timer" ):
112+ kernel ._qt_timer = QtCore .QTimer (kernel .app )
113+ kernel ._qt_timer .setSingleShot (True )
114+ kernel ._qt_timer .timeout .connect (process_stream_events )
115+ kernel ._qt_timer .start (0 )
139116
140- os .environ ["QT_API" ] = "pyqt5"
141- except ImportError :
142- try :
143- import PySide2 # noqa
144117
145- os .environ ["QT_API" ] = "pyside2"
146- except ImportError :
147- os .environ ["QT_API" ] = "pyqt5"
148- return loop_qt4 (kernel )
118+ @register_integration ("qt" , "qt4" , "qt5" , "qt6" )
119+ def loop_qt (kernel ):
120+ """Event loop for all versions of Qt."""
121+ _notify_stream_qt (kernel ) # install hook to stop event loop.
122+ # Start the event loop.
123+ kernel .app ._in_event_loop = True
124+ # `exec` blocks until there's ZMQ activity.
125+ el = kernel .app .qt_event_loop # for brevity
126+ el .exec () if hasattr (el , 'exec' ) else el .exec_ ()
127+ kernel .app ._in_event_loop = False
149128
150129
151130# exit and watch are the same for qt 4 and 5
152- @loop_qt4 .exit
153- @loop_qt5 .exit
131+ @loop_qt .exit
154132def loop_qt_exit (kernel ):
155133 kernel .app .exit ()
156134
@@ -450,6 +428,135 @@ def close_loop():
450428 loop .close ()
451429
452430
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+
444+ def set_qt_api_env_from_gui (gui ):
445+ """
446+ Sets the QT_API environment variable by trying to import PyQtx or PySidex.
447+
448+ If QT_API is already set, ignore the request.
449+ """
450+ qt_api = os .environ .get ("QT_API" , None )
451+
452+ from IPython .external .qt_loaders import (
453+ QT_API_PYQT ,
454+ QT_API_PYQT5 ,
455+ QT_API_PYQT6 ,
456+ QT_API_PYSIDE ,
457+ QT_API_PYSIDE2 ,
458+ QT_API_PYSIDE6 ,
459+ QT_API_PYQTv1 ,
460+ loaded_api ,
461+ )
462+
463+ loaded = loaded_api ()
464+
465+ qt_env2gui = {
466+ QT_API_PYSIDE : 'qt4' ,
467+ QT_API_PYQTv1 : 'qt4' ,
468+ QT_API_PYQT : 'qt4' ,
469+ QT_API_PYSIDE2 : 'qt5' ,
470+ QT_API_PYQT5 : 'qt5' ,
471+ QT_API_PYSIDE6 : 'qt6' ,
472+ QT_API_PYQT6 : 'qt6' ,
473+ }
474+ if loaded is not None and gui != 'qt' :
475+ if qt_env2gui [loaded ] != gui :
476+ raise ImportError (
477+ f'Cannot switch Qt versions for this session; must use { qt_env2gui [loaded ]} .'
478+ )
479+
480+ if qt_api is not None and gui != 'qt' :
481+ if qt_env2gui [qt_api ] != gui :
482+ print (
483+ f'Request for "{ gui } " will be ignored because `QT_API` '
484+ f'environment variable is set to "{ qt_api } "'
485+ )
486+ else :
487+ if gui == 'qt4' :
488+ try :
489+ import PyQt # noqa
490+
491+ os .environ ["QT_API" ] = "pyqt"
492+ except ImportError :
493+ try :
494+ import PySide # noqa
495+
496+ os .environ ["QT_API" ] = "pyside"
497+ except ImportError :
498+ # Neither implementation installed; set it to something so IPython gives an error
499+ os .environ ["QT_API" ] = "pyqt"
500+ elif gui == 'qt5' :
501+ try :
502+ import PyQt5 # noqa
503+
504+ os .environ ["QT_API" ] = "pyqt5"
505+ except ImportError :
506+ try :
507+ import PySide2 # noqa
508+
509+ os .environ ["QT_API" ] = "pyside2"
510+ except ImportError :
511+ os .environ ["QT_API" ] = "pyqt5"
512+ elif gui == 'qt6' :
513+ try :
514+ import PyQt6 # noqa
515+
516+ os .environ ["QT_API" ] = "pyqt6"
517+ except ImportError :
518+ try :
519+ import PySide6 # noqa
520+
521+ os .environ ["QT_API" ] = "pyside6"
522+ except ImportError :
523+ os .environ ["QT_API" ] = "pyqt6"
524+ elif gui == 'qt' :
525+ # Don't set QT_API; let IPython logic choose the version.
526+ if 'QT_API' in os .environ .keys ():
527+ del os .environ ['QT_API' ]
528+ else :
529+ raise ValueError (
530+ f'Unrecognized Qt version: { gui } . Should be "qt4", "qt5", "qt6", or "qt".'
531+ )
532+
533+ # Do the actual import now that the environment variable is set to make sure it works.
534+ try :
535+ from IPython .external .qt_for_kernel import QtCore , QtGui # noqa
536+ except ImportError :
537+ # Clear the environment variable for the next attempt.
538+ if 'QT_API' in os .environ .keys ():
539+ del os .environ ["QT_API" ]
540+ raise
541+
542+
543+ def make_qt_app_for_kernel (gui , kernel ):
544+ """Sets the `QT_API` environment variable if it isn't already set."""
545+ if hasattr (kernel , 'app' ):
546+ raise RuntimeError ('Kernel already running a Qt event loop.' )
547+
548+ set_qt_api_env_from_gui (gui )
549+ # This import is guaranteed to work now:
550+ from IPython .external .qt_for_kernel import QtCore , QtGui
551+ from IPython .lib .guisupport import get_app_qt4
552+
553+ kernel .app = get_app_qt4 ([" " ])
554+ if isinstance (kernel .app , QtGui .QApplication ):
555+ kernel .app .setQuitOnLastWindowClosed (False )
556+
557+ kernel .app .qt_event_loop = QtCore .QEventLoop (kernel .app )
558+
559+
453560def enable_gui (gui , kernel = None ):
454561 """Enable integration with a given GUI"""
455562 if gui not in loop_map :
@@ -463,7 +570,18 @@ def enable_gui(gui, kernel=None):
463570 "You didn't specify a kernel,"
464571 " and no IPython Application with a kernel appears to be running."
465572 )
573+ if gui is None :
574+ # User wants to turn off integration; clear any evidence if Qt was the last one.
575+ if hasattr (kernel , 'app' ):
576+ delattr (kernel , 'app' )
577+ else :
578+ if gui .startswith ('qt' ):
579+ # Prepare the kernel here so any exceptions are displayed in the client.
580+ make_qt_app_for_kernel (gui , kernel )
581+
466582 loop = loop_map [gui ]
467583 if loop and kernel .eventloop is not None and kernel .eventloop is not loop :
468584 raise RuntimeError ("Cannot activate multiple GUI eventloops" )
469585 kernel .eventloop = loop
586+ # We set `eventloop`; the function the user chose is executed in `Kernel.enter_eventloop`, thus
587+ # any exceptions raised during the event loop will not be shown in the client.
0 commit comments