@@ -21,41 +21,6 @@ def _use_appnope():
21
21
return sys .platform == "darwin" and V (platform .mac_ver ()[0 ]) >= V ("10.9" )
22
22
23
23
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
-
59
24
# mapping of keys to loop functions
60
25
loop_map = {
61
26
"inline" : None ,
@@ -103,54 +68,67 @@ def exit_decorator(exit_func):
103
68
return decorator
104
69
105
70
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
117
74
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
121
76
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 :
124
80
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__ ])
129
84
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 ()
131
94
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 )
132
103
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 )
139
116
140
- os .environ ["QT_API" ] = "pyqt5"
141
- except ImportError :
142
- try :
143
- import PySide2 # noqa
144
117
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
149
128
150
129
151
130
# exit and watch are the same for qt 4 and 5
152
- @loop_qt4 .exit
153
- @loop_qt5 .exit
131
+ @loop_qt .exit
154
132
def loop_qt_exit (kernel ):
155
133
kernel .app .exit ()
156
134
@@ -450,6 +428,135 @@ def close_loop():
450
428
loop .close ()
451
429
452
430
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
+
453
560
def enable_gui (gui , kernel = None ):
454
561
"""Enable integration with a given GUI"""
455
562
if gui not in loop_map :
@@ -463,7 +570,18 @@ def enable_gui(gui, kernel=None):
463
570
"You didn't specify a kernel,"
464
571
" and no IPython Application with a kernel appears to be running."
465
572
)
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
+
466
582
loop = loop_map [gui ]
467
583
if loop and kernel .eventloop is not None and kernel .eventloop is not loop :
468
584
raise RuntimeError ("Cannot activate multiple GUI eventloops" )
469
585
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