##// END OF EJS Templates
class-attribute sets default _local_kernel in FrontendWidget
MinRK -
Show More
@@ -1,590 +1,590 b''
1 1 from __future__ import print_function
2 2
3 3 # Standard library imports
4 4 from collections import namedtuple
5 5 import sys
6 6 import time
7 7
8 8 # System library imports
9 9 from pygments.lexers import PythonLexer
10 10 from PyQt4 import QtCore, QtGui
11 11
12 12 # Local imports
13 13 from IPython.core.inputsplitter import InputSplitter, transform_classic_prompt
14 14 from IPython.core.oinspect import call_tip
15 15 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
16 16 from IPython.utils.traitlets import Bool
17 17 from bracket_matcher import BracketMatcher
18 18 from call_tip_widget import CallTipWidget
19 19 from completion_lexer import CompletionLexer
20 20 from history_console_widget import HistoryConsoleWidget
21 21 from pygments_highlighter import PygmentsHighlighter
22 22
23 23
24 24 class FrontendHighlighter(PygmentsHighlighter):
25 25 """ A PygmentsHighlighter that can be turned on and off and that ignores
26 26 prompts.
27 27 """
28 28
29 29 def __init__(self, frontend):
30 30 super(FrontendHighlighter, self).__init__(frontend._control.document())
31 31 self._current_offset = 0
32 32 self._frontend = frontend
33 33 self.highlighting_on = False
34 34
35 35 def highlightBlock(self, qstring):
36 36 """ Highlight a block of text. Reimplemented to highlight selectively.
37 37 """
38 38 if not self.highlighting_on:
39 39 return
40 40
41 41 # The input to this function is unicode string that may contain
42 42 # paragraph break characters, non-breaking spaces, etc. Here we acquire
43 43 # the string as plain text so we can compare it.
44 44 current_block = self.currentBlock()
45 45 string = self._frontend._get_block_plain_text(current_block)
46 46
47 47 # Decide whether to check for the regular or continuation prompt.
48 48 if current_block.contains(self._frontend._prompt_pos):
49 49 prompt = self._frontend._prompt
50 50 else:
51 51 prompt = self._frontend._continuation_prompt
52 52
53 53 # Don't highlight the part of the string that contains the prompt.
54 54 if string.startswith(prompt):
55 55 self._current_offset = len(prompt)
56 56 qstring.remove(0, len(prompt))
57 57 else:
58 58 self._current_offset = 0
59 59
60 60 PygmentsHighlighter.highlightBlock(self, qstring)
61 61
62 62 def rehighlightBlock(self, block):
63 63 """ Reimplemented to temporarily enable highlighting if disabled.
64 64 """
65 65 old = self.highlighting_on
66 66 self.highlighting_on = True
67 67 super(FrontendHighlighter, self).rehighlightBlock(block)
68 68 self.highlighting_on = old
69 69
70 70 def setFormat(self, start, count, format):
71 71 """ Reimplemented to highlight selectively.
72 72 """
73 73 start += self._current_offset
74 74 PygmentsHighlighter.setFormat(self, start, count, format)
75 75
76 76
77 77 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
78 78 """ A Qt frontend for a generic Python kernel.
79 79 """
80 80
81 81 # An option and corresponding signal for overriding the default kernel
82 82 # interrupt behavior.
83 83 custom_interrupt = Bool(False)
84 84 custom_interrupt_requested = QtCore.pyqtSignal()
85 85
86 86 # An option and corresponding signals for overriding the default kernel
87 87 # restart behavior.
88 88 custom_restart = Bool(False)
89 89 custom_restart_kernel_died = QtCore.pyqtSignal(float)
90 90 custom_restart_requested = QtCore.pyqtSignal()
91 91
92 92 # Emitted when an 'execute_reply' has been received from the kernel and
93 93 # processed by the FrontendWidget.
94 94 executed = QtCore.pyqtSignal(object)
95 95
96 96 # Emitted when an exit request has been received from the kernel.
97 97 exit_requested = QtCore.pyqtSignal()
98 98
99 99 # Protected class variables.
100 100 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
101 101 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
102 102 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
103 103 _input_splitter_class = InputSplitter
104 104 _local_kernel = False
105 105
106 106 #---------------------------------------------------------------------------
107 107 # 'object' interface
108 108 #---------------------------------------------------------------------------
109 109
110 110 def __init__(self, *args, **kw):
111 111 super(FrontendWidget, self).__init__(*args, **kw)
112 112
113 113 # FrontendWidget protected variables.
114 114 self._bracket_matcher = BracketMatcher(self._control)
115 115 self._call_tip_widget = CallTipWidget(self._control)
116 116 self._completion_lexer = CompletionLexer(PythonLexer())
117 117 self._copy_raw_action = QtGui.QAction('Copy (Raw Text)', None)
118 118 self._hidden = False
119 119 self._highlighter = FrontendHighlighter(self)
120 120 self._input_splitter = self._input_splitter_class(input_mode='cell')
121 121 self._kernel_manager = None
122 122 self._request_info = {}
123 123
124 124 # Configure the ConsoleWidget.
125 125 self.tab_width = 4
126 126 self._set_continuation_prompt('... ')
127 127
128 128 # Configure the CallTipWidget.
129 129 self._call_tip_widget.setFont(self.font)
130 130 self.font_changed.connect(self._call_tip_widget.setFont)
131 131
132 132 # Configure actions.
133 133 action = self._copy_raw_action
134 134 key = QtCore.Qt.CTRL | QtCore.Qt.SHIFT | QtCore.Qt.Key_C
135 135 action.setEnabled(False)
136 136 action.setShortcut(QtGui.QKeySequence(key))
137 137 action.setShortcutContext(QtCore.Qt.WidgetWithChildrenShortcut)
138 138 action.triggered.connect(self.copy_raw)
139 139 self.copy_available.connect(action.setEnabled)
140 140 self.addAction(action)
141 141
142 142 # Connect signal handlers.
143 143 document = self._control.document()
144 144 document.contentsChange.connect(self._document_contents_change)
145 145
146 146 # set flag for whether we are connected via localhost
147 self._local_kernel = kw.get('local_kernel', False)
147 self._local_kernel = kw.get('local_kernel', FrontendWidget._local_kernel)
148 148
149 149 #---------------------------------------------------------------------------
150 150 # 'ConsoleWidget' public interface
151 151 #---------------------------------------------------------------------------
152 152
153 153 def copy(self):
154 154 """ Copy the currently selected text to the clipboard, removing prompts.
155 155 """
156 156 text = unicode(self._control.textCursor().selection().toPlainText())
157 157 if text:
158 158 lines = map(transform_classic_prompt, text.splitlines())
159 159 text = '\n'.join(lines)
160 160 QtGui.QApplication.clipboard().setText(text)
161 161
162 162 #---------------------------------------------------------------------------
163 163 # 'ConsoleWidget' abstract interface
164 164 #---------------------------------------------------------------------------
165 165
166 166 def _is_complete(self, source, interactive):
167 167 """ Returns whether 'source' can be completely processed and a new
168 168 prompt created. When triggered by an Enter/Return key press,
169 169 'interactive' is True; otherwise, it is False.
170 170 """
171 171 complete = self._input_splitter.push(source)
172 172 if interactive:
173 173 complete = not self._input_splitter.push_accepts_more()
174 174 return complete
175 175
176 176 def _execute(self, source, hidden):
177 177 """ Execute 'source'. If 'hidden', do not show any output.
178 178
179 179 See parent class :meth:`execute` docstring for full details.
180 180 """
181 181 msg_id = self.kernel_manager.xreq_channel.execute(source, hidden)
182 182 self._request_info['execute'] = self._ExecutionRequest(msg_id, 'user')
183 183 self._hidden = hidden
184 184
185 185 def _prompt_started_hook(self):
186 186 """ Called immediately after a new prompt is displayed.
187 187 """
188 188 if not self._reading:
189 189 self._highlighter.highlighting_on = True
190 190
191 191 def _prompt_finished_hook(self):
192 192 """ Called immediately after a prompt is finished, i.e. when some input
193 193 will be processed and a new prompt displayed.
194 194 """
195 195 if not self._reading:
196 196 self._highlighter.highlighting_on = False
197 197
198 198 def _tab_pressed(self):
199 199 """ Called when the tab key is pressed. Returns whether to continue
200 200 processing the event.
201 201 """
202 202 # Perform tab completion if:
203 203 # 1) The cursor is in the input buffer.
204 204 # 2) There is a non-whitespace character before the cursor.
205 205 text = self._get_input_buffer_cursor_line()
206 206 if text is None:
207 207 return False
208 208 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
209 209 if complete:
210 210 self._complete()
211 211 return not complete
212 212
213 213 #---------------------------------------------------------------------------
214 214 # 'ConsoleWidget' protected interface
215 215 #---------------------------------------------------------------------------
216 216
217 217 def _context_menu_make(self, pos):
218 218 """ Reimplemented to add an action for raw copy.
219 219 """
220 220 menu = super(FrontendWidget, self)._context_menu_make(pos)
221 221 for before_action in menu.actions():
222 222 if before_action.shortcut().matches(QtGui.QKeySequence.Paste) == \
223 223 QtGui.QKeySequence.ExactMatch:
224 224 menu.insertAction(before_action, self._copy_raw_action)
225 225 break
226 226 return menu
227 227
228 228 def _event_filter_console_keypress(self, event):
229 229 """ Reimplemented for execution interruption and smart backspace.
230 230 """
231 231 key = event.key()
232 232 if self._control_key_down(event.modifiers(), include_command=False):
233 233
234 234 if key == QtCore.Qt.Key_C and self._executing:
235 235 self.interrupt_kernel()
236 236 return True
237 237
238 238 elif key == QtCore.Qt.Key_Period:
239 239 message = 'Are you sure you want to restart the kernel?'
240 240 self.restart_kernel(message, now=False)
241 241 return True
242 242
243 243 elif not event.modifiers() & QtCore.Qt.AltModifier:
244 244
245 245 # Smart backspace: remove four characters in one backspace if:
246 246 # 1) everything left of the cursor is whitespace
247 247 # 2) the four characters immediately left of the cursor are spaces
248 248 if key == QtCore.Qt.Key_Backspace:
249 249 col = self._get_input_buffer_cursor_column()
250 250 cursor = self._control.textCursor()
251 251 if col > 3 and not cursor.hasSelection():
252 252 text = self._get_input_buffer_cursor_line()[:col]
253 253 if text.endswith(' ') and not text.strip():
254 254 cursor.movePosition(QtGui.QTextCursor.Left,
255 255 QtGui.QTextCursor.KeepAnchor, 4)
256 256 cursor.removeSelectedText()
257 257 return True
258 258
259 259 return super(FrontendWidget, self)._event_filter_console_keypress(event)
260 260
261 261 def _insert_continuation_prompt(self, cursor):
262 262 """ Reimplemented for auto-indentation.
263 263 """
264 264 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
265 265 cursor.insertText(' ' * self._input_splitter.indent_spaces)
266 266
267 267 #---------------------------------------------------------------------------
268 268 # 'BaseFrontendMixin' abstract interface
269 269 #---------------------------------------------------------------------------
270 270
271 271 def _handle_complete_reply(self, rep):
272 272 """ Handle replies for tab completion.
273 273 """
274 274 cursor = self._get_cursor()
275 275 info = self._request_info.get('complete')
276 276 if info and info.id == rep['parent_header']['msg_id'] and \
277 277 info.pos == cursor.position():
278 278 text = '.'.join(self._get_context())
279 279 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
280 280 self._complete_with_items(cursor, rep['content']['matches'])
281 281
282 282 def _handle_execute_reply(self, msg):
283 283 """ Handles replies for code execution.
284 284 """
285 285 info = self._request_info.get('execute')
286 286 if info and info.id == msg['parent_header']['msg_id'] and \
287 287 info.kind == 'user' and not self._hidden:
288 288 # Make sure that all output from the SUB channel has been processed
289 289 # before writing a new prompt.
290 290 self.kernel_manager.sub_channel.flush()
291 291
292 292 # Reset the ANSI style information to prevent bad text in stdout
293 293 # from messing up our colors. We're not a true terminal so we're
294 294 # allowed to do this.
295 295 if self.ansi_codes:
296 296 self._ansi_processor.reset_sgr()
297 297
298 298 content = msg['content']
299 299 status = content['status']
300 300 if status == 'ok':
301 301 self._process_execute_ok(msg)
302 302 elif status == 'error':
303 303 self._process_execute_error(msg)
304 304 elif status == 'abort':
305 305 self._process_execute_abort(msg)
306 306
307 307 self._show_interpreter_prompt_for_reply(msg)
308 308 self.executed.emit(msg)
309 309
310 310 def _handle_input_request(self, msg):
311 311 """ Handle requests for raw_input.
312 312 """
313 313 if self._hidden:
314 314 raise RuntimeError('Request for raw input during hidden execution.')
315 315
316 316 # Make sure that all output from the SUB channel has been processed
317 317 # before entering readline mode.
318 318 self.kernel_manager.sub_channel.flush()
319 319
320 320 def callback(line):
321 321 self.kernel_manager.rep_channel.input(line)
322 322 self._readline(msg['content']['prompt'], callback=callback)
323 323
324 324 def _handle_kernel_died(self, since_last_heartbeat):
325 325 """ Handle the kernel's death by asking if the user wants to restart.
326 326 """
327 327 if self.custom_restart:
328 328 self.custom_restart_kernel_died.emit(since_last_heartbeat)
329 329 else:
330 330 message = 'The kernel heartbeat has been inactive for %.2f ' \
331 331 'seconds. Do you want to restart the kernel? You may ' \
332 332 'first want to check the network connection.' % \
333 333 since_last_heartbeat
334 334 self.restart_kernel(message, now=True)
335 335
336 336 def _handle_object_info_reply(self, rep):
337 337 """ Handle replies for call tips.
338 338 """
339 339 cursor = self._get_cursor()
340 340 info = self._request_info.get('call_tip')
341 341 if info and info.id == rep['parent_header']['msg_id'] and \
342 342 info.pos == cursor.position():
343 343 # Get the information for a call tip. For now we format the call
344 344 # line as string, later we can pass False to format_call and
345 345 # syntax-highlight it ourselves for nicer formatting in the
346 346 # calltip.
347 347 call_info, doc = call_tip(rep['content'], format_call=True)
348 348 if call_info or doc:
349 349 self._call_tip_widget.show_call_info(call_info, doc)
350 350
351 351 def _handle_pyout(self, msg):
352 352 """ Handle display hook output.
353 353 """
354 354 if not self._hidden and self._is_from_this_session(msg):
355 355 self._append_plain_text(msg['content']['data'] + '\n')
356 356
357 357 def _handle_stream(self, msg):
358 358 """ Handle stdout, stderr, and stdin.
359 359 """
360 360 if not self._hidden and self._is_from_this_session(msg):
361 361 # Most consoles treat tabs as being 8 space characters. Convert tabs
362 362 # to spaces so that output looks as expected regardless of this
363 363 # widget's tab width.
364 364 text = msg['content']['data'].expandtabs(8)
365 365
366 366 self._append_plain_text(text)
367 367 self._control.moveCursor(QtGui.QTextCursor.End)
368 368
369 369 def _handle_shutdown_reply(self, msg):
370 370 """ Handle shutdown signal, only if from other console.
371 371 """
372 372 if not self._hidden and not self._is_from_this_session(msg):
373 373 if self._local_kernel:
374 374 if not msg['content']['restart']:
375 375 sys.exit(0)
376 376 else:
377 377 # we just got notified of a restart!
378 378 time.sleep(0.25) # wait 1/4 sec to reset
379 379 # lest the request for a new prompt
380 380 # goes to the old kernel
381 381 self.reset()
382 382 else: # remote kernel, prompt on Kernel shutdown/reset
383 383 title = self.window().windowTitle()
384 384 if not msg['content']['restart']:
385 385 reply = QtGui.QMessageBox.question(self, title,
386 386 "Kernel has been shutdown permanently. Close the Console?",
387 387 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
388 388 if reply == QtGui.QMessageBox.Yes:
389 389 sys.exit(0)
390 390 else:
391 391 reply = QtGui.QMessageBox.question(self, title,
392 392 "Kernel has been reset. Clear the Console?",
393 393 QtGui.QMessageBox.Yes,QtGui.QMessageBox.No)
394 394 if reply == QtGui.QMessageBox.Yes:
395 395 time.sleep(0.25) # wait 1/4 sec to reset
396 396 # lest the request for a new prompt
397 397 # goes to the old kernel
398 398 self.reset()
399 399
400 400 def _started_channels(self):
401 401 """ Called when the KernelManager channels have started listening or
402 402 when the frontend is assigned an already listening KernelManager.
403 403 """
404 404 self.reset()
405 405
406 406 #---------------------------------------------------------------------------
407 407 # 'FrontendWidget' public interface
408 408 #---------------------------------------------------------------------------
409 409
410 410 def copy_raw(self):
411 411 """ Copy the currently selected text to the clipboard without attempting
412 412 to remove prompts or otherwise alter the text.
413 413 """
414 414 self._control.copy()
415 415
416 416 def execute_file(self, path, hidden=False):
417 417 """ Attempts to execute file with 'path'. If 'hidden', no output is
418 418 shown.
419 419 """
420 420 self.execute('execfile("%s")' % path, hidden=hidden)
421 421
422 422 def interrupt_kernel(self):
423 423 """ Attempts to interrupt the running kernel.
424 424 """
425 425 if self.custom_interrupt:
426 426 self.custom_interrupt_requested.emit()
427 427 elif self.kernel_manager.has_kernel:
428 428 self.kernel_manager.interrupt_kernel()
429 429 else:
430 430 self._append_plain_text('Kernel process is either remote or '
431 431 'unspecified. Cannot interrupt.\n')
432 432
433 433 def reset(self):
434 434 """ Resets the widget to its initial state. Similar to ``clear``, but
435 435 also re-writes the banner and aborts execution if necessary.
436 436 """
437 437 if self._executing:
438 438 self._executing = False
439 439 self._request_info['execute'] = None
440 440 self._reading = False
441 441 self._highlighter.highlighting_on = False
442 442
443 443 self._control.clear()
444 444 self._append_plain_text(self._get_banner())
445 445 self._show_interpreter_prompt()
446 446
447 447 def restart_kernel(self, message, now=False):
448 448 """ Attempts to restart the running kernel.
449 449 """
450 450 # FIXME: now should be configurable via a checkbox in the dialog. Right
451 451 # now at least the heartbeat path sets it to True and the manual restart
452 452 # to False. But those should just be the pre-selected states of a
453 453 # checkbox that the user could override if so desired. But I don't know
454 454 # enough Qt to go implementing the checkbox now.
455 455
456 456 if self.custom_restart:
457 457 self.custom_restart_requested.emit()
458 458
459 459 elif self.kernel_manager.has_kernel:
460 460 # Pause the heart beat channel to prevent further warnings.
461 461 self.kernel_manager.hb_channel.pause()
462 462
463 463 # Prompt the user to restart the kernel. Un-pause the heartbeat if
464 464 # they decline. (If they accept, the heartbeat will be un-paused
465 465 # automatically when the kernel is restarted.)
466 466 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
467 467 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
468 468 message, buttons)
469 469 if result == QtGui.QMessageBox.Yes:
470 470 try:
471 471 self.kernel_manager.restart_kernel(now=now)
472 472 except RuntimeError:
473 473 self._append_plain_text('Kernel started externally. '
474 474 'Cannot restart.\n')
475 475 else:
476 476 self.reset()
477 477 else:
478 478 self.kernel_manager.hb_channel.unpause()
479 479
480 480 else:
481 481 self._append_plain_text('Kernel process is either remote or '
482 482 'unspecified. Cannot restart.\n')
483 483
484 484 #---------------------------------------------------------------------------
485 485 # 'FrontendWidget' protected interface
486 486 #---------------------------------------------------------------------------
487 487
488 488 def _call_tip(self):
489 489 """ Shows a call tip, if appropriate, at the current cursor location.
490 490 """
491 491 # Decide if it makes sense to show a call tip
492 492 cursor = self._get_cursor()
493 493 cursor.movePosition(QtGui.QTextCursor.Left)
494 494 if cursor.document().characterAt(cursor.position()).toAscii() != '(':
495 495 return False
496 496 context = self._get_context(cursor)
497 497 if not context:
498 498 return False
499 499
500 500 # Send the metadata request to the kernel
501 501 name = '.'.join(context)
502 502 msg_id = self.kernel_manager.xreq_channel.object_info(name)
503 503 pos = self._get_cursor().position()
504 504 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
505 505 return True
506 506
507 507 def _complete(self):
508 508 """ Performs completion at the current cursor location.
509 509 """
510 510 context = self._get_context()
511 511 if context:
512 512 # Send the completion request to the kernel
513 513 msg_id = self.kernel_manager.xreq_channel.complete(
514 514 '.'.join(context), # text
515 515 self._get_input_buffer_cursor_line(), # line
516 516 self._get_input_buffer_cursor_column(), # cursor_pos
517 517 self.input_buffer) # block
518 518 pos = self._get_cursor().position()
519 519 info = self._CompletionRequest(msg_id, pos)
520 520 self._request_info['complete'] = info
521 521
522 522 def _get_banner(self):
523 523 """ Gets a banner to display at the beginning of a session.
524 524 """
525 525 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
526 526 '"license" for more information.'
527 527 return banner % (sys.version, sys.platform)
528 528
529 529 def _get_context(self, cursor=None):
530 530 """ Gets the context for the specified cursor (or the current cursor
531 531 if none is specified).
532 532 """
533 533 if cursor is None:
534 534 cursor = self._get_cursor()
535 535 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
536 536 QtGui.QTextCursor.KeepAnchor)
537 537 text = unicode(cursor.selection().toPlainText())
538 538 return self._completion_lexer.get_context(text)
539 539
540 540 def _process_execute_abort(self, msg):
541 541 """ Process a reply for an aborted execution request.
542 542 """
543 543 self._append_plain_text("ERROR: execution aborted\n")
544 544
545 545 def _process_execute_error(self, msg):
546 546 """ Process a reply for an execution request that resulted in an error.
547 547 """
548 548 content = msg['content']
549 549 traceback = ''.join(content['traceback'])
550 550 self._append_plain_text(traceback)
551 551
552 552 def _process_execute_ok(self, msg):
553 553 """ Process a reply for a successful execution equest.
554 554 """
555 555 payload = msg['content']['payload']
556 556 for item in payload:
557 557 if not self._process_execute_payload(item):
558 558 warning = 'Warning: received unknown payload of type %s'
559 559 print(warning % repr(item['source']))
560 560
561 561 def _process_execute_payload(self, item):
562 562 """ Process a single payload item from the list of payload items in an
563 563 execution reply. Returns whether the payload was handled.
564 564 """
565 565 # The basic FrontendWidget doesn't handle payloads, as they are a
566 566 # mechanism for going beyond the standard Python interpreter model.
567 567 return False
568 568
569 569 def _show_interpreter_prompt(self):
570 570 """ Shows a prompt for the interpreter.
571 571 """
572 572 self._show_prompt('>>> ')
573 573
574 574 def _show_interpreter_prompt_for_reply(self, msg):
575 575 """ Shows a prompt for the interpreter given an 'execute_reply' message.
576 576 """
577 577 self._show_interpreter_prompt()
578 578
579 579 #------ Signal handlers ----------------------------------------------------
580 580
581 581 def _document_contents_change(self, position, removed, added):
582 582 """ Called whenever the document's content changes. Display a call tip
583 583 if appropriate.
584 584 """
585 585 # Calculate where the cursor should be *after* the change:
586 586 position += added
587 587
588 588 document = self._control.document()
589 589 if position == self._get_cursor().position():
590 590 self._call_tip()
General Comments 0
You need to be logged in to leave comments. Login now