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