##// END OF EJS Templates
Shaperilio/qtgui fixes (#13957)...
Matthias Bussonnier -
r28163:88d1fedc merge
parent child Browse files
Show More
@@ -1,405 +1,410 b''
1 """
1 """
2 This module contains factory functions that attempt
2 This module contains factory functions that attempt
3 to return Qt submodules from the various python Qt bindings.
3 to return Qt submodules from the various python Qt bindings.
4
4
5 It also protects against double-importing Qt with different
5 It also protects against double-importing Qt with different
6 bindings, which is unstable and likely to crash
6 bindings, which is unstable and likely to crash
7
7
8 This is used primarily by qt and qt_for_kernel, and shouldn't
8 This is used primarily by qt and qt_for_kernel, and shouldn't
9 be accessed directly from the outside
9 be accessed directly from the outside
10 """
10 """
11 import importlib.abc
11 import importlib.abc
12 import sys
12 import sys
13 import os
13 import types
14 import types
14 from functools import partial, lru_cache
15 from functools import partial, lru_cache
15 import operator
16 import operator
16
17
17 # ### Available APIs.
18 # ### Available APIs.
18 # Qt6
19 # Qt6
19 QT_API_PYQT6 = "pyqt6"
20 QT_API_PYQT6 = "pyqt6"
20 QT_API_PYSIDE6 = "pyside6"
21 QT_API_PYSIDE6 = "pyside6"
21
22
22 # Qt5
23 # Qt5
23 QT_API_PYQT5 = 'pyqt5'
24 QT_API_PYQT5 = 'pyqt5'
24 QT_API_PYSIDE2 = 'pyside2'
25 QT_API_PYSIDE2 = 'pyside2'
25
26
26 # Qt4
27 # Qt4
27 # NOTE: Here for legacy matplotlib compatibility, but not really supported on the IPython side.
28 # NOTE: Here for legacy matplotlib compatibility, but not really supported on the IPython side.
28 QT_API_PYQT = "pyqt" # Force version 2
29 QT_API_PYQT = "pyqt" # Force version 2
29 QT_API_PYQTv1 = "pyqtv1" # Force version 2
30 QT_API_PYQTv1 = "pyqtv1" # Force version 2
30 QT_API_PYSIDE = "pyside"
31 QT_API_PYSIDE = "pyside"
31
32
32 QT_API_PYQT_DEFAULT = "pyqtdefault" # use system default for version 1 vs. 2
33 QT_API_PYQT_DEFAULT = "pyqtdefault" # use system default for version 1 vs. 2
33
34
34 api_to_module = {
35 api_to_module = {
35 # Qt6
36 # Qt6
36 QT_API_PYQT6: "PyQt6",
37 QT_API_PYQT6: "PyQt6",
37 QT_API_PYSIDE6: "PySide6",
38 QT_API_PYSIDE6: "PySide6",
38 # Qt5
39 # Qt5
39 QT_API_PYQT5: "PyQt5",
40 QT_API_PYQT5: "PyQt5",
40 QT_API_PYSIDE2: "PySide2",
41 QT_API_PYSIDE2: "PySide2",
41 # Qt4
42 # Qt4
42 QT_API_PYSIDE: "PySide",
43 QT_API_PYSIDE: "PySide",
43 QT_API_PYQT: "PyQt4",
44 QT_API_PYQT: "PyQt4",
44 QT_API_PYQTv1: "PyQt4",
45 QT_API_PYQTv1: "PyQt4",
45 # default
46 # default
46 QT_API_PYQT_DEFAULT: "PyQt6",
47 QT_API_PYQT_DEFAULT: "PyQt6",
47 }
48 }
48
49
49
50
50 class ImportDenier(importlib.abc.MetaPathFinder):
51 class ImportDenier(importlib.abc.MetaPathFinder):
51 """Import Hook that will guard against bad Qt imports
52 """Import Hook that will guard against bad Qt imports
52 once IPython commits to a specific binding
53 once IPython commits to a specific binding
53 """
54 """
54
55
55 def __init__(self):
56 def __init__(self):
56 self.__forbidden = set()
57 self.__forbidden = set()
57
58
58 def forbid(self, module_name):
59 def forbid(self, module_name):
59 sys.modules.pop(module_name, None)
60 sys.modules.pop(module_name, None)
60 self.__forbidden.add(module_name)
61 self.__forbidden.add(module_name)
61
62
62 def find_spec(self, fullname, path, target=None):
63 def find_spec(self, fullname, path, target=None):
63 if path:
64 if path:
64 return
65 return
65 if fullname in self.__forbidden:
66 if fullname in self.__forbidden:
66 raise ImportError(
67 raise ImportError(
67 """
68 """
68 Importing %s disabled by IPython, which has
69 Importing %s disabled by IPython, which has
69 already imported an Incompatible QT Binding: %s
70 already imported an Incompatible QT Binding: %s
70 """
71 """
71 % (fullname, loaded_api())
72 % (fullname, loaded_api())
72 )
73 )
73
74
74
75
75 ID = ImportDenier()
76 ID = ImportDenier()
76 sys.meta_path.insert(0, ID)
77 sys.meta_path.insert(0, ID)
77
78
78
79
79 def commit_api(api):
80 def commit_api(api):
80 """Commit to a particular API, and trigger ImportErrors on subsequent
81 """Commit to a particular API, and trigger ImportErrors on subsequent
81 dangerous imports"""
82 dangerous imports"""
82 modules = set(api_to_module.values())
83 modules = set(api_to_module.values())
83
84
84 modules.remove(api_to_module[api])
85 modules.remove(api_to_module[api])
85 for mod in modules:
86 for mod in modules:
86 ID.forbid(mod)
87 ID.forbid(mod)
87
88
88
89
89 def loaded_api():
90 def loaded_api():
90 """Return which API is loaded, if any
91 """Return which API is loaded, if any
91
92
92 If this returns anything besides None,
93 If this returns anything besides None,
93 importing any other Qt binding is unsafe.
94 importing any other Qt binding is unsafe.
94
95
95 Returns
96 Returns
96 -------
97 -------
97 None, 'pyside6', 'pyqt6', 'pyside2', 'pyside', 'pyqt', 'pyqt5', 'pyqtv1'
98 None, 'pyside6', 'pyqt6', 'pyside2', 'pyside', 'pyqt', 'pyqt5', 'pyqtv1'
98 """
99 """
99 if sys.modules.get("PyQt6.QtCore"):
100 if sys.modules.get("PyQt6.QtCore"):
100 return QT_API_PYQT6
101 return QT_API_PYQT6
101 elif sys.modules.get("PySide6.QtCore"):
102 elif sys.modules.get("PySide6.QtCore"):
102 return QT_API_PYSIDE6
103 return QT_API_PYSIDE6
103 elif sys.modules.get("PyQt5.QtCore"):
104 elif sys.modules.get("PyQt5.QtCore"):
104 return QT_API_PYQT5
105 return QT_API_PYQT5
105 elif sys.modules.get("PySide2.QtCore"):
106 elif sys.modules.get("PySide2.QtCore"):
106 return QT_API_PYSIDE2
107 return QT_API_PYSIDE2
107 elif sys.modules.get("PyQt4.QtCore"):
108 elif sys.modules.get("PyQt4.QtCore"):
108 if qtapi_version() == 2:
109 if qtapi_version() == 2:
109 return QT_API_PYQT
110 return QT_API_PYQT
110 else:
111 else:
111 return QT_API_PYQTv1
112 return QT_API_PYQTv1
112 elif sys.modules.get("PySide.QtCore"):
113 elif sys.modules.get("PySide.QtCore"):
113 return QT_API_PYSIDE
114 return QT_API_PYSIDE
114
115
115 return None
116 return None
116
117
117
118
118 def has_binding(api):
119 def has_binding(api):
119 """Safely check for PyQt4/5, PySide or PySide2, without importing submodules
120 """Safely check for PyQt4/5, PySide or PySide2, without importing submodules
120
121
121 Parameters
122 Parameters
122 ----------
123 ----------
123 api : str [ 'pyqtv1' | 'pyqt' | 'pyqt5' | 'pyside' | 'pyside2' | 'pyqtdefault']
124 api : str [ 'pyqtv1' | 'pyqt' | 'pyqt5' | 'pyside' | 'pyside2' | 'pyqtdefault']
124 Which module to check for
125 Which module to check for
125
126
126 Returns
127 Returns
127 -------
128 -------
128 True if the relevant module appears to be importable
129 True if the relevant module appears to be importable
129 """
130 """
130 module_name = api_to_module[api]
131 module_name = api_to_module[api]
131 from importlib.util import find_spec
132 from importlib.util import find_spec
132
133
133 required = ['QtCore', 'QtGui', 'QtSvg']
134 required = ['QtCore', 'QtGui', 'QtSvg']
134 if api in (QT_API_PYQT5, QT_API_PYSIDE2, QT_API_PYQT6, QT_API_PYSIDE6):
135 if api in (QT_API_PYQT5, QT_API_PYSIDE2, QT_API_PYQT6, QT_API_PYSIDE6):
135 # QT5 requires QtWidgets too
136 # QT5 requires QtWidgets too
136 required.append('QtWidgets')
137 required.append('QtWidgets')
137
138
138 for submod in required:
139 for submod in required:
139 try:
140 try:
140 spec = find_spec('%s.%s' % (module_name, submod))
141 spec = find_spec('%s.%s' % (module_name, submod))
141 except ImportError:
142 except ImportError:
142 # Package (e.g. PyQt5) not found
143 # Package (e.g. PyQt5) not found
143 return False
144 return False
144 else:
145 else:
145 if spec is None:
146 if spec is None:
146 # Submodule (e.g. PyQt5.QtCore) not found
147 # Submodule (e.g. PyQt5.QtCore) not found
147 return False
148 return False
148
149
149 if api == QT_API_PYSIDE:
150 if api == QT_API_PYSIDE:
150 # We can also safely check PySide version
151 # We can also safely check PySide version
151 import PySide
152 import PySide
152
153
153 return PySide.__version_info__ >= (1, 0, 3)
154 return PySide.__version_info__ >= (1, 0, 3)
154
155
155 return True
156 return True
156
157
157
158
158 def qtapi_version():
159 def qtapi_version():
159 """Return which QString API has been set, if any
160 """Return which QString API has been set, if any
160
161
161 Returns
162 Returns
162 -------
163 -------
163 The QString API version (1 or 2), or None if not set
164 The QString API version (1 or 2), or None if not set
164 """
165 """
165 try:
166 try:
166 import sip
167 import sip
167 except ImportError:
168 except ImportError:
168 # as of PyQt5 5.11, sip is no longer available as a top-level
169 # as of PyQt5 5.11, sip is no longer available as a top-level
169 # module and needs to be imported from the PyQt5 namespace
170 # module and needs to be imported from the PyQt5 namespace
170 try:
171 try:
171 from PyQt5 import sip
172 from PyQt5 import sip
172 except ImportError:
173 except ImportError:
173 return
174 return
174 try:
175 try:
175 return sip.getapi('QString')
176 return sip.getapi('QString')
176 except ValueError:
177 except ValueError:
177 return
178 return
178
179
179
180
180 def can_import(api):
181 def can_import(api):
181 """Safely query whether an API is importable, without importing it"""
182 """Safely query whether an API is importable, without importing it"""
182 if not has_binding(api):
183 if not has_binding(api):
183 return False
184 return False
184
185
185 current = loaded_api()
186 current = loaded_api()
186 if api == QT_API_PYQT_DEFAULT:
187 if api == QT_API_PYQT_DEFAULT:
187 return current in [QT_API_PYQT6, None]
188 return current in [QT_API_PYQT6, None]
188 else:
189 else:
189 return current in [api, None]
190 return current in [api, None]
190
191
191
192
192 def import_pyqt4(version=2):
193 def import_pyqt4(version=2):
193 """
194 """
194 Import PyQt4
195 Import PyQt4
195
196
196 Parameters
197 Parameters
197 ----------
198 ----------
198 version : 1, 2, or None
199 version : 1, 2, or None
199 Which QString/QVariant API to use. Set to None to use the system
200 Which QString/QVariant API to use. Set to None to use the system
200 default
201 default
201 ImportErrors raised within this function are non-recoverable
202 ImportErrors raised within this function are non-recoverable
202 """
203 """
203 # The new-style string API (version=2) automatically
204 # The new-style string API (version=2) automatically
204 # converts QStrings to Unicode Python strings. Also, automatically unpacks
205 # converts QStrings to Unicode Python strings. Also, automatically unpacks
205 # QVariants to their underlying objects.
206 # QVariants to their underlying objects.
206 import sip
207 import sip
207
208
208 if version is not None:
209 if version is not None:
209 sip.setapi('QString', version)
210 sip.setapi('QString', version)
210 sip.setapi('QVariant', version)
211 sip.setapi('QVariant', version)
211
212
212 from PyQt4 import QtGui, QtCore, QtSvg
213 from PyQt4 import QtGui, QtCore, QtSvg
213
214
214 if QtCore.PYQT_VERSION < 0x040700:
215 if QtCore.PYQT_VERSION < 0x040700:
215 raise ImportError("IPython requires PyQt4 >= 4.7, found %s" %
216 raise ImportError("IPython requires PyQt4 >= 4.7, found %s" %
216 QtCore.PYQT_VERSION_STR)
217 QtCore.PYQT_VERSION_STR)
217
218
218 # Alias PyQt-specific functions for PySide compatibility.
219 # Alias PyQt-specific functions for PySide compatibility.
219 QtCore.Signal = QtCore.pyqtSignal
220 QtCore.Signal = QtCore.pyqtSignal
220 QtCore.Slot = QtCore.pyqtSlot
221 QtCore.Slot = QtCore.pyqtSlot
221
222
222 # query for the API version (in case version == None)
223 # query for the API version (in case version == None)
223 version = sip.getapi('QString')
224 version = sip.getapi('QString')
224 api = QT_API_PYQTv1 if version == 1 else QT_API_PYQT
225 api = QT_API_PYQTv1 if version == 1 else QT_API_PYQT
225 return QtCore, QtGui, QtSvg, api
226 return QtCore, QtGui, QtSvg, api
226
227
227
228
228 def import_pyqt5():
229 def import_pyqt5():
229 """
230 """
230 Import PyQt5
231 Import PyQt5
231
232
232 ImportErrors raised within this function are non-recoverable
233 ImportErrors raised within this function are non-recoverable
233 """
234 """
234
235
235 from PyQt5 import QtCore, QtSvg, QtWidgets, QtGui
236 from PyQt5 import QtCore, QtSvg, QtWidgets, QtGui
236
237
237 # Alias PyQt-specific functions for PySide compatibility.
238 # Alias PyQt-specific functions for PySide compatibility.
238 QtCore.Signal = QtCore.pyqtSignal
239 QtCore.Signal = QtCore.pyqtSignal
239 QtCore.Slot = QtCore.pyqtSlot
240 QtCore.Slot = QtCore.pyqtSlot
240
241
241 # Join QtGui and QtWidgets for Qt4 compatibility.
242 # Join QtGui and QtWidgets for Qt4 compatibility.
242 QtGuiCompat = types.ModuleType('QtGuiCompat')
243 QtGuiCompat = types.ModuleType('QtGuiCompat')
243 QtGuiCompat.__dict__.update(QtGui.__dict__)
244 QtGuiCompat.__dict__.update(QtGui.__dict__)
244 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
245 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
245
246
246 api = QT_API_PYQT5
247 api = QT_API_PYQT5
247 return QtCore, QtGuiCompat, QtSvg, api
248 return QtCore, QtGuiCompat, QtSvg, api
248
249
249
250
250 def import_pyqt6():
251 def import_pyqt6():
251 """
252 """
252 Import PyQt6
253 Import PyQt6
253
254
254 ImportErrors raised within this function are non-recoverable
255 ImportErrors raised within this function are non-recoverable
255 """
256 """
256
257
257 from PyQt6 import QtCore, QtSvg, QtWidgets, QtGui
258 from PyQt6 import QtCore, QtSvg, QtWidgets, QtGui
258
259
259 # Alias PyQt-specific functions for PySide compatibility.
260 # Alias PyQt-specific functions for PySide compatibility.
260 QtCore.Signal = QtCore.pyqtSignal
261 QtCore.Signal = QtCore.pyqtSignal
261 QtCore.Slot = QtCore.pyqtSlot
262 QtCore.Slot = QtCore.pyqtSlot
262
263
263 # Join QtGui and QtWidgets for Qt4 compatibility.
264 # Join QtGui and QtWidgets for Qt4 compatibility.
264 QtGuiCompat = types.ModuleType("QtGuiCompat")
265 QtGuiCompat = types.ModuleType("QtGuiCompat")
265 QtGuiCompat.__dict__.update(QtGui.__dict__)
266 QtGuiCompat.__dict__.update(QtGui.__dict__)
266 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
267 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
267
268
268 api = QT_API_PYQT6
269 api = QT_API_PYQT6
269 return QtCore, QtGuiCompat, QtSvg, api
270 return QtCore, QtGuiCompat, QtSvg, api
270
271
271
272
272 def import_pyside():
273 def import_pyside():
273 """
274 """
274 Import PySide
275 Import PySide
275
276
276 ImportErrors raised within this function are non-recoverable
277 ImportErrors raised within this function are non-recoverable
277 """
278 """
278 from PySide import QtGui, QtCore, QtSvg
279 from PySide import QtGui, QtCore, QtSvg
279 return QtCore, QtGui, QtSvg, QT_API_PYSIDE
280 return QtCore, QtGui, QtSvg, QT_API_PYSIDE
280
281
281 def import_pyside2():
282 def import_pyside2():
282 """
283 """
283 Import PySide2
284 Import PySide2
284
285
285 ImportErrors raised within this function are non-recoverable
286 ImportErrors raised within this function are non-recoverable
286 """
287 """
287 from PySide2 import QtGui, QtCore, QtSvg, QtWidgets, QtPrintSupport
288 from PySide2 import QtGui, QtCore, QtSvg, QtWidgets, QtPrintSupport
288
289
289 # Join QtGui and QtWidgets for Qt4 compatibility.
290 # Join QtGui and QtWidgets for Qt4 compatibility.
290 QtGuiCompat = types.ModuleType('QtGuiCompat')
291 QtGuiCompat = types.ModuleType('QtGuiCompat')
291 QtGuiCompat.__dict__.update(QtGui.__dict__)
292 QtGuiCompat.__dict__.update(QtGui.__dict__)
292 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
293 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
293 QtGuiCompat.__dict__.update(QtPrintSupport.__dict__)
294 QtGuiCompat.__dict__.update(QtPrintSupport.__dict__)
294
295
295 return QtCore, QtGuiCompat, QtSvg, QT_API_PYSIDE2
296 return QtCore, QtGuiCompat, QtSvg, QT_API_PYSIDE2
296
297
297
298
298 def import_pyside6():
299 def import_pyside6():
299 """
300 """
300 Import PySide6
301 Import PySide6
301
302
302 ImportErrors raised within this function are non-recoverable
303 ImportErrors raised within this function are non-recoverable
303 """
304 """
304 from PySide6 import QtGui, QtCore, QtSvg, QtWidgets, QtPrintSupport
305 from PySide6 import QtGui, QtCore, QtSvg, QtWidgets, QtPrintSupport
305
306
306 # Join QtGui and QtWidgets for Qt4 compatibility.
307 # Join QtGui and QtWidgets for Qt4 compatibility.
307 QtGuiCompat = types.ModuleType("QtGuiCompat")
308 QtGuiCompat = types.ModuleType("QtGuiCompat")
308 QtGuiCompat.__dict__.update(QtGui.__dict__)
309 QtGuiCompat.__dict__.update(QtGui.__dict__)
309 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
310 QtGuiCompat.__dict__.update(QtWidgets.__dict__)
310 QtGuiCompat.__dict__.update(QtPrintSupport.__dict__)
311 QtGuiCompat.__dict__.update(QtPrintSupport.__dict__)
311
312
312 return QtCore, QtGuiCompat, QtSvg, QT_API_PYSIDE6
313 return QtCore, QtGuiCompat, QtSvg, QT_API_PYSIDE6
313
314
314
315
315 def load_qt(api_options):
316 def load_qt(api_options):
316 """
317 """
317 Attempt to import Qt, given a preference list
318 Attempt to import Qt, given a preference list
318 of permissible bindings
319 of permissible bindings
319
320
320 It is safe to call this function multiple times.
321 It is safe to call this function multiple times.
321
322
322 Parameters
323 Parameters
323 ----------
324 ----------
324 api_options : List of strings
325 api_options : List of strings
325 The order of APIs to try. Valid items are 'pyside', 'pyside2',
326 The order of APIs to try. Valid items are 'pyside', 'pyside2',
326 'pyqt', 'pyqt5', 'pyqtv1' and 'pyqtdefault'
327 'pyqt', 'pyqt5', 'pyqtv1' and 'pyqtdefault'
327
328
328 Returns
329 Returns
329 -------
330 -------
330 A tuple of QtCore, QtGui, QtSvg, QT_API
331 A tuple of QtCore, QtGui, QtSvg, QT_API
331 The first three are the Qt modules. The last is the
332 The first three are the Qt modules. The last is the
332 string indicating which module was loaded.
333 string indicating which module was loaded.
333
334
334 Raises
335 Raises
335 ------
336 ------
336 ImportError, if it isn't possible to import any requested
337 ImportError, if it isn't possible to import any requested
337 bindings (either because they aren't installed, or because
338 bindings (either because they aren't installed, or because
338 an incompatible library has already been installed)
339 an incompatible library has already been installed)
339 """
340 """
340 loaders = {
341 loaders = {
341 # Qt6
342 # Qt6
342 QT_API_PYQT6: import_pyqt6,
343 QT_API_PYQT6: import_pyqt6,
343 QT_API_PYSIDE6: import_pyside6,
344 QT_API_PYSIDE6: import_pyside6,
344 # Qt5
345 # Qt5
345 QT_API_PYQT5: import_pyqt5,
346 QT_API_PYQT5: import_pyqt5,
346 QT_API_PYSIDE2: import_pyside2,
347 QT_API_PYSIDE2: import_pyside2,
347 # Qt4
348 # Qt4
348 QT_API_PYSIDE: import_pyside,
349 QT_API_PYSIDE: import_pyside,
349 QT_API_PYQT: import_pyqt4,
350 QT_API_PYQT: import_pyqt4,
350 QT_API_PYQTv1: partial(import_pyqt4, version=1),
351 QT_API_PYQTv1: partial(import_pyqt4, version=1),
351 # default
352 # default
352 QT_API_PYQT_DEFAULT: import_pyqt6,
353 QT_API_PYQT_DEFAULT: import_pyqt6,
353 }
354 }
354
355
355 for api in api_options:
356 for api in api_options:
356
357
357 if api not in loaders:
358 if api not in loaders:
358 raise RuntimeError(
359 raise RuntimeError(
359 "Invalid Qt API %r, valid values are: %s" %
360 "Invalid Qt API %r, valid values are: %s" %
360 (api, ", ".join(["%r" % k for k in loaders.keys()])))
361 (api, ", ".join(["%r" % k for k in loaders.keys()])))
361
362
362 if not can_import(api):
363 if not can_import(api):
363 continue
364 continue
364
365
365 #cannot safely recover from an ImportError during this
366 #cannot safely recover from an ImportError during this
366 result = loaders[api]()
367 result = loaders[api]()
367 api = result[-1] # changed if api = QT_API_PYQT_DEFAULT
368 api = result[-1] # changed if api = QT_API_PYQT_DEFAULT
368 commit_api(api)
369 commit_api(api)
369 return result
370 return result
370 else:
371 else:
372 # Clear the environment variable since it doesn't work.
373 if "QT_API" in os.environ:
374 del os.environ["QT_API"]
375
371 raise ImportError(
376 raise ImportError(
372 """
377 """
373 Could not load requested Qt binding. Please ensure that
378 Could not load requested Qt binding. Please ensure that
374 PyQt4 >= 4.7, PyQt5, PyQt6, PySide >= 1.0.3, PySide2, or
379 PyQt4 >= 4.7, PyQt5, PyQt6, PySide >= 1.0.3, PySide2, or
375 PySide6 is available, and only one is imported per session.
380 PySide6 is available, and only one is imported per session.
376
381
377 Currently-imported Qt library: %r
382 Currently-imported Qt library: %r
378 PyQt5 available (requires QtCore, QtGui, QtSvg, QtWidgets): %s
383 PyQt5 available (requires QtCore, QtGui, QtSvg, QtWidgets): %s
379 PyQt6 available (requires QtCore, QtGui, QtSvg, QtWidgets): %s
384 PyQt6 available (requires QtCore, QtGui, QtSvg, QtWidgets): %s
380 PySide2 installed: %s
385 PySide2 installed: %s
381 PySide6 installed: %s
386 PySide6 installed: %s
382 Tried to load: %r
387 Tried to load: %r
383 """
388 """
384 % (
389 % (
385 loaded_api(),
390 loaded_api(),
386 has_binding(QT_API_PYQT5),
391 has_binding(QT_API_PYQT5),
387 has_binding(QT_API_PYQT6),
392 has_binding(QT_API_PYQT6),
388 has_binding(QT_API_PYSIDE2),
393 has_binding(QT_API_PYSIDE2),
389 has_binding(QT_API_PYSIDE6),
394 has_binding(QT_API_PYSIDE6),
390 api_options,
395 api_options,
391 )
396 )
392 )
397 )
393
398
394
399
395 def enum_factory(QT_API, QtCore):
400 def enum_factory(QT_API, QtCore):
396 """Construct an enum helper to account for PyQt5 <-> PyQt6 changes."""
401 """Construct an enum helper to account for PyQt5 <-> PyQt6 changes."""
397
402
398 @lru_cache(None)
403 @lru_cache(None)
399 def _enum(name):
404 def _enum(name):
400 # foo.bar.Enum.Entry (PyQt6) <=> foo.bar.Entry (non-PyQt6).
405 # foo.bar.Enum.Entry (PyQt6) <=> foo.bar.Entry (non-PyQt6).
401 return operator.attrgetter(
406 return operator.attrgetter(
402 name if QT_API == QT_API_PYQT6 else name.rpartition(".")[0]
407 name if QT_API == QT_API_PYQT6 else name.rpartition(".")[0]
403 )(sys.modules[QtCore.__package__])
408 )(sys.modules[QtCore.__package__])
404
409
405 return _enum
410 return _enum
@@ -1,980 +1,992 b''
1 """IPython terminal interface using prompt_toolkit"""
1 """IPython terminal interface using prompt_toolkit"""
2
2
3 import asyncio
3 import asyncio
4 import os
4 import os
5 import sys
5 import sys
6 from warnings import warn
6 from warnings import warn
7 from typing import Union as UnionType
7 from typing import Union as UnionType
8
8
9 from IPython.core.async_helpers import get_asyncio_loop
9 from IPython.core.async_helpers import get_asyncio_loop
10 from IPython.core.interactiveshell import InteractiveShell, InteractiveShellABC
10 from IPython.core.interactiveshell import InteractiveShell, InteractiveShellABC
11 from IPython.utils.py3compat import input
11 from IPython.utils.py3compat import input
12 from IPython.utils.terminal import toggle_set_term_title, set_term_title, restore_term_title
12 from IPython.utils.terminal import toggle_set_term_title, set_term_title, restore_term_title
13 from IPython.utils.process import abbrev_cwd
13 from IPython.utils.process import abbrev_cwd
14 from traitlets import (
14 from traitlets import (
15 Bool,
15 Bool,
16 Unicode,
16 Unicode,
17 Dict,
17 Dict,
18 Integer,
18 Integer,
19 List,
19 List,
20 observe,
20 observe,
21 Instance,
21 Instance,
22 Type,
22 Type,
23 default,
23 default,
24 Enum,
24 Enum,
25 Union,
25 Union,
26 Any,
26 Any,
27 validate,
27 validate,
28 Float,
28 Float,
29 )
29 )
30
30
31 from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
31 from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
32 from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode
32 from prompt_toolkit.enums import DEFAULT_BUFFER, EditingMode
33 from prompt_toolkit.filters import HasFocus, Condition, IsDone
33 from prompt_toolkit.filters import HasFocus, Condition, IsDone
34 from prompt_toolkit.formatted_text import PygmentsTokens
34 from prompt_toolkit.formatted_text import PygmentsTokens
35 from prompt_toolkit.history import History
35 from prompt_toolkit.history import History
36 from prompt_toolkit.layout.processors import ConditionalProcessor, HighlightMatchingBracketProcessor
36 from prompt_toolkit.layout.processors import ConditionalProcessor, HighlightMatchingBracketProcessor
37 from prompt_toolkit.output import ColorDepth
37 from prompt_toolkit.output import ColorDepth
38 from prompt_toolkit.patch_stdout import patch_stdout
38 from prompt_toolkit.patch_stdout import patch_stdout
39 from prompt_toolkit.shortcuts import PromptSession, CompleteStyle, print_formatted_text
39 from prompt_toolkit.shortcuts import PromptSession, CompleteStyle, print_formatted_text
40 from prompt_toolkit.styles import DynamicStyle, merge_styles
40 from prompt_toolkit.styles import DynamicStyle, merge_styles
41 from prompt_toolkit.styles.pygments import style_from_pygments_cls, style_from_pygments_dict
41 from prompt_toolkit.styles.pygments import style_from_pygments_cls, style_from_pygments_dict
42 from prompt_toolkit import __version__ as ptk_version
42 from prompt_toolkit import __version__ as ptk_version
43
43
44 from pygments.styles import get_style_by_name
44 from pygments.styles import get_style_by_name
45 from pygments.style import Style
45 from pygments.style import Style
46 from pygments.token import Token
46 from pygments.token import Token
47
47
48 from .debugger import TerminalPdb, Pdb
48 from .debugger import TerminalPdb, Pdb
49 from .magics import TerminalMagics
49 from .magics import TerminalMagics
50 from .pt_inputhooks import get_inputhook_name_and_func
50 from .pt_inputhooks import get_inputhook_name_and_func
51 from .prompts import Prompts, ClassicPrompts, RichPromptDisplayHook
51 from .prompts import Prompts, ClassicPrompts, RichPromptDisplayHook
52 from .ptutils import IPythonPTCompleter, IPythonPTLexer
52 from .ptutils import IPythonPTCompleter, IPythonPTLexer
53 from .shortcuts import (
53 from .shortcuts import (
54 create_ipython_shortcuts,
54 create_ipython_shortcuts,
55 create_identifier,
55 create_identifier,
56 RuntimeBinding,
56 RuntimeBinding,
57 add_binding,
57 add_binding,
58 )
58 )
59 from .shortcuts.filters import KEYBINDING_FILTERS, filter_from_string
59 from .shortcuts.filters import KEYBINDING_FILTERS, filter_from_string
60 from .shortcuts.auto_suggest import (
60 from .shortcuts.auto_suggest import (
61 NavigableAutoSuggestFromHistory,
61 NavigableAutoSuggestFromHistory,
62 AppendAutoSuggestionInAnyLine,
62 AppendAutoSuggestionInAnyLine,
63 )
63 )
64
64
65 PTK3 = ptk_version.startswith('3.')
65 PTK3 = ptk_version.startswith('3.')
66
66
67
67
68 class _NoStyle(Style): pass
68 class _NoStyle(Style): pass
69
69
70
70
71
71
72 _style_overrides_light_bg = {
72 _style_overrides_light_bg = {
73 Token.Prompt: '#ansibrightblue',
73 Token.Prompt: '#ansibrightblue',
74 Token.PromptNum: '#ansiblue bold',
74 Token.PromptNum: '#ansiblue bold',
75 Token.OutPrompt: '#ansibrightred',
75 Token.OutPrompt: '#ansibrightred',
76 Token.OutPromptNum: '#ansired bold',
76 Token.OutPromptNum: '#ansired bold',
77 }
77 }
78
78
79 _style_overrides_linux = {
79 _style_overrides_linux = {
80 Token.Prompt: '#ansibrightgreen',
80 Token.Prompt: '#ansibrightgreen',
81 Token.PromptNum: '#ansigreen bold',
81 Token.PromptNum: '#ansigreen bold',
82 Token.OutPrompt: '#ansibrightred',
82 Token.OutPrompt: '#ansibrightred',
83 Token.OutPromptNum: '#ansired bold',
83 Token.OutPromptNum: '#ansired bold',
84 }
84 }
85
85
86 def get_default_editor():
86 def get_default_editor():
87 try:
87 try:
88 return os.environ['EDITOR']
88 return os.environ['EDITOR']
89 except KeyError:
89 except KeyError:
90 pass
90 pass
91 except UnicodeError:
91 except UnicodeError:
92 warn("$EDITOR environment variable is not pure ASCII. Using platform "
92 warn("$EDITOR environment variable is not pure ASCII. Using platform "
93 "default editor.")
93 "default editor.")
94
94
95 if os.name == 'posix':
95 if os.name == 'posix':
96 return 'vi' # the only one guaranteed to be there!
96 return 'vi' # the only one guaranteed to be there!
97 else:
97 else:
98 return 'notepad' # same in Windows!
98 return 'notepad' # same in Windows!
99
99
100 # conservatively check for tty
100 # conservatively check for tty
101 # overridden streams can result in things like:
101 # overridden streams can result in things like:
102 # - sys.stdin = None
102 # - sys.stdin = None
103 # - no isatty method
103 # - no isatty method
104 for _name in ('stdin', 'stdout', 'stderr'):
104 for _name in ('stdin', 'stdout', 'stderr'):
105 _stream = getattr(sys, _name)
105 _stream = getattr(sys, _name)
106 try:
106 try:
107 if not _stream or not hasattr(_stream, "isatty") or not _stream.isatty():
107 if not _stream or not hasattr(_stream, "isatty") or not _stream.isatty():
108 _is_tty = False
108 _is_tty = False
109 break
109 break
110 except ValueError:
110 except ValueError:
111 # stream is closed
111 # stream is closed
112 _is_tty = False
112 _is_tty = False
113 break
113 break
114 else:
114 else:
115 _is_tty = True
115 _is_tty = True
116
116
117
117
118 _use_simple_prompt = ('IPY_TEST_SIMPLE_PROMPT' in os.environ) or (not _is_tty)
118 _use_simple_prompt = ('IPY_TEST_SIMPLE_PROMPT' in os.environ) or (not _is_tty)
119
119
120 def black_reformat_handler(text_before_cursor):
120 def black_reformat_handler(text_before_cursor):
121 """
121 """
122 We do not need to protect against error,
122 We do not need to protect against error,
123 this is taken care at a higher level where any reformat error is ignored.
123 this is taken care at a higher level where any reformat error is ignored.
124 Indeed we may call reformatting on incomplete code.
124 Indeed we may call reformatting on incomplete code.
125 """
125 """
126 import black
126 import black
127
127
128 formatted_text = black.format_str(text_before_cursor, mode=black.FileMode())
128 formatted_text = black.format_str(text_before_cursor, mode=black.FileMode())
129 if not text_before_cursor.endswith("\n") and formatted_text.endswith("\n"):
129 if not text_before_cursor.endswith("\n") and formatted_text.endswith("\n"):
130 formatted_text = formatted_text[:-1]
130 formatted_text = formatted_text[:-1]
131 return formatted_text
131 return formatted_text
132
132
133
133
134 def yapf_reformat_handler(text_before_cursor):
134 def yapf_reformat_handler(text_before_cursor):
135 from yapf.yapflib import file_resources
135 from yapf.yapflib import file_resources
136 from yapf.yapflib import yapf_api
136 from yapf.yapflib import yapf_api
137
137
138 style_config = file_resources.GetDefaultStyleForDir(os.getcwd())
138 style_config = file_resources.GetDefaultStyleForDir(os.getcwd())
139 formatted_text, was_formatted = yapf_api.FormatCode(
139 formatted_text, was_formatted = yapf_api.FormatCode(
140 text_before_cursor, style_config=style_config
140 text_before_cursor, style_config=style_config
141 )
141 )
142 if was_formatted:
142 if was_formatted:
143 if not text_before_cursor.endswith("\n") and formatted_text.endswith("\n"):
143 if not text_before_cursor.endswith("\n") and formatted_text.endswith("\n"):
144 formatted_text = formatted_text[:-1]
144 formatted_text = formatted_text[:-1]
145 return formatted_text
145 return formatted_text
146 else:
146 else:
147 return text_before_cursor
147 return text_before_cursor
148
148
149
149
150 class PtkHistoryAdapter(History):
150 class PtkHistoryAdapter(History):
151 """
151 """
152 Prompt toolkit has it's own way of handling history, Where it assumes it can
152 Prompt toolkit has it's own way of handling history, Where it assumes it can
153 Push/pull from history.
153 Push/pull from history.
154
154
155 """
155 """
156
156
157 def __init__(self, shell):
157 def __init__(self, shell):
158 super().__init__()
158 super().__init__()
159 self.shell = shell
159 self.shell = shell
160 self._refresh()
160 self._refresh()
161
161
162 def append_string(self, string):
162 def append_string(self, string):
163 # we rely on sql for that.
163 # we rely on sql for that.
164 self._loaded = False
164 self._loaded = False
165 self._refresh()
165 self._refresh()
166
166
167 def _refresh(self):
167 def _refresh(self):
168 if not self._loaded:
168 if not self._loaded:
169 self._loaded_strings = list(self.load_history_strings())
169 self._loaded_strings = list(self.load_history_strings())
170
170
171 def load_history_strings(self):
171 def load_history_strings(self):
172 last_cell = ""
172 last_cell = ""
173 res = []
173 res = []
174 for __, ___, cell in self.shell.history_manager.get_tail(
174 for __, ___, cell in self.shell.history_manager.get_tail(
175 self.shell.history_load_length, include_latest=True
175 self.shell.history_load_length, include_latest=True
176 ):
176 ):
177 # Ignore blank lines and consecutive duplicates
177 # Ignore blank lines and consecutive duplicates
178 cell = cell.rstrip()
178 cell = cell.rstrip()
179 if cell and (cell != last_cell):
179 if cell and (cell != last_cell):
180 res.append(cell)
180 res.append(cell)
181 last_cell = cell
181 last_cell = cell
182 yield from res[::-1]
182 yield from res[::-1]
183
183
184 def store_string(self, string: str) -> None:
184 def store_string(self, string: str) -> None:
185 pass
185 pass
186
186
187 class TerminalInteractiveShell(InteractiveShell):
187 class TerminalInteractiveShell(InteractiveShell):
188 mime_renderers = Dict().tag(config=True)
188 mime_renderers = Dict().tag(config=True)
189
189
190 space_for_menu = Integer(6, help='Number of line at the bottom of the screen '
190 space_for_menu = Integer(6, help='Number of line at the bottom of the screen '
191 'to reserve for the tab completion menu, '
191 'to reserve for the tab completion menu, '
192 'search history, ...etc, the height of '
192 'search history, ...etc, the height of '
193 'these menus will at most this value. '
193 'these menus will at most this value. '
194 'Increase it is you prefer long and skinny '
194 'Increase it is you prefer long and skinny '
195 'menus, decrease for short and wide.'
195 'menus, decrease for short and wide.'
196 ).tag(config=True)
196 ).tag(config=True)
197
197
198 pt_app: UnionType[PromptSession, None] = None
198 pt_app: UnionType[PromptSession, None] = None
199 auto_suggest: UnionType[
199 auto_suggest: UnionType[
200 AutoSuggestFromHistory, NavigableAutoSuggestFromHistory, None
200 AutoSuggestFromHistory, NavigableAutoSuggestFromHistory, None
201 ] = None
201 ] = None
202 debugger_history = None
202 debugger_history = None
203
203
204 debugger_history_file = Unicode(
204 debugger_history_file = Unicode(
205 "~/.pdbhistory", help="File in which to store and read history"
205 "~/.pdbhistory", help="File in which to store and read history"
206 ).tag(config=True)
206 ).tag(config=True)
207
207
208 simple_prompt = Bool(_use_simple_prompt,
208 simple_prompt = Bool(_use_simple_prompt,
209 help="""Use `raw_input` for the REPL, without completion and prompt colors.
209 help="""Use `raw_input` for the REPL, without completion and prompt colors.
210
210
211 Useful when controlling IPython as a subprocess, and piping STDIN/OUT/ERR. Known usage are:
211 Useful when controlling IPython as a subprocess, and piping STDIN/OUT/ERR. Known usage are:
212 IPython own testing machinery, and emacs inferior-shell integration through elpy.
212 IPython own testing machinery, and emacs inferior-shell integration through elpy.
213
213
214 This mode default to `True` if the `IPY_TEST_SIMPLE_PROMPT`
214 This mode default to `True` if the `IPY_TEST_SIMPLE_PROMPT`
215 environment variable is set, or the current terminal is not a tty."""
215 environment variable is set, or the current terminal is not a tty."""
216 ).tag(config=True)
216 ).tag(config=True)
217
217
218 @property
218 @property
219 def debugger_cls(self):
219 def debugger_cls(self):
220 return Pdb if self.simple_prompt else TerminalPdb
220 return Pdb if self.simple_prompt else TerminalPdb
221
221
222 confirm_exit = Bool(True,
222 confirm_exit = Bool(True,
223 help="""
223 help="""
224 Set to confirm when you try to exit IPython with an EOF (Control-D
224 Set to confirm when you try to exit IPython with an EOF (Control-D
225 in Unix, Control-Z/Enter in Windows). By typing 'exit' or 'quit',
225 in Unix, Control-Z/Enter in Windows). By typing 'exit' or 'quit',
226 you can force a direct exit without any confirmation.""",
226 you can force a direct exit without any confirmation.""",
227 ).tag(config=True)
227 ).tag(config=True)
228
228
229 editing_mode = Unicode('emacs',
229 editing_mode = Unicode('emacs',
230 help="Shortcut style to use at the prompt. 'vi' or 'emacs'.",
230 help="Shortcut style to use at the prompt. 'vi' or 'emacs'.",
231 ).tag(config=True)
231 ).tag(config=True)
232
232
233 emacs_bindings_in_vi_insert_mode = Bool(
233 emacs_bindings_in_vi_insert_mode = Bool(
234 True,
234 True,
235 help="Add shortcuts from 'emacs' insert mode to 'vi' insert mode.",
235 help="Add shortcuts from 'emacs' insert mode to 'vi' insert mode.",
236 ).tag(config=True)
236 ).tag(config=True)
237
237
238 modal_cursor = Bool(
238 modal_cursor = Bool(
239 True,
239 True,
240 help="""
240 help="""
241 Cursor shape changes depending on vi mode: beam in vi insert mode,
241 Cursor shape changes depending on vi mode: beam in vi insert mode,
242 block in nav mode, underscore in replace mode.""",
242 block in nav mode, underscore in replace mode.""",
243 ).tag(config=True)
243 ).tag(config=True)
244
244
245 ttimeoutlen = Float(
245 ttimeoutlen = Float(
246 0.01,
246 0.01,
247 help="""The time in milliseconds that is waited for a key code
247 help="""The time in milliseconds that is waited for a key code
248 to complete.""",
248 to complete.""",
249 ).tag(config=True)
249 ).tag(config=True)
250
250
251 timeoutlen = Float(
251 timeoutlen = Float(
252 0.5,
252 0.5,
253 help="""The time in milliseconds that is waited for a mapped key
253 help="""The time in milliseconds that is waited for a mapped key
254 sequence to complete.""",
254 sequence to complete.""",
255 ).tag(config=True)
255 ).tag(config=True)
256
256
257 autoformatter = Unicode(
257 autoformatter = Unicode(
258 None,
258 None,
259 help="Autoformatter to reformat Terminal code. Can be `'black'`, `'yapf'` or `None`",
259 help="Autoformatter to reformat Terminal code. Can be `'black'`, `'yapf'` or `None`",
260 allow_none=True
260 allow_none=True
261 ).tag(config=True)
261 ).tag(config=True)
262
262
263 auto_match = Bool(
263 auto_match = Bool(
264 False,
264 False,
265 help="""
265 help="""
266 Automatically add/delete closing bracket or quote when opening bracket or quote is entered/deleted.
266 Automatically add/delete closing bracket or quote when opening bracket or quote is entered/deleted.
267 Brackets: (), [], {}
267 Brackets: (), [], {}
268 Quotes: '', \"\"
268 Quotes: '', \"\"
269 """,
269 """,
270 ).tag(config=True)
270 ).tag(config=True)
271
271
272 mouse_support = Bool(False,
272 mouse_support = Bool(False,
273 help="Enable mouse support in the prompt\n(Note: prevents selecting text with the mouse)"
273 help="Enable mouse support in the prompt\n(Note: prevents selecting text with the mouse)"
274 ).tag(config=True)
274 ).tag(config=True)
275
275
276 # We don't load the list of styles for the help string, because loading
276 # We don't load the list of styles for the help string, because loading
277 # Pygments plugins takes time and can cause unexpected errors.
277 # Pygments plugins takes time and can cause unexpected errors.
278 highlighting_style = Union([Unicode('legacy'), Type(klass=Style)],
278 highlighting_style = Union([Unicode('legacy'), Type(klass=Style)],
279 help="""The name or class of a Pygments style to use for syntax
279 help="""The name or class of a Pygments style to use for syntax
280 highlighting. To see available styles, run `pygmentize -L styles`."""
280 highlighting. To see available styles, run `pygmentize -L styles`."""
281 ).tag(config=True)
281 ).tag(config=True)
282
282
283 @validate('editing_mode')
283 @validate('editing_mode')
284 def _validate_editing_mode(self, proposal):
284 def _validate_editing_mode(self, proposal):
285 if proposal['value'].lower() == 'vim':
285 if proposal['value'].lower() == 'vim':
286 proposal['value']= 'vi'
286 proposal['value']= 'vi'
287 elif proposal['value'].lower() == 'default':
287 elif proposal['value'].lower() == 'default':
288 proposal['value']= 'emacs'
288 proposal['value']= 'emacs'
289
289
290 if hasattr(EditingMode, proposal['value'].upper()):
290 if hasattr(EditingMode, proposal['value'].upper()):
291 return proposal['value'].lower()
291 return proposal['value'].lower()
292
292
293 return self.editing_mode
293 return self.editing_mode
294
294
295
295
296 @observe('editing_mode')
296 @observe('editing_mode')
297 def _editing_mode(self, change):
297 def _editing_mode(self, change):
298 if self.pt_app:
298 if self.pt_app:
299 self.pt_app.editing_mode = getattr(EditingMode, change.new.upper())
299 self.pt_app.editing_mode = getattr(EditingMode, change.new.upper())
300
300
301 def _set_formatter(self, formatter):
301 def _set_formatter(self, formatter):
302 if formatter is None:
302 if formatter is None:
303 self.reformat_handler = lambda x:x
303 self.reformat_handler = lambda x:x
304 elif formatter == 'black':
304 elif formatter == 'black':
305 self.reformat_handler = black_reformat_handler
305 self.reformat_handler = black_reformat_handler
306 elif formatter == "yapf":
306 elif formatter == "yapf":
307 self.reformat_handler = yapf_reformat_handler
307 self.reformat_handler = yapf_reformat_handler
308 else:
308 else:
309 raise ValueError
309 raise ValueError
310
310
311 @observe("autoformatter")
311 @observe("autoformatter")
312 def _autoformatter_changed(self, change):
312 def _autoformatter_changed(self, change):
313 formatter = change.new
313 formatter = change.new
314 self._set_formatter(formatter)
314 self._set_formatter(formatter)
315
315
316 @observe('highlighting_style')
316 @observe('highlighting_style')
317 @observe('colors')
317 @observe('colors')
318 def _highlighting_style_changed(self, change):
318 def _highlighting_style_changed(self, change):
319 self.refresh_style()
319 self.refresh_style()
320
320
321 def refresh_style(self):
321 def refresh_style(self):
322 self._style = self._make_style_from_name_or_cls(self.highlighting_style)
322 self._style = self._make_style_from_name_or_cls(self.highlighting_style)
323
323
324
324
325 highlighting_style_overrides = Dict(
325 highlighting_style_overrides = Dict(
326 help="Override highlighting format for specific tokens"
326 help="Override highlighting format for specific tokens"
327 ).tag(config=True)
327 ).tag(config=True)
328
328
329 true_color = Bool(False,
329 true_color = Bool(False,
330 help="""Use 24bit colors instead of 256 colors in prompt highlighting.
330 help="""Use 24bit colors instead of 256 colors in prompt highlighting.
331 If your terminal supports true color, the following command should
331 If your terminal supports true color, the following command should
332 print ``TRUECOLOR`` in orange::
332 print ``TRUECOLOR`` in orange::
333
333
334 printf \"\\x1b[38;2;255;100;0mTRUECOLOR\\x1b[0m\\n\"
334 printf \"\\x1b[38;2;255;100;0mTRUECOLOR\\x1b[0m\\n\"
335 """,
335 """,
336 ).tag(config=True)
336 ).tag(config=True)
337
337
338 editor = Unicode(get_default_editor(),
338 editor = Unicode(get_default_editor(),
339 help="Set the editor used by IPython (default to $EDITOR/vi/notepad)."
339 help="Set the editor used by IPython (default to $EDITOR/vi/notepad)."
340 ).tag(config=True)
340 ).tag(config=True)
341
341
342 prompts_class = Type(Prompts, help='Class used to generate Prompt token for prompt_toolkit').tag(config=True)
342 prompts_class = Type(Prompts, help='Class used to generate Prompt token for prompt_toolkit').tag(config=True)
343
343
344 prompts = Instance(Prompts)
344 prompts = Instance(Prompts)
345
345
346 @default('prompts')
346 @default('prompts')
347 def _prompts_default(self):
347 def _prompts_default(self):
348 return self.prompts_class(self)
348 return self.prompts_class(self)
349
349
350 # @observe('prompts')
350 # @observe('prompts')
351 # def _(self, change):
351 # def _(self, change):
352 # self._update_layout()
352 # self._update_layout()
353
353
354 @default('displayhook_class')
354 @default('displayhook_class')
355 def _displayhook_class_default(self):
355 def _displayhook_class_default(self):
356 return RichPromptDisplayHook
356 return RichPromptDisplayHook
357
357
358 term_title = Bool(True,
358 term_title = Bool(True,
359 help="Automatically set the terminal title"
359 help="Automatically set the terminal title"
360 ).tag(config=True)
360 ).tag(config=True)
361
361
362 term_title_format = Unicode("IPython: {cwd}",
362 term_title_format = Unicode("IPython: {cwd}",
363 help="Customize the terminal title format. This is a python format string. " +
363 help="Customize the terminal title format. This is a python format string. " +
364 "Available substitutions are: {cwd}."
364 "Available substitutions are: {cwd}."
365 ).tag(config=True)
365 ).tag(config=True)
366
366
367 display_completions = Enum(('column', 'multicolumn','readlinelike'),
367 display_completions = Enum(('column', 'multicolumn','readlinelike'),
368 help= ( "Options for displaying tab completions, 'column', 'multicolumn', and "
368 help= ( "Options for displaying tab completions, 'column', 'multicolumn', and "
369 "'readlinelike'. These options are for `prompt_toolkit`, see "
369 "'readlinelike'. These options are for `prompt_toolkit`, see "
370 "`prompt_toolkit` documentation for more information."
370 "`prompt_toolkit` documentation for more information."
371 ),
371 ),
372 default_value='multicolumn').tag(config=True)
372 default_value='multicolumn').tag(config=True)
373
373
374 highlight_matching_brackets = Bool(True,
374 highlight_matching_brackets = Bool(True,
375 help="Highlight matching brackets.",
375 help="Highlight matching brackets.",
376 ).tag(config=True)
376 ).tag(config=True)
377
377
378 extra_open_editor_shortcuts = Bool(False,
378 extra_open_editor_shortcuts = Bool(False,
379 help="Enable vi (v) or Emacs (C-X C-E) shortcuts to open an external editor. "
379 help="Enable vi (v) or Emacs (C-X C-E) shortcuts to open an external editor. "
380 "This is in addition to the F2 binding, which is always enabled."
380 "This is in addition to the F2 binding, which is always enabled."
381 ).tag(config=True)
381 ).tag(config=True)
382
382
383 handle_return = Any(None,
383 handle_return = Any(None,
384 help="Provide an alternative handler to be called when the user presses "
384 help="Provide an alternative handler to be called when the user presses "
385 "Return. This is an advanced option intended for debugging, which "
385 "Return. This is an advanced option intended for debugging, which "
386 "may be changed or removed in later releases."
386 "may be changed or removed in later releases."
387 ).tag(config=True)
387 ).tag(config=True)
388
388
389 enable_history_search = Bool(True,
389 enable_history_search = Bool(True,
390 help="Allows to enable/disable the prompt toolkit history search"
390 help="Allows to enable/disable the prompt toolkit history search"
391 ).tag(config=True)
391 ).tag(config=True)
392
392
393 autosuggestions_provider = Unicode(
393 autosuggestions_provider = Unicode(
394 "NavigableAutoSuggestFromHistory",
394 "NavigableAutoSuggestFromHistory",
395 help="Specifies from which source automatic suggestions are provided. "
395 help="Specifies from which source automatic suggestions are provided. "
396 "Can be set to ``'NavigableAutoSuggestFromHistory'`` (:kbd:`up` and "
396 "Can be set to ``'NavigableAutoSuggestFromHistory'`` (:kbd:`up` and "
397 ":kbd:`down` swap suggestions), ``'AutoSuggestFromHistory'``, "
397 ":kbd:`down` swap suggestions), ``'AutoSuggestFromHistory'``, "
398 " or ``None`` to disable automatic suggestions. "
398 " or ``None`` to disable automatic suggestions. "
399 "Default is `'NavigableAutoSuggestFromHistory`'.",
399 "Default is `'NavigableAutoSuggestFromHistory`'.",
400 allow_none=True,
400 allow_none=True,
401 ).tag(config=True)
401 ).tag(config=True)
402
402
403 def _set_autosuggestions(self, provider):
403 def _set_autosuggestions(self, provider):
404 # disconnect old handler
404 # disconnect old handler
405 if self.auto_suggest and isinstance(
405 if self.auto_suggest and isinstance(
406 self.auto_suggest, NavigableAutoSuggestFromHistory
406 self.auto_suggest, NavigableAutoSuggestFromHistory
407 ):
407 ):
408 self.auto_suggest.disconnect()
408 self.auto_suggest.disconnect()
409 if provider is None:
409 if provider is None:
410 self.auto_suggest = None
410 self.auto_suggest = None
411 elif provider == "AutoSuggestFromHistory":
411 elif provider == "AutoSuggestFromHistory":
412 self.auto_suggest = AutoSuggestFromHistory()
412 self.auto_suggest = AutoSuggestFromHistory()
413 elif provider == "NavigableAutoSuggestFromHistory":
413 elif provider == "NavigableAutoSuggestFromHistory":
414 self.auto_suggest = NavigableAutoSuggestFromHistory()
414 self.auto_suggest = NavigableAutoSuggestFromHistory()
415 else:
415 else:
416 raise ValueError("No valid provider.")
416 raise ValueError("No valid provider.")
417 if self.pt_app:
417 if self.pt_app:
418 self.pt_app.auto_suggest = self.auto_suggest
418 self.pt_app.auto_suggest = self.auto_suggest
419
419
420 @observe("autosuggestions_provider")
420 @observe("autosuggestions_provider")
421 def _autosuggestions_provider_changed(self, change):
421 def _autosuggestions_provider_changed(self, change):
422 provider = change.new
422 provider = change.new
423 self._set_autosuggestions(provider)
423 self._set_autosuggestions(provider)
424
424
425 shortcuts = List(
425 shortcuts = List(
426 trait=Dict(
426 trait=Dict(
427 key_trait=Enum(
427 key_trait=Enum(
428 [
428 [
429 "command",
429 "command",
430 "match_keys",
430 "match_keys",
431 "match_filter",
431 "match_filter",
432 "new_keys",
432 "new_keys",
433 "new_filter",
433 "new_filter",
434 "create",
434 "create",
435 ]
435 ]
436 ),
436 ),
437 per_key_traits={
437 per_key_traits={
438 "command": Unicode(),
438 "command": Unicode(),
439 "match_keys": List(Unicode()),
439 "match_keys": List(Unicode()),
440 "match_filter": Unicode(),
440 "match_filter": Unicode(),
441 "new_keys": List(Unicode()),
441 "new_keys": List(Unicode()),
442 "new_filter": Unicode(),
442 "new_filter": Unicode(),
443 "create": Bool(False),
443 "create": Bool(False),
444 },
444 },
445 ),
445 ),
446 help="""Add, disable or modifying shortcuts.
446 help="""Add, disable or modifying shortcuts.
447
447
448 Each entry on the list should be a dictionary with ``command`` key
448 Each entry on the list should be a dictionary with ``command`` key
449 identifying the target function executed by the shortcut and at least
449 identifying the target function executed by the shortcut and at least
450 one of the following:
450 one of the following:
451
451
452 - ``match_keys``: list of keys used to match an existing shortcut,
452 - ``match_keys``: list of keys used to match an existing shortcut,
453 - ``match_filter``: shortcut filter used to match an existing shortcut,
453 - ``match_filter``: shortcut filter used to match an existing shortcut,
454 - ``new_keys``: list of keys to set,
454 - ``new_keys``: list of keys to set,
455 - ``new_filter``: a new shortcut filter to set
455 - ``new_filter``: a new shortcut filter to set
456
456
457 The filters have to be composed of pre-defined verbs and joined by one
457 The filters have to be composed of pre-defined verbs and joined by one
458 of the following conjunctions: ``&`` (and), ``|`` (or), ``~`` (not).
458 of the following conjunctions: ``&`` (and), ``|`` (or), ``~`` (not).
459 The pre-defined verbs are:
459 The pre-defined verbs are:
460
460
461 {}
461 {}
462
462
463
463
464 To disable a shortcut set ``new_keys`` to an empty list.
464 To disable a shortcut set ``new_keys`` to an empty list.
465 To add a shortcut add key ``create`` with value ``True``.
465 To add a shortcut add key ``create`` with value ``True``.
466
466
467 When modifying/disabling shortcuts, ``match_keys``/``match_filter`` can
467 When modifying/disabling shortcuts, ``match_keys``/``match_filter`` can
468 be omitted if the provided specification uniquely identifies a shortcut
468 be omitted if the provided specification uniquely identifies a shortcut
469 to be modified/disabled. When modifying a shortcut ``new_filter`` or
469 to be modified/disabled. When modifying a shortcut ``new_filter`` or
470 ``new_keys`` can be omitted which will result in reuse of the existing
470 ``new_keys`` can be omitted which will result in reuse of the existing
471 filter/keys.
471 filter/keys.
472
472
473 Only shortcuts defined in IPython (and not default prompt-toolkit
473 Only shortcuts defined in IPython (and not default prompt-toolkit
474 shortcuts) can be modified or disabled. The full list of shortcuts,
474 shortcuts) can be modified or disabled. The full list of shortcuts,
475 command identifiers and filters is available under
475 command identifiers and filters is available under
476 :ref:`terminal-shortcuts-list`.
476 :ref:`terminal-shortcuts-list`.
477 """.format(
477 """.format(
478 "\n ".join([f"- `{k}`" for k in KEYBINDING_FILTERS])
478 "\n ".join([f"- `{k}`" for k in KEYBINDING_FILTERS])
479 ),
479 ),
480 ).tag(config=True)
480 ).tag(config=True)
481
481
482 @observe("shortcuts")
482 @observe("shortcuts")
483 def _shortcuts_changed(self, change):
483 def _shortcuts_changed(self, change):
484 if self.pt_app:
484 if self.pt_app:
485 self.pt_app.key_bindings = self._merge_shortcuts(user_shortcuts=change.new)
485 self.pt_app.key_bindings = self._merge_shortcuts(user_shortcuts=change.new)
486
486
487 def _merge_shortcuts(self, user_shortcuts):
487 def _merge_shortcuts(self, user_shortcuts):
488 # rebuild the bindings list from scratch
488 # rebuild the bindings list from scratch
489 key_bindings = create_ipython_shortcuts(self)
489 key_bindings = create_ipython_shortcuts(self)
490
490
491 # for now we only allow adding shortcuts for commands which are already
491 # for now we only allow adding shortcuts for commands which are already
492 # registered; this is a security precaution.
492 # registered; this is a security precaution.
493 known_commands = {
493 known_commands = {
494 create_identifier(binding.handler): binding.handler
494 create_identifier(binding.handler): binding.handler
495 for binding in key_bindings.bindings
495 for binding in key_bindings.bindings
496 }
496 }
497 shortcuts_to_skip = []
497 shortcuts_to_skip = []
498 shortcuts_to_add = []
498 shortcuts_to_add = []
499
499
500 for shortcut in user_shortcuts:
500 for shortcut in user_shortcuts:
501 command_id = shortcut["command"]
501 command_id = shortcut["command"]
502 if command_id not in known_commands:
502 if command_id not in known_commands:
503 allowed_commands = "\n - ".join(known_commands)
503 allowed_commands = "\n - ".join(known_commands)
504 raise ValueError(
504 raise ValueError(
505 f"{command_id} is not a known shortcut command."
505 f"{command_id} is not a known shortcut command."
506 f" Allowed commands are: \n - {allowed_commands}"
506 f" Allowed commands are: \n - {allowed_commands}"
507 )
507 )
508 old_keys = shortcut.get("match_keys", None)
508 old_keys = shortcut.get("match_keys", None)
509 old_filter = (
509 old_filter = (
510 filter_from_string(shortcut["match_filter"])
510 filter_from_string(shortcut["match_filter"])
511 if "match_filter" in shortcut
511 if "match_filter" in shortcut
512 else None
512 else None
513 )
513 )
514 matching = [
514 matching = [
515 binding
515 binding
516 for binding in key_bindings.bindings
516 for binding in key_bindings.bindings
517 if (
517 if (
518 (old_filter is None or binding.filter == old_filter)
518 (old_filter is None or binding.filter == old_filter)
519 and (old_keys is None or [k for k in binding.keys] == old_keys)
519 and (old_keys is None or [k for k in binding.keys] == old_keys)
520 and create_identifier(binding.handler) == command_id
520 and create_identifier(binding.handler) == command_id
521 )
521 )
522 ]
522 ]
523
523
524 new_keys = shortcut.get("new_keys", None)
524 new_keys = shortcut.get("new_keys", None)
525 new_filter = shortcut.get("new_filter", None)
525 new_filter = shortcut.get("new_filter", None)
526
526
527 command = known_commands[command_id]
527 command = known_commands[command_id]
528
528
529 creating_new = shortcut.get("create", False)
529 creating_new = shortcut.get("create", False)
530 modifying_existing = not creating_new and (
530 modifying_existing = not creating_new and (
531 new_keys is not None or new_filter
531 new_keys is not None or new_filter
532 )
532 )
533
533
534 if creating_new and new_keys == []:
534 if creating_new and new_keys == []:
535 raise ValueError("Cannot add a shortcut without keys")
535 raise ValueError("Cannot add a shortcut without keys")
536
536
537 if modifying_existing:
537 if modifying_existing:
538 specification = {
538 specification = {
539 key: shortcut[key]
539 key: shortcut[key]
540 for key in ["command", "filter"]
540 for key in ["command", "filter"]
541 if key in shortcut
541 if key in shortcut
542 }
542 }
543 if len(matching) == 0:
543 if len(matching) == 0:
544 raise ValueError(
544 raise ValueError(
545 f"No shortcuts matching {specification} found in {key_bindings.bindings}"
545 f"No shortcuts matching {specification} found in {key_bindings.bindings}"
546 )
546 )
547 elif len(matching) > 1:
547 elif len(matching) > 1:
548 raise ValueError(
548 raise ValueError(
549 f"Multiple shortcuts matching {specification} found,"
549 f"Multiple shortcuts matching {specification} found,"
550 f" please add keys/filter to select one of: {matching}"
550 f" please add keys/filter to select one of: {matching}"
551 )
551 )
552
552
553 matched = matching[0]
553 matched = matching[0]
554 old_filter = matched.filter
554 old_filter = matched.filter
555 old_keys = list(matched.keys)
555 old_keys = list(matched.keys)
556 shortcuts_to_skip.append(
556 shortcuts_to_skip.append(
557 RuntimeBinding(
557 RuntimeBinding(
558 command,
558 command,
559 keys=old_keys,
559 keys=old_keys,
560 filter=old_filter,
560 filter=old_filter,
561 )
561 )
562 )
562 )
563
563
564 if new_keys != []:
564 if new_keys != []:
565 shortcuts_to_add.append(
565 shortcuts_to_add.append(
566 RuntimeBinding(
566 RuntimeBinding(
567 command,
567 command,
568 keys=new_keys or old_keys,
568 keys=new_keys or old_keys,
569 filter=filter_from_string(new_filter)
569 filter=filter_from_string(new_filter)
570 if new_filter is not None
570 if new_filter is not None
571 else (
571 else (
572 old_filter
572 old_filter
573 if old_filter is not None
573 if old_filter is not None
574 else filter_from_string("always")
574 else filter_from_string("always")
575 ),
575 ),
576 )
576 )
577 )
577 )
578
578
579 # rebuild the bindings list from scratch
579 # rebuild the bindings list from scratch
580 key_bindings = create_ipython_shortcuts(self, skip=shortcuts_to_skip)
580 key_bindings = create_ipython_shortcuts(self, skip=shortcuts_to_skip)
581 for binding in shortcuts_to_add:
581 for binding in shortcuts_to_add:
582 add_binding(key_bindings, binding)
582 add_binding(key_bindings, binding)
583
583
584 return key_bindings
584 return key_bindings
585
585
586 prompt_includes_vi_mode = Bool(True,
586 prompt_includes_vi_mode = Bool(True,
587 help="Display the current vi mode (when using vi editing mode)."
587 help="Display the current vi mode (when using vi editing mode)."
588 ).tag(config=True)
588 ).tag(config=True)
589
589
590 @observe('term_title')
590 @observe('term_title')
591 def init_term_title(self, change=None):
591 def init_term_title(self, change=None):
592 # Enable or disable the terminal title.
592 # Enable or disable the terminal title.
593 if self.term_title and _is_tty:
593 if self.term_title and _is_tty:
594 toggle_set_term_title(True)
594 toggle_set_term_title(True)
595 set_term_title(self.term_title_format.format(cwd=abbrev_cwd()))
595 set_term_title(self.term_title_format.format(cwd=abbrev_cwd()))
596 else:
596 else:
597 toggle_set_term_title(False)
597 toggle_set_term_title(False)
598
598
599 def restore_term_title(self):
599 def restore_term_title(self):
600 if self.term_title and _is_tty:
600 if self.term_title and _is_tty:
601 restore_term_title()
601 restore_term_title()
602
602
603 def init_display_formatter(self):
603 def init_display_formatter(self):
604 super(TerminalInteractiveShell, self).init_display_formatter()
604 super(TerminalInteractiveShell, self).init_display_formatter()
605 # terminal only supports plain text
605 # terminal only supports plain text
606 self.display_formatter.active_types = ["text/plain"]
606 self.display_formatter.active_types = ["text/plain"]
607
607
608 def init_prompt_toolkit_cli(self):
608 def init_prompt_toolkit_cli(self):
609 if self.simple_prompt:
609 if self.simple_prompt:
610 # Fall back to plain non-interactive output for tests.
610 # Fall back to plain non-interactive output for tests.
611 # This is very limited.
611 # This is very limited.
612 def prompt():
612 def prompt():
613 prompt_text = "".join(x[1] for x in self.prompts.in_prompt_tokens())
613 prompt_text = "".join(x[1] for x in self.prompts.in_prompt_tokens())
614 lines = [input(prompt_text)]
614 lines = [input(prompt_text)]
615 prompt_continuation = "".join(x[1] for x in self.prompts.continuation_prompt_tokens())
615 prompt_continuation = "".join(x[1] for x in self.prompts.continuation_prompt_tokens())
616 while self.check_complete('\n'.join(lines))[0] == 'incomplete':
616 while self.check_complete('\n'.join(lines))[0] == 'incomplete':
617 lines.append( input(prompt_continuation) )
617 lines.append( input(prompt_continuation) )
618 return '\n'.join(lines)
618 return '\n'.join(lines)
619 self.prompt_for_code = prompt
619 self.prompt_for_code = prompt
620 return
620 return
621
621
622 # Set up keyboard shortcuts
622 # Set up keyboard shortcuts
623 key_bindings = self._merge_shortcuts(user_shortcuts=self.shortcuts)
623 key_bindings = self._merge_shortcuts(user_shortcuts=self.shortcuts)
624
624
625 # Pre-populate history from IPython's history database
625 # Pre-populate history from IPython's history database
626 history = PtkHistoryAdapter(self)
626 history = PtkHistoryAdapter(self)
627
627
628 self._style = self._make_style_from_name_or_cls(self.highlighting_style)
628 self._style = self._make_style_from_name_or_cls(self.highlighting_style)
629 self.style = DynamicStyle(lambda: self._style)
629 self.style = DynamicStyle(lambda: self._style)
630
630
631 editing_mode = getattr(EditingMode, self.editing_mode.upper())
631 editing_mode = getattr(EditingMode, self.editing_mode.upper())
632
632
633 self.pt_loop = asyncio.new_event_loop()
633 self.pt_loop = asyncio.new_event_loop()
634 self.pt_app = PromptSession(
634 self.pt_app = PromptSession(
635 auto_suggest=self.auto_suggest,
635 auto_suggest=self.auto_suggest,
636 editing_mode=editing_mode,
636 editing_mode=editing_mode,
637 key_bindings=key_bindings,
637 key_bindings=key_bindings,
638 history=history,
638 history=history,
639 completer=IPythonPTCompleter(shell=self),
639 completer=IPythonPTCompleter(shell=self),
640 enable_history_search=self.enable_history_search,
640 enable_history_search=self.enable_history_search,
641 style=self.style,
641 style=self.style,
642 include_default_pygments_style=False,
642 include_default_pygments_style=False,
643 mouse_support=self.mouse_support,
643 mouse_support=self.mouse_support,
644 enable_open_in_editor=self.extra_open_editor_shortcuts,
644 enable_open_in_editor=self.extra_open_editor_shortcuts,
645 color_depth=self.color_depth,
645 color_depth=self.color_depth,
646 tempfile_suffix=".py",
646 tempfile_suffix=".py",
647 **self._extra_prompt_options(),
647 **self._extra_prompt_options(),
648 )
648 )
649 if isinstance(self.auto_suggest, NavigableAutoSuggestFromHistory):
649 if isinstance(self.auto_suggest, NavigableAutoSuggestFromHistory):
650 self.auto_suggest.connect(self.pt_app)
650 self.auto_suggest.connect(self.pt_app)
651
651
652 def _make_style_from_name_or_cls(self, name_or_cls):
652 def _make_style_from_name_or_cls(self, name_or_cls):
653 """
653 """
654 Small wrapper that make an IPython compatible style from a style name
654 Small wrapper that make an IPython compatible style from a style name
655
655
656 We need that to add style for prompt ... etc.
656 We need that to add style for prompt ... etc.
657 """
657 """
658 style_overrides = {}
658 style_overrides = {}
659 if name_or_cls == 'legacy':
659 if name_or_cls == 'legacy':
660 legacy = self.colors.lower()
660 legacy = self.colors.lower()
661 if legacy == 'linux':
661 if legacy == 'linux':
662 style_cls = get_style_by_name('monokai')
662 style_cls = get_style_by_name('monokai')
663 style_overrides = _style_overrides_linux
663 style_overrides = _style_overrides_linux
664 elif legacy == 'lightbg':
664 elif legacy == 'lightbg':
665 style_overrides = _style_overrides_light_bg
665 style_overrides = _style_overrides_light_bg
666 style_cls = get_style_by_name('pastie')
666 style_cls = get_style_by_name('pastie')
667 elif legacy == 'neutral':
667 elif legacy == 'neutral':
668 # The default theme needs to be visible on both a dark background
668 # The default theme needs to be visible on both a dark background
669 # and a light background, because we can't tell what the terminal
669 # and a light background, because we can't tell what the terminal
670 # looks like. These tweaks to the default theme help with that.
670 # looks like. These tweaks to the default theme help with that.
671 style_cls = get_style_by_name('default')
671 style_cls = get_style_by_name('default')
672 style_overrides.update({
672 style_overrides.update({
673 Token.Number: '#ansigreen',
673 Token.Number: '#ansigreen',
674 Token.Operator: 'noinherit',
674 Token.Operator: 'noinherit',
675 Token.String: '#ansiyellow',
675 Token.String: '#ansiyellow',
676 Token.Name.Function: '#ansiblue',
676 Token.Name.Function: '#ansiblue',
677 Token.Name.Class: 'bold #ansiblue',
677 Token.Name.Class: 'bold #ansiblue',
678 Token.Name.Namespace: 'bold #ansiblue',
678 Token.Name.Namespace: 'bold #ansiblue',
679 Token.Name.Variable.Magic: '#ansiblue',
679 Token.Name.Variable.Magic: '#ansiblue',
680 Token.Prompt: '#ansigreen',
680 Token.Prompt: '#ansigreen',
681 Token.PromptNum: '#ansibrightgreen bold',
681 Token.PromptNum: '#ansibrightgreen bold',
682 Token.OutPrompt: '#ansired',
682 Token.OutPrompt: '#ansired',
683 Token.OutPromptNum: '#ansibrightred bold',
683 Token.OutPromptNum: '#ansibrightred bold',
684 })
684 })
685
685
686 # Hack: Due to limited color support on the Windows console
686 # Hack: Due to limited color support on the Windows console
687 # the prompt colors will be wrong without this
687 # the prompt colors will be wrong without this
688 if os.name == 'nt':
688 if os.name == 'nt':
689 style_overrides.update({
689 style_overrides.update({
690 Token.Prompt: '#ansidarkgreen',
690 Token.Prompt: '#ansidarkgreen',
691 Token.PromptNum: '#ansigreen bold',
691 Token.PromptNum: '#ansigreen bold',
692 Token.OutPrompt: '#ansidarkred',
692 Token.OutPrompt: '#ansidarkred',
693 Token.OutPromptNum: '#ansired bold',
693 Token.OutPromptNum: '#ansired bold',
694 })
694 })
695 elif legacy =='nocolor':
695 elif legacy =='nocolor':
696 style_cls=_NoStyle
696 style_cls=_NoStyle
697 style_overrides = {}
697 style_overrides = {}
698 else :
698 else :
699 raise ValueError('Got unknown colors: ', legacy)
699 raise ValueError('Got unknown colors: ', legacy)
700 else :
700 else :
701 if isinstance(name_or_cls, str):
701 if isinstance(name_or_cls, str):
702 style_cls = get_style_by_name(name_or_cls)
702 style_cls = get_style_by_name(name_or_cls)
703 else:
703 else:
704 style_cls = name_or_cls
704 style_cls = name_or_cls
705 style_overrides = {
705 style_overrides = {
706 Token.Prompt: '#ansigreen',
706 Token.Prompt: '#ansigreen',
707 Token.PromptNum: '#ansibrightgreen bold',
707 Token.PromptNum: '#ansibrightgreen bold',
708 Token.OutPrompt: '#ansired',
708 Token.OutPrompt: '#ansired',
709 Token.OutPromptNum: '#ansibrightred bold',
709 Token.OutPromptNum: '#ansibrightred bold',
710 }
710 }
711 style_overrides.update(self.highlighting_style_overrides)
711 style_overrides.update(self.highlighting_style_overrides)
712 style = merge_styles([
712 style = merge_styles([
713 style_from_pygments_cls(style_cls),
713 style_from_pygments_cls(style_cls),
714 style_from_pygments_dict(style_overrides),
714 style_from_pygments_dict(style_overrides),
715 ])
715 ])
716
716
717 return style
717 return style
718
718
719 @property
719 @property
720 def pt_complete_style(self):
720 def pt_complete_style(self):
721 return {
721 return {
722 'multicolumn': CompleteStyle.MULTI_COLUMN,
722 'multicolumn': CompleteStyle.MULTI_COLUMN,
723 'column': CompleteStyle.COLUMN,
723 'column': CompleteStyle.COLUMN,
724 'readlinelike': CompleteStyle.READLINE_LIKE,
724 'readlinelike': CompleteStyle.READLINE_LIKE,
725 }[self.display_completions]
725 }[self.display_completions]
726
726
727 @property
727 @property
728 def color_depth(self):
728 def color_depth(self):
729 return (ColorDepth.TRUE_COLOR if self.true_color else None)
729 return (ColorDepth.TRUE_COLOR if self.true_color else None)
730
730
731 def _extra_prompt_options(self):
731 def _extra_prompt_options(self):
732 """
732 """
733 Return the current layout option for the current Terminal InteractiveShell
733 Return the current layout option for the current Terminal InteractiveShell
734 """
734 """
735 def get_message():
735 def get_message():
736 return PygmentsTokens(self.prompts.in_prompt_tokens())
736 return PygmentsTokens(self.prompts.in_prompt_tokens())
737
737
738 if self.editing_mode == 'emacs':
738 if self.editing_mode == 'emacs':
739 # with emacs mode the prompt is (usually) static, so we call only
739 # with emacs mode the prompt is (usually) static, so we call only
740 # the function once. With VI mode it can toggle between [ins] and
740 # the function once. With VI mode it can toggle between [ins] and
741 # [nor] so we can't precompute.
741 # [nor] so we can't precompute.
742 # here I'm going to favor the default keybinding which almost
742 # here I'm going to favor the default keybinding which almost
743 # everybody uses to decrease CPU usage.
743 # everybody uses to decrease CPU usage.
744 # if we have issues with users with custom Prompts we can see how to
744 # if we have issues with users with custom Prompts we can see how to
745 # work around this.
745 # work around this.
746 get_message = get_message()
746 get_message = get_message()
747
747
748 options = {
748 options = {
749 "complete_in_thread": False,
749 "complete_in_thread": False,
750 "lexer": IPythonPTLexer(),
750 "lexer": IPythonPTLexer(),
751 "reserve_space_for_menu": self.space_for_menu,
751 "reserve_space_for_menu": self.space_for_menu,
752 "message": get_message,
752 "message": get_message,
753 "prompt_continuation": (
753 "prompt_continuation": (
754 lambda width, lineno, is_soft_wrap: PygmentsTokens(
754 lambda width, lineno, is_soft_wrap: PygmentsTokens(
755 self.prompts.continuation_prompt_tokens(width)
755 self.prompts.continuation_prompt_tokens(width)
756 )
756 )
757 ),
757 ),
758 "multiline": True,
758 "multiline": True,
759 "complete_style": self.pt_complete_style,
759 "complete_style": self.pt_complete_style,
760 "input_processors": [
760 "input_processors": [
761 # Highlight matching brackets, but only when this setting is
761 # Highlight matching brackets, but only when this setting is
762 # enabled, and only when the DEFAULT_BUFFER has the focus.
762 # enabled, and only when the DEFAULT_BUFFER has the focus.
763 ConditionalProcessor(
763 ConditionalProcessor(
764 processor=HighlightMatchingBracketProcessor(chars="[](){}"),
764 processor=HighlightMatchingBracketProcessor(chars="[](){}"),
765 filter=HasFocus(DEFAULT_BUFFER)
765 filter=HasFocus(DEFAULT_BUFFER)
766 & ~IsDone()
766 & ~IsDone()
767 & Condition(lambda: self.highlight_matching_brackets),
767 & Condition(lambda: self.highlight_matching_brackets),
768 ),
768 ),
769 # Show auto-suggestion in lines other than the last line.
769 # Show auto-suggestion in lines other than the last line.
770 ConditionalProcessor(
770 ConditionalProcessor(
771 processor=AppendAutoSuggestionInAnyLine(),
771 processor=AppendAutoSuggestionInAnyLine(),
772 filter=HasFocus(DEFAULT_BUFFER)
772 filter=HasFocus(DEFAULT_BUFFER)
773 & ~IsDone()
773 & ~IsDone()
774 & Condition(
774 & Condition(
775 lambda: isinstance(
775 lambda: isinstance(
776 self.auto_suggest, NavigableAutoSuggestFromHistory
776 self.auto_suggest, NavigableAutoSuggestFromHistory
777 )
777 )
778 ),
778 ),
779 ),
779 ),
780 ],
780 ],
781 }
781 }
782 if not PTK3:
782 if not PTK3:
783 options['inputhook'] = self.inputhook
783 options['inputhook'] = self.inputhook
784
784
785 return options
785 return options
786
786
787 def prompt_for_code(self):
787 def prompt_for_code(self):
788 if self.rl_next_input:
788 if self.rl_next_input:
789 default = self.rl_next_input
789 default = self.rl_next_input
790 self.rl_next_input = None
790 self.rl_next_input = None
791 else:
791 else:
792 default = ''
792 default = ''
793
793
794 # In order to make sure that asyncio code written in the
794 # In order to make sure that asyncio code written in the
795 # interactive shell doesn't interfere with the prompt, we run the
795 # interactive shell doesn't interfere with the prompt, we run the
796 # prompt in a different event loop.
796 # prompt in a different event loop.
797 # If we don't do this, people could spawn coroutine with a
797 # If we don't do this, people could spawn coroutine with a
798 # while/true inside which will freeze the prompt.
798 # while/true inside which will freeze the prompt.
799
799
800 policy = asyncio.get_event_loop_policy()
800 policy = asyncio.get_event_loop_policy()
801 old_loop = get_asyncio_loop()
801 old_loop = get_asyncio_loop()
802
802
803 # FIXME: prompt_toolkit is using the deprecated `asyncio.get_event_loop`
803 # FIXME: prompt_toolkit is using the deprecated `asyncio.get_event_loop`
804 # to get the current event loop.
804 # to get the current event loop.
805 # This will probably be replaced by an attribute or input argument,
805 # This will probably be replaced by an attribute or input argument,
806 # at which point we can stop calling the soon-to-be-deprecated `set_event_loop` here.
806 # at which point we can stop calling the soon-to-be-deprecated `set_event_loop` here.
807 if old_loop is not self.pt_loop:
807 if old_loop is not self.pt_loop:
808 policy.set_event_loop(self.pt_loop)
808 policy.set_event_loop(self.pt_loop)
809 try:
809 try:
810 with patch_stdout(raw=True):
810 with patch_stdout(raw=True):
811 text = self.pt_app.prompt(
811 text = self.pt_app.prompt(
812 default=default,
812 default=default,
813 **self._extra_prompt_options())
813 **self._extra_prompt_options())
814 finally:
814 finally:
815 # Restore the original event loop.
815 # Restore the original event loop.
816 if old_loop is not None and old_loop is not self.pt_loop:
816 if old_loop is not None and old_loop is not self.pt_loop:
817 policy.set_event_loop(old_loop)
817 policy.set_event_loop(old_loop)
818
818
819 return text
819 return text
820
820
821 def enable_win_unicode_console(self):
821 def enable_win_unicode_console(self):
822 # Since IPython 7.10 doesn't support python < 3.6 and PEP 528, Python uses the unicode APIs for the Windows
822 # Since IPython 7.10 doesn't support python < 3.6 and PEP 528, Python uses the unicode APIs for the Windows
823 # console by default, so WUC shouldn't be needed.
823 # console by default, so WUC shouldn't be needed.
824 warn("`enable_win_unicode_console` is deprecated since IPython 7.10, does not do anything and will be removed in the future",
824 warn("`enable_win_unicode_console` is deprecated since IPython 7.10, does not do anything and will be removed in the future",
825 DeprecationWarning,
825 DeprecationWarning,
826 stacklevel=2)
826 stacklevel=2)
827
827
828 def init_io(self):
828 def init_io(self):
829 if sys.platform not in {'win32', 'cli'}:
829 if sys.platform not in {'win32', 'cli'}:
830 return
830 return
831
831
832 import colorama
832 import colorama
833 colorama.init()
833 colorama.init()
834
834
835 def init_magics(self):
835 def init_magics(self):
836 super(TerminalInteractiveShell, self).init_magics()
836 super(TerminalInteractiveShell, self).init_magics()
837 self.register_magics(TerminalMagics)
837 self.register_magics(TerminalMagics)
838
838
839 def init_alias(self):
839 def init_alias(self):
840 # The parent class defines aliases that can be safely used with any
840 # The parent class defines aliases that can be safely used with any
841 # frontend.
841 # frontend.
842 super(TerminalInteractiveShell, self).init_alias()
842 super(TerminalInteractiveShell, self).init_alias()
843
843
844 # Now define aliases that only make sense on the terminal, because they
844 # Now define aliases that only make sense on the terminal, because they
845 # need direct access to the console in a way that we can't emulate in
845 # need direct access to the console in a way that we can't emulate in
846 # GUI or web frontend
846 # GUI or web frontend
847 if os.name == 'posix':
847 if os.name == 'posix':
848 for cmd in ('clear', 'more', 'less', 'man'):
848 for cmd in ('clear', 'more', 'less', 'man'):
849 self.alias_manager.soft_define_alias(cmd, cmd)
849 self.alias_manager.soft_define_alias(cmd, cmd)
850
850
851
851
852 def __init__(self, *args, **kwargs) -> None:
852 def __init__(self, *args, **kwargs) -> None:
853 super(TerminalInteractiveShell, self).__init__(*args, **kwargs)
853 super(TerminalInteractiveShell, self).__init__(*args, **kwargs)
854 self._set_autosuggestions(self.autosuggestions_provider)
854 self._set_autosuggestions(self.autosuggestions_provider)
855 self.init_prompt_toolkit_cli()
855 self.init_prompt_toolkit_cli()
856 self.init_term_title()
856 self.init_term_title()
857 self.keep_running = True
857 self.keep_running = True
858 self._set_formatter(self.autoformatter)
858 self._set_formatter(self.autoformatter)
859
859
860
860
861 def ask_exit(self):
861 def ask_exit(self):
862 self.keep_running = False
862 self.keep_running = False
863
863
864 rl_next_input = None
864 rl_next_input = None
865
865
866 def interact(self):
866 def interact(self):
867 self.keep_running = True
867 self.keep_running = True
868 while self.keep_running:
868 while self.keep_running:
869 print(self.separate_in, end='')
869 print(self.separate_in, end='')
870
870
871 try:
871 try:
872 code = self.prompt_for_code()
872 code = self.prompt_for_code()
873 except EOFError:
873 except EOFError:
874 if (not self.confirm_exit) \
874 if (not self.confirm_exit) \
875 or self.ask_yes_no('Do you really want to exit ([y]/n)?','y','n'):
875 or self.ask_yes_no('Do you really want to exit ([y]/n)?','y','n'):
876 self.ask_exit()
876 self.ask_exit()
877
877
878 else:
878 else:
879 if code:
879 if code:
880 self.run_cell(code, store_history=True)
880 self.run_cell(code, store_history=True)
881
881
882 def mainloop(self):
882 def mainloop(self):
883 # An extra layer of protection in case someone mashing Ctrl-C breaks
883 # An extra layer of protection in case someone mashing Ctrl-C breaks
884 # out of our internal code.
884 # out of our internal code.
885 while True:
885 while True:
886 try:
886 try:
887 self.interact()
887 self.interact()
888 break
888 break
889 except KeyboardInterrupt as e:
889 except KeyboardInterrupt as e:
890 print("\n%s escaped interact()\n" % type(e).__name__)
890 print("\n%s escaped interact()\n" % type(e).__name__)
891 finally:
891 finally:
892 # An interrupt during the eventloop will mess up the
892 # An interrupt during the eventloop will mess up the
893 # internal state of the prompt_toolkit library.
893 # internal state of the prompt_toolkit library.
894 # Stopping the eventloop fixes this, see
894 # Stopping the eventloop fixes this, see
895 # https://github.com/ipython/ipython/pull/9867
895 # https://github.com/ipython/ipython/pull/9867
896 if hasattr(self, '_eventloop'):
896 if hasattr(self, '_eventloop'):
897 self._eventloop.stop()
897 self._eventloop.stop()
898
898
899 self.restore_term_title()
899 self.restore_term_title()
900
900
901 # try to call some at-exit operation optimistically as some things can't
901 # try to call some at-exit operation optimistically as some things can't
902 # be done during interpreter shutdown. this is technically inaccurate as
902 # be done during interpreter shutdown. this is technically inaccurate as
903 # this make mainlool not re-callable, but that should be a rare if not
903 # this make mainlool not re-callable, but that should be a rare if not
904 # in existent use case.
904 # in existent use case.
905
905
906 self._atexit_once()
906 self._atexit_once()
907
907
908
908
909 _inputhook = None
909 _inputhook = None
910 def inputhook(self, context):
910 def inputhook(self, context):
911 if self._inputhook is not None:
911 if self._inputhook is not None:
912 self._inputhook(context)
912 self._inputhook(context)
913
913
914 active_eventloop = None
914 active_eventloop = None
915 def enable_gui(self, gui=None):
915 def enable_gui(self, gui=None):
916 if self._inputhook is None and gui is None:
917 print("No event loop hook running.")
918 return
919
916 if self._inputhook is not None and gui is not None:
920 if self._inputhook is not None and gui is not None:
917 warn(
921 print(
918 f"Shell was already running a gui event loop for {self.active_eventloop}; switching to {gui}."
922 f"Shell is already running a gui event loop for {self.active_eventloop}. "
923 "Call with no arguments to disable the current loop."
919 )
924 )
925 return
926 if self._inputhook is not None and gui is None:
927 self.active_eventloop = self._inputhook = None
928
920 if gui and (gui not in {"inline", "webagg"}):
929 if gui and (gui not in {"inline", "webagg"}):
921 # This hook runs with each cycle of the `prompt_toolkit`'s event loop.
930 # This hook runs with each cycle of the `prompt_toolkit`'s event loop.
922 self.active_eventloop, self._inputhook = get_inputhook_name_and_func(gui)
931 self.active_eventloop, self._inputhook = get_inputhook_name_and_func(gui)
923 else:
932 else:
924 self.active_eventloop = self._inputhook = None
933 self.active_eventloop = self._inputhook = None
925
934
926 # For prompt_toolkit 3.0. We have to create an asyncio event loop with
935 # For prompt_toolkit 3.0. We have to create an asyncio event loop with
927 # this inputhook.
936 # this inputhook.
928 if PTK3:
937 if PTK3:
929 import asyncio
938 import asyncio
930 from prompt_toolkit.eventloop import new_eventloop_with_inputhook
939 from prompt_toolkit.eventloop import new_eventloop_with_inputhook
931
940
932 if gui == 'asyncio':
941 if gui == 'asyncio':
933 # When we integrate the asyncio event loop, run the UI in the
942 # When we integrate the asyncio event loop, run the UI in the
934 # same event loop as the rest of the code. don't use an actual
943 # same event loop as the rest of the code. don't use an actual
935 # input hook. (Asyncio is not made for nesting event loops.)
944 # input hook. (Asyncio is not made for nesting event loops.)
936 self.pt_loop = get_asyncio_loop()
945 self.pt_loop = get_asyncio_loop()
946 print("Installed asyncio event loop hook.")
937
947
938 elif self._inputhook:
948 elif self._inputhook:
939 # If an inputhook was set, create a new asyncio event loop with
949 # If an inputhook was set, create a new asyncio event loop with
940 # this inputhook for the prompt.
950 # this inputhook for the prompt.
941 self.pt_loop = new_eventloop_with_inputhook(self._inputhook)
951 self.pt_loop = new_eventloop_with_inputhook(self._inputhook)
952 print(f"Installed {self.active_eventloop} event loop hook.")
942 else:
953 else:
943 # When there's no inputhook, run the prompt in a separate
954 # When there's no inputhook, run the prompt in a separate
944 # asyncio event loop.
955 # asyncio event loop.
945 self.pt_loop = asyncio.new_event_loop()
956 self.pt_loop = asyncio.new_event_loop()
957 print("GUI event loop hook disabled.")
946
958
947 # Run !system commands directly, not through pipes, so terminal programs
959 # Run !system commands directly, not through pipes, so terminal programs
948 # work correctly.
960 # work correctly.
949 system = InteractiveShell.system_raw
961 system = InteractiveShell.system_raw
950
962
951 def auto_rewrite_input(self, cmd):
963 def auto_rewrite_input(self, cmd):
952 """Overridden from the parent class to use fancy rewriting prompt"""
964 """Overridden from the parent class to use fancy rewriting prompt"""
953 if not self.show_rewritten_input:
965 if not self.show_rewritten_input:
954 return
966 return
955
967
956 tokens = self.prompts.rewrite_prompt_tokens()
968 tokens = self.prompts.rewrite_prompt_tokens()
957 if self.pt_app:
969 if self.pt_app:
958 print_formatted_text(PygmentsTokens(tokens), end='',
970 print_formatted_text(PygmentsTokens(tokens), end='',
959 style=self.pt_app.app.style)
971 style=self.pt_app.app.style)
960 print(cmd)
972 print(cmd)
961 else:
973 else:
962 prompt = ''.join(s for t, s in tokens)
974 prompt = ''.join(s for t, s in tokens)
963 print(prompt, cmd, sep='')
975 print(prompt, cmd, sep='')
964
976
965 _prompts_before = None
977 _prompts_before = None
966 def switch_doctest_mode(self, mode):
978 def switch_doctest_mode(self, mode):
967 """Switch prompts to classic for %doctest_mode"""
979 """Switch prompts to classic for %doctest_mode"""
968 if mode:
980 if mode:
969 self._prompts_before = self.prompts
981 self._prompts_before = self.prompts
970 self.prompts = ClassicPrompts(self)
982 self.prompts = ClassicPrompts(self)
971 elif self._prompts_before:
983 elif self._prompts_before:
972 self.prompts = self._prompts_before
984 self.prompts = self._prompts_before
973 self._prompts_before = None
985 self._prompts_before = None
974 # self._update_layout()
986 # self._update_layout()
975
987
976
988
977 InteractiveShellABC.register(TerminalInteractiveShell)
989 InteractiveShellABC.register(TerminalInteractiveShell)
978
990
979 if __name__ == '__main__':
991 if __name__ == '__main__':
980 TerminalInteractiveShell.instance().interact()
992 TerminalInteractiveShell.instance().interact()
@@ -1,132 +1,138 b''
1 import importlib
1 import importlib
2 import os
2 import os
3
3
4 aliases = {
4 aliases = {
5 'qt4': 'qt',
5 'qt4': 'qt',
6 'gtk2': 'gtk',
6 'gtk2': 'gtk',
7 }
7 }
8
8
9 backends = [
9 backends = [
10 "qt",
10 "qt",
11 "qt5",
11 "qt5",
12 "qt6",
12 "qt6",
13 "gtk",
13 "gtk",
14 "gtk2",
14 "gtk2",
15 "gtk3",
15 "gtk3",
16 "gtk4",
16 "gtk4",
17 "tk",
17 "tk",
18 "wx",
18 "wx",
19 "pyglet",
19 "pyglet",
20 "glut",
20 "glut",
21 "osx",
21 "osx",
22 "asyncio",
22 "asyncio",
23 ]
23 ]
24
24
25 registered = {}
25 registered = {}
26
26
27 def register(name, inputhook):
27 def register(name, inputhook):
28 """Register the function *inputhook* as an event loop integration."""
28 """Register the function *inputhook* as an event loop integration."""
29 registered[name] = inputhook
29 registered[name] = inputhook
30
30
31
31
32 class UnknownBackend(KeyError):
32 class UnknownBackend(KeyError):
33 def __init__(self, name):
33 def __init__(self, name):
34 self.name = name
34 self.name = name
35
35
36 def __str__(self):
36 def __str__(self):
37 return ("No event loop integration for {!r}. "
37 return ("No event loop integration for {!r}. "
38 "Supported event loops are: {}").format(self.name,
38 "Supported event loops are: {}").format(self.name,
39 ', '.join(backends + sorted(registered)))
39 ', '.join(backends + sorted(registered)))
40
40
41
41
42 def set_qt_api(gui):
42 def set_qt_api(gui):
43 """Sets the `QT_API` environment variable if it isn't already set."""
43 """Sets the `QT_API` environment variable if it isn't already set."""
44
44
45 qt_api = os.environ.get("QT_API", None)
45 qt_api = os.environ.get("QT_API", None)
46
46
47 from IPython.external.qt_loaders import (
47 from IPython.external.qt_loaders import (
48 QT_API_PYQT,
48 QT_API_PYQT,
49 QT_API_PYQT5,
49 QT_API_PYQT5,
50 QT_API_PYQT6,
50 QT_API_PYQT6,
51 QT_API_PYSIDE,
51 QT_API_PYSIDE,
52 QT_API_PYSIDE2,
52 QT_API_PYSIDE2,
53 QT_API_PYSIDE6,
53 QT_API_PYSIDE6,
54 QT_API_PYQTv1,
54 QT_API_PYQTv1,
55 loaded_api,
55 loaded_api,
56 )
56 )
57
57
58 loaded = loaded_api()
58 loaded = loaded_api()
59
59
60 qt_env2gui = {
60 qt_env2gui = {
61 QT_API_PYSIDE: "qt4",
61 QT_API_PYSIDE: "qt4",
62 QT_API_PYQTv1: "qt4",
62 QT_API_PYQTv1: "qt4",
63 QT_API_PYQT: "qt4",
63 QT_API_PYQT: "qt4",
64 QT_API_PYSIDE2: "qt5",
64 QT_API_PYSIDE2: "qt5",
65 QT_API_PYQT5: "qt5",
65 QT_API_PYQT5: "qt5",
66 QT_API_PYSIDE6: "qt6",
66 QT_API_PYSIDE6: "qt6",
67 QT_API_PYQT6: "qt6",
67 QT_API_PYQT6: "qt6",
68 }
68 }
69 if loaded is not None and gui != "qt":
69 if loaded is not None and gui != "qt":
70 if qt_env2gui[loaded] != gui:
70 if qt_env2gui[loaded] != gui:
71 print(
71 print(
72 f"Cannot switch Qt versions for this session; must use {qt_env2gui[loaded]}."
72 f"Cannot switch Qt versions for this session; will use {qt_env2gui[loaded]}."
73 )
73 )
74 return
74 return qt_env2gui[loaded]
75
75
76 if qt_api is not None and gui != "qt":
76 if qt_api is not None and gui != "qt":
77 if qt_env2gui[qt_api] != gui:
77 if qt_env2gui[qt_api] != gui:
78 print(
78 print(
79 f'Request for "{gui}" will be ignored because `QT_API` '
79 f'Request for "{gui}" will be ignored because `QT_API` '
80 f'environment variable is set to "{qt_api}"'
80 f'environment variable is set to "{qt_api}"'
81 )
81 )
82 return qt_env2gui[qt_api]
82 else:
83 else:
83 if gui == "qt5":
84 if gui == "qt5":
84 try:
85 try:
85 import PyQt5 # noqa
86 import PyQt5 # noqa
86
87
87 os.environ["QT_API"] = "pyqt5"
88 os.environ["QT_API"] = "pyqt5"
88 except ImportError:
89 except ImportError:
89 try:
90 try:
90 import PySide2 # noqa
91 import PySide2 # noqa
91
92
92 os.environ["QT_API"] = "pyside2"
93 os.environ["QT_API"] = "pyside2"
93 except ImportError:
94 except ImportError:
94 os.environ["QT_API"] = "pyqt5"
95 os.environ["QT_API"] = "pyqt5"
95 elif gui == "qt6":
96 elif gui == "qt6":
96 try:
97 try:
97 import PyQt6 # noqa
98 import PyQt6 # noqa
98
99
99 os.environ["QT_API"] = "pyqt6"
100 os.environ["QT_API"] = "pyqt6"
100 except ImportError:
101 except ImportError:
101 try:
102 try:
102 import PySide6 # noqa
103 import PySide6 # noqa
103
104
104 os.environ["QT_API"] = "pyside6"
105 os.environ["QT_API"] = "pyside6"
105 except ImportError:
106 except ImportError:
106 os.environ["QT_API"] = "pyqt6"
107 os.environ["QT_API"] = "pyqt6"
107 elif gui == "qt":
108 elif gui == "qt":
108 # Don't set QT_API; let IPython logic choose the version.
109 # Don't set QT_API; let IPython logic choose the version.
109 if "QT_API" in os.environ.keys():
110 if "QT_API" in os.environ.keys():
110 del os.environ["QT_API"]
111 del os.environ["QT_API"]
111 else:
112 else:
112 print(f'Unrecognized Qt version: {gui}. Should be "qt5", "qt6", or "qt".')
113 print(f'Unrecognized Qt version: {gui}. Should be "qt5", "qt6", or "qt".')
113 return
114 return
114
115
116 # Import it now so we can figure out which version it is.
117 from IPython.external.qt_for_kernel import QT_API
118
119 return qt_env2gui[QT_API]
120
115
121
116 def get_inputhook_name_and_func(gui):
122 def get_inputhook_name_and_func(gui):
117 if gui in registered:
123 if gui in registered:
118 return gui, registered[gui]
124 return gui, registered[gui]
119
125
120 if gui not in backends:
126 if gui not in backends:
121 raise UnknownBackend(gui)
127 raise UnknownBackend(gui)
122
128
123 if gui in aliases:
129 if gui in aliases:
124 return get_inputhook_name_and_func(aliases[gui])
130 return get_inputhook_name_and_func(aliases[gui])
125
131
126 gui_mod = gui
132 gui_mod = gui
127 if gui.startswith("qt"):
133 if gui.startswith("qt"):
128 set_qt_api(gui)
134 gui = set_qt_api(gui)
129 gui_mod = "qt"
135 gui_mod = "qt"
130
136
131 mod = importlib.import_module("IPython.terminal.pt_inputhooks." + gui_mod)
137 mod = importlib.import_module("IPython.terminal.pt_inputhooks." + gui_mod)
132 return gui, mod.inputhook
138 return gui, mod.inputhook
@@ -1,50 +1,50 b''
1 import os
1 import os
2 import importlib
2 import importlib
3
3
4 import pytest
4 import pytest
5
5
6 from IPython.terminal.pt_inputhooks import set_qt_api, get_inputhook_name_and_func
6 from IPython.terminal.pt_inputhooks import set_qt_api, get_inputhook_name_and_func
7
7
8
8
9 guis_avail = []
9 guis_avail = []
10
10
11
11
12 def _get_qt_vers():
12 def _get_qt_vers():
13 """If any version of Qt is available, this will populate `guis_avail` with 'qt' and 'qtx'. Due
13 """If any version of Qt is available, this will populate `guis_avail` with 'qt' and 'qtx'. Due
14 to the import mechanism, we can't import multiple versions of Qt in one session."""
14 to the import mechanism, we can't import multiple versions of Qt in one session."""
15 for gui in ["qt", "qt6", "qt5"]:
15 for gui in ["qt", "qt6", "qt5"]:
16 print(f"Trying {gui}")
16 print(f"Trying {gui}")
17 try:
17 try:
18 set_qt_api(gui)
18 set_qt_api(gui)
19 importlib.import_module("IPython.terminal.pt_inputhooks.qt")
19 importlib.import_module("IPython.terminal.pt_inputhooks.qt")
20 guis_avail.append(gui)
20 guis_avail.append(gui)
21 if "QT_API" in os.environ.keys():
21 if "QT_API" in os.environ.keys():
22 del os.environ["QT_API"]
22 del os.environ["QT_API"]
23 except ImportError:
23 except ImportError:
24 pass # that version of Qt isn't available.
24 pass # that version of Qt isn't available.
25 except RuntimeError:
25 except RuntimeError:
26 pass # the version of IPython doesn't know what to do with this Qt version.
26 pass # the version of IPython doesn't know what to do with this Qt version.
27
27
28
28
29 _get_qt_vers()
29 _get_qt_vers()
30
30
31
31
32 @pytest.mark.skipif(
32 @pytest.mark.skipif(
33 len(guis_avail) == 0, reason="No viable version of PyQt or PySide installed."
33 len(guis_avail) == 0, reason="No viable version of PyQt or PySide installed."
34 )
34 )
35 def test_inputhook_qt():
35 def test_inputhook_qt():
36 gui = guis_avail[0]
36 # Choose the "best" Qt version.
37
37 gui_ret, _ = get_inputhook_name_and_func("qt")
38 # Choose a qt version and get the input hook function. This will import Qt...
38
39 get_inputhook_name_and_func(gui)
39 assert gui_ret != "qt" # you get back the specific version that was loaded.
40
40 assert gui_ret in guis_avail
41 # ...and now we're stuck with this version of Qt for good; can't switch.
41
42 for not_gui in ["qt6", "qt5"]:
42 if len(guis_avail) > 2:
43 if not_gui not in guis_avail:
43 # ...and now we're stuck with this version of Qt for good; can't switch.
44 break
44 for not_gui in ["qt6", "qt5"]:
45
45 if not_gui != gui_ret:
46 with pytest.raises(ImportError):
46 break
47 get_inputhook_name_and_func(not_gui)
47 # Try to import the other gui; it won't work.
48
48 gui_ret2, _ = get_inputhook_name_and_func(not_gui)
49 # A gui of 'qt' means "best available", or in this case, the last one that was used.
49 assert gui_ret2 == gui_ret
50 get_inputhook_name_and_func("qt")
50 assert gui_ret2 != not_gui
General Comments 0
You need to be logged in to leave comments. Login now