##// END OF EJS Templates
Added --no-banner arg to qtconsole...
Helen ST -
Show More
@@ -1,807 +1,808
1 1 """Frontend widget for the Qt Console"""
2 2
3 3 # Copyright (c) IPython Development Team.
4 4 # Distributed under the terms of the Modified BSD License.
5 5
6 6 from __future__ import print_function
7 7
8 8 from collections import namedtuple
9 9 import sys
10 10 import uuid
11 11
12 12 from IPython.external import qt
13 13 from IPython.external.qt import QtCore, QtGui
14 14 from IPython.utils import py3compat
15 15 from IPython.utils.importstring import import_item
16 16
17 17 from IPython.core.inputsplitter import InputSplitter, IPythonInputSplitter
18 18 from IPython.core.inputtransformer import classic_prompt
19 19 from IPython.core.oinspect import call_tip
20 20 from IPython.qt.base_frontend_mixin import BaseFrontendMixin
21 21 from IPython.utils.traitlets import Any, Bool, Instance, Unicode, DottedObjectName
22 22 from .bracket_matcher import BracketMatcher
23 23 from .call_tip_widget import CallTipWidget
24 24 from .history_console_widget import HistoryConsoleWidget
25 25 from .pygments_highlighter import PygmentsHighlighter
26 26
27 27
28 28 class FrontendHighlighter(PygmentsHighlighter):
29 29 """ A PygmentsHighlighter that understands and ignores prompts.
30 30 """
31 31
32 32 def __init__(self, frontend, lexer=None):
33 33 super(FrontendHighlighter, self).__init__(frontend._control.document(), lexer=lexer)
34 34 self._current_offset = 0
35 35 self._frontend = frontend
36 36 self.highlighting_on = False
37 37
38 38 def highlightBlock(self, string):
39 39 """ Highlight a block of text. Reimplemented to highlight selectively.
40 40 """
41 41 if not self.highlighting_on:
42 42 return
43 43
44 44 # The input to this function is a unicode string that may contain
45 45 # paragraph break characters, non-breaking spaces, etc. Here we acquire
46 46 # the string as plain text so we can compare it.
47 47 current_block = self.currentBlock()
48 48 string = self._frontend._get_block_plain_text(current_block)
49 49
50 50 # Decide whether to check for the regular or continuation prompt.
51 51 if current_block.contains(self._frontend._prompt_pos):
52 52 prompt = self._frontend._prompt
53 53 else:
54 54 prompt = self._frontend._continuation_prompt
55 55
56 56 # Only highlight if we can identify a prompt, but make sure not to
57 57 # highlight the prompt.
58 58 if string.startswith(prompt):
59 59 self._current_offset = len(prompt)
60 60 string = string[len(prompt):]
61 61 super(FrontendHighlighter, self).highlightBlock(string)
62 62
63 63 def rehighlightBlock(self, block):
64 64 """ Reimplemented to temporarily enable highlighting if disabled.
65 65 """
66 66 old = self.highlighting_on
67 67 self.highlighting_on = True
68 68 super(FrontendHighlighter, self).rehighlightBlock(block)
69 69 self.highlighting_on = old
70 70
71 71 def setFormat(self, start, count, format):
72 72 """ Reimplemented to highlight selectively.
73 73 """
74 74 start += self._current_offset
75 75 super(FrontendHighlighter, self).setFormat(start, count, format)
76 76
77 77
78 78 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
79 79 """ A Qt frontend for a generic Python kernel.
80 80 """
81 81
82 82 # The text to show when the kernel is (re)started.
83 83 banner = Unicode(config=True)
84 84 kernel_banner = Unicode()
85 85
86 86 # An option and corresponding signal for overriding the default kernel
87 87 # interrupt behavior.
88 88 custom_interrupt = Bool(False)
89 89 custom_interrupt_requested = QtCore.Signal()
90 90
91 91 # An option and corresponding signals for overriding the default kernel
92 92 # restart behavior.
93 93 custom_restart = Bool(False)
94 94 custom_restart_kernel_died = QtCore.Signal(float)
95 95 custom_restart_requested = QtCore.Signal()
96 96
97 97 # Whether to automatically show calltips on open-parentheses.
98 98 enable_calltips = Bool(True, config=True,
99 99 help="Whether to draw information calltips on open-parentheses.")
100 100
101 101 clear_on_kernel_restart = Bool(True, config=True,
102 102 help="Whether to clear the console when the kernel is restarted")
103 103
104 104 confirm_restart = Bool(True, config=True,
105 105 help="Whether to ask for user confirmation when restarting kernel")
106 106
107 107 lexer_class = DottedObjectName(config=True,
108 108 help="The pygments lexer class to use."
109 109 )
110 110 def _lexer_class_changed(self, name, old, new):
111 111 lexer_class = import_item(new)
112 112 self.lexer = lexer_class()
113 113
114 114 def _lexer_class_default(self):
115 115 if py3compat.PY3:
116 116 return 'pygments.lexers.Python3Lexer'
117 117 else:
118 118 return 'pygments.lexers.PythonLexer'
119 119
120 120 lexer = Any()
121 121 def _lexer_default(self):
122 122 lexer_class = import_item(self.lexer_class)
123 123 return lexer_class()
124 124
125 125 # Emitted when a user visible 'execute_request' has been submitted to the
126 126 # kernel from the FrontendWidget. Contains the code to be executed.
127 127 executing = QtCore.Signal(object)
128 128
129 129 # Emitted when a user-visible 'execute_reply' has been received from the
130 130 # kernel and processed by the FrontendWidget. Contains the response message.
131 131 executed = QtCore.Signal(object)
132 132
133 133 # Emitted when an exit request has been received from the kernel.
134 134 exit_requested = QtCore.Signal(object)
135 135
136 136 # Protected class variables.
137 137 _prompt_transformer = IPythonInputSplitter(physical_line_transforms=[classic_prompt()],
138 138 logical_line_transforms=[],
139 139 python_line_transforms=[],
140 140 )
141 141 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
142 142 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
143 143 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
144 144 _input_splitter_class = InputSplitter
145 145 _local_kernel = False
146 146 _highlighter = Instance(FrontendHighlighter)
147 147
148 148 #---------------------------------------------------------------------------
149 149 # 'object' interface
150 150 #---------------------------------------------------------------------------
151 151
152 152 def __init__(self, *args, **kw):
153 153 super(FrontendWidget, self).__init__(*args, **kw)
154 154 # FIXME: remove this when PySide min version is updated past 1.0.7
155 155 # forcefully disable calltips if PySide is < 1.0.7, because they crash
156 156 if qt.QT_API == qt.QT_API_PYSIDE:
157 157 import PySide
158 158 if PySide.__version_info__ < (1,0,7):
159 159 self.log.warn("PySide %s < 1.0.7 detected, disabling calltips" % PySide.__version__)
160 160 self.enable_calltips = False
161 161
162 162 # FrontendWidget protected variables.
163 163 self._bracket_matcher = BracketMatcher(self._control)
164 164 self._call_tip_widget = CallTipWidget(self._control)
165 165 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
166 166 self._hidden = False
167 167 self._highlighter = FrontendHighlighter(self, lexer=self.lexer)
168 168 self._input_splitter = self._input_splitter_class()
169 169 self._kernel_manager = None
170 170 self._kernel_client = None
171 171 self._request_info = {}
172 172 self._request_info['execute'] = {};
173 173 self._callback_dict = {}
174 174
175 175 # Configure the ConsoleWidget.
176 176 self.tab_width = 4
177 177 self._set_continuation_prompt('... ')
178 178
179 179 # Configure the CallTipWidget.
180 180 self._call_tip_widget.setFont(self.font)
181 181 self.font_changed.connect(self._call_tip_widget.setFont)
182 182
183 183 # Configure actions.
184 184 action = self._copy_raw_action
185 185 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
186 186 action.setEnabled(False)
187 187 action.setShortcut(QtGui.QKeySequence(key))
188 188 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
189 189 action.triggered.connect(self.copy_raw)
190 190 self.copy_available.connect(action.setEnabled)
191 191 self.addAction(action)
192 192
193 193 # Connect signal handlers.
194 194 document = self._control.document()
195 195 document.contentsChange.connect(self._document_contents_change)
196 196
197 197 # Set flag for whether we are connected via localhost.
198 198 self._local_kernel = kw.get('local_kernel',
199 199 FrontendWidget._local_kernel)
200 200
201 201 # Whether or not a clear_output call is pending new output.
202 202 self._pending_clearoutput = False
203 203
204 204 #---------------------------------------------------------------------------
205 205 # 'ConsoleWidget' public interface
206 206 #---------------------------------------------------------------------------
207 207
208 208 def copy(self):
209 209 """ Copy the currently selected text to the clipboard, removing prompts.
210 210 """
211 211 if self._page_control is not None and self._page_control.hasFocus():
212 212 self._page_control.copy()
213 213 elif self._control.hasFocus():
214 214 text = self._control.textCursor().selection().toPlainText()
215 215 if text:
216 216 was_newline = text[-1] == '\n'
217 217 text = self._prompt_transformer.transform_cell(text)
218 218 if not was_newline: # user doesn't need newline
219 219 text = text[:-1]
220 220 QtGui.QApplication.clipboard().setText(text)
221 221 else:
222 222 self.log.debug("frontend widget : unknown copy target")
223 223
224 224 #---------------------------------------------------------------------------
225 225 # 'ConsoleWidget' abstract interface
226 226 #---------------------------------------------------------------------------
227 227
228 228 def _is_complete(self, source, interactive):
229 229 """ Returns whether 'source' can be completely processed and a new
230 230 prompt created. When triggered by an Enter/Return key press,
231 231 'interactive' is True; otherwise, it is False.
232 232 """
233 233 self._input_splitter.reset()
234 234 try:
235 235 complete = self._input_splitter.push(source)
236 236 except SyntaxError:
237 237 return True
238 238 if interactive:
239 239 complete = not self._input_splitter.push_accepts_more()
240 240 return complete
241 241
242 242 def _execute(self, source, hidden):
243 243 """ Execute 'source'. If 'hidden', do not show any output.
244 244
245 245 See parent class :meth:`execute` docstring for full details.
246 246 """
247 247 msg_id = self.kernel_client.execute(source, hidden)
248 248 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'user')
249 249 self._hidden = hidden
250 250 if not hidden:
251 251 self.executing.emit(source)
252 252
253 253 def _prompt_started_hook(self):
254 254 """ Called immediately after a new prompt is displayed.
255 255 """
256 256 if not self._reading:
257 257 self._highlighter.highlighting_on = True
258 258
259 259 def _prompt_finished_hook(self):
260 260 """ Called immediately after a prompt is finished, i.e. when some input
261 261 will be processed and a new prompt displayed.
262 262 """
263 263 # Flush all state from the input splitter so the next round of
264 264 # reading input starts with a clean buffer.
265 265 self._input_splitter.reset()
266 266
267 267 if not self._reading:
268 268 self._highlighter.highlighting_on = False
269 269
270 270 def _tab_pressed(self):
271 271 """ Called when the tab key is pressed. Returns whether to continue
272 272 processing the event.
273 273 """
274 274 # Perform tab completion if:
275 275 # 1) The cursor is in the input buffer.
276 276 # 2) There is a non-whitespace character before the cursor.
277 277 text = self._get_input_buffer_cursor_line()
278 278 if text is None:
279 279 return False
280 280 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
281 281 if complete:
282 282 self._complete()
283 283 return not complete
284 284
285 285 #---------------------------------------------------------------------------
286 286 # 'ConsoleWidget' protected interface
287 287 #---------------------------------------------------------------------------
288 288
289 289 def _context_menu_make(self, pos):
290 290 """ Reimplemented to add an action for raw copy.
291 291 """
292 292 menu = super(FrontendWidget, self)._context_menu_make(pos)
293 293 for before_action in menu.actions():
294 294 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
295 295 QtGui.QKeySequence.ExactMatch:
296 296 menu.insertAction(before_action, self._copy_raw_action)
297 297 break
298 298 return menu
299 299
300 300 def request_interrupt_kernel(self):
301 301 if self._executing:
302 302 self.interrupt_kernel()
303 303
304 304 def request_restart_kernel(self):
305 305 message = 'Are you sure you want to restart the kernel?'
306 306 self.restart_kernel(message, now=False)
307 307
308 308 def _event_filter_console_keypress(self, event):
309 309 """ Reimplemented for execution interruption and smart backspace.
310 310 """
311 311 key = event.key()
312 312 if self._control_key_down(event.modifiers(), include_command=False):
313 313
314 314 if key == QtCore.Qt.Key_C and self._executing:
315 315 self.request_interrupt_kernel()
316 316 return True
317 317
318 318 elif key == QtCore.Qt.Key_Period:
319 319 self.request_restart_kernel()
320 320 return True
321 321
322 322 elif not event.modifiers() & QtCore.Qt.AltModifier:
323 323
324 324 # Smart backspace: remove four characters in one backspace if:
325 325 # 1) everything left of the cursor is whitespace
326 326 # 2) the four characters immediately left of the cursor are spaces
327 327 if key == QtCore.Qt.Key_Backspace:
328 328 col = self._get_input_buffer_cursor_column()
329 329 cursor = self._control.textCursor()
330 330 if col > 3 and not cursor.hasSelection():
331 331 text = self._get_input_buffer_cursor_line()[:col]
332 332 if text.endswith(' ') and not text.strip():
333 333 cursor.movePosition(QtGui.QTextCursor.Left,
334 334 QtGui.QTextCursor.KeepAnchor, 4)
335 335 cursor.removeSelectedText()
336 336 return True
337 337
338 338 return super(FrontendWidget, self)._event_filter_console_keypress(event)
339 339
340 340 def _insert_continuation_prompt(self, cursor):
341 341 """ Reimplemented for auto-indentation.
342 342 """
343 343 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
344 344 cursor.insertText(' ' * self._input_splitter.indent_spaces)
345 345
346 346 #---------------------------------------------------------------------------
347 347 # 'BaseFrontendMixin' abstract interface
348 348 #---------------------------------------------------------------------------
349 349 def _handle_clear_output(self, msg):
350 350 """Handle clear output messages."""
351 351 if include_output(msg):
352 352 wait = msg['content'].get('wait', True)
353 353 if wait:
354 354 self._pending_clearoutput = True
355 355 else:
356 356 self.clear_output()
357 357
358 358 def _silent_exec_callback(self, expr, callback):
359 359 """Silently execute `expr` in the kernel and call `callback` with reply
360 360
361 361 the `expr` is evaluated silently in the kernel (without) output in
362 362 the frontend. Call `callback` with the
363 363 `repr <http://docs.python.org/library/functions.html#repr> `_ as first argument
364 364
365 365 Parameters
366 366 ----------
367 367 expr : string
368 368 valid string to be executed by the kernel.
369 369 callback : function
370 370 function accepting one argument, as a string. The string will be
371 371 the `repr` of the result of evaluating `expr`
372 372
373 373 The `callback` is called with the `repr()` of the result of `expr` as
374 374 first argument. To get the object, do `eval()` on the passed value.
375 375
376 376 See Also
377 377 --------
378 378 _handle_exec_callback : private method, deal with calling callback with reply
379 379
380 380 """
381 381
382 382 # generate uuid, which would be used as an indication of whether or
383 383 # not the unique request originated from here (can use msg id ?)
384 384 local_uuid = str(uuid.uuid1())
385 385 msg_id = self.kernel_client.execute('',
386 386 silent=True, user_expressions={ local_uuid:expr })
387 387 self._callback_dict[local_uuid] = callback
388 388 self._request_info['execute'][msg_id] = self._ExecutionRequest(msg_id, 'silent_exec_callback')
389 389
390 390 def _handle_exec_callback(self, msg):
391 391 """Execute `callback` corresponding to `msg` reply, after ``_silent_exec_callback``
392 392
393 393 Parameters
394 394 ----------
395 395 msg : raw message send by the kernel containing an `user_expressions`
396 396 and having a 'silent_exec_callback' kind.
397 397
398 398 Notes
399 399 -----
400 400 This function will look for a `callback` associated with the
401 401 corresponding message id. Association has been made by
402 402 `_silent_exec_callback`. `callback` is then called with the `repr()`
403 403 of the value of corresponding `user_expressions` as argument.
404 404 `callback` is then removed from the known list so that any message
405 405 coming again with the same id won't trigger it.
406 406
407 407 """
408 408
409 409 user_exp = msg['content'].get('user_expressions')
410 410 if not user_exp:
411 411 return
412 412 for expression in user_exp:
413 413 if expression in self._callback_dict:
414 414 self._callback_dict.pop(expression)(user_exp[expression])
415 415
416 416 def _handle_execute_reply(self, msg):
417 417 """ Handles replies for code execution.
418 418 """
419 419 self.log.debug("execute: %s", msg.get('content', ''))
420 420 msg_id = msg['parent_header']['msg_id']
421 421 info = self._request_info['execute'].get(msg_id)
422 422 # unset reading flag, because if execute finished, raw_input can't
423 423 # still be pending.
424 424 self._reading = False
425 425 if info and info.kind == 'user' and not self._hidden:
426 426 # Make sure that all output from the SUB channel has been processed
427 427 # before writing a new prompt.
428 428 self.kernel_client.iopub_channel.flush()
429 429
430 430 # Reset the ANSI style information to prevent bad text in stdout
431 431 # from messing up our colors. We're not a true terminal so we're
432 432 # allowed to do this.
433 433 if self.ansi_codes:
434 434 self._ansi_processor.reset_sgr()
435 435
436 436 content = msg['content']
437 437 status = content['status']
438 438 if status == 'ok':
439 439 self._process_execute_ok(msg)
440 440 elif status == 'error':
441 441 self._process_execute_error(msg)
442 442 elif status == 'aborted':
443 443 self._process_execute_abort(msg)
444 444
445 445 self._show_interpreter_prompt_for_reply(msg)
446 446 self.executed.emit(msg)
447 447 self._request_info['execute'].pop(msg_id)
448 448 elif info and info.kind == 'silent_exec_callback' and not self._hidden:
449 449 self._handle_exec_callback(msg)
450 450 self._request_info['execute'].pop(msg_id)
451 451 else:
452 452 super(FrontendWidget, self)._handle_execute_reply(msg)
453 453
454 454 def _handle_input_request(self, msg):
455 455 """ Handle requests for raw_input.
456 456 """
457 457 self.log.debug("input: %s", msg.get('content', ''))
458 458 if self._hidden:
459 459 raise RuntimeError('Request for raw input during hidden execution.')
460 460
461 461 # Make sure that all output from the SUB channel has been processed
462 462 # before entering readline mode.
463 463 self.kernel_client.iopub_channel.flush()
464 464
465 465 def callback(line):
466 466 self.kernel_client.stdin_channel.input(line)
467 467 if self._reading:
468 468 self.log.debug("Got second input request, assuming first was interrupted.")
469 469 self._reading = False
470 470 self._readline(msg['content']['prompt'], callback=callback)
471 471
472 472 def _kernel_restarted_message(self, died=True):
473 473 msg = "Kernel died, restarting" if died else "Kernel restarting"
474 474 self._append_html("<br>%s<hr><br>" % msg,
475 475 before_prompt=False
476 476 )
477 477
478 478 def _handle_kernel_died(self, since_last_heartbeat):
479 479 """Handle the kernel's death (if we do not own the kernel).
480 480 """
481 481 self.log.warn("kernel died: %s", since_last_heartbeat)
482 482 if self.custom_restart:
483 483 self.custom_restart_kernel_died.emit(since_last_heartbeat)
484 484 else:
485 485 self._kernel_restarted_message(died=True)
486 486 self.reset()
487 487
488 488 def _handle_kernel_restarted(self, died=True):
489 489 """Notice that the autorestarter restarted the kernel.
490 490
491 491 There's nothing to do but show a message.
492 492 """
493 493 self.log.warn("kernel restarted")
494 494 self._kernel_restarted_message(died=died)
495 495 self.reset()
496 496
497 497 def _handle_inspect_reply(self, rep):
498 498 """Handle replies for call tips."""
499 499 self.log.debug("oinfo: %s", rep.get('content', ''))
500 500 cursor = self._get_cursor()
501 501 info = self._request_info.get('call_tip')
502 502 if info and info.id == rep['parent_header']['msg_id'] and \
503 503 info.pos == cursor.position():
504 504 content = rep['content']
505 505 if content.get('status') == 'ok' and content.get('found', False):
506 506 self._call_tip_widget.show_inspect_data(content)
507 507
508 508 def _handle_execute_result(self, msg):
509 509 """ Handle display hook output.
510 510 """
511 511 self.log.debug("execute_result: %s", msg.get('content', ''))
512 512 if self.include_output(msg):
513 513 self.flush_clearoutput()
514 514 text = msg['content']['data']
515 515 self._append_plain_text(text + '\n', before_prompt=True)
516 516
517 517 def _handle_stream(self, msg):
518 518 """ Handle stdout, stderr, and stdin.
519 519 """
520 520 self.log.debug("stream: %s", msg.get('content', ''))
521 521 if self.include_output(msg):
522 522 self.flush_clearoutput()
523 523 self.append_stream(msg['content']['text'])
524 524
525 525 def _handle_shutdown_reply(self, msg):
526 526 """ Handle shutdown signal, only if from other console.
527 527 """
528 528 self.log.info("shutdown: %s", msg.get('content', ''))
529 529 restart = msg.get('content', {}).get('restart', False)
530 530 if not self._hidden and not self.from_here(msg):
531 531 # got shutdown reply, request came from session other than ours
532 532 if restart:
533 533 # someone restarted the kernel, handle it
534 534 self._handle_kernel_restarted(died=False)
535 535 else:
536 536 # kernel was shutdown permanently
537 537 # this triggers exit_requested if the kernel was local,
538 538 # and a dialog if the kernel was remote,
539 539 # so we don't suddenly clear the qtconsole without asking.
540 540 if self._local_kernel:
541 541 self.exit_requested.emit(self)
542 542 else:
543 543 title = self.window().windowTitle()
544 544 reply = QtGui.QMessageBox.question(self, title,
545 545 "Kernel has been shutdown permanently. "
546 546 "Close the Console?",
547 547 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
548 548 if reply == QtGui.QMessageBox.Yes:
549 549 self.exit_requested.emit(self)
550 550
551 551 def _handle_status(self, msg):
552 552 """Handle status message"""
553 553 # This is where a busy/idle indicator would be triggered,
554 554 # when we make one.
555 555 state = msg['content'].get('execution_state', '')
556 556 if state == 'starting':
557 557 # kernel started while we were running
558 558 if self._executing:
559 559 self._handle_kernel_restarted(died=True)
560 560 elif state == 'idle':
561 561 pass
562 562 elif state == 'busy':
563 563 pass
564 564
565 565 def _started_channels(self):
566 566 """ Called when the KernelManager channels have started listening or
567 567 when the frontend is assigned an already listening KernelManager.
568 568 """
569 569 self.reset(clear=True)
570 570
571 571 #---------------------------------------------------------------------------
572 572 # 'FrontendWidget' public interface
573 573 #---------------------------------------------------------------------------
574 574
575 575 def copy_raw(self):
576 576 """ Copy the currently selected text to the clipboard without attempting
577 577 to remove prompts or otherwise alter the text.
578 578 """
579 579 self._control.copy()
580 580
581 581 def execute_file(self, path, hidden=False):
582 582 """ Attempts to execute file with 'path'. If 'hidden', no output is
583 583 shown.
584 584 """
585 585 self.execute('execfile(%r)' % path, hidden=hidden)
586 586
587 587 def interrupt_kernel(self):
588 588 """ Attempts to interrupt the running kernel.
589 589
590 590 Also unsets _reading flag, to avoid runtime errors
591 591 if raw_input is called again.
592 592 """
593 593 if self.custom_interrupt:
594 594 self._reading = False
595 595 self.custom_interrupt_requested.emit()
596 596 elif self.kernel_manager:
597 597 self._reading = False
598 598 self.kernel_manager.interrupt_kernel()
599 599 else:
600 600 self._append_plain_text('Cannot interrupt a kernel I did not start.\n')
601 601
602 602 def reset(self, clear=False):
603 603 """ Resets the widget to its initial state if ``clear`` parameter
604 604 is True, otherwise
605 605 prints a visual indication of the fact that the kernel restarted, but
606 606 does not clear the traces from previous usage of the kernel before it
607 607 was restarted. With ``clear=True``, it is similar to ``%clear``, but
608 608 also re-writes the banner and aborts execution if necessary.
609 609 """
610 610 if self._executing:
611 611 self._executing = False
612 612 self._request_info['execute'] = {}
613 613 self._reading = False
614 614 self._highlighter.highlighting_on = False
615 615
616 616 if clear:
617 617 self._control.clear()
618 self._append_plain_text(self.banner)
619 if self.kernel_banner:
620 self._append_plain_text(self.kernel_banner)
621
618 if self._display_banner:
619 self._append_plain_text(self.banner)
620 if self.kernel_banner:
621 self._append_plain_text(self.kernel_banner)
622
622 623 # update output marker for stdout/stderr, so that startup
623 624 # messages appear after banner:
624 625 self._append_before_prompt_pos = self._get_cursor().position()
625 626 self._show_interpreter_prompt()
626 627
627 628 def restart_kernel(self, message, now=False):
628 629 """ Attempts to restart the running kernel.
629 630 """
630 631 # FIXME: now should be configurable via a checkbox in the dialog. Right
631 632 # now at least the heartbeat path sets it to True and the manual restart
632 633 # to False. But those should just be the pre-selected states of a
633 634 # checkbox that the user could override if so desired. But I don't know
634 635 # enough Qt to go implementing the checkbox now.
635 636
636 637 if self.custom_restart:
637 638 self.custom_restart_requested.emit()
638 639 return
639 640
640 641 if self.kernel_manager:
641 642 # Pause the heart beat channel to prevent further warnings.
642 643 self.kernel_client.hb_channel.pause()
643 644
644 645 # Prompt the user to restart the kernel. Un-pause the heartbeat if
645 646 # they decline. (If they accept, the heartbeat will be un-paused
646 647 # automatically when the kernel is restarted.)
647 648 if self.confirm_restart:
648 649 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
649 650 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
650 651 message, buttons)
651 652 do_restart = result == QtGui.QMessageBox.Yes
652 653 else:
653 654 # confirm_restart is False, so we don't need to ask user
654 655 # anything, just do the restart
655 656 do_restart = True
656 657 if do_restart:
657 658 try:
658 659 self.kernel_manager.restart_kernel(now=now)
659 660 except RuntimeError as e:
660 661 self._append_plain_text(
661 662 'Error restarting kernel: %s\n' % e,
662 663 before_prompt=True
663 664 )
664 665 else:
665 666 self._append_html("<br>Restarting kernel...\n<hr><br>",
666 667 before_prompt=True,
667 668 )
668 669 else:
669 670 self.kernel_client.hb_channel.unpause()
670 671
671 672 else:
672 673 self._append_plain_text(
673 674 'Cannot restart a Kernel I did not start\n',
674 675 before_prompt=True
675 676 )
676 677
677 678 def append_stream(self, text):
678 679 """Appends text to the output stream."""
679 680 # Most consoles treat tabs as being 8 space characters. Convert tabs
680 681 # to spaces so that output looks as expected regardless of this
681 682 # widget's tab width.
682 683 text = text.expandtabs(8)
683 684 self._append_plain_text(text, before_prompt=True)
684 685 self._control.moveCursor(QtGui.QTextCursor.End)
685 686
686 687 def flush_clearoutput(self):
687 688 """If a clearoutput is pending, execute it."""
688 689 if self._pending_clearoutput:
689 690 self._pending_clearoutput = False
690 691 self.clear_output()
691 692
692 693 def clear_output(self):
693 694 """Clears the current line of output."""
694 695 cursor = self._control.textCursor()
695 696 cursor.beginEditBlock()
696 697 cursor.movePosition(cursor.StartOfLine, cursor.KeepAnchor)
697 698 cursor.insertText('')
698 699 cursor.endEditBlock()
699 700
700 701 #---------------------------------------------------------------------------
701 702 # 'FrontendWidget' protected interface
702 703 #---------------------------------------------------------------------------
703 704
704 705 def _auto_call_tip(self):
705 706 """Trigger call tip automatically on open parenthesis
706 707
707 708 Call tips can be requested explcitly with `_call_tip`.
708 709 """
709 710 cursor = self._get_cursor()
710 711 cursor.movePosition(QtGui.QTextCursor.Left)
711 712 if cursor.document().characterAt(cursor.position()) == '(':
712 713 # trigger auto call tip on open paren
713 714 self._call_tip()
714 715
715 716 def _call_tip(self):
716 717 """Shows a call tip, if appropriate, at the current cursor location."""
717 718 # Decide if it makes sense to show a call tip
718 719 if not self.enable_calltips or not self.kernel_client.shell_channel.is_alive():
719 720 return False
720 721 cursor_pos = self._get_input_buffer_cursor_pos()
721 722 code = self.input_buffer
722 723 # Send the metadata request to the kernel
723 724 msg_id = self.kernel_client.inspect(code, cursor_pos)
724 725 pos = self._get_cursor().position()
725 726 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
726 727 return True
727 728
728 729 def _complete(self):
729 730 """ Performs completion at the current cursor location.
730 731 """
731 732 # Send the completion request to the kernel
732 733 msg_id = self.kernel_client.complete(
733 734 code=self.input_buffer,
734 735 cursor_pos=self._get_input_buffer_cursor_pos(),
735 736 )
736 737 pos = self._get_cursor().position()
737 738 info = self._CompletionRequest(msg_id, pos)
738 739 self._request_info['complete'] = info
739 740
740 741 def _process_execute_abort(self, msg):
741 742 """ Process a reply for an aborted execution request.
742 743 """
743 744 self._append_plain_text("ERROR: execution aborted\n")
744 745
745 746 def _process_execute_error(self, msg):
746 747 """ Process a reply for an execution request that resulted in an error.
747 748 """
748 749 content = msg['content']
749 750 # If a SystemExit is passed along, this means exit() was called - also
750 751 # all the ipython %exit magic syntax of '-k' to be used to keep
751 752 # the kernel running
752 753 if content['ename']=='SystemExit':
753 754 keepkernel = content['evalue']=='-k' or content['evalue']=='True'
754 755 self._keep_kernel_on_exit = keepkernel
755 756 self.exit_requested.emit(self)
756 757 else:
757 758 traceback = ''.join(content['traceback'])
758 759 self._append_plain_text(traceback)
759 760
760 761 def _process_execute_ok(self, msg):
761 762 """ Process a reply for a successful execution request.
762 763 """
763 764 payload = msg['content']['payload']
764 765 for item in payload:
765 766 if not self._process_execute_payload(item):
766 767 warning = 'Warning: received unknown payload of type %s'
767 768 print(warning % repr(item['source']))
768 769
769 770 def _process_execute_payload(self, item):
770 771 """ Process a single payload item from the list of payload items in an
771 772 execution reply. Returns whether the payload was handled.
772 773 """
773 774 # The basic FrontendWidget doesn't handle payloads, as they are a
774 775 # mechanism for going beyond the standard Python interpreter model.
775 776 return False
776 777
777 778 def _show_interpreter_prompt(self):
778 779 """ Shows a prompt for the interpreter.
779 780 """
780 781 self._show_prompt('>>> ')
781 782
782 783 def _show_interpreter_prompt_for_reply(self, msg):
783 784 """ Shows a prompt for the interpreter given an 'execute_reply' message.
784 785 """
785 786 self._show_interpreter_prompt()
786 787
787 788 #------ Signal handlers ----------------------------------------------------
788 789
789 790 def _document_contents_change(self, position, removed, added):
790 791 """ Called whenever the document's content changes. Display a call tip
791 792 if appropriate.
792 793 """
793 794 # Calculate where the cursor should be *after* the change:
794 795 position += added
795 796
796 797 document = self._control.document()
797 798 if position == self._get_cursor().position():
798 799 self._auto_call_tip()
799 800
800 801 #------ Trait default initializers -----------------------------------------
801 802
802 803 def _banner_default(self):
803 804 """ Returns the standard Python banner.
804 805 """
805 806 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
806 807 '"license" for more information.'
807 808 return banner % (sys.version, sys.platform)
@@ -1,373 +1,384
1 1 """ A minimal application using the Qt console-style IPython frontend.
2 2
3 3 This is not a complete console app, as subprocess will not be able to receive
4 4 input, there is no real readline support, among other limitations.
5 5 """
6 6
7 7 # Copyright (c) IPython Development Team.
8 8 # Distributed under the terms of the Modified BSD License.
9 9
10 10 #-----------------------------------------------------------------------------
11 11 # Imports
12 12 #-----------------------------------------------------------------------------
13 13
14 14 # stdlib imports
15 15 import os
16 16 import signal
17 17 import sys
18 18
19 19 # If run on Windows, install an exception hook which pops up a
20 20 # message box. Pythonw.exe hides the console, so without this
21 21 # the application silently fails to load.
22 22 #
23 23 # We always install this handler, because the expectation is for
24 24 # qtconsole to bring up a GUI even if called from the console.
25 25 # The old handler is called, so the exception is printed as well.
26 26 # If desired, check for pythonw with an additional condition
27 27 # (sys.executable.lower().find('pythonw.exe') >= 0).
28 28 if os.name == 'nt':
29 29 old_excepthook = sys.excepthook
30 30
31 31 # Exclude this from our autogenerated API docs.
32 32 undoc = lambda func: func
33 33
34 34 @undoc
35 35 def gui_excepthook(exctype, value, tb):
36 36 try:
37 37 import ctypes, traceback
38 38 MB_ICONERROR = 0x00000010
39 39 title = u'Error starting IPython QtConsole'
40 40 msg = u''.join(traceback.format_exception(exctype, value, tb))
41 41 ctypes.windll.user32.MessageBoxW(0, msg, title, MB_ICONERROR)
42 42 finally:
43 43 # Also call the old exception hook to let it do
44 44 # its thing too.
45 45 old_excepthook(exctype, value, tb)
46 46
47 47 sys.excepthook = gui_excepthook
48 48
49 49 # System library imports
50 50 from IPython.external.qt import QtCore, QtGui
51 51
52 52 # Local imports
53 from IPython.config.application import boolean_flag
53 54 from IPython.config.application import catch_config_error
54 55 from IPython.core.application import BaseIPythonApplication
55 56 from IPython.qt.console.ipython_widget import IPythonWidget
56 57 from IPython.qt.console.rich_ipython_widget import RichIPythonWidget
57 58 from IPython.qt.console import styles
58 59 from IPython.qt.console.mainwindow import MainWindow
59 60 from IPython.qt.client import QtKernelClient
60 61 from IPython.qt.manager import QtKernelManager
61 62 from IPython.utils.traitlets import (
62 63 Dict, Unicode, CBool, Any
63 64 )
64 65
65 66 from IPython.consoleapp import (
66 67 IPythonConsoleApp, app_aliases, app_flags, flags, aliases
67 68 )
68 69
69 70 #-----------------------------------------------------------------------------
70 71 # Network Constants
71 72 #-----------------------------------------------------------------------------
72 73
73 74 from IPython.utils.localinterfaces import is_local_ip
74 75
75 76 #-----------------------------------------------------------------------------
76 77 # Globals
77 78 #-----------------------------------------------------------------------------
78 79
79 80 _examples = """
80 81 ipython qtconsole # start the qtconsole
81 82 ipython qtconsole --matplotlib=inline # start with matplotlib inline plotting mode
82 83 """
83 84
84 85 #-----------------------------------------------------------------------------
85 86 # Aliases and Flags
86 87 #-----------------------------------------------------------------------------
87 88
88 89 # start with copy of flags
89 90 flags = dict(flags)
90 91 qt_flags = {
91 92 'plain' : ({'IPythonQtConsoleApp' : {'plain' : True}},
92 93 "Disable rich text support."),
93 94 }
95 qt_flags.update(boolean_flag(
96 'banner', 'IPythonQtConsoleApp.display_banner',
97 "Display a banner upon starting IPython.",
98 "Don't display a banner upon starting IPython."
99 ))
94 100
95 101 # and app_flags from the Console Mixin
96 102 qt_flags.update(app_flags)
97 103 # add frontend flags to the full set
98 104 flags.update(qt_flags)
99 105
100 106 # start with copy of front&backend aliases list
101 107 aliases = dict(aliases)
102 108 qt_aliases = dict(
103 109 style = 'IPythonWidget.syntax_style',
104 110 stylesheet = 'IPythonQtConsoleApp.stylesheet',
105 111 colors = 'ZMQInteractiveShell.colors',
106 112
107 113 editor = 'IPythonWidget.editor',
108 114 paging = 'ConsoleWidget.paging',
109 115 )
110 116 # and app_aliases from the Console Mixin
111 117 qt_aliases.update(app_aliases)
112 118 qt_aliases.update({'gui-completion':'ConsoleWidget.gui_completion'})
113 119 # add frontend aliases to the full set
114 120 aliases.update(qt_aliases)
115 121
116 122 # get flags&aliases into sets, and remove a couple that
117 123 # shouldn't be scrubbed from backend flags:
118 124 qt_aliases = set(qt_aliases.keys())
119 125 qt_aliases.remove('colors')
120 126 qt_flags = set(qt_flags.keys())
121 127
122 128 #-----------------------------------------------------------------------------
123 129 # Classes
124 130 #-----------------------------------------------------------------------------
125 131
126 132 #-----------------------------------------------------------------------------
127 133 # IPythonQtConsole
128 134 #-----------------------------------------------------------------------------
129 135
130 136
131 137 class IPythonQtConsoleApp(BaseIPythonApplication, IPythonConsoleApp):
132 138 name = 'ipython-qtconsole'
133 139
134 140 description = """
135 141 The IPython QtConsole.
136 142
137 143 This launches a Console-style application using Qt. It is not a full
138 144 console, in that launched terminal subprocesses will not be able to accept
139 145 input.
140 146
141 147 The QtConsole supports various extra features beyond the Terminal IPython
142 148 shell, such as inline plotting with matplotlib, via:
143 149
144 150 ipython qtconsole --matplotlib=inline
145 151
146 152 as well as saving your session as HTML, and printing the output.
147 153
148 154 """
149 155 examples = _examples
150 156
151 157 classes = [IPythonWidget] + IPythonConsoleApp.classes
152 158 flags = Dict(flags)
153 159 aliases = Dict(aliases)
154 160 frontend_flags = Any(qt_flags)
155 161 frontend_aliases = Any(qt_aliases)
156 162 kernel_client_class = QtKernelClient
157 163 kernel_manager_class = QtKernelManager
158 164
159 165 stylesheet = Unicode('', config=True,
160 166 help="path to a custom CSS stylesheet")
161 167
162 168 hide_menubar = CBool(False, config=True,
163 169 help="Start the console window with the menu bar hidden.")
164 170
165 171 maximize = CBool(False, config=True,
166 172 help="Start the console window maximized.")
167 173
168 174 plain = CBool(False, config=True,
169 175 help="Use a plaintext widget instead of rich text (plain can't print/save).")
170 176
177 display_banner = CBool(True, config=True,
178 help="Whether to display a banner upon starting IPython."
179 )
180
171 181 def _plain_changed(self, name, old, new):
172 182 kind = 'plain' if new else 'rich'
173 183 self.config.ConsoleWidget.kind = kind
174 184 if new:
175 185 self.widget_factory = IPythonWidget
176 186 else:
177 187 self.widget_factory = RichIPythonWidget
178 188
179 189 # the factory for creating a widget
180 190 widget_factory = Any(RichIPythonWidget)
181 191
182 192 def parse_command_line(self, argv=None):
183 193 super(IPythonQtConsoleApp, self).parse_command_line(argv)
184 194 self.build_kernel_argv(argv)
185 195
186 196
187 197 def new_frontend_master(self):
188 198 """ Create and return new frontend attached to new kernel, launched on localhost.
189 199 """
190 200 kernel_manager = self.kernel_manager_class(
191 201 connection_file=self._new_connection_file(),
192 202 parent=self,
193 203 autorestart=True,
194 204 )
195 205 # start the kernel
196 206 kwargs = {}
197 207 # FIXME: remove special treatment of IPython kernels
198 208 if self.kernel_manager.ipython_kernel:
199 209 kwargs['extra_arguments'] = self.kernel_argv
200 210 kernel_manager.start_kernel(**kwargs)
201 211 kernel_manager.client_factory = self.kernel_client_class
202 212 kernel_client = kernel_manager.client()
203 213 kernel_client.start_channels(shell=True, iopub=True)
204 214 widget = self.widget_factory(config=self.config,
205 215 local_kernel=True)
206 216 self.init_colors(widget)
207 217 widget.kernel_manager = kernel_manager
208 218 widget.kernel_client = kernel_client
209 219 widget._existing = False
210 220 widget._may_close = True
211 221 widget._confirm_exit = self.confirm_exit
212 222 return widget
213 223
214 224 def new_frontend_slave(self, current_widget):
215 225 """Create and return a new frontend attached to an existing kernel.
216 226
217 227 Parameters
218 228 ----------
219 229 current_widget : IPythonWidget
220 230 The IPythonWidget whose kernel this frontend is to share
221 231 """
222 232 kernel_client = self.kernel_client_class(
223 233 connection_file=current_widget.kernel_client.connection_file,
224 234 config = self.config,
225 235 )
226 236 kernel_client.load_connection_file()
227 237 kernel_client.start_channels()
228 238 widget = self.widget_factory(config=self.config,
229 239 local_kernel=False)
230 240 self.init_colors(widget)
231 241 widget._existing = True
232 242 widget._may_close = False
233 243 widget._confirm_exit = False
234 244 widget.kernel_client = kernel_client
235 245 widget.kernel_manager = current_widget.kernel_manager
236 246 return widget
237 247
238 248 def init_qt_app(self):
239 249 # separate from qt_elements, because it must run first
240 250 self.app = QtGui.QApplication([])
241 251
242 252 def init_qt_elements(self):
243 253 # Create the widget.
244 254
245 255 base_path = os.path.abspath(os.path.dirname(__file__))
246 256 icon_path = os.path.join(base_path, 'resources', 'icon', 'IPythonConsole.svg')
247 257 self.app.icon = QtGui.QIcon(icon_path)
248 258 QtGui.QApplication.setWindowIcon(self.app.icon)
249 259
250 260 ip = self.ip
251 261 local_kernel = (not self.existing) or is_local_ip(ip)
252 262 self.widget = self.widget_factory(config=self.config,
253 263 local_kernel=local_kernel)
254 264 self.init_colors(self.widget)
255 265 self.widget._existing = self.existing
256 266 self.widget._may_close = not self.existing
257 267 self.widget._confirm_exit = self.confirm_exit
268 self.widget._display_banner = self.display_banner
258 269
259 270 self.widget.kernel_manager = self.kernel_manager
260 271 self.widget.kernel_client = self.kernel_client
261 272 self.window = MainWindow(self.app,
262 273 confirm_exit=self.confirm_exit,
263 274 new_frontend_factory=self.new_frontend_master,
264 275 slave_frontend_factory=self.new_frontend_slave,
265 276 )
266 277 self.window.log = self.log
267 278 self.window.add_tab_with_frontend(self.widget)
268 279 self.window.init_magic_helper()
269 280 self.window.init_menu_bar()
270 281
271 282 # Ignore on OSX, where there is always a menu bar
272 283 if sys.platform != 'darwin' and self.hide_menubar:
273 284 self.window.menuBar().setVisible(False)
274 285
275 286 self.window.setWindowTitle('IPython')
276 287
277 288 def init_colors(self, widget):
278 289 """Configure the coloring of the widget"""
279 290 # Note: This will be dramatically simplified when colors
280 291 # are removed from the backend.
281 292
282 293 # parse the colors arg down to current known labels
283 294 cfg = self.config
284 295 colors = cfg.ZMQInteractiveShell.colors if 'ZMQInteractiveShell.colors' in cfg else None
285 296 style = cfg.IPythonWidget.syntax_style if 'IPythonWidget.syntax_style' in cfg else None
286 297 sheet = cfg.IPythonWidget.style_sheet if 'IPythonWidget.style_sheet' in cfg else None
287 298
288 299 # find the value for colors:
289 300 if colors:
290 301 colors=colors.lower()
291 302 if colors in ('lightbg', 'light'):
292 303 colors='lightbg'
293 304 elif colors in ('dark', 'linux'):
294 305 colors='linux'
295 306 else:
296 307 colors='nocolor'
297 308 elif style:
298 309 if style=='bw':
299 310 colors='nocolor'
300 311 elif styles.dark_style(style):
301 312 colors='linux'
302 313 else:
303 314 colors='lightbg'
304 315 else:
305 316 colors=None
306 317
307 318 # Configure the style
308 319 if style:
309 320 widget.style_sheet = styles.sheet_from_template(style, colors)
310 321 widget.syntax_style = style
311 322 widget._syntax_style_changed()
312 323 widget._style_sheet_changed()
313 324 elif colors:
314 325 # use a default dark/light/bw style
315 326 widget.set_default_style(colors=colors)
316 327
317 328 if self.stylesheet:
318 329 # we got an explicit stylesheet
319 330 if os.path.isfile(self.stylesheet):
320 331 with open(self.stylesheet) as f:
321 332 sheet = f.read()
322 333 else:
323 334 raise IOError("Stylesheet %r not found." % self.stylesheet)
324 335 if sheet:
325 336 widget.style_sheet = sheet
326 337 widget._style_sheet_changed()
327 338
328 339
329 340 def init_signal(self):
330 341 """allow clean shutdown on sigint"""
331 342 signal.signal(signal.SIGINT, lambda sig, frame: self.exit(-2))
332 343 # need a timer, so that QApplication doesn't block until a real
333 344 # Qt event fires (can require mouse movement)
334 345 # timer trick from http://stackoverflow.com/q/4938723/938949
335 346 timer = QtCore.QTimer()
336 347 # Let the interpreter run each 200 ms:
337 348 timer.timeout.connect(lambda: None)
338 349 timer.start(200)
339 350 # hold onto ref, so the timer doesn't get cleaned up
340 351 self._sigint_timer = timer
341 352
342 353 @catch_config_error
343 354 def initialize(self, argv=None):
344 355 self.init_qt_app()
345 356 super(IPythonQtConsoleApp, self).initialize(argv)
346 357 IPythonConsoleApp.initialize(self,argv)
347 358 self.init_qt_elements()
348 359 self.init_signal()
349 360
350 361 def start(self):
351 362
352 363 # draw the window
353 364 if self.maximize:
354 365 self.window.showMaximized()
355 366 else:
356 367 self.window.show()
357 368 self.window.raise_()
358 369
359 370 # Start the application main loop.
360 371 self.app.exec_()
361 372
362 373 #-----------------------------------------------------------------------------
363 374 # Main entry point
364 375 #-----------------------------------------------------------------------------
365 376
366 377 def main():
367 378 app = IPythonQtConsoleApp()
368 379 app.initialize()
369 380 app.start()
370 381
371 382
372 383 if __name__ == '__main__':
373 384 main()
General Comments 0
You need to be logged in to leave comments. Login now