##// END OF EJS Templates
* Moved shutdown_kernel method from FrontendWidget to KernelManager....
epatters -
Show More
@@ -1,1567 +1,1568 b''
1 1 # Standard library imports
2 2 from os.path import commonprefix
3 3 import re
4 4 import sys
5 5 from textwrap import dedent
6 6
7 7 # System library imports
8 8 from PyQt4 import QtCore, QtGui
9 9
10 10 # Local imports
11 11 from IPython.config.configurable import Configurable
12 12 from IPython.frontend.qt.util import MetaQObjectHasTraits
13 13 from IPython.utils.traitlets import Bool, Enum, Int
14 14 from ansi_code_processor import QtAnsiCodeProcessor
15 15 from completion_widget import CompletionWidget
16 16
17 17
18 18 class ConsolePlainTextEdit(QtGui.QPlainTextEdit):
19 19 """ A QPlainTextEdit suitable for use with ConsoleWidget.
20 20 """
21 21 # Prevents text from being moved by drag and drop. Note that is not, for
22 22 # some reason, sufficient to catch drag events in the ConsoleWidget's
23 23 # event filter.
24 24 def dragEnterEvent(self, event): pass
25 25 def dragLeaveEvent(self, event): pass
26 26 def dragMoveEvent(self, event): pass
27 27 def dropEvent(self, event): pass
28 28
29 29 class ConsoleTextEdit(QtGui.QTextEdit):
30 30 """ A QTextEdit suitable for use with ConsoleWidget.
31 31 """
32 32 # See above.
33 33 def dragEnterEvent(self, event): pass
34 34 def dragLeaveEvent(self, event): pass
35 35 def dragMoveEvent(self, event): pass
36 36 def dropEvent(self, event): pass
37 37
38 38
39 39 class ConsoleWidget(Configurable, QtGui.QWidget):
40 40 """ An abstract base class for console-type widgets. This class has
41 41 functionality for:
42 42
43 43 * Maintaining a prompt and editing region
44 44 * Providing the traditional Unix-style console keyboard shortcuts
45 45 * Performing tab completion
46 46 * Paging text
47 47 * Handling ANSI escape codes
48 48
49 49 ConsoleWidget also provides a number of utility methods that will be
50 50 convenient to implementors of a console-style widget.
51 51 """
52 52 __metaclass__ = MetaQObjectHasTraits
53 53
54 54 # Whether to process ANSI escape codes.
55 55 ansi_codes = Bool(True, config=True)
56 56
57 57 # The maximum number of lines of text before truncation. Specifying a
58 58 # non-positive number disables text truncation (not recommended).
59 59 buffer_size = Int(500, config=True)
60 60
61 61 # Whether to use a list widget or plain text output for tab completion.
62 62 gui_completion = Bool(False, config=True)
63 63
64 64 # The type of underlying text widget to use. Valid values are 'plain', which
65 65 # specifies a QPlainTextEdit, and 'rich', which specifies a QTextEdit.
66 66 # NOTE: this value can only be specified during initialization.
67 67 kind = Enum(['plain', 'rich'], default_value='plain', config=True)
68 68
69 69 # The type of paging to use. Valid values are:
70 70 # 'inside' : The widget pages like a traditional terminal pager.
71 71 # 'hsplit' : When paging is requested, the widget is split
72 72 # horizontally. The top pane contains the console, and the
73 73 # bottom pane contains the paged text.
74 74 # 'vsplit' : Similar to 'hsplit', except that a vertical splitter used.
75 75 # 'custom' : No action is taken by the widget beyond emitting a
76 76 # 'custom_page_requested(str)' signal.
77 77 # 'none' : The text is written directly to the console.
78 78 # NOTE: this value can only be specified during initialization.
79 79 paging = Enum(['inside', 'hsplit', 'vsplit', 'custom', 'none'],
80 80 default_value='inside', config=True)
81 81
82 82 # Whether to override ShortcutEvents for the keybindings defined by this
83 83 # widget (Ctrl+n, Ctrl+a, etc). Enable this if you want this widget to take
84 84 # priority (when it has focus) over, e.g., window-level menu shortcuts.
85 85 override_shortcuts = Bool(False)
86 86
87 87 # Signals that indicate ConsoleWidget state.
88 88 copy_available = QtCore.pyqtSignal(bool)
89 89 redo_available = QtCore.pyqtSignal(bool)
90 90 undo_available = QtCore.pyqtSignal(bool)
91 91
92 92 # Signal emitted when paging is needed and the paging style has been
93 93 # specified as 'custom'.
94 94 custom_page_requested = QtCore.pyqtSignal(object)
95 95
96 96 # Protected class variables.
97 97 _ctrl_down_remap = { QtCore.Qt.Key_B : QtCore.Qt.Key_Left,
98 98 QtCore.Qt.Key_F : QtCore.Qt.Key_Right,
99 99 QtCore.Qt.Key_A : QtCore.Qt.Key_Home,
100 100 QtCore.Qt.Key_E : QtCore.Qt.Key_End,
101 101 QtCore.Qt.Key_P : QtCore.Qt.Key_Up,
102 102 QtCore.Qt.Key_N : QtCore.Qt.Key_Down,
103 103 QtCore.Qt.Key_D : QtCore.Qt.Key_Delete, }
104 104 _shortcuts = set(_ctrl_down_remap.keys() +
105 105 [ QtCore.Qt.Key_C, QtCore.Qt.Key_G, QtCore.Qt.Key_O,
106 106 QtCore.Qt.Key_V ])
107 107
108 108 #---------------------------------------------------------------------------
109 109 # 'QObject' interface
110 110 #---------------------------------------------------------------------------
111 111
112 112 def __init__(self, parent=None, **kw):
113 113 """ Create a ConsoleWidget.
114 114
115 115 Parameters:
116 116 -----------
117 117 parent : QWidget, optional [default None]
118 118 The parent for this widget.
119 119 """
120 120 QtGui.QWidget.__init__(self, parent)
121 121 Configurable.__init__(self, **kw)
122 122
123 123 # Create the layout and underlying text widget.
124 124 layout = QtGui.QStackedLayout(self)
125 125 layout.setContentsMargins(0, 0, 0, 0)
126 126 self._control = self._create_control()
127 127 self._page_control = None
128 128 self._splitter = None
129 129 if self.paging in ('hsplit', 'vsplit'):
130 130 self._splitter = QtGui.QSplitter()
131 131 if self.paging == 'hsplit':
132 132 self._splitter.setOrientation(QtCore.Qt.Horizontal)
133 133 else:
134 134 self._splitter.setOrientation(QtCore.Qt.Vertical)
135 135 self._splitter.addWidget(self._control)
136 136 layout.addWidget(self._splitter)
137 137 else:
138 138 layout.addWidget(self._control)
139 139
140 140 # Create the paging widget, if necessary.
141 141 if self.paging in ('inside', 'hsplit', 'vsplit'):
142 142 self._page_control = self._create_page_control()
143 143 if self._splitter:
144 144 self._page_control.hide()
145 145 self._splitter.addWidget(self._page_control)
146 146 else:
147 147 layout.addWidget(self._page_control)
148 148
149 149 # Initialize protected variables. Some variables contain useful state
150 150 # information for subclasses; they should be considered read-only.
151 151 self._ansi_processor = QtAnsiCodeProcessor()
152 152 self._completion_widget = CompletionWidget(self._control)
153 153 self._continuation_prompt = '> '
154 154 self._continuation_prompt_html = None
155 155 self._executing = False
156 156 self._prompt = ''
157 157 self._prompt_html = None
158 158 self._prompt_pos = 0
159 159 self._prompt_sep = ''
160 160 self._reading = False
161 161 self._reading_callback = None
162 162 self._tab_width = 8
163 163 self._text_completing_pos = 0
164 164
165 165 # Set a monospaced font.
166 166 self.reset_font()
167 167
168 168 def eventFilter(self, obj, event):
169 169 """ Reimplemented to ensure a console-like behavior in the underlying
170 170 text widgets.
171 171 """
172 172 etype = event.type()
173 173 if etype == QtCore.QEvent.KeyPress:
174 174
175 175 # Re-map keys for all filtered widgets.
176 176 key = event.key()
177 177 if self._control_key_down(event.modifiers()) and \
178 178 key in self._ctrl_down_remap:
179 179 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
180 180 self._ctrl_down_remap[key],
181 181 QtCore.Qt.NoModifier)
182 182 QtGui.qApp.sendEvent(obj, new_event)
183 183 return True
184 184
185 185 elif obj == self._control:
186 186 return self._event_filter_console_keypress(event)
187 187
188 188 elif obj == self._page_control:
189 189 return self._event_filter_page_keypress(event)
190 190
191 191 # Make middle-click paste safe.
192 192 elif etype == QtCore.QEvent.MouseButtonRelease and \
193 193 event.button() == QtCore.Qt.MidButton and \
194 194 obj == self._control.viewport():
195 195 cursor = self._control.cursorForPosition(event.pos())
196 196 self._control.setTextCursor(cursor)
197 197 self.paste(QtGui.QClipboard.Selection)
198 198 return True
199 199
200 200 # Override shortcuts for all filtered widgets.
201 201 elif etype == QtCore.QEvent.ShortcutOverride and \
202 self.override_shortcuts and \
202 203 self._control_key_down(event.modifiers()) and \
203 204 event.key() in self._shortcuts:
204 205 event.accept()
205 206 return False
206 207
207 208 return super(ConsoleWidget, self).eventFilter(obj, event)
208 209
209 210 #---------------------------------------------------------------------------
210 211 # 'QWidget' interface
211 212 #---------------------------------------------------------------------------
212 213
213 214 def sizeHint(self):
214 215 """ Reimplemented to suggest a size that is 80 characters wide and
215 216 25 lines high.
216 217 """
217 218 font_metrics = QtGui.QFontMetrics(self.font)
218 219 margin = (self._control.frameWidth() +
219 220 self._control.document().documentMargin()) * 2
220 221 style = self.style()
221 222 splitwidth = style.pixelMetric(QtGui.QStyle.PM_SplitterWidth)
222 223
223 224 # Note 1: Despite my best efforts to take the various margins into
224 225 # account, the width is still coming out a bit too small, so we include
225 226 # a fudge factor of one character here.
226 227 # Note 2: QFontMetrics.maxWidth is not used here or anywhere else due
227 228 # to a Qt bug on certain Mac OS systems where it returns 0.
228 229 width = font_metrics.width(' ') * 81 + margin
229 230 width += style.pixelMetric(QtGui.QStyle.PM_ScrollBarExtent)
230 231 if self.paging == 'hsplit':
231 232 width = width * 2 + splitwidth
232 233
233 234 height = font_metrics.height() * 25 + margin
234 235 if self.paging == 'vsplit':
235 236 height = height * 2 + splitwidth
236 237
237 238 return QtCore.QSize(width, height)
238 239
239 240 #---------------------------------------------------------------------------
240 241 # 'ConsoleWidget' public interface
241 242 #---------------------------------------------------------------------------
242 243
243 244 def can_paste(self):
244 245 """ Returns whether text can be pasted from the clipboard.
245 246 """
246 247 # Only accept text that can be ASCII encoded.
247 248 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
248 249 text = QtGui.QApplication.clipboard().text()
249 250 if not text.isEmpty():
250 251 try:
251 252 str(text)
252 253 return True
253 254 except UnicodeEncodeError:
254 255 pass
255 256 return False
256 257
257 258 def clear(self, keep_input=True):
258 259 """ Clear the console, then write a new prompt. If 'keep_input' is set,
259 260 restores the old input buffer when the new prompt is written.
260 261 """
261 262 if keep_input:
262 263 input_buffer = self.input_buffer
263 264 self._control.clear()
264 265 self._show_prompt()
265 266 if keep_input:
266 267 self.input_buffer = input_buffer
267 268
268 269 def copy(self):
269 270 """ Copy the current selected text to the clipboard.
270 271 """
271 272 self._control.copy()
272 273
273 274 def execute(self, source=None, hidden=False, interactive=False):
274 275 """ Executes source or the input buffer, possibly prompting for more
275 276 input.
276 277
277 278 Parameters:
278 279 -----------
279 280 source : str, optional
280 281
281 282 The source to execute. If not specified, the input buffer will be
282 283 used. If specified and 'hidden' is False, the input buffer will be
283 284 replaced with the source before execution.
284 285
285 286 hidden : bool, optional (default False)
286 287
287 288 If set, no output will be shown and the prompt will not be modified.
288 289 In other words, it will be completely invisible to the user that
289 290 an execution has occurred.
290 291
291 292 interactive : bool, optional (default False)
292 293
293 294 Whether the console is to treat the source as having been manually
294 295 entered by the user. The effect of this parameter depends on the
295 296 subclass implementation.
296 297
297 298 Raises:
298 299 -------
299 300 RuntimeError
300 301 If incomplete input is given and 'hidden' is True. In this case,
301 302 it is not possible to prompt for more input.
302 303
303 304 Returns:
304 305 --------
305 306 A boolean indicating whether the source was executed.
306 307 """
307 308 # WARNING: The order in which things happen here is very particular, in
308 309 # large part because our syntax highlighting is fragile. If you change
309 310 # something, test carefully!
310 311
311 312 # Decide what to execute.
312 313 if source is None:
313 314 source = self.input_buffer
314 315 if not hidden:
315 316 # A newline is appended later, but it should be considered part
316 317 # of the input buffer.
317 318 source += '\n'
318 319 elif not hidden:
319 320 self.input_buffer = source
320 321
321 322 # Execute the source or show a continuation prompt if it is incomplete.
322 323 complete = self._is_complete(source, interactive)
323 324 if hidden:
324 325 if complete:
325 326 self._execute(source, hidden)
326 327 else:
327 328 error = 'Incomplete noninteractive input: "%s"'
328 329 raise RuntimeError(error % source)
329 330 else:
330 331 if complete:
331 332 self._append_plain_text('\n')
332 333 self._executing_input_buffer = self.input_buffer
333 334 self._executing = True
334 335 self._prompt_finished()
335 336
336 337 # The maximum block count is only in effect during execution.
337 338 # This ensures that _prompt_pos does not become invalid due to
338 339 # text truncation.
339 340 self._control.document().setMaximumBlockCount(self.buffer_size)
340 341
341 342 # Setting a positive maximum block count will automatically
342 343 # disable the undo/redo history, but just to be safe:
343 344 self._control.setUndoRedoEnabled(False)
344 345
345 346 self._execute(source, hidden)
346 347
347 348 else:
348 349 # Do this inside an edit block so continuation prompts are
349 350 # removed seamlessly via undo/redo.
350 351 cursor = self._get_end_cursor()
351 352 cursor.beginEditBlock()
352 353 cursor.insertText('\n')
353 354 self._insert_continuation_prompt(cursor)
354 355 cursor.endEditBlock()
355 356
356 357 # Do not do this inside the edit block. It works as expected
357 358 # when using a QPlainTextEdit control, but does not have an
358 359 # effect when using a QTextEdit. I believe this is a Qt bug.
359 360 self._control.moveCursor(QtGui.QTextCursor.End)
360 361
361 362 return complete
362 363
363 364 def _get_input_buffer(self):
364 365 """ The text that the user has entered entered at the current prompt.
365 366 """
366 367 # If we're executing, the input buffer may not even exist anymore due to
367 368 # the limit imposed by 'buffer_size'. Therefore, we store it.
368 369 if self._executing:
369 370 return self._executing_input_buffer
370 371
371 372 cursor = self._get_end_cursor()
372 373 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
373 374 input_buffer = str(cursor.selection().toPlainText())
374 375
375 376 # Strip out continuation prompts.
376 377 return input_buffer.replace('\n' + self._continuation_prompt, '\n')
377 378
378 379 def _set_input_buffer(self, string):
379 380 """ Replaces the text in the input buffer with 'string'.
380 381 """
381 382 # For now, it is an error to modify the input buffer during execution.
382 383 if self._executing:
383 384 raise RuntimeError("Cannot change input buffer during execution.")
384 385
385 386 # Remove old text.
386 387 cursor = self._get_end_cursor()
387 388 cursor.beginEditBlock()
388 389 cursor.setPosition(self._prompt_pos, QtGui.QTextCursor.KeepAnchor)
389 390 cursor.removeSelectedText()
390 391
391 392 # Insert new text with continuation prompts.
392 393 lines = string.splitlines(True)
393 394 if lines:
394 395 self._append_plain_text(lines[0])
395 396 for i in xrange(1, len(lines)):
396 397 if self._continuation_prompt_html is None:
397 398 self._append_plain_text(self._continuation_prompt)
398 399 else:
399 400 self._append_html(self._continuation_prompt_html)
400 401 self._append_plain_text(lines[i])
401 402 cursor.endEditBlock()
402 403 self._control.moveCursor(QtGui.QTextCursor.End)
403 404
404 405 input_buffer = property(_get_input_buffer, _set_input_buffer)
405 406
406 407 def _get_font(self):
407 408 """ The base font being used by the ConsoleWidget.
408 409 """
409 410 return self._control.document().defaultFont()
410 411
411 412 def _set_font(self, font):
412 413 """ Sets the base font for the ConsoleWidget to the specified QFont.
413 414 """
414 415 font_metrics = QtGui.QFontMetrics(font)
415 416 self._control.setTabStopWidth(self.tab_width * font_metrics.width(' '))
416 417
417 418 self._completion_widget.setFont(font)
418 419 self._control.document().setDefaultFont(font)
419 420 if self._page_control:
420 421 self._page_control.document().setDefaultFont(font)
421 422
422 423 font = property(_get_font, _set_font)
423 424
424 425 def paste(self, mode=QtGui.QClipboard.Clipboard):
425 426 """ Paste the contents of the clipboard into the input region.
426 427
427 428 Parameters:
428 429 -----------
429 430 mode : QClipboard::Mode, optional [default QClipboard::Clipboard]
430 431
431 432 Controls which part of the system clipboard is used. This can be
432 433 used to access the selection clipboard in X11 and the Find buffer
433 434 in Mac OS. By default, the regular clipboard is used.
434 435 """
435 436 if self._control.textInteractionFlags() & QtCore.Qt.TextEditable:
436 437 try:
437 438 text = str(QtGui.QApplication.clipboard().text(mode))
438 439 except UnicodeEncodeError:
439 440 pass
440 441 else:
441 442 self._insert_plain_text_into_buffer(dedent(text))
442 443
443 444 def print_(self, printer):
444 445 """ Print the contents of the ConsoleWidget to the specified QPrinter.
445 446 """
446 447 self._control.print_(printer)
447 448
448 449 def redo(self):
449 450 """ Redo the last operation. If there is no operation to redo, nothing
450 451 happens.
451 452 """
452 453 self._control.redo()
453 454
454 455 def reset_font(self):
455 456 """ Sets the font to the default fixed-width font for this platform.
456 457 """
457 458 if sys.platform == 'win32':
458 459 # FIXME: we should test whether Consolas is available and use it
459 460 # first if it is. Consolas ships by default from Vista onwards,
460 461 # it's *vastly* more readable and prettier than Courier, and is
461 462 # often installed even on XP systems. So we should first check for
462 463 # it, and only fallback to Courier if absolutely necessary.
463 464 name = 'Courier'
464 465 elif sys.platform == 'darwin':
465 466 name = 'Monaco'
466 467 else:
467 468 name = 'Monospace'
468 469 font = QtGui.QFont(name, QtGui.qApp.font().pointSize())
469 470 font.setStyleHint(QtGui.QFont.TypeWriter)
470 471 self._set_font(font)
471 472
472 473 def select_all(self):
473 474 """ Selects all the text in the buffer.
474 475 """
475 476 self._control.selectAll()
476 477
477 478 def _get_tab_width(self):
478 479 """ The width (in terms of space characters) for tab characters.
479 480 """
480 481 return self._tab_width
481 482
482 483 def _set_tab_width(self, tab_width):
483 484 """ Sets the width (in terms of space characters) for tab characters.
484 485 """
485 486 font_metrics = QtGui.QFontMetrics(self.font)
486 487 self._control.setTabStopWidth(tab_width * font_metrics.width(' '))
487 488
488 489 self._tab_width = tab_width
489 490
490 491 tab_width = property(_get_tab_width, _set_tab_width)
491 492
492 493 def undo(self):
493 494 """ Undo the last operation. If there is no operation to undo, nothing
494 495 happens.
495 496 """
496 497 self._control.undo()
497 498
498 499 #---------------------------------------------------------------------------
499 500 # 'ConsoleWidget' abstract interface
500 501 #---------------------------------------------------------------------------
501 502
502 503 def _is_complete(self, source, interactive):
503 504 """ Returns whether 'source' can be executed. When triggered by an
504 505 Enter/Return key press, 'interactive' is True; otherwise, it is
505 506 False.
506 507 """
507 508 raise NotImplementedError
508 509
509 510 def _execute(self, source, hidden):
510 511 """ Execute 'source'. If 'hidden', do not show any output.
511 512 """
512 513 raise NotImplementedError
513 514
514 515 def _prompt_started_hook(self):
515 516 """ Called immediately after a new prompt is displayed.
516 517 """
517 518 pass
518 519
519 520 def _prompt_finished_hook(self):
520 521 """ Called immediately after a prompt is finished, i.e. when some input
521 522 will be processed and a new prompt displayed.
522 523 """
523 524 pass
524 525
525 526 def _up_pressed(self):
526 527 """ Called when the up key is pressed. Returns whether to continue
527 528 processing the event.
528 529 """
529 530 return True
530 531
531 532 def _down_pressed(self):
532 533 """ Called when the down key is pressed. Returns whether to continue
533 534 processing the event.
534 535 """
535 536 return True
536 537
537 538 def _tab_pressed(self):
538 539 """ Called when the tab key is pressed. Returns whether to continue
539 540 processing the event.
540 541 """
541 542 return False
542 543
543 544 #--------------------------------------------------------------------------
544 545 # 'ConsoleWidget' protected interface
545 546 #--------------------------------------------------------------------------
546 547
547 548 def _append_html(self, html):
548 549 """ Appends html at the end of the console buffer.
549 550 """
550 551 cursor = self._get_end_cursor()
551 552 self._insert_html(cursor, html)
552 553
553 554 def _append_html_fetching_plain_text(self, html):
554 555 """ Appends 'html', then returns the plain text version of it.
555 556 """
556 557 cursor = self._get_end_cursor()
557 558 return self._insert_html_fetching_plain_text(cursor, html)
558 559
559 560 def _append_plain_text(self, text):
560 561 """ Appends plain text at the end of the console buffer, processing
561 562 ANSI codes if enabled.
562 563 """
563 564 cursor = self._get_end_cursor()
564 565 self._insert_plain_text(cursor, text)
565 566
566 567 def _append_plain_text_keeping_prompt(self, text):
567 568 """ Writes 'text' after the current prompt, then restores the old prompt
568 569 with its old input buffer.
569 570 """
570 571 input_buffer = self.input_buffer
571 572 self._append_plain_text('\n')
572 573 self._prompt_finished()
573 574
574 575 self._append_plain_text(text)
575 576 self._show_prompt()
576 577 self.input_buffer = input_buffer
577 578
578 579 def _cancel_text_completion(self):
579 580 """ If text completion is progress, cancel it.
580 581 """
581 582 if self._text_completing_pos:
582 583 self._clear_temporary_buffer()
583 584 self._text_completing_pos = 0
584 585
585 586 def _clear_temporary_buffer(self):
586 587 """ Clears the "temporary text" buffer, i.e. all the text following
587 588 the prompt region.
588 589 """
589 590 # Select and remove all text below the input buffer.
590 591 cursor = self._get_prompt_cursor()
591 592 prompt = self._continuation_prompt.lstrip()
592 593 while cursor.movePosition(QtGui.QTextCursor.NextBlock):
593 594 temp_cursor = QtGui.QTextCursor(cursor)
594 595 temp_cursor.select(QtGui.QTextCursor.BlockUnderCursor)
595 596 text = str(temp_cursor.selection().toPlainText()).lstrip()
596 597 if not text.startswith(prompt):
597 598 break
598 599 else:
599 600 # We've reached the end of the input buffer and no text follows.
600 601 return
601 602 cursor.movePosition(QtGui.QTextCursor.Left) # Grab the newline.
602 603 cursor.movePosition(QtGui.QTextCursor.End,
603 604 QtGui.QTextCursor.KeepAnchor)
604 605 cursor.removeSelectedText()
605 606
606 607 # After doing this, we have no choice but to clear the undo/redo
607 608 # history. Otherwise, the text is not "temporary" at all, because it
608 609 # can be recalled with undo/redo. Unfortunately, Qt does not expose
609 610 # fine-grained control to the undo/redo system.
610 611 if self._control.isUndoRedoEnabled():
611 612 self._control.setUndoRedoEnabled(False)
612 613 self._control.setUndoRedoEnabled(True)
613 614
614 615 def _complete_with_items(self, cursor, items):
615 616 """ Performs completion with 'items' at the specified cursor location.
616 617 """
617 618 self._cancel_text_completion()
618 619
619 620 if len(items) == 1:
620 621 cursor.setPosition(self._control.textCursor().position(),
621 622 QtGui.QTextCursor.KeepAnchor)
622 623 cursor.insertText(items[0])
623 624
624 625 elif len(items) > 1:
625 626 current_pos = self._control.textCursor().position()
626 627 prefix = commonprefix(items)
627 628 if prefix:
628 629 cursor.setPosition(current_pos, QtGui.QTextCursor.KeepAnchor)
629 630 cursor.insertText(prefix)
630 631 current_pos = cursor.position()
631 632
632 633 if self.gui_completion:
633 634 cursor.movePosition(QtGui.QTextCursor.Left, n=len(prefix))
634 635 self._completion_widget.show_items(cursor, items)
635 636 else:
636 637 cursor.beginEditBlock()
637 638 self._append_plain_text('\n')
638 639 self._page(self._format_as_columns(items))
639 640 cursor.endEditBlock()
640 641
641 642 cursor.setPosition(current_pos)
642 643 self._control.moveCursor(QtGui.QTextCursor.End)
643 644 self._control.setTextCursor(cursor)
644 645 self._text_completing_pos = current_pos
645 646
646 647 def _control_key_down(self, modifiers, include_command=True):
647 648 """ Given a KeyboardModifiers flags object, return whether the Control
648 649 key is down.
649 650
650 651 Parameters:
651 652 -----------
652 653 include_command : bool, optional (default True)
653 654 Whether to treat the Command key as a (mutually exclusive) synonym
654 655 for Control when in Mac OS.
655 656 """
656 657 # Note that on Mac OS, ControlModifier corresponds to the Command key
657 658 # while MetaModifier corresponds to the Control key.
658 659 if sys.platform == 'darwin':
659 660 down = include_command and (modifiers & QtCore.Qt.ControlModifier)
660 661 return bool(down) ^ bool(modifiers & QtCore.Qt.MetaModifier)
661 662 else:
662 663 return bool(modifiers & QtCore.Qt.ControlModifier)
663 664
664 665 def _create_control(self):
665 666 """ Creates and connects the underlying text widget.
666 667 """
667 668 # Create the underlying control.
668 669 if self.kind == 'plain':
669 670 control = ConsolePlainTextEdit()
670 671 elif self.kind == 'rich':
671 672 control = ConsoleTextEdit()
672 673 control.setAcceptRichText(False)
673 674
674 675 # Install event filters. The filter on the viewport is needed for
675 676 # mouse events.
676 677 control.installEventFilter(self)
677 678 control.viewport().installEventFilter(self)
678 679
679 680 # Connect signals.
680 681 control.cursorPositionChanged.connect(self._cursor_position_changed)
681 682 control.customContextMenuRequested.connect(self._show_context_menu)
682 683 control.copyAvailable.connect(self.copy_available)
683 684 control.redoAvailable.connect(self.redo_available)
684 685 control.undoAvailable.connect(self.undo_available)
685 686
686 687 # Configure the control.
687 688 control.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
688 689 control.setReadOnly(True)
689 690 control.setUndoRedoEnabled(False)
690 691 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
691 692 return control
692 693
693 694 def _create_page_control(self):
694 695 """ Creates and connects the underlying paging widget.
695 696 """
696 697 control = ConsolePlainTextEdit()
697 698 control.installEventFilter(self)
698 699 control.setReadOnly(True)
699 700 control.setUndoRedoEnabled(False)
700 701 control.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
701 702 return control
702 703
703 704 def _event_filter_console_keypress(self, event):
704 705 """ Filter key events for the underlying text widget to create a
705 706 console-like interface.
706 707 """
707 708 intercepted = False
708 709 cursor = self._control.textCursor()
709 710 position = cursor.position()
710 711 key = event.key()
711 712 ctrl_down = self._control_key_down(event.modifiers())
712 713 alt_down = event.modifiers() & QtCore.Qt.AltModifier
713 714 shift_down = event.modifiers() & QtCore.Qt.ShiftModifier
714 715
715 716 if event.matches(QtGui.QKeySequence.Paste):
716 717 # Call our paste instead of the underlying text widget's.
717 718 self.paste()
718 719 intercepted = True
719 720
720 721 elif ctrl_down:
721 722 if key == QtCore.Qt.Key_G:
722 723 self._keyboard_quit()
723 724 intercepted = True
724 725
725 726 elif key == QtCore.Qt.Key_K:
726 727 if self._in_buffer(position):
727 728 cursor.movePosition(QtGui.QTextCursor.EndOfLine,
728 729 QtGui.QTextCursor.KeepAnchor)
729 730 if not cursor.hasSelection():
730 731 # Line deletion (remove continuation prompt)
731 732 cursor.movePosition(QtGui.QTextCursor.NextBlock,
732 733 QtGui.QTextCursor.KeepAnchor)
733 734 cursor.movePosition(QtGui.QTextCursor.Right,
734 735 QtGui.QTextCursor.KeepAnchor,
735 736 len(self._continuation_prompt))
736 737 cursor.removeSelectedText()
737 738 intercepted = True
738 739
739 740 elif key == QtCore.Qt.Key_L:
740 741 # It would be better to simply move the prompt block to the top
741 742 # of the control viewport. QPlainTextEdit has a private method
742 743 # to do this (setTopBlock), but it cannot be duplicated here
743 744 # because it requires access to the QTextControl that underlies
744 745 # both QPlainTextEdit and QTextEdit. In short, this can only be
745 746 # achieved by appending newlines after the prompt, which is a
746 747 # gigantic hack and likely to cause other problems.
747 748 self.clear()
748 749 intercepted = True
749 750
750 751 elif key == QtCore.Qt.Key_O:
751 752 if self._page_control and self._page_control.isVisible():
752 753 self._page_control.setFocus()
753 754 intercept = True
754 755
755 756 elif key == QtCore.Qt.Key_X:
756 757 # FIXME: Instead of disabling cut completely, only allow it
757 758 # when safe.
758 759 intercepted = True
759 760
760 761 elif key == QtCore.Qt.Key_Y:
761 762 self.paste()
762 763 intercepted = True
763 764
764 765 elif key in (QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
765 766 intercepted = True
766 767
767 768 elif alt_down:
768 769 if key == QtCore.Qt.Key_B:
769 770 self._set_cursor(self._get_word_start_cursor(position))
770 771 intercepted = True
771 772
772 773 elif key == QtCore.Qt.Key_F:
773 774 self._set_cursor(self._get_word_end_cursor(position))
774 775 intercepted = True
775 776
776 777 elif key == QtCore.Qt.Key_Backspace:
777 778 cursor = self._get_word_start_cursor(position)
778 779 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
779 780 cursor.removeSelectedText()
780 781 intercepted = True
781 782
782 783 elif key == QtCore.Qt.Key_D:
783 784 cursor = self._get_word_end_cursor(position)
784 785 cursor.setPosition(position, QtGui.QTextCursor.KeepAnchor)
785 786 cursor.removeSelectedText()
786 787 intercepted = True
787 788
788 789 elif key == QtCore.Qt.Key_Delete:
789 790 intercepted = True
790 791
791 792 elif key == QtCore.Qt.Key_Greater:
792 793 self._control.moveCursor(QtGui.QTextCursor.End)
793 794 intercepted = True
794 795
795 796 elif key == QtCore.Qt.Key_Less:
796 797 self._control.setTextCursor(self._get_prompt_cursor())
797 798 intercepted = True
798 799
799 800 else:
800 801 if key in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
801 802 intercepted = True
802 803
803 804 # Special handling when tab completing in text mode.
804 805 self._cancel_text_completion()
805 806
806 807 if self._in_buffer(position):
807 808 if self._reading:
808 809 self._append_plain_text('\n')
809 810 self._reading = False
810 811 if self._reading_callback:
811 812 self._reading_callback()
812 813
813 814 # If there is only whitespace after the cursor, execute.
814 815 # Otherwise, split the line with a continuation prompt.
815 816 elif not self._executing:
816 817 cursor.movePosition(QtGui.QTextCursor.End,
817 818 QtGui.QTextCursor.KeepAnchor)
818 819 at_end = cursor.selectedText().trimmed().isEmpty()
819 820 if at_end or shift_down:
820 821 self.execute(interactive = not shift_down)
821 822 else:
822 823 # Do this inside an edit block for clean undo/redo.
823 824 cursor.beginEditBlock()
824 825 cursor.setPosition(position)
825 826 cursor.insertText('\n')
826 827 self._insert_continuation_prompt(cursor)
827 828 cursor.endEditBlock()
828 829
829 830 # Ensure that the whole input buffer is visible.
830 831 # FIXME: This will not be usable if the input buffer
831 832 # is taller than the console widget.
832 833 self._control.moveCursor(QtGui.QTextCursor.End)
833 834 self._control.setTextCursor(cursor)
834 835
835 836 elif key == QtCore.Qt.Key_Escape:
836 837 self._keyboard_quit()
837 838 intercepted = True
838 839
839 840 elif key == QtCore.Qt.Key_Up:
840 841 if self._reading or not self._up_pressed():
841 842 intercepted = True
842 843 else:
843 844 prompt_line = self._get_prompt_cursor().blockNumber()
844 845 intercepted = cursor.blockNumber() <= prompt_line
845 846
846 847 elif key == QtCore.Qt.Key_Down:
847 848 if self._reading or not self._down_pressed():
848 849 intercepted = True
849 850 else:
850 851 end_line = self._get_end_cursor().blockNumber()
851 852 intercepted = cursor.blockNumber() == end_line
852 853
853 854 elif key == QtCore.Qt.Key_Tab:
854 855 if not self._reading:
855 856 intercepted = not self._tab_pressed()
856 857
857 858 elif key == QtCore.Qt.Key_Left:
858 859 intercepted = not self._in_buffer(position - 1)
859 860
860 861 elif key == QtCore.Qt.Key_Home:
861 862 start_line = cursor.blockNumber()
862 863 if start_line == self._get_prompt_cursor().blockNumber():
863 864 start_pos = self._prompt_pos
864 865 else:
865 866 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
866 867 QtGui.QTextCursor.KeepAnchor)
867 868 start_pos = cursor.position()
868 869 start_pos += len(self._continuation_prompt)
869 870 cursor.setPosition(position)
870 871 if shift_down and self._in_buffer(position):
871 872 cursor.setPosition(start_pos, QtGui.QTextCursor.KeepAnchor)
872 873 else:
873 874 cursor.setPosition(start_pos)
874 875 self._set_cursor(cursor)
875 876 intercepted = True
876 877
877 878 elif key == QtCore.Qt.Key_Backspace:
878 879
879 880 # Line deletion (remove continuation prompt)
880 881 line, col = cursor.blockNumber(), cursor.columnNumber()
881 882 if not self._reading and \
882 883 col == len(self._continuation_prompt) and \
883 884 line > self._get_prompt_cursor().blockNumber():
884 885 cursor.beginEditBlock()
885 886 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
886 887 QtGui.QTextCursor.KeepAnchor)
887 888 cursor.removeSelectedText()
888 889 cursor.deletePreviousChar()
889 890 cursor.endEditBlock()
890 891 intercepted = True
891 892
892 893 # Regular backwards deletion
893 894 else:
894 895 anchor = cursor.anchor()
895 896 if anchor == position:
896 897 intercepted = not self._in_buffer(position - 1)
897 898 else:
898 899 intercepted = not self._in_buffer(min(anchor, position))
899 900
900 901 elif key == QtCore.Qt.Key_Delete:
901 902
902 903 # Line deletion (remove continuation prompt)
903 904 if not self._reading and self._in_buffer(position) and \
904 905 cursor.atBlockEnd() and not cursor.hasSelection():
905 906 cursor.movePosition(QtGui.QTextCursor.NextBlock,
906 907 QtGui.QTextCursor.KeepAnchor)
907 908 cursor.movePosition(QtGui.QTextCursor.Right,
908 909 QtGui.QTextCursor.KeepAnchor,
909 910 len(self._continuation_prompt))
910 911 cursor.removeSelectedText()
911 912 intercepted = True
912 913
913 914 # Regular forwards deletion:
914 915 else:
915 916 anchor = cursor.anchor()
916 917 intercepted = (not self._in_buffer(anchor) or
917 918 not self._in_buffer(position))
918 919
919 920 # Don't move the cursor if control is down to allow copy-paste using
920 921 # the keyboard in any part of the buffer.
921 922 if not ctrl_down:
922 923 self._keep_cursor_in_buffer()
923 924
924 925 return intercepted
925 926
926 927 def _event_filter_page_keypress(self, event):
927 928 """ Filter key events for the paging widget to create console-like
928 929 interface.
929 930 """
930 931 key = event.key()
931 932 ctrl_down = self._control_key_down(event.modifiers())
932 933 alt_down = event.modifiers() & QtCore.Qt.AltModifier
933 934
934 935 if ctrl_down:
935 936 if key == QtCore.Qt.Key_O:
936 937 self._control.setFocus()
937 938 intercept = True
938 939
939 940 elif alt_down:
940 941 if key == QtCore.Qt.Key_Greater:
941 942 self._page_control.moveCursor(QtGui.QTextCursor.End)
942 943 intercepted = True
943 944
944 945 elif key == QtCore.Qt.Key_Less:
945 946 self._page_control.moveCursor(QtGui.QTextCursor.Start)
946 947 intercepted = True
947 948
948 949 elif key in (QtCore.Qt.Key_Q, QtCore.Qt.Key_Escape):
949 950 if self._splitter:
950 951 self._page_control.hide()
951 952 else:
952 953 self.layout().setCurrentWidget(self._control)
953 954 return True
954 955
955 956 elif key in (QtCore.Qt.Key_Enter, QtCore.Qt.Key_Return):
956 957 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
957 958 QtCore.Qt.Key_PageDown,
958 959 QtCore.Qt.NoModifier)
959 960 QtGui.qApp.sendEvent(self._page_control, new_event)
960 961 return True
961 962
962 963 elif key == QtCore.Qt.Key_Backspace:
963 964 new_event = QtGui.QKeyEvent(QtCore.QEvent.KeyPress,
964 965 QtCore.Qt.Key_PageUp,
965 966 QtCore.Qt.NoModifier)
966 967 QtGui.qApp.sendEvent(self._page_control, new_event)
967 968 return True
968 969
969 970 return False
970 971
971 972 def _format_as_columns(self, items, separator=' '):
972 973 """ Transform a list of strings into a single string with columns.
973 974
974 975 Parameters
975 976 ----------
976 977 items : sequence of strings
977 978 The strings to process.
978 979
979 980 separator : str, optional [default is two spaces]
980 981 The string that separates columns.
981 982
982 983 Returns
983 984 -------
984 985 The formatted string.
985 986 """
986 987 # Note: this code is adapted from columnize 0.3.2.
987 988 # See http://code.google.com/p/pycolumnize/
988 989
989 990 # Calculate the number of characters available.
990 991 width = self._control.viewport().width()
991 992 char_width = QtGui.QFontMetrics(self.font).width(' ')
992 993 displaywidth = max(10, (width / char_width) - 1)
993 994
994 995 # Some degenerate cases.
995 996 size = len(items)
996 997 if size == 0:
997 998 return '\n'
998 999 elif size == 1:
999 1000 return '%s\n' % str(items[0])
1000 1001
1001 1002 # Try every row count from 1 upwards
1002 1003 array_index = lambda nrows, row, col: nrows*col + row
1003 1004 for nrows in range(1, size):
1004 1005 ncols = (size + nrows - 1) // nrows
1005 1006 colwidths = []
1006 1007 totwidth = -len(separator)
1007 1008 for col in range(ncols):
1008 1009 # Get max column width for this column
1009 1010 colwidth = 0
1010 1011 for row in range(nrows):
1011 1012 i = array_index(nrows, row, col)
1012 1013 if i >= size: break
1013 1014 x = items[i]
1014 1015 colwidth = max(colwidth, len(x))
1015 1016 colwidths.append(colwidth)
1016 1017 totwidth += colwidth + len(separator)
1017 1018 if totwidth > displaywidth:
1018 1019 break
1019 1020 if totwidth <= displaywidth:
1020 1021 break
1021 1022
1022 1023 # The smallest number of rows computed and the max widths for each
1023 1024 # column has been obtained. Now we just have to format each of the rows.
1024 1025 string = ''
1025 1026 for row in range(nrows):
1026 1027 texts = []
1027 1028 for col in range(ncols):
1028 1029 i = row + nrows*col
1029 1030 if i >= size:
1030 1031 texts.append('')
1031 1032 else:
1032 1033 texts.append(items[i])
1033 1034 while texts and not texts[-1]:
1034 1035 del texts[-1]
1035 1036 for col in range(len(texts)):
1036 1037 texts[col] = texts[col].ljust(colwidths[col])
1037 1038 string += '%s\n' % str(separator.join(texts))
1038 1039 return string
1039 1040
1040 1041 def _get_block_plain_text(self, block):
1041 1042 """ Given a QTextBlock, return its unformatted text.
1042 1043 """
1043 1044 cursor = QtGui.QTextCursor(block)
1044 1045 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1045 1046 cursor.movePosition(QtGui.QTextCursor.EndOfBlock,
1046 1047 QtGui.QTextCursor.KeepAnchor)
1047 1048 return str(cursor.selection().toPlainText())
1048 1049
1049 1050 def _get_cursor(self):
1050 1051 """ Convenience method that returns a cursor for the current position.
1051 1052 """
1052 1053 return self._control.textCursor()
1053 1054
1054 1055 def _get_end_cursor(self):
1055 1056 """ Convenience method that returns a cursor for the last character.
1056 1057 """
1057 1058 cursor = self._control.textCursor()
1058 1059 cursor.movePosition(QtGui.QTextCursor.End)
1059 1060 return cursor
1060 1061
1061 1062 def _get_input_buffer_cursor_column(self):
1062 1063 """ Returns the column of the cursor in the input buffer, excluding the
1063 1064 contribution by the prompt, or -1 if there is no such column.
1064 1065 """
1065 1066 prompt = self._get_input_buffer_cursor_prompt()
1066 1067 if prompt is None:
1067 1068 return -1
1068 1069 else:
1069 1070 cursor = self._control.textCursor()
1070 1071 return cursor.columnNumber() - len(prompt)
1071 1072
1072 1073 def _get_input_buffer_cursor_line(self):
1073 1074 """ Returns line of the input buffer that contains the cursor, or None
1074 1075 if there is no such line.
1075 1076 """
1076 1077 prompt = self._get_input_buffer_cursor_prompt()
1077 1078 if prompt is None:
1078 1079 return None
1079 1080 else:
1080 1081 cursor = self._control.textCursor()
1081 1082 text = self._get_block_plain_text(cursor.block())
1082 1083 return text[len(prompt):]
1083 1084
1084 1085 def _get_input_buffer_cursor_prompt(self):
1085 1086 """ Returns the (plain text) prompt for line of the input buffer that
1086 1087 contains the cursor, or None if there is no such line.
1087 1088 """
1088 1089 if self._executing:
1089 1090 return None
1090 1091 cursor = self._control.textCursor()
1091 1092 if cursor.position() >= self._prompt_pos:
1092 1093 if cursor.blockNumber() == self._get_prompt_cursor().blockNumber():
1093 1094 return self._prompt
1094 1095 else:
1095 1096 return self._continuation_prompt
1096 1097 else:
1097 1098 return None
1098 1099
1099 1100 def _get_prompt_cursor(self):
1100 1101 """ Convenience method that returns a cursor for the prompt position.
1101 1102 """
1102 1103 cursor = self._control.textCursor()
1103 1104 cursor.setPosition(self._prompt_pos)
1104 1105 return cursor
1105 1106
1106 1107 def _get_selection_cursor(self, start, end):
1107 1108 """ Convenience method that returns a cursor with text selected between
1108 1109 the positions 'start' and 'end'.
1109 1110 """
1110 1111 cursor = self._control.textCursor()
1111 1112 cursor.setPosition(start)
1112 1113 cursor.setPosition(end, QtGui.QTextCursor.KeepAnchor)
1113 1114 return cursor
1114 1115
1115 1116 def _get_word_start_cursor(self, position):
1116 1117 """ Find the start of the word to the left the given position. If a
1117 1118 sequence of non-word characters precedes the first word, skip over
1118 1119 them. (This emulates the behavior of bash, emacs, etc.)
1119 1120 """
1120 1121 document = self._control.document()
1121 1122 position -= 1
1122 1123 while position >= self._prompt_pos and \
1123 1124 not document.characterAt(position).isLetterOrNumber():
1124 1125 position -= 1
1125 1126 while position >= self._prompt_pos and \
1126 1127 document.characterAt(position).isLetterOrNumber():
1127 1128 position -= 1
1128 1129 cursor = self._control.textCursor()
1129 1130 cursor.setPosition(position + 1)
1130 1131 return cursor
1131 1132
1132 1133 def _get_word_end_cursor(self, position):
1133 1134 """ Find the end of the word to the right the given position. If a
1134 1135 sequence of non-word characters precedes the first word, skip over
1135 1136 them. (This emulates the behavior of bash, emacs, etc.)
1136 1137 """
1137 1138 document = self._control.document()
1138 1139 end = self._get_end_cursor().position()
1139 1140 while position < end and \
1140 1141 not document.characterAt(position).isLetterOrNumber():
1141 1142 position += 1
1142 1143 while position < end and \
1143 1144 document.characterAt(position).isLetterOrNumber():
1144 1145 position += 1
1145 1146 cursor = self._control.textCursor()
1146 1147 cursor.setPosition(position)
1147 1148 return cursor
1148 1149
1149 1150 def _insert_continuation_prompt(self, cursor):
1150 1151 """ Inserts new continuation prompt using the specified cursor.
1151 1152 """
1152 1153 if self._continuation_prompt_html is None:
1153 1154 self._insert_plain_text(cursor, self._continuation_prompt)
1154 1155 else:
1155 1156 self._continuation_prompt = self._insert_html_fetching_plain_text(
1156 1157 cursor, self._continuation_prompt_html)
1157 1158
1158 1159 def _insert_html(self, cursor, html):
1159 1160 """ Inserts HTML using the specified cursor in such a way that future
1160 1161 formatting is unaffected.
1161 1162 """
1162 1163 cursor.beginEditBlock()
1163 1164 cursor.insertHtml(html)
1164 1165
1165 1166 # After inserting HTML, the text document "remembers" it's in "html
1166 1167 # mode", which means that subsequent calls adding plain text will result
1167 1168 # in unwanted formatting, lost tab characters, etc. The following code
1168 1169 # hacks around this behavior, which I consider to be a bug in Qt, by
1169 1170 # (crudely) resetting the document's style state.
1170 1171 cursor.movePosition(QtGui.QTextCursor.Left,
1171 1172 QtGui.QTextCursor.KeepAnchor)
1172 1173 if cursor.selection().toPlainText() == ' ':
1173 1174 cursor.removeSelectedText()
1174 1175 else:
1175 1176 cursor.movePosition(QtGui.QTextCursor.Right)
1176 1177 cursor.insertText(' ', QtGui.QTextCharFormat())
1177 1178 cursor.endEditBlock()
1178 1179
1179 1180 def _insert_html_fetching_plain_text(self, cursor, html):
1180 1181 """ Inserts HTML using the specified cursor, then returns its plain text
1181 1182 version.
1182 1183 """
1183 1184 cursor.beginEditBlock()
1184 1185 cursor.removeSelectedText()
1185 1186
1186 1187 start = cursor.position()
1187 1188 self._insert_html(cursor, html)
1188 1189 end = cursor.position()
1189 1190 cursor.setPosition(start, QtGui.QTextCursor.KeepAnchor)
1190 1191 text = str(cursor.selection().toPlainText())
1191 1192
1192 1193 cursor.setPosition(end)
1193 1194 cursor.endEditBlock()
1194 1195 return text
1195 1196
1196 1197 def _insert_plain_text(self, cursor, text):
1197 1198 """ Inserts plain text using the specified cursor, processing ANSI codes
1198 1199 if enabled.
1199 1200 """
1200 1201 cursor.beginEditBlock()
1201 1202 if self.ansi_codes:
1202 1203 for substring in self._ansi_processor.split_string(text):
1203 1204 for action in self._ansi_processor.actions:
1204 1205 if action.kind == 'erase' and action.area == 'screen':
1205 1206 cursor.select(QtGui.QTextCursor.Document)
1206 1207 cursor.removeSelectedText()
1207 1208 format = self._ansi_processor.get_format()
1208 1209 cursor.insertText(substring, format)
1209 1210 else:
1210 1211 cursor.insertText(text)
1211 1212 cursor.endEditBlock()
1212 1213
1213 1214 def _insert_plain_text_into_buffer(self, text):
1214 1215 """ Inserts text into the input buffer at the current cursor position,
1215 1216 ensuring that continuation prompts are inserted as necessary.
1216 1217 """
1217 1218 lines = str(text).splitlines(True)
1218 1219 if lines:
1219 1220 self._keep_cursor_in_buffer()
1220 1221 cursor = self._control.textCursor()
1221 1222 cursor.beginEditBlock()
1222 1223 cursor.insertText(lines[0])
1223 1224 for line in lines[1:]:
1224 1225 if self._continuation_prompt_html is None:
1225 1226 cursor.insertText(self._continuation_prompt)
1226 1227 else:
1227 1228 self._continuation_prompt = \
1228 1229 self._insert_html_fetching_plain_text(
1229 1230 cursor, self._continuation_prompt_html)
1230 1231 cursor.insertText(line)
1231 1232 cursor.endEditBlock()
1232 1233 self._control.setTextCursor(cursor)
1233 1234
1234 1235 def _in_buffer(self, position=None):
1235 1236 """ Returns whether the current cursor (or, if specified, a position) is
1236 1237 inside the editing region.
1237 1238 """
1238 1239 cursor = self._control.textCursor()
1239 1240 if position is None:
1240 1241 position = cursor.position()
1241 1242 else:
1242 1243 cursor.setPosition(position)
1243 1244 line = cursor.blockNumber()
1244 1245 prompt_line = self._get_prompt_cursor().blockNumber()
1245 1246 if line == prompt_line:
1246 1247 return position >= self._prompt_pos
1247 1248 elif line > prompt_line:
1248 1249 cursor.movePosition(QtGui.QTextCursor.StartOfBlock)
1249 1250 prompt_pos = cursor.position() + len(self._continuation_prompt)
1250 1251 return position >= prompt_pos
1251 1252 return False
1252 1253
1253 1254 def _keep_cursor_in_buffer(self):
1254 1255 """ Ensures that the cursor is inside the editing region. Returns
1255 1256 whether the cursor was moved.
1256 1257 """
1257 1258 moved = not self._in_buffer()
1258 1259 if moved:
1259 1260 cursor = self._control.textCursor()
1260 1261 cursor.movePosition(QtGui.QTextCursor.End)
1261 1262 self._control.setTextCursor(cursor)
1262 1263 return moved
1263 1264
1264 1265 def _keyboard_quit(self):
1265 1266 """ Cancels the current editing task ala Ctrl-G in Emacs.
1266 1267 """
1267 1268 if self._text_completing_pos:
1268 1269 self._cancel_text_completion()
1269 1270 else:
1270 1271 self.input_buffer = ''
1271 1272
1272 1273 def _page(self, text):
1273 1274 """ Displays text using the pager if it exceeds the height of the
1274 1275 visible area.
1275 1276 """
1276 1277 if self.paging == 'none':
1277 1278 self._append_plain_text(text)
1278 1279 else:
1279 1280 line_height = QtGui.QFontMetrics(self.font).height()
1280 1281 minlines = self._control.viewport().height() / line_height
1281 1282 if re.match("(?:[^\n]*\n){%i}" % minlines, text):
1282 1283 if self.paging == 'custom':
1283 1284 self.custom_page_requested.emit(text)
1284 1285 else:
1285 1286 self._page_control.clear()
1286 1287 cursor = self._page_control.textCursor()
1287 1288 self._insert_plain_text(cursor, text)
1288 1289 self._page_control.moveCursor(QtGui.QTextCursor.Start)
1289 1290
1290 1291 self._page_control.viewport().resize(self._control.size())
1291 1292 if self._splitter:
1292 1293 self._page_control.show()
1293 1294 self._page_control.setFocus()
1294 1295 else:
1295 1296 self.layout().setCurrentWidget(self._page_control)
1296 1297 else:
1297 1298 self._append_plain_text(text)
1298 1299
1299 1300 def _prompt_started(self):
1300 1301 """ Called immediately after a new prompt is displayed.
1301 1302 """
1302 1303 # Temporarily disable the maximum block count to permit undo/redo and
1303 1304 # to ensure that the prompt position does not change due to truncation.
1304 1305 # Because setting this property clears the undo/redo history, we only
1305 1306 # set it if we have to.
1306 1307 if self._control.document().maximumBlockCount() > 0:
1307 1308 self._control.document().setMaximumBlockCount(0)
1308 1309 self._control.setUndoRedoEnabled(True)
1309 1310
1310 1311 self._control.setReadOnly(False)
1311 1312 self._control.moveCursor(QtGui.QTextCursor.End)
1312 1313
1313 1314 self._executing = False
1314 1315 self._prompt_started_hook()
1315 1316
1316 1317 def _prompt_finished(self):
1317 1318 """ Called immediately after a prompt is finished, i.e. when some input
1318 1319 will be processed and a new prompt displayed.
1319 1320 """
1320 1321 self._control.setReadOnly(True)
1321 1322 self._prompt_finished_hook()
1322 1323
1323 1324 def _readline(self, prompt='', callback=None):
1324 1325 """ Reads one line of input from the user.
1325 1326
1326 1327 Parameters
1327 1328 ----------
1328 1329 prompt : str, optional
1329 1330 The prompt to print before reading the line.
1330 1331
1331 1332 callback : callable, optional
1332 1333 A callback to execute with the read line. If not specified, input is
1333 1334 read *synchronously* and this method does not return until it has
1334 1335 been read.
1335 1336
1336 1337 Returns
1337 1338 -------
1338 1339 If a callback is specified, returns nothing. Otherwise, returns the
1339 1340 input string with the trailing newline stripped.
1340 1341 """
1341 1342 if self._reading:
1342 1343 raise RuntimeError('Cannot read a line. Widget is already reading.')
1343 1344
1344 1345 if not callback and not self.isVisible():
1345 1346 # If the user cannot see the widget, this function cannot return.
1346 1347 raise RuntimeError('Cannot synchronously read a line if the widget '
1347 1348 'is not visible!')
1348 1349
1349 1350 self._reading = True
1350 1351 self._show_prompt(prompt, newline=False)
1351 1352
1352 1353 if callback is None:
1353 1354 self._reading_callback = None
1354 1355 while self._reading:
1355 1356 QtCore.QCoreApplication.processEvents()
1356 1357 return self.input_buffer.rstrip('\n')
1357 1358
1358 1359 else:
1359 1360 self._reading_callback = lambda: \
1360 1361 callback(self.input_buffer.rstrip('\n'))
1361 1362
1362 1363 def _set_continuation_prompt(self, prompt, html=False):
1363 1364 """ Sets the continuation prompt.
1364 1365
1365 1366 Parameters
1366 1367 ----------
1367 1368 prompt : str
1368 1369 The prompt to show when more input is needed.
1369 1370
1370 1371 html : bool, optional (default False)
1371 1372 If set, the prompt will be inserted as formatted HTML. Otherwise,
1372 1373 the prompt will be treated as plain text, though ANSI color codes
1373 1374 will be handled.
1374 1375 """
1375 1376 if html:
1376 1377 self._continuation_prompt_html = prompt
1377 1378 else:
1378 1379 self._continuation_prompt = prompt
1379 1380 self._continuation_prompt_html = None
1380 1381
1381 1382 def _set_cursor(self, cursor):
1382 1383 """ Convenience method to set the current cursor.
1383 1384 """
1384 1385 self._control.setTextCursor(cursor)
1385 1386
1386 1387 def _show_context_menu(self, pos):
1387 1388 """ Shows a context menu at the given QPoint (in widget coordinates).
1388 1389 """
1389 1390 menu = QtGui.QMenu()
1390 1391
1391 1392 copy_action = menu.addAction('Copy', self.copy)
1392 1393 copy_action.setEnabled(self._get_cursor().hasSelection())
1393 1394 copy_action.setShortcut(QtGui.QKeySequence.Copy)
1394 1395
1395 1396 paste_action = menu.addAction('Paste', self.paste)
1396 1397 paste_action.setEnabled(self.can_paste())
1397 1398 paste_action.setShortcut(QtGui.QKeySequence.Paste)
1398 1399
1399 1400 menu.addSeparator()
1400 1401 menu.addAction('Select All', self.select_all)
1401 1402
1402 1403 menu.exec_(self._control.mapToGlobal(pos))
1403 1404
1404 1405 def _show_prompt(self, prompt=None, html=False, newline=True):
1405 1406 """ Writes a new prompt at the end of the buffer.
1406 1407
1407 1408 Parameters
1408 1409 ----------
1409 1410 prompt : str, optional
1410 1411 The prompt to show. If not specified, the previous prompt is used.
1411 1412
1412 1413 html : bool, optional (default False)
1413 1414 Only relevant when a prompt is specified. If set, the prompt will
1414 1415 be inserted as formatted HTML. Otherwise, the prompt will be treated
1415 1416 as plain text, though ANSI color codes will be handled.
1416 1417
1417 1418 newline : bool, optional (default True)
1418 1419 If set, a new line will be written before showing the prompt if
1419 1420 there is not already a newline at the end of the buffer.
1420 1421 """
1421 1422 # Insert a preliminary newline, if necessary.
1422 1423 if newline:
1423 1424 cursor = self._get_end_cursor()
1424 1425 if cursor.position() > 0:
1425 1426 cursor.movePosition(QtGui.QTextCursor.Left,
1426 1427 QtGui.QTextCursor.KeepAnchor)
1427 1428 if str(cursor.selection().toPlainText()) != '\n':
1428 1429 self._append_plain_text('\n')
1429 1430
1430 1431 # Write the prompt.
1431 1432 self._append_plain_text(self._prompt_sep)
1432 1433 if prompt is None:
1433 1434 if self._prompt_html is None:
1434 1435 self._append_plain_text(self._prompt)
1435 1436 else:
1436 1437 self._append_html(self._prompt_html)
1437 1438 else:
1438 1439 if html:
1439 1440 self._prompt = self._append_html_fetching_plain_text(prompt)
1440 1441 self._prompt_html = prompt
1441 1442 else:
1442 1443 self._append_plain_text(prompt)
1443 1444 self._prompt = prompt
1444 1445 self._prompt_html = None
1445 1446
1446 1447 self._prompt_pos = self._get_end_cursor().position()
1447 1448 self._prompt_started()
1448 1449
1449 1450 #------ Signal handlers ----------------------------------------------------
1450 1451
1451 1452 def _cursor_position_changed(self):
1452 1453 """ Clears the temporary buffer based on the cursor position.
1453 1454 """
1454 1455 if self._text_completing_pos:
1455 1456 document = self._control.document()
1456 1457 if self._text_completing_pos < document.characterCount():
1457 1458 cursor = self._control.textCursor()
1458 1459 pos = cursor.position()
1459 1460 text_cursor = self._control.textCursor()
1460 1461 text_cursor.setPosition(self._text_completing_pos)
1461 1462 if pos < self._text_completing_pos or \
1462 1463 cursor.blockNumber() > text_cursor.blockNumber():
1463 1464 self._clear_temporary_buffer()
1464 1465 self._text_completing_pos = 0
1465 1466 else:
1466 1467 self._clear_temporary_buffer()
1467 1468 self._text_completing_pos = 0
1468 1469
1469 1470
1470 1471 class HistoryConsoleWidget(ConsoleWidget):
1471 1472 """ A ConsoleWidget that keeps a history of the commands that have been
1472 1473 executed.
1473 1474 """
1474 1475
1475 1476 #---------------------------------------------------------------------------
1476 1477 # 'object' interface
1477 1478 #---------------------------------------------------------------------------
1478 1479
1479 1480 def __init__(self, *args, **kw):
1480 1481 super(HistoryConsoleWidget, self).__init__(*args, **kw)
1481 1482 self._history = []
1482 1483 self._history_index = 0
1483 1484
1484 1485 #---------------------------------------------------------------------------
1485 1486 # 'ConsoleWidget' public interface
1486 1487 #---------------------------------------------------------------------------
1487 1488
1488 1489 def execute(self, source=None, hidden=False, interactive=False):
1489 1490 """ Reimplemented to the store history.
1490 1491 """
1491 1492 if not hidden:
1492 1493 history = self.input_buffer if source is None else source
1493 1494
1494 1495 executed = super(HistoryConsoleWidget, self).execute(
1495 1496 source, hidden, interactive)
1496 1497
1497 1498 if executed and not hidden:
1498 1499 # Save the command unless it was a blank line.
1499 1500 history = history.rstrip()
1500 1501 if history:
1501 1502 self._history.append(history)
1502 1503 self._history_index = len(self._history)
1503 1504
1504 1505 return executed
1505 1506
1506 1507 #---------------------------------------------------------------------------
1507 1508 # 'ConsoleWidget' abstract interface
1508 1509 #---------------------------------------------------------------------------
1509 1510
1510 1511 def _up_pressed(self):
1511 1512 """ Called when the up key is pressed. Returns whether to continue
1512 1513 processing the event.
1513 1514 """
1514 1515 prompt_cursor = self._get_prompt_cursor()
1515 1516 if self._get_cursor().blockNumber() == prompt_cursor.blockNumber():
1516 1517 self.history_previous()
1517 1518
1518 1519 # Go to the first line of prompt for seemless history scrolling.
1519 1520 cursor = self._get_prompt_cursor()
1520 1521 cursor.movePosition(QtGui.QTextCursor.EndOfLine)
1521 1522 self._set_cursor(cursor)
1522 1523
1523 1524 return False
1524 1525 return True
1525 1526
1526 1527 def _down_pressed(self):
1527 1528 """ Called when the down key is pressed. Returns whether to continue
1528 1529 processing the event.
1529 1530 """
1530 1531 end_cursor = self._get_end_cursor()
1531 1532 if self._get_cursor().blockNumber() == end_cursor.blockNumber():
1532 1533 self.history_next()
1533 1534 return False
1534 1535 return True
1535 1536
1536 1537 #---------------------------------------------------------------------------
1537 1538 # 'HistoryConsoleWidget' public interface
1538 1539 #---------------------------------------------------------------------------
1539 1540
1540 1541 def history_previous(self):
1541 1542 """ If possible, set the input buffer to the previous item in the
1542 1543 history.
1543 1544 """
1544 1545 if self._history_index > 0:
1545 1546 self._history_index -= 1
1546 1547 self.input_buffer = self._history[self._history_index]
1547 1548
1548 1549 def history_next(self):
1549 1550 """ Set the input buffer to the next item in the history, or a blank
1550 1551 line if there is no subsequent item.
1551 1552 """
1552 1553 if self._history_index < len(self._history):
1553 1554 self._history_index += 1
1554 1555 if self._history_index < len(self._history):
1555 1556 self.input_buffer = self._history[self._history_index]
1556 1557 else:
1557 1558 self.input_buffer = ''
1558 1559
1559 1560 #---------------------------------------------------------------------------
1560 1561 # 'HistoryConsoleWidget' protected interface
1561 1562 #---------------------------------------------------------------------------
1562 1563
1563 1564 def _set_history(self, history):
1564 1565 """ Replace the current history with a sequence of history items.
1565 1566 """
1566 1567 self._history = list(history)
1567 1568 self._history_index = len(self._history)
@@ -1,512 +1,467 b''
1 1 # Standard library imports
2 2 from collections import namedtuple
3 3 import signal
4 4 import sys
5 import time
6 5
7 6 # System library imports
8 7 from pygments.lexers import PythonLexer
9 8 from PyQt4 import QtCore, QtGui
10 9
11 10 # Local imports
12 11 from IPython.core.inputsplitter import InputSplitter
13 12 from IPython.frontend.qt.base_frontend_mixin import BaseFrontendMixin
14 13 from IPython.utils.traitlets import Bool
15 14 from bracket_matcher import BracketMatcher
16 15 from call_tip_widget import CallTipWidget
17 16 from completion_lexer import CompletionLexer
18 17 from console_widget import HistoryConsoleWidget
19 18 from pygments_highlighter import PygmentsHighlighter
20 19
21 20
22 21 class FrontendHighlighter(PygmentsHighlighter):
23 22 """ A PygmentsHighlighter that can be turned on and off and that ignores
24 23 prompts.
25 24 """
26 25
27 26 def __init__(self, frontend):
28 27 super(FrontendHighlighter, self).__init__(frontend._control.document())
29 28 self._current_offset = 0
30 29 self._frontend = frontend
31 30 self.highlighting_on = False
32 31
33 32 def highlightBlock(self, qstring):
34 33 """ Highlight a block of text. Reimplemented to highlight selectively.
35 34 """
36 35 if not self.highlighting_on:
37 36 return
38 37
39 38 # The input to this function is unicode string that may contain
40 39 # paragraph break characters, non-breaking spaces, etc. Here we acquire
41 40 # the string as plain text so we can compare it.
42 41 current_block = self.currentBlock()
43 42 string = self._frontend._get_block_plain_text(current_block)
44 43
45 44 # Decide whether to check for the regular or continuation prompt.
46 45 if current_block.contains(self._frontend._prompt_pos):
47 46 prompt = self._frontend._prompt
48 47 else:
49 48 prompt = self._frontend._continuation_prompt
50 49
51 50 # Don't highlight the part of the string that contains the prompt.
52 51 if string.startswith(prompt):
53 52 self._current_offset = len(prompt)
54 53 qstring.remove(0, len(prompt))
55 54 else:
56 55 self._current_offset = 0
57 56
58 57 PygmentsHighlighter.highlightBlock(self, qstring)
59 58
60 59 def rehighlightBlock(self, block):
61 60 """ Reimplemented to temporarily enable highlighting if disabled.
62 61 """
63 62 old = self.highlighting_on
64 63 self.highlighting_on = True
65 64 super(FrontendHighlighter, self).rehighlightBlock(block)
66 65 self.highlighting_on = old
67 66
68 67 def setFormat(self, start, count, format):
69 68 """ Reimplemented to highlight selectively.
70 69 """
71 70 start += self._current_offset
72 71 PygmentsHighlighter.setFormat(self, start, count, format)
73 72
74 73
75 74 class FrontendWidget(HistoryConsoleWidget, BaseFrontendMixin):
76 75 """ A Qt frontend for a generic Python kernel.
77 76 """
78 77
79 78 # An option and corresponding signal for overriding the default kernel
80 79 # interrupt behavior.
81 80 custom_interrupt = Bool(False)
82 81 custom_interrupt_requested = QtCore.pyqtSignal()
83 82
84 83 # An option and corresponding signals for overriding the default kernel
85 84 # restart behavior.
86 85 custom_restart = Bool(False)
87 86 custom_restart_kernel_died = QtCore.pyqtSignal(float)
88 87 custom_restart_requested = QtCore.pyqtSignal()
89 88
90 89 # Emitted when an 'execute_reply' has been received from the kernel and
91 90 # processed by the FrontendWidget.
92 91 executed = QtCore.pyqtSignal(object)
92
93 # Emitted when an exit request has been received from the kernel.
94 exit_requested = QtCore.pyqtSignal()
93 95
94 96 # Protected class variables.
95 97 _CallTipRequest = namedtuple('_CallTipRequest', ['id', 'pos'])
96 98 _CompletionRequest = namedtuple('_CompletionRequest', ['id', 'pos'])
97 99 _ExecutionRequest = namedtuple('_ExecutionRequest', ['id', 'kind'])
98 100 _input_splitter_class = InputSplitter
99 101
100 102 #---------------------------------------------------------------------------
101 103 # 'object' interface
102 104 #---------------------------------------------------------------------------
103 105
104 106 def __init__(self, *args, **kw):
105 107 super(FrontendWidget, self).__init__(*args, **kw)
106 108
107 109 # FrontendWidget protected variables.
108 110 self._bracket_matcher = BracketMatcher(self._control)
109 111 self._call_tip_widget = CallTipWidget(self._control)
110 112 self._completion_lexer = CompletionLexer(PythonLexer())
111 113 self._hidden = False
112 114 self._highlighter = FrontendHighlighter(self)
113 115 self._input_splitter = self._input_splitter_class(input_mode='block')
114 116 self._kernel_manager = None
115 117 self._possible_kernel_restart = False
116 118 self._request_info = {}
117 119
118 120 # Configure the ConsoleWidget.
119 121 self.tab_width = 4
120 122 self._set_continuation_prompt('... ')
121 123
122 124 # Connect signal handlers.
123 125 document = self._control.document()
124 126 document.contentsChange.connect(self._document_contents_change)
125 127
126 128 #---------------------------------------------------------------------------
127 129 # 'ConsoleWidget' abstract interface
128 130 #---------------------------------------------------------------------------
129 131
130 132 def _is_complete(self, source, interactive):
131 133 """ Returns whether 'source' can be completely processed and a new
132 134 prompt created. When triggered by an Enter/Return key press,
133 135 'interactive' is True; otherwise, it is False.
134 136 """
135 137 complete = self._input_splitter.push(source.expandtabs(4))
136 138 if interactive:
137 139 complete = not self._input_splitter.push_accepts_more()
138 140 return complete
139 141
140 def _execute(self, source, hidden, user_variables=None,
141 user_expressions=None):
142 def _execute(self, source, hidden):
142 143 """ Execute 'source'. If 'hidden', do not show any output.
143 144
144 145 See parent class :meth:`execute` docstring for full details.
145 146 """
146 # tmp code for testing, disable in real use with 'if 0'. Only delete
147 # this code once we have automated tests for these fields.
148 if 0:
149 user_variables = ['x', 'y', 'z']
150 user_expressions = {'sum' : '1+1',
151 'bad syntax' : 'klsdafj kasd f',
152 'bad call' : 'range("hi")',
153 'time' : 'time.time()',
154 }
155 # /end tmp code
156
157 # FIXME - user_variables/expressions are not visible in API above us.
158 msg_id = self.kernel_manager.xreq_channel.execute(source, hidden,
159 user_variables,
160 user_expressions)
147 msg_id = self.kernel_manager.xreq_channel.execute(source, hidden)
161 148 self._request_info['execute'] = self._ExecutionRequest(msg_id, 'user')
162 149 self._hidden = hidden
163 150
164 151 def _prompt_started_hook(self):
165 152 """ Called immediately after a new prompt is displayed.
166 153 """
167 154 if not self._reading:
168 155 self._highlighter.highlighting_on = True
169 156
170 157 def _prompt_finished_hook(self):
171 158 """ Called immediately after a prompt is finished, i.e. when some input
172 159 will be processed and a new prompt displayed.
173 160 """
174 161 if not self._reading:
175 162 self._highlighter.highlighting_on = False
176 163
177 164 def _tab_pressed(self):
178 165 """ Called when the tab key is pressed. Returns whether to continue
179 166 processing the event.
180 167 """
181 168 # Perform tab completion if:
182 169 # 1) The cursor is in the input buffer.
183 170 # 2) There is a non-whitespace character before the cursor.
184 171 text = self._get_input_buffer_cursor_line()
185 172 if text is None:
186 173 return False
187 174 complete = bool(text[:self._get_input_buffer_cursor_column()].strip())
188 175 if complete:
189 176 self._complete()
190 177 return not complete
191 178
192 179 #---------------------------------------------------------------------------
193 180 # 'ConsoleWidget' protected interface
194 181 #---------------------------------------------------------------------------
195 182
196 183 def _event_filter_console_keypress(self, event):
197 184 """ Reimplemented to allow execution interruption.
198 185 """
199 186 key = event.key()
200 187 if self._control_key_down(event.modifiers(), include_command=False):
201 188 if key == QtCore.Qt.Key_C and self._executing:
202 189 self.interrupt_kernel()
203 190 return True
204 191 elif key == QtCore.Qt.Key_Period:
205 192 message = 'Are you sure you want to restart the kernel?'
206 193 self.restart_kernel(message)
207 194 return True
208 195 return super(FrontendWidget, self)._event_filter_console_keypress(event)
209 196
210 197 def _insert_continuation_prompt(self, cursor):
211 198 """ Reimplemented for auto-indentation.
212 199 """
213 200 super(FrontendWidget, self)._insert_continuation_prompt(cursor)
214 201 spaces = self._input_splitter.indent_spaces
215 202 cursor.insertText('\t' * (spaces / self.tab_width))
216 203 cursor.insertText(' ' * (spaces % self.tab_width))
217 204
218 205 #---------------------------------------------------------------------------
219 206 # 'BaseFrontendMixin' abstract interface
220 207 #---------------------------------------------------------------------------
221 208
222 209 def _handle_complete_reply(self, rep):
223 210 """ Handle replies for tab completion.
224 211 """
225 212 cursor = self._get_cursor()
226 213 info = self._request_info.get('complete')
227 214 if info and info.id == rep['parent_header']['msg_id'] and \
228 215 info.pos == cursor.position():
229 216 text = '.'.join(self._get_context())
230 217 cursor.movePosition(QtGui.QTextCursor.Left, n=len(text))
231 218 self._complete_with_items(cursor, rep['content']['matches'])
232 219
233 220 def _handle_execute_reply(self, msg):
234 221 """ Handles replies for code execution.
235 222 """
236 223 info = self._request_info.get('execute')
237 224 if info and info.id == msg['parent_header']['msg_id'] and \
238 not self._hidden:
225 info.kind == 'user' and not self._hidden:
239 226 # Make sure that all output from the SUB channel has been processed
240 227 # before writing a new prompt.
241 228 self.kernel_manager.sub_channel.flush()
242 229
243 230 content = msg['content']
244 231 status = content['status']
245 232 if status == 'ok':
246 233 self._process_execute_ok(msg)
247 234 elif status == 'error':
248 235 self._process_execute_error(msg)
249 236 elif status == 'abort':
250 237 self._process_execute_abort(msg)
251 238
252 239 self._show_interpreter_prompt_for_reply(msg)
253 240 self.executed.emit(msg)
254 241
255 242 def _handle_input_request(self, msg):
256 243 """ Handle requests for raw_input.
257 244 """
258 245 if self._hidden:
259 246 raise RuntimeError('Request for raw input during hidden execution.')
260 247
261 248 # Make sure that all output from the SUB channel has been processed
262 249 # before entering readline mode.
263 250 self.kernel_manager.sub_channel.flush()
264 251
265 252 def callback(line):
266 253 self.kernel_manager.rep_channel.input(line)
267 254 self._readline(msg['content']['prompt'], callback=callback)
268 255
269 256 def _handle_kernel_died(self, since_last_heartbeat):
270 257 """ Handle the kernel's death by asking if the user wants to restart.
271 258 """
272 259 message = 'The kernel heartbeat has been inactive for %.2f ' \
273 260 'seconds. Do you want to restart the kernel? You may ' \
274 261 'first want to check the network connection.' % \
275 262 since_last_heartbeat
276 263 if self.custom_restart:
277 264 self.custom_restart_kernel_died.emit(since_last_heartbeat)
278 265 else:
279 266 self.restart_kernel(message)
280 267
281 268 def _handle_object_info_reply(self, rep):
282 269 """ Handle replies for call tips.
283 270 """
284 271 cursor = self._get_cursor()
285 272 info = self._request_info.get('call_tip')
286 273 if info and info.id == rep['parent_header']['msg_id'] and \
287 274 info.pos == cursor.position():
288 275 doc = rep['content']['docstring']
289 276 if doc:
290 277 self._call_tip_widget.show_docstring(doc)
291 278
292 279 def _handle_pyout(self, msg):
293 280 """ Handle display hook output.
294 281 """
295 282 if not self._hidden and self._is_from_this_session(msg):
296 283 self._append_plain_text(msg['content']['data'] + '\n')
297 284
298 285 def _handle_stream(self, msg):
299 286 """ Handle stdout, stderr, and stdin.
300 287 """
301 288 if not self._hidden and self._is_from_this_session(msg):
302 289 self._append_plain_text(msg['content']['data'])
303 290 self._control.moveCursor(QtGui.QTextCursor.End)
304 291
305 292 def _started_channels(self):
306 293 """ Called when the KernelManager channels have started listening or
307 294 when the frontend is assigned an already listening KernelManager.
308 295 """
309 296 self._control.clear()
310 297 self._append_plain_text(self._get_banner())
311 298 self._show_interpreter_prompt()
312 299
313 300 def _stopped_channels(self):
314 301 """ Called when the KernelManager channels have stopped listening or
315 302 when a listening KernelManager is removed from the frontend.
316 303 """
317 304 self._executing = self._reading = False
318 305 self._highlighter.highlighting_on = False
319 306
320 307 #---------------------------------------------------------------------------
321 308 # 'FrontendWidget' interface
322 309 #---------------------------------------------------------------------------
323 310
324 311 def execute_file(self, path, hidden=False):
325 312 """ Attempts to execute file with 'path'. If 'hidden', no output is
326 313 shown.
327 314 """
328 315 self.execute('execfile("%s")' % path, hidden=hidden)
329 316
330 317 def interrupt_kernel(self):
331 318 """ Attempts to interrupt the running kernel.
332 319 """
333 320 if self.custom_interrupt:
334 321 self.custom_interrupt_requested.emit()
335 322 elif self.kernel_manager.has_kernel:
336 323 self.kernel_manager.signal_kernel(signal.SIGINT)
337 324 else:
338 325 self._append_plain_text('Kernel process is either remote or '
339 326 'unspecified. Cannot interrupt.\n')
340 327
341 328 def restart_kernel(self, message):
342 329 """ Attempts to restart the running kernel.
343 330 """
344 331 # We want to make sure that if this dialog is already happening, that
345 332 # other signals don't trigger it again. This can happen when the
346 333 # kernel_died heartbeat signal is emitted and the user is slow to
347 334 # respond to the dialog.
348 335 if not self._possible_kernel_restart:
349 336 if self.custom_restart:
350 337 self.custom_restart_requested.emit()
351 338 elif self.kernel_manager.has_kernel:
352 339 # Setting this to True will prevent this logic from happening
353 340 # again until the current pass is completed.
354 341 self._possible_kernel_restart = True
355 342 buttons = QtGui.QMessageBox.Yes | QtGui.QMessageBox.No
356 343 result = QtGui.QMessageBox.question(self, 'Restart kernel?',
357 344 message, buttons)
358 345 if result == QtGui.QMessageBox.Yes:
359 346 try:
360 347 self.kernel_manager.restart_kernel()
361 348 except RuntimeError:
362 349 message = 'Kernel started externally. Cannot restart.\n'
363 350 self._append_plain_text(message)
364 351 else:
365 352 self._stopped_channels()
366 353 self._append_plain_text('Kernel restarting...\n')
367 354 self._show_interpreter_prompt()
368 355 # This might need to be moved to another location?
369 356 self._possible_kernel_restart = False
370 357 else:
371 358 self._append_plain_text('Kernel process is either remote or '
372 359 'unspecified. Cannot restart.\n')
373 360
374 def closeEvent(self, event):
375 reply = QtGui.QMessageBox.question(self, 'Python',
376 'Close console?', QtGui.QMessageBox.Yes, QtGui.QMessageBox.No)
377 if reply == QtGui.QMessageBox.Yes:
378 self.shutdown_kernel()
379 event.accept()
380 else:
381 event.ignore()
382
383 # Move elsewhere to a better location later, possibly rename
384 def shutdown_kernel(self):
385 # Send quit message to kernel. Once we implement kernel-side setattr,
386 # this should probably be done that way, but for now this will do.
387 self.kernel_manager.xreq_channel.execute(
388 'get_ipython().exit_now=True', silent=True)
389
390 # Don't send any additional kernel kill messages immediately, to give
391 # the kernel a chance to properly execute shutdown actions.
392 # Wait for at most 2s, check every 0.1s.
393 for i in range(20):
394 if self.kernel_manager.is_alive:
395 time.sleep(0.1)
396 else:
397 break
398 else:
399 # OK, we've waited long enough.
400 self.kernel_manager.kill_kernel()
401
402 # FIXME: This logic may not be quite right... Perhaps the quit call is
403 # made elsewhere by Qt...
404 QtGui.QApplication.quit()
405
406 361 #---------------------------------------------------------------------------
407 362 # 'FrontendWidget' protected interface
408 363 #---------------------------------------------------------------------------
409 364
410 365 def _call_tip(self):
411 366 """ Shows a call tip, if appropriate, at the current cursor location.
412 367 """
413 368 # Decide if it makes sense to show a call tip
414 369 cursor = self._get_cursor()
415 370 cursor.movePosition(QtGui.QTextCursor.Left)
416 371 if cursor.document().characterAt(cursor.position()).toAscii() != '(':
417 372 return False
418 373 context = self._get_context(cursor)
419 374 if not context:
420 375 return False
421 376
422 377 # Send the metadata request to the kernel
423 378 name = '.'.join(context)
424 379 msg_id = self.kernel_manager.xreq_channel.object_info(name)
425 380 pos = self._get_cursor().position()
426 381 self._request_info['call_tip'] = self._CallTipRequest(msg_id, pos)
427 382 return True
428 383
429 384 def _complete(self):
430 385 """ Performs completion at the current cursor location.
431 386 """
432 387 context = self._get_context()
433 388 if context:
434 389 # Send the completion request to the kernel
435 390 msg_id = self.kernel_manager.xreq_channel.complete(
436 391 '.'.join(context), # text
437 392 self._get_input_buffer_cursor_line(), # line
438 393 self._get_input_buffer_cursor_column(), # cursor_pos
439 394 self.input_buffer) # block
440 395 pos = self._get_cursor().position()
441 396 info = self._CompletionRequest(msg_id, pos)
442 397 self._request_info['complete'] = info
443 398
444 399 def _get_banner(self):
445 400 """ Gets a banner to display at the beginning of a session.
446 401 """
447 402 banner = 'Python %s on %s\nType "help", "copyright", "credits" or ' \
448 403 '"license" for more information.'
449 404 return banner % (sys.version, sys.platform)
450 405
451 406 def _get_context(self, cursor=None):
452 407 """ Gets the context for the specified cursor (or the current cursor
453 408 if none is specified).
454 409 """
455 410 if cursor is None:
456 411 cursor = self._get_cursor()
457 412 cursor.movePosition(QtGui.QTextCursor.StartOfBlock,
458 413 QtGui.QTextCursor.KeepAnchor)
459 414 text = str(cursor.selection().toPlainText())
460 415 return self._completion_lexer.get_context(text)
461 416
462 417 def _process_execute_abort(self, msg):
463 418 """ Process a reply for an aborted execution request.
464 419 """
465 420 self._append_plain_text("ERROR: execution aborted\n")
466 421
467 422 def _process_execute_error(self, msg):
468 423 """ Process a reply for an execution request that resulted in an error.
469 424 """
470 425 content = msg['content']
471 426 traceback = ''.join(content['traceback'])
472 427 self._append_plain_text(traceback)
473 428
474 429 def _process_execute_ok(self, msg):
475 430 """ Process a reply for a successful execution equest.
476 431 """
477 432 payload = msg['content']['payload']
478 433 for item in payload:
479 434 if not self._process_execute_payload(item):
480 435 warning = 'Received unknown payload of type %s\n'
481 436 self._append_plain_text(warning % repr(item['source']))
482 437
483 438 def _process_execute_payload(self, item):
484 439 """ Process a single payload item from the list of payload items in an
485 440 execution reply. Returns whether the payload was handled.
486 441 """
487 442 # The basic FrontendWidget doesn't handle payloads, as they are a
488 443 # mechanism for going beyond the standard Python interpreter model.
489 444 return False
490 445
491 446 def _show_interpreter_prompt(self):
492 447 """ Shows a prompt for the interpreter.
493 448 """
494 449 self._show_prompt('>>> ')
495 450
496 451 def _show_interpreter_prompt_for_reply(self, msg):
497 452 """ Shows a prompt for the interpreter given an 'execute_reply' message.
498 453 """
499 454 self._show_interpreter_prompt()
500 455
501 456 #------ Signal handlers ----------------------------------------------------
502 457
503 458 def _document_contents_change(self, position, removed, added):
504 459 """ Called whenever the document's content changes. Display a call tip
505 460 if appropriate.
506 461 """
507 462 # Calculate where the cursor should be *after* the change:
508 463 position += added
509 464
510 465 document = self._control.document()
511 466 if position == self._get_cursor().position():
512 467 self._call_tip()
@@ -1,436 +1,438 b''
1 1 """ A FrontendWidget that emulates the interface of the console IPython and
2 2 supports the additional functionality provided by the IPython kernel.
3 3
4 4 TODO: Add support for retrieving the system default editor. Requires code
5 5 paths for Windows (use the registry), Mac OS (use LaunchServices), and
6 6 Linux (use the xdg system).
7 7 """
8 8
9 9 #-----------------------------------------------------------------------------
10 10 # Imports
11 11 #-----------------------------------------------------------------------------
12 12
13 13 # Standard library imports
14 14 from collections import namedtuple
15 15 import re
16 16 from subprocess import Popen
17 17
18 18 # System library imports
19 19 from PyQt4 import QtCore, QtGui
20 20
21 21 # Local imports
22 22 from IPython.core.inputsplitter import IPythonInputSplitter
23 23 from IPython.core.usage import default_banner
24 24 from IPython.utils.traitlets import Bool, Str
25 25 from frontend_widget import FrontendWidget
26 26
27 27 #-----------------------------------------------------------------------------
28 28 # Constants
29 29 #-----------------------------------------------------------------------------
30 30
31 31 # The default light style sheet: black text on a white background.
32 32 default_light_style_sheet = '''
33 33 .error { color: red; }
34 34 .in-prompt { color: navy; }
35 35 .in-prompt-number { font-weight: bold; }
36 36 .out-prompt { color: darkred; }
37 37 .out-prompt-number { font-weight: bold; }
38 38 '''
39 39 default_light_syntax_style = 'default'
40 40
41 41 # The default dark style sheet: white text on a black background.
42 42 default_dark_style_sheet = '''
43 43 QPlainTextEdit, QTextEdit { background-color: black; color: white }
44 44 QFrame { border: 1px solid grey; }
45 45 .error { color: red; }
46 46 .in-prompt { color: lime; }
47 47 .in-prompt-number { color: lime; font-weight: bold; }
48 48 .out-prompt { color: red; }
49 49 .out-prompt-number { color: red; font-weight: bold; }
50 50 '''
51 51 default_dark_syntax_style = 'monokai'
52 52
53 53 # Default strings to build and display input and output prompts (and separators
54 54 # in between)
55 55 default_in_prompt = 'In [<span class="in-prompt-number">%i</span>]: '
56 56 default_out_prompt = 'Out[<span class="out-prompt-number">%i</span>]: '
57 57 default_input_sep = '\n'
58 58 default_output_sep = ''
59 59 default_output_sep2 = ''
60 60
61 61 #-----------------------------------------------------------------------------
62 62 # IPythonWidget class
63 63 #-----------------------------------------------------------------------------
64 64
65 65 class IPythonWidget(FrontendWidget):
66 66 """ A FrontendWidget for an IPython kernel.
67 67 """
68 68
69 69 # If set, the 'custom_edit_requested(str, int)' signal will be emitted when
70 70 # an editor is needed for a file. This overrides 'editor' and 'editor_line'
71 71 # settings.
72 72 custom_edit = Bool(False)
73 73 custom_edit_requested = QtCore.pyqtSignal(object, object)
74 74
75 75 # A command for invoking a system text editor. If the string contains a
76 76 # {filename} format specifier, it will be used. Otherwise, the filename will
77 77 # be appended to the end the command.
78 78 editor = Str('default', config=True)
79 79
80 80 # The editor command to use when a specific line number is requested. The
81 81 # string should contain two format specifiers: {line} and {filename}. If
82 82 # this parameter is not specified, the line number option to the %edit magic
83 83 # will be ignored.
84 84 editor_line = Str(config=True)
85 85
86 86 # A CSS stylesheet. The stylesheet can contain classes for:
87 87 # 1. Qt: QPlainTextEdit, QFrame, QWidget, etc
88 88 # 2. Pygments: .c, .k, .o, etc (see PygmentsHighlighter)
89 89 # 3. IPython: .error, .in-prompt, .out-prompt, etc
90 90 style_sheet = Str(config=True)
91 91
92 92 # If not empty, use this Pygments style for syntax highlighting. Otherwise,
93 93 # the style sheet is queried for Pygments style information.
94 94 syntax_style = Str(config=True)
95 95
96 96 # Prompts.
97 97 in_prompt = Str(default_in_prompt, config=True)
98 98 out_prompt = Str(default_out_prompt, config=True)
99 99 input_sep = Str(default_input_sep, config=True)
100 100 output_sep = Str(default_output_sep, config=True)
101 101 output_sep2 = Str(default_output_sep2, config=True)
102 102
103 103 # FrontendWidget protected class variables.
104 104 _input_splitter_class = IPythonInputSplitter
105 105
106 106 # IPythonWidget protected class variables.
107 107 _PromptBlock = namedtuple('_PromptBlock', ['block', 'length', 'number'])
108 108 _payload_source_edit = 'IPython.zmq.zmqshell.ZMQInteractiveShell.edit_magic'
109 109 _payload_source_exit = 'IPython.zmq.zmqshell.ZMQInteractiveShell.ask_exit'
110 110 _payload_source_page = 'IPython.zmq.page.page'
111 111
112 112 #---------------------------------------------------------------------------
113 113 # 'object' interface
114 114 #---------------------------------------------------------------------------
115 115
116 116 def __init__(self, *args, **kw):
117 117 super(IPythonWidget, self).__init__(*args, **kw)
118 118
119 119 # IPythonWidget protected variables.
120 self._previous_prompt_obj = None
121 self._payload_handlers = {
120 self._payload_handlers = {
122 121 self._payload_source_edit : self._handle_payload_edit,
123 122 self._payload_source_exit : self._handle_payload_exit,
124 self._payload_source_page : self._handle_payload_page,
125 }
123 self._payload_source_page : self._handle_payload_page }
124 self._previous_prompt_obj = None
126 125
127 126 # Initialize widget styling.
128 127 if self.style_sheet:
129 128 self._style_sheet_changed()
130 129 self._syntax_style_changed()
131 130 else:
132 131 self.set_default_style()
133 132
134 133 #---------------------------------------------------------------------------
135 134 # 'BaseFrontendMixin' abstract interface
136 135 #---------------------------------------------------------------------------
137 136
138 137 def _handle_complete_reply(self, rep):
139 138 """ Reimplemented to support IPython's improved completion machinery.
140 139 """
141 140 cursor = self._get_cursor()
142 141 info = self._request_info.get('complete')
143 142 if info and info.id == rep['parent_header']['msg_id'] and \
144 143 info.pos == cursor.position():
145 144 matches = rep['content']['matches']
146 145 text = rep['content']['matched_text']
147 146 offset = len(text)
148 147
149 148 # Clean up matches with period and path separators if the matched
150 149 # text has not been transformed. This is done by truncating all
151 150 # but the last component and then suitably decreasing the offset
152 151 # between the current cursor position and the start of completion.
153 152 if len(matches) > 1 and matches[0][:offset] == text:
154 153 parts = re.split(r'[./\\]', text)
155 154 sep_count = len(parts) - 1
156 155 if sep_count:
157 156 chop_length = sum(map(len, parts[:sep_count])) + sep_count
158 157 matches = [ match[chop_length:] for match in matches ]
159 158 offset -= chop_length
160 159
161 160 # Move the cursor to the start of the match and complete.
162 161 cursor.movePosition(QtGui.QTextCursor.Left, n=offset)
163 162 self._complete_with_items(cursor, matches)
164 163
165 164 def _handle_execute_reply(self, msg):
166 165 """ Reimplemented to support prompt requests.
167 166 """
168 167 info = self._request_info.get('execute')
169 168 if info and info.id == msg['parent_header']['msg_id']:
170 169 if info.kind == 'prompt':
171 170 number = msg['content']['execution_count'] + 1
172 171 self._show_interpreter_prompt(number)
173 172 else:
174 173 super(IPythonWidget, self)._handle_execute_reply(msg)
175 174
176 175 def _handle_history_reply(self, msg):
177 176 """ Implemented to handle history replies, which are only supported by
178 177 the IPython kernel.
179 178 """
180 179 history_dict = msg['content']['history']
181 180 items = [ history_dict[key] for key in sorted(history_dict.keys()) ]
182 181 self._set_history(items)
183 182
184 183 def _handle_pyout(self, msg):
185 184 """ Reimplemented for IPython-style "display hook".
186 185 """
187 186 if not self._hidden and self._is_from_this_session(msg):
188 187 content = msg['content']
189 188 prompt_number = content['execution_count']
190 189 self._append_plain_text(self.output_sep)
191 190 self._append_html(self._make_out_prompt(prompt_number))
192 191 self._append_plain_text(content['data']+self.output_sep2)
193 192
194 193 def _started_channels(self):
195 194 """ Reimplemented to make a history request.
196 195 """
197 196 super(IPythonWidget, self)._started_channels()
198 197 # FIXME: Disabled until history requests are properly implemented.
199 198 #self.kernel_manager.xreq_channel.history(raw=True, output=False)
200 199
201 200 #---------------------------------------------------------------------------
202 201 # 'FrontendWidget' interface
203 202 #---------------------------------------------------------------------------
204 203
205 204 def execute_file(self, path, hidden=False):
206 205 """ Reimplemented to use the 'run' magic.
207 206 """
208 207 self.execute('%%run %s' % path, hidden=hidden)
209 208
210 209 #---------------------------------------------------------------------------
211 210 # 'FrontendWidget' protected interface
212 211 #---------------------------------------------------------------------------
213 212
214 213 def _complete(self):
215 214 """ Reimplemented to support IPython's improved completion machinery.
216 215 """
217 216 # We let the kernel split the input line, so we *always* send an empty
218 217 # text field. Readline-based frontends do get a real text field which
219 218 # they can use.
220 219 text = ''
221 220
222 221 # Send the completion request to the kernel
223 222 msg_id = self.kernel_manager.xreq_channel.complete(
224 223 text, # text
225 224 self._get_input_buffer_cursor_line(), # line
226 225 self._get_input_buffer_cursor_column(), # cursor_pos
227 226 self.input_buffer) # block
228 227 pos = self._get_cursor().position()
229 228 info = self._CompletionRequest(msg_id, pos)
230 229 self._request_info['complete'] = info
231 230
232 231 def _get_banner(self):
233 232 """ Reimplemented to return IPython's default banner.
234 233 """
235 234 return default_banner + '\n'
236 235
237 236 def _process_execute_error(self, msg):
238 237 """ Reimplemented for IPython-style traceback formatting.
239 238 """
240 239 content = msg['content']
241 240 traceback = '\n'.join(content['traceback']) + '\n'
242 241 if False:
243 242 # FIXME: For now, tracebacks come as plain text, so we can't use
244 243 # the html renderer yet. Once we refactor ultratb to produce
245 244 # properly styled tracebacks, this branch should be the default
246 245 traceback = traceback.replace(' ', '&nbsp;')
247 246 traceback = traceback.replace('\n', '<br/>')
248 247
249 248 ename = content['ename']
250 249 ename_styled = '<span class="error">%s</span>' % ename
251 250 traceback = traceback.replace(ename, ename_styled)
252 251
253 252 self._append_html(traceback)
254 253 else:
255 254 # This is the fallback for now, using plain text with ansi escapes
256 self._append_plain_text(traceback)
257
258 # Payload handlers with generic interface: each takes the opaque payload
259 # dict, unpacks it and calls the underlying functions with the necessary
260 # arguments
261 def _handle_payload_edit(self, item):
262 self._edit(item['filename'], item['line_number'])
263
264 def _handle_payload_exit(self, item):
265 QtCore.QCoreApplication.postEvent(self, QtGui.QCloseEvent())
266
267 def _handle_payload_page(self, item):
268 self._page(item['data'])
255 self._append_plain_text(traceback)
269 256
270 257 def _process_execute_payload(self, item):
271 """ Reimplemented to handle %edit and paging payloads.
258 """ Reimplemented to dispatch payloads to handler methods.
272 259 """
273 260 handler = self._payload_handlers.get(item['source'])
274 261 if handler is None:
275 262 # We have no handler for this type of payload, simply ignore it
276 263 return False
277 264 else:
278 265 handler(item)
279 266 return True
280 267
281 268 def _show_interpreter_prompt(self, number=None):
282 269 """ Reimplemented for IPython-style prompts.
283 270 """
284 271 # If a number was not specified, make a prompt number request.
285 272 if number is None:
286 273 msg_id = self.kernel_manager.xreq_channel.execute('', silent=True)
287 274 info = self._ExecutionRequest(msg_id, 'prompt')
288 275 self._request_info['execute'] = info
289 276 return
290 277
291 278 # Show a new prompt and save information about it so that it can be
292 279 # updated later if the prompt number turns out to be wrong.
293 280 self._prompt_sep = self.input_sep
294 281 self._show_prompt(self._make_in_prompt(number), html=True)
295 282 block = self._control.document().lastBlock()
296 283 length = len(self._prompt)
297 284 self._previous_prompt_obj = self._PromptBlock(block, length, number)
298 285
299 286 # Update continuation prompt to reflect (possibly) new prompt length.
300 287 self._set_continuation_prompt(
301 288 self._make_continuation_prompt(self._prompt), html=True)
302 289
303 290 def _show_interpreter_prompt_for_reply(self, msg):
304 291 """ Reimplemented for IPython-style prompts.
305 292 """
306 293 # Update the old prompt number if necessary.
307 294 content = msg['content']
308 295 previous_prompt_number = content['execution_count']
309 296 if self._previous_prompt_obj and \
310 297 self._previous_prompt_obj.number != previous_prompt_number:
311 298 block = self._previous_prompt_obj.block
312 299
313 300 # Make sure the prompt block has not been erased.
314 301 if block.isValid() and not block.text().isEmpty():
315 302
316 303 # Remove the old prompt and insert a new prompt.
317 304 cursor = QtGui.QTextCursor(block)
318 305 cursor.movePosition(QtGui.QTextCursor.Right,
319 306 QtGui.QTextCursor.KeepAnchor,
320 307 self._previous_prompt_obj.length)
321 308 prompt = self._make_in_prompt(previous_prompt_number)
322 309 self._prompt = self._insert_html_fetching_plain_text(
323 310 cursor, prompt)
324 311
325 312 # When the HTML is inserted, Qt blows away the syntax
326 313 # highlighting for the line, so we need to rehighlight it.
327 314 self._highlighter.rehighlightBlock(cursor.block())
328 315
329 316 self._previous_prompt_obj = None
330 317
331 318 # Show a new prompt with the kernel's estimated prompt number.
332 319 self._show_interpreter_prompt(previous_prompt_number+1)
333 320
334 321 #---------------------------------------------------------------------------
335 322 # 'IPythonWidget' interface
336 323 #---------------------------------------------------------------------------
337 324
338 325 def set_default_style(self, lightbg=True):
339 326 """ Sets the widget style to the class defaults.
340 327
341 328 Parameters:
342 329 -----------
343 330 lightbg : bool, optional (default True)
344 331 Whether to use the default IPython light background or dark
345 332 background style.
346 333 """
347 334 if lightbg:
348 335 self.style_sheet = default_light_style_sheet
349 336 self.syntax_style = default_light_syntax_style
350 337 else:
351 338 self.style_sheet = default_dark_style_sheet
352 339 self.syntax_style = default_dark_syntax_style
353 340
354 341 #---------------------------------------------------------------------------
355 342 # 'IPythonWidget' protected interface
356 343 #---------------------------------------------------------------------------
357 344
358 345 def _edit(self, filename, line=None):
359 346 """ Opens a Python script for editing.
360 347
361 348 Parameters:
362 349 -----------
363 350 filename : str
364 351 A path to a local system file.
365 352
366 353 line : int, optional
367 354 A line of interest in the file.
368 355 """
369 356 if self.custom_edit:
370 357 self.custom_edit_requested.emit(filename, line)
371 358 elif self.editor == 'default':
372 359 self._append_plain_text('No default editor available.\n')
373 360 else:
374 361 try:
375 362 filename = '"%s"' % filename
376 363 if line and self.editor_line:
377 364 command = self.editor_line.format(filename=filename,
378 365 line=line)
379 366 else:
380 367 try:
381 368 command = self.editor.format()
382 369 except KeyError:
383 370 command = self.editor.format(filename=filename)
384 371 else:
385 372 command += ' ' + filename
386 373 except KeyError:
387 374 self._append_plain_text('Invalid editor command.\n')
388 375 else:
389 376 try:
390 377 Popen(command, shell=True)
391 378 except OSError:
392 379 msg = 'Opening editor with command "%s" failed.\n'
393 380 self._append_plain_text(msg % command)
394 381
395 382 def _make_in_prompt(self, number):
396 383 """ Given a prompt number, returns an HTML In prompt.
397 384 """
398 385 body = self.in_prompt % number
399 386 return '<span class="in-prompt">%s</span>' % body
400 387
401 388 def _make_continuation_prompt(self, prompt):
402 389 """ Given a plain text version of an In prompt, returns an HTML
403 390 continuation prompt.
404 391 """
405 392 end_chars = '...: '
406 393 space_count = len(prompt.lstrip('\n')) - len(end_chars)
407 394 body = '&nbsp;' * space_count + end_chars
408 395 return '<span class="in-prompt">%s</span>' % body
409 396
410 397 def _make_out_prompt(self, number):
411 398 """ Given a prompt number, returns an HTML Out prompt.
412 399 """
413 400 body = self.out_prompt % number
414 401 return '<span class="out-prompt">%s</span>' % body
415 402
403 #------ Payload handlers --------------------------------------------------
404
405 # Payload handlers with a generic interface: each takes the opaque payload
406 # dict, unpacks it and calls the underlying functions with the necessary
407 # arguments.
408
409 def _handle_payload_edit(self, item):
410 self._edit(item['filename'], item['line_number'])
411
412 def _handle_payload_exit(self, item):
413 self.exit_requested.emit()
414
415 def _handle_payload_page(self, item):
416 self._page(item['data'])
417
416 418 #------ Trait change handlers ---------------------------------------------
417 419
418 420 def _style_sheet_changed(self):
419 421 """ Set the style sheets of the underlying widgets.
420 422 """
421 423 self.setStyleSheet(self.style_sheet)
422 424 self._control.document().setDefaultStyleSheet(self.style_sheet)
423 425 if self._page_control:
424 426 self._page_control.document().setDefaultStyleSheet(self.style_sheet)
425 427
426 428 bg_color = self._control.palette().background().color()
427 429 self._ansi_processor.set_background_color(bg_color)
428 430
429 431 def _syntax_style_changed(self):
430 432 """ Set the style for the syntax highlighter.
431 433 """
432 434 if self.syntax_style:
433 435 self._highlighter.set_style(self.syntax_style)
434 436 else:
435 437 self._highlighter.set_style_sheet(self.style_sheet)
436 438
@@ -1,100 +1,146 b''
1 1 """ A minimal application using the Qt console-style IPython frontend.
2 2 """
3 3
4 #-----------------------------------------------------------------------------
5 # Imports
6 #-----------------------------------------------------------------------------
7
4 8 # Systemm library imports
5 9 from PyQt4 import QtGui
6 10
7 11 # Local imports
8 12 from IPython.external.argparse import ArgumentParser
9 13 from IPython.frontend.qt.console.frontend_widget import FrontendWidget
10 14 from IPython.frontend.qt.console.ipython_widget import IPythonWidget
11 15 from IPython.frontend.qt.console.rich_ipython_widget import RichIPythonWidget
12 16 from IPython.frontend.qt.kernelmanager import QtKernelManager
13 17
18 #-----------------------------------------------------------------------------
14 19 # Constants
20 #-----------------------------------------------------------------------------
21
15 22 LOCALHOST = '127.0.0.1'
16 23
24 #-----------------------------------------------------------------------------
25 # Classes
26 #-----------------------------------------------------------------------------
27
28 class MainWindow(QtGui.QMainWindow):
29
30 #---------------------------------------------------------------------------
31 # 'object' interface
32 #---------------------------------------------------------------------------
33
34 def __init__(self, frontend):
35 """ Create a MainWindow for the specified FrontendWidget.
36 """
37 super(MainWindow, self).__init__()
38 self._frontend = frontend
39 self._frontend.exit_requested.connect(self.close)
40 self.setCentralWidget(frontend)
41
42 #---------------------------------------------------------------------------
43 # QWidget interface
44 #---------------------------------------------------------------------------
45
46 def closeEvent(self, event):
47 """ Reimplemented to prompt the user and close the kernel cleanly.
48 """
49 reply = QtGui.QMessageBox.question(self, self.window().windowTitle(),
50 'Close console?', QtGui.QMessageBox.Yes, QtGui.QMessageBox.No)
51 if reply == QtGui.QMessageBox.Yes:
52 self._frontend.kernel_manager.shutdown_kernel()
53 event.accept()
54 else:
55 event.ignore()
56
57 #-----------------------------------------------------------------------------
58 # Main entry point
59 #-----------------------------------------------------------------------------
17 60
18 61 def main():
19 62 """ Entry point for application.
20 63 """
21 64 # Parse command line arguments.
22 65 parser = ArgumentParser()
23 66 kgroup = parser.add_argument_group('kernel options')
24 67 kgroup.add_argument('-e', '--existing', action='store_true',
25 68 help='connect to an existing kernel')
26 69 kgroup.add_argument('--ip', type=str, default=LOCALHOST,
27 70 help='set the kernel\'s IP address [default localhost]')
28 71 kgroup.add_argument('--xreq', type=int, metavar='PORT', default=0,
29 72 help='set the XREQ channel port [default random]')
30 73 kgroup.add_argument('--sub', type=int, metavar='PORT', default=0,
31 74 help='set the SUB channel port [default random]')
32 75 kgroup.add_argument('--rep', type=int, metavar='PORT', default=0,
33 76 help='set the REP channel port [default random]')
34 77 kgroup.add_argument('--hb', type=int, metavar='PORT', default=0,
35 78 help='set the heartbeat port [default: random]')
36 79
37 80 egroup = kgroup.add_mutually_exclusive_group()
38 81 egroup.add_argument('--pure', action='store_true', help = \
39 82 'use a pure Python kernel instead of an IPython kernel')
40 83 egroup.add_argument('--pylab', type=str, metavar='GUI', nargs='?',
41 84 const='auto', help = \
42 85 "Pre-load matplotlib and numpy for interactive use. If GUI is not \
43 86 given, the GUI backend is matplotlib's, otherwise use one of: \
44 87 ['tk', 'gtk', 'qt', 'wx', 'payload-svg'].")
45 88
46 89 wgroup = parser.add_argument_group('widget options')
47 90 wgroup.add_argument('--paging', type=str, default='inside',
48 91 choices = ['inside', 'hsplit', 'vsplit', 'none'],
49 92 help='set the paging style [default inside]')
50 93 wgroup.add_argument('--rich', action='store_true',
51 94 help='enable rich text support')
52 95 wgroup.add_argument('--gui-completion', action='store_true',
53 96 help='use a GUI widget for tab completion')
54 97
55 98 args = parser.parse_args()
56 99
57 100 # Don't let Qt or ZMQ swallow KeyboardInterupts.
58 101 import signal
59 102 signal.signal(signal.SIGINT, signal.SIG_DFL)
60 103
61 104 # Create a KernelManager and start a kernel.
62 105 kernel_manager = QtKernelManager(xreq_address=(args.ip, args.xreq),
63 106 sub_address=(args.ip, args.sub),
64 107 rep_address=(args.ip, args.rep),
65 108 hb_address=(args.ip, args.hb))
66 109 if args.ip == LOCALHOST and not args.existing:
67 110 if args.pure:
68 111 kernel_manager.start_kernel(ipython=False)
69 112 elif args.pylab:
70 113 if args.rich:
71 114 kernel_manager.start_kernel(pylab='payload-svg')
72 115 else:
73 116 if args.pylab == 'auto':
74 117 kernel_manager.start_kernel(pylab='qt4')
75 118 else:
76 119 kernel_manager.start_kernel(pylab=args.pylab)
77 120 else:
78 121 kernel_manager.start_kernel()
79 122 kernel_manager.start_channels()
80 123
81 124 # Create the widget.
82 125 app = QtGui.QApplication([])
83 126 if args.pure:
84 127 kind = 'rich' if args.rich else 'plain'
85 128 widget = FrontendWidget(kind=kind, paging=args.paging)
86 129 elif args.rich:
87 130 widget = RichIPythonWidget(paging=args.paging)
88 131 else:
89 132 widget = IPythonWidget(paging=args.paging)
90 133 widget.gui_completion = args.gui_completion
91 134 widget.kernel_manager = kernel_manager
92 widget.setWindowTitle('Python' if args.pure else 'IPython')
93 widget.show()
135
136 # Create the main window.
137 window = MainWindow(widget)
138 window.setWindowTitle('Python' if args.pure else 'IPython')
139 window.show()
94 140
95 141 # Start the application main loop.
96 142 app.exec_()
97 143
98 144
99 145 if __name__ == '__main__':
100 146 main()
@@ -1,785 +1,806 b''
1 1 """Base classes to manage the interaction with a running kernel.
2 2
3 3 Todo
4 4 ====
5 5
6 6 * Create logger to handle debugging and console messages.
7 7 """
8 8
9 9 #-----------------------------------------------------------------------------
10 10 # Copyright (C) 2008-2010 The IPython Development Team
11 11 #
12 12 # Distributed under the terms of the BSD License. The full license is in
13 13 # the file COPYING, distributed as part of this software.
14 14 #-----------------------------------------------------------------------------
15 15
16 16 #-----------------------------------------------------------------------------
17 17 # Imports
18 18 #-----------------------------------------------------------------------------
19 19
20 20 # Standard library imports.
21 21 from Queue import Queue, Empty
22 22 from subprocess import Popen
23 23 from threading import Thread
24 24 import time
25 25
26 26 # System library imports.
27 27 import zmq
28 28 from zmq import POLLIN, POLLOUT, POLLERR
29 29 from zmq.eventloop import ioloop
30 30
31 31 # Local imports.
32 32 from IPython.utils import io
33 33 from IPython.utils.traitlets import HasTraits, Any, Instance, Type, TCPAddress
34 34 from session import Session
35 35
36 36 #-----------------------------------------------------------------------------
37 37 # Constants and exceptions
38 38 #-----------------------------------------------------------------------------
39 39
40 40 LOCALHOST = '127.0.0.1'
41 41
42 42 class InvalidPortNumber(Exception):
43 43 pass
44 44
45 45 #-----------------------------------------------------------------------------
46 46 # Utility functions
47 47 #-----------------------------------------------------------------------------
48 48
49 49 # some utilities to validate message structure, these might get moved elsewhere
50 50 # if they prove to have more generic utility
51 51
52 52 def validate_string_list(lst):
53 53 """Validate that the input is a list of strings.
54 54
55 55 Raises ValueError if not."""
56 56 if not isinstance(lst, list):
57 57 raise ValueError('input %r must be a list' % lst)
58 58 for x in lst:
59 59 if not isinstance(x, basestring):
60 60 raise ValueError('element %r in list must be a string' % x)
61 61
62 62
63 63 def validate_string_dict(dct):
64 64 """Validate that the input is a dict with string keys and values.
65 65
66 66 Raises ValueError if not."""
67 67 for k,v in dct.iteritems():
68 68 if not isinstance(k, basestring):
69 69 raise ValueError('key %r in dict must be a string' % k)
70 70 if not isinstance(v, basestring):
71 71 raise ValueError('value %r in dict must be a string' % v)
72 72
73 73
74 74 #-----------------------------------------------------------------------------
75 75 # ZMQ Socket Channel classes
76 76 #-----------------------------------------------------------------------------
77 77
78 78 class ZmqSocketChannel(Thread):
79 79 """The base class for the channels that use ZMQ sockets.
80 80 """
81 81 context = None
82 82 session = None
83 83 socket = None
84 84 ioloop = None
85 85 iostate = None
86 86 _address = None
87 87
88 88 def __init__(self, context, session, address):
89 89 """Create a channel
90 90
91 91 Parameters
92 92 ----------
93 93 context : :class:`zmq.Context`
94 94 The ZMQ context to use.
95 95 session : :class:`session.Session`
96 96 The session to use.
97 97 address : tuple
98 98 Standard (ip, port) tuple that the kernel is listening on.
99 99 """
100 100 super(ZmqSocketChannel, self).__init__()
101 101 self.daemon = True
102 102
103 103 self.context = context
104 104 self.session = session
105 105 if address[1] == 0:
106 106 message = 'The port number for a channel cannot be 0.'
107 107 raise InvalidPortNumber(message)
108 108 self._address = address
109 109
110 110 def stop(self):
111 111 """Stop the channel's activity.
112 112
113 113 This calls :method:`Thread.join` and returns when the thread
114 114 terminates. :class:`RuntimeError` will be raised if
115 115 :method:`self.start` is called again.
116 116 """
117 117 self.join()
118 118
119 119 @property
120 120 def address(self):
121 121 """Get the channel's address as an (ip, port) tuple.
122 122
123 123 By the default, the address is (localhost, 0), where 0 means a random
124 124 port.
125 125 """
126 126 return self._address
127 127
128 128 def add_io_state(self, state):
129 129 """Add IO state to the eventloop.
130 130
131 131 Parameters
132 132 ----------
133 133 state : zmq.POLLIN|zmq.POLLOUT|zmq.POLLERR
134 134 The IO state flag to set.
135 135
136 136 This is thread safe as it uses the thread safe IOLoop.add_callback.
137 137 """
138 138 def add_io_state_callback():
139 139 if not self.iostate & state:
140 140 self.iostate = self.iostate | state
141 141 self.ioloop.update_handler(self.socket, self.iostate)
142 142 self.ioloop.add_callback(add_io_state_callback)
143 143
144 144 def drop_io_state(self, state):
145 145 """Drop IO state from the eventloop.
146 146
147 147 Parameters
148 148 ----------
149 149 state : zmq.POLLIN|zmq.POLLOUT|zmq.POLLERR
150 150 The IO state flag to set.
151 151
152 152 This is thread safe as it uses the thread safe IOLoop.add_callback.
153 153 """
154 154 def drop_io_state_callback():
155 155 if self.iostate & state:
156 156 self.iostate = self.iostate & (~state)
157 157 self.ioloop.update_handler(self.socket, self.iostate)
158 158 self.ioloop.add_callback(drop_io_state_callback)
159 159
160 160
161 161 class XReqSocketChannel(ZmqSocketChannel):
162 162 """The XREQ channel for issues request/replies to the kernel.
163 163 """
164 164
165 165 command_queue = None
166 166
167 167 def __init__(self, context, session, address):
168 168 self.command_queue = Queue()
169 169 super(XReqSocketChannel, self).__init__(context, session, address)
170 170
171 171 def run(self):
172 172 """The thread's main activity. Call start() instead."""
173 173 self.socket = self.context.socket(zmq.XREQ)
174 174 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
175 175 self.socket.connect('tcp://%s:%i' % self.address)
176 176 self.ioloop = ioloop.IOLoop()
177 177 self.iostate = POLLERR|POLLIN
178 178 self.ioloop.add_handler(self.socket, self._handle_events,
179 179 self.iostate)
180 180 self.ioloop.start()
181 181
182 182 def stop(self):
183 183 self.ioloop.stop()
184 184 super(XReqSocketChannel, self).stop()
185 185
186 186 def call_handlers(self, msg):
187 187 """This method is called in the ioloop thread when a message arrives.
188 188
189 189 Subclasses should override this method to handle incoming messages.
190 190 It is important to remember that this method is called in the thread
191 191 so that some logic must be done to ensure that the application leve
192 192 handlers are called in the application thread.
193 193 """
194 194 raise NotImplementedError('call_handlers must be defined in a subclass.')
195 195
196 196 def execute(self, code, silent=False,
197 197 user_variables=None, user_expressions=None):
198 198 """Execute code in the kernel.
199 199
200 200 Parameters
201 201 ----------
202 202 code : str
203 203 A string of Python code.
204 204
205 205 silent : bool, optional (default False)
206 206 If set, the kernel will execute the code as quietly possible.
207 207
208 208 user_variables : list, optional
209 209 A list of variable names to pull from the user's namespace. They
210 210 will come back as a dict with these names as keys and their
211 211 :func:`repr` as values.
212 212
213 213 user_expressions : dict, optional
214 214 A dict with string keys and to pull from the user's
215 215 namespace. They will come back as a dict with these names as keys
216 216 and their :func:`repr` as values.
217 217
218 218 Returns
219 219 -------
220 220 The msg_id of the message sent.
221 221 """
222 222 if user_variables is None:
223 223 user_variables = []
224 224 if user_expressions is None:
225 225 user_expressions = {}
226 226
227 227 # Don't waste network traffic if inputs are invalid
228 228 if not isinstance(code, basestring):
229 229 raise ValueError('code %r must be a string' % code)
230 230 validate_string_list(user_variables)
231 231 validate_string_dict(user_expressions)
232 232
233 233 # Create class for content/msg creation. Related to, but possibly
234 234 # not in Session.
235 235 content = dict(code=code, silent=silent,
236 236 user_variables=user_variables,
237 237 user_expressions=user_expressions)
238 238 msg = self.session.msg('execute_request', content)
239 239 self._queue_request(msg)
240 240 return msg['header']['msg_id']
241 241
242 242 def complete(self, text, line, cursor_pos, block=None):
243 243 """Tab complete text in the kernel's namespace.
244 244
245 245 Parameters
246 246 ----------
247 247 text : str
248 248 The text to complete.
249 249 line : str
250 250 The full line of text that is the surrounding context for the
251 251 text to complete.
252 252 cursor_pos : int
253 253 The position of the cursor in the line where the completion was
254 254 requested.
255 255 block : str, optional
256 256 The full block of code in which the completion is being requested.
257 257
258 258 Returns
259 259 -------
260 260 The msg_id of the message sent.
261 261 """
262 262 content = dict(text=text, line=line, block=block, cursor_pos=cursor_pos)
263 263 msg = self.session.msg('complete_request', content)
264 264 self._queue_request(msg)
265 265 return msg['header']['msg_id']
266 266
267 267 def object_info(self, oname):
268 268 """Get metadata information about an object.
269 269
270 270 Parameters
271 271 ----------
272 272 oname : str
273 273 A string specifying the object name.
274 274
275 275 Returns
276 276 -------
277 277 The msg_id of the message sent.
278 278 """
279 279 content = dict(oname=oname)
280 280 msg = self.session.msg('object_info_request', content)
281 281 self._queue_request(msg)
282 282 return msg['header']['msg_id']
283 283
284 284 def history(self, index=None, raw=False, output=True):
285 285 """Get the history list.
286 286
287 287 Parameters
288 288 ----------
289 289 index : n or (n1, n2) or None
290 290 If n, then the last entries. If a tuple, then all in
291 291 range(n1, n2). If None, then all entries. Raises IndexError if
292 292 the format of index is incorrect.
293 293 raw : bool
294 294 If True, return the raw input.
295 295 output : bool
296 296 If True, then return the output as well.
297 297
298 298 Returns
299 299 -------
300 300 The msg_id of the message sent.
301 301 """
302 302 content = dict(index=index, raw=raw, output=output)
303 303 msg = self.session.msg('history_request', content)
304 304 self._queue_request(msg)
305 305 return msg['header']['msg_id']
306 306
307 307 def _handle_events(self, socket, events):
308 308 if events & POLLERR:
309 309 self._handle_err()
310 310 if events & POLLOUT:
311 311 self._handle_send()
312 312 if events & POLLIN:
313 313 self._handle_recv()
314 314
315 315 def _handle_recv(self):
316 316 msg = self.socket.recv_json()
317 317 self.call_handlers(msg)
318 318
319 319 def _handle_send(self):
320 320 try:
321 321 msg = self.command_queue.get(False)
322 322 except Empty:
323 323 pass
324 324 else:
325 325 self.socket.send_json(msg)
326 326 if self.command_queue.empty():
327 327 self.drop_io_state(POLLOUT)
328 328
329 329 def _handle_err(self):
330 330 # We don't want to let this go silently, so eventually we should log.
331 331 raise zmq.ZMQError()
332 332
333 333 def _queue_request(self, msg):
334 334 self.command_queue.put(msg)
335 335 self.add_io_state(POLLOUT)
336 336
337 337
338 338 class SubSocketChannel(ZmqSocketChannel):
339 339 """The SUB channel which listens for messages that the kernel publishes.
340 340 """
341 341
342 342 def __init__(self, context, session, address):
343 343 super(SubSocketChannel, self).__init__(context, session, address)
344 344
345 345 def run(self):
346 346 """The thread's main activity. Call start() instead."""
347 347 self.socket = self.context.socket(zmq.SUB)
348 348 self.socket.setsockopt(zmq.SUBSCRIBE,'')
349 349 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
350 350 self.socket.connect('tcp://%s:%i' % self.address)
351 351 self.ioloop = ioloop.IOLoop()
352 352 self.iostate = POLLIN|POLLERR
353 353 self.ioloop.add_handler(self.socket, self._handle_events,
354 354 self.iostate)
355 355 self.ioloop.start()
356 356
357 357 def stop(self):
358 358 self.ioloop.stop()
359 359 super(SubSocketChannel, self).stop()
360 360
361 361 def call_handlers(self, msg):
362 362 """This method is called in the ioloop thread when a message arrives.
363 363
364 364 Subclasses should override this method to handle incoming messages.
365 365 It is important to remember that this method is called in the thread
366 366 so that some logic must be done to ensure that the application leve
367 367 handlers are called in the application thread.
368 368 """
369 369 raise NotImplementedError('call_handlers must be defined in a subclass.')
370 370
371 371 def flush(self, timeout=1.0):
372 372 """Immediately processes all pending messages on the SUB channel.
373 373
374 374 Callers should use this method to ensure that :method:`call_handlers`
375 375 has been called for all messages that have been received on the
376 376 0MQ SUB socket of this channel.
377 377
378 378 This method is thread safe.
379 379
380 380 Parameters
381 381 ----------
382 382 timeout : float, optional
383 383 The maximum amount of time to spend flushing, in seconds. The
384 384 default is one second.
385 385 """
386 386 # We do the IOLoop callback process twice to ensure that the IOLoop
387 387 # gets to perform at least one full poll.
388 388 stop_time = time.time() + timeout
389 389 for i in xrange(2):
390 390 self._flushed = False
391 391 self.ioloop.add_callback(self._flush)
392 392 while not self._flushed and time.time() < stop_time:
393 393 time.sleep(0.01)
394 394
395 395 def _handle_events(self, socket, events):
396 396 # Turn on and off POLLOUT depending on if we have made a request
397 397 if events & POLLERR:
398 398 self._handle_err()
399 399 if events & POLLIN:
400 400 self._handle_recv()
401 401
402 402 def _handle_err(self):
403 403 # We don't want to let this go silently, so eventually we should log.
404 404 raise zmq.ZMQError()
405 405
406 406 def _handle_recv(self):
407 407 # Get all of the messages we can
408 408 while True:
409 409 try:
410 410 msg = self.socket.recv_json(zmq.NOBLOCK)
411 411 except zmq.ZMQError:
412 412 # Check the errno?
413 413 # Will this trigger POLLERR?
414 414 break
415 415 else:
416 416 self.call_handlers(msg)
417 417
418 418 def _flush(self):
419 419 """Callback for :method:`self.flush`."""
420 420 self._flushed = True
421 421
422 422
423 423 class RepSocketChannel(ZmqSocketChannel):
424 424 """A reply channel to handle raw_input requests that the kernel makes."""
425 425
426 426 msg_queue = None
427 427
428 428 def __init__(self, context, session, address):
429 429 self.msg_queue = Queue()
430 430 super(RepSocketChannel, self).__init__(context, session, address)
431 431
432 432 def run(self):
433 433 """The thread's main activity. Call start() instead."""
434 434 self.socket = self.context.socket(zmq.XREQ)
435 435 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
436 436 self.socket.connect('tcp://%s:%i' % self.address)
437 437 self.ioloop = ioloop.IOLoop()
438 438 self.iostate = POLLERR|POLLIN
439 439 self.ioloop.add_handler(self.socket, self._handle_events,
440 440 self.iostate)
441 441 self.ioloop.start()
442 442
443 443 def stop(self):
444 444 self.ioloop.stop()
445 445 super(RepSocketChannel, self).stop()
446 446
447 447 def call_handlers(self, msg):
448 448 """This method is called in the ioloop thread when a message arrives.
449 449
450 450 Subclasses should override this method to handle incoming messages.
451 451 It is important to remember that this method is called in the thread
452 452 so that some logic must be done to ensure that the application leve
453 453 handlers are called in the application thread.
454 454 """
455 455 raise NotImplementedError('call_handlers must be defined in a subclass.')
456 456
457 457 def input(self, string):
458 458 """Send a string of raw input to the kernel."""
459 459 content = dict(value=string)
460 460 msg = self.session.msg('input_reply', content)
461 461 self._queue_reply(msg)
462 462
463 463 def _handle_events(self, socket, events):
464 464 if events & POLLERR:
465 465 self._handle_err()
466 466 if events & POLLOUT:
467 467 self._handle_send()
468 468 if events & POLLIN:
469 469 self._handle_recv()
470 470
471 471 def _handle_recv(self):
472 472 msg = self.socket.recv_json()
473 473 self.call_handlers(msg)
474 474
475 475 def _handle_send(self):
476 476 try:
477 477 msg = self.msg_queue.get(False)
478 478 except Empty:
479 479 pass
480 480 else:
481 481 self.socket.send_json(msg)
482 482 if self.msg_queue.empty():
483 483 self.drop_io_state(POLLOUT)
484 484
485 485 def _handle_err(self):
486 486 # We don't want to let this go silently, so eventually we should log.
487 487 raise zmq.ZMQError()
488 488
489 489 def _queue_reply(self, msg):
490 490 self.msg_queue.put(msg)
491 491 self.add_io_state(POLLOUT)
492 492
493 493
494 494 class HBSocketChannel(ZmqSocketChannel):
495 495 """The heartbeat channel which monitors the kernel heartbeat."""
496 496
497 497 time_to_dead = 3.0
498 498 socket = None
499 499 poller = None
500 500
501 501 def __init__(self, context, session, address):
502 502 super(HBSocketChannel, self).__init__(context, session, address)
503 503 self._running = False
504 504
505 505 def _create_socket(self):
506 506 self.socket = self.context.socket(zmq.REQ)
507 507 self.socket.setsockopt(zmq.IDENTITY, self.session.session)
508 508 self.socket.connect('tcp://%s:%i' % self.address)
509 509 self.poller = zmq.Poller()
510 510 self.poller.register(self.socket, zmq.POLLIN)
511 511
512 512 def run(self):
513 513 """The thread's main activity. Call start() instead."""
514 514 self._create_socket()
515 515 self._running = True
516 516 # Wait 2 seconds for the kernel to come up and the sockets to auto
517 517 # connect. If we don't we will see the kernel as dead. Also, before
518 518 # the sockets are connected, the poller.poll line below is returning
519 519 # too fast. This avoids that because the polling doesn't start until
520 520 # after the sockets are connected.
521 521 time.sleep(2.0)
522 522 while self._running:
523 523 since_last_heartbeat = 0.0
524 524 request_time = time.time()
525 525 try:
526 526 #io.rprint('Ping from HB channel') # dbg
527 527 self.socket.send_json('ping')
528 528 except zmq.ZMQError, e:
529 529 #io.rprint('*** HB Error:', e) # dbg
530 530 if e.errno == zmq.EFSM:
531 531 #io.rprint('sleep...', self.time_to_dead) # dbg
532 532 time.sleep(self.time_to_dead)
533 533 self._create_socket()
534 534 else:
535 535 raise
536 536 else:
537 537 while True:
538 538 try:
539 539 self.socket.recv_json(zmq.NOBLOCK)
540 540 except zmq.ZMQError, e:
541 541 #io.rprint('*** HB Error 2:', e) # dbg
542 542 if e.errno == zmq.EAGAIN:
543 543 before_poll = time.time()
544 544 until_dead = self.time_to_dead - (before_poll -
545 545 request_time)
546 546
547 547 # When the return value of poll() is an empty list,
548 548 # that is when things have gone wrong (zeromq bug).
549 549 # As long as it is not an empty list, poll is
550 550 # working correctly even if it returns quickly.
551 551 # Note: poll timeout is in milliseconds.
552 552 self.poller.poll(1000*until_dead)
553 553
554 554 since_last_heartbeat = time.time() - request_time
555 555 if since_last_heartbeat > self.time_to_dead:
556 556 self.call_handlers(since_last_heartbeat)
557 557 break
558 558 else:
559 559 # FIXME: We should probably log this instead.
560 560 raise
561 561 else:
562 562 until_dead = self.time_to_dead - (time.time() -
563 563 request_time)
564 564 if until_dead > 0.0:
565 565 #io.rprint('sleep...', self.time_to_dead) # dbg
566 566 time.sleep(until_dead)
567 567 break
568 568
569 569 def stop(self):
570 570 self._running = False
571 571 super(HBSocketChannel, self).stop()
572 572
573 573 def call_handlers(self, since_last_heartbeat):
574 574 """This method is called in the ioloop thread when a message arrives.
575 575
576 576 Subclasses should override this method to handle incoming messages.
577 577 It is important to remember that this method is called in the thread
578 578 so that some logic must be done to ensure that the application leve
579 579 handlers are called in the application thread.
580 580 """
581 581 raise NotImplementedError('call_handlers must be defined in a subclass.')
582 582
583 583
584 584 #-----------------------------------------------------------------------------
585 585 # Main kernel manager class
586 586 #-----------------------------------------------------------------------------
587 587
588 588 class KernelManager(HasTraits):
589 589 """ Manages a kernel for a frontend.
590 590
591 591 The SUB channel is for the frontend to receive messages published by the
592 592 kernel.
593 593
594 594 The REQ channel is for the frontend to make requests of the kernel.
595 595
596 596 The REP channel is for the kernel to request stdin (raw_input) from the
597 597 frontend.
598 598 """
599 599 # The PyZMQ Context to use for communication with the kernel.
600 600 context = Instance(zmq.Context,(),{})
601 601
602 602 # The Session to use for communication with the kernel.
603 603 session = Instance(Session,(),{})
604 604
605 605 # The kernel process with which the KernelManager is communicating.
606 606 kernel = Instance(Popen)
607 607
608 608 # The addresses for the communication channels.
609 609 xreq_address = TCPAddress((LOCALHOST, 0))
610 610 sub_address = TCPAddress((LOCALHOST, 0))
611 611 rep_address = TCPAddress((LOCALHOST, 0))
612 612 hb_address = TCPAddress((LOCALHOST, 0))
613 613
614 614 # The classes to use for the various channels.
615 615 xreq_channel_class = Type(XReqSocketChannel)
616 616 sub_channel_class = Type(SubSocketChannel)
617 617 rep_channel_class = Type(RepSocketChannel)
618 618 hb_channel_class = Type(HBSocketChannel)
619 619
620 620 # Protected traits.
621 621 _launch_args = Any
622 622 _xreq_channel = Any
623 623 _sub_channel = Any
624 624 _rep_channel = Any
625 625 _hb_channel = Any
626 626
627 627 #--------------------------------------------------------------------------
628 628 # Channel management methods:
629 629 #--------------------------------------------------------------------------
630 630
631 631 def start_channels(self):
632 632 """Starts the channels for this kernel.
633 633
634 634 This will create the channels if they do not exist and then start
635 635 them. If port numbers of 0 are being used (random ports) then you
636 636 must first call :method:`start_kernel`. If the channels have been
637 637 stopped and you call this, :class:`RuntimeError` will be raised.
638 638 """
639 639 self.xreq_channel.start()
640 640 self.sub_channel.start()
641 641 self.rep_channel.start()
642 642 self.hb_channel.start()
643 643
644 644 def stop_channels(self):
645 645 """Stops the channels for this kernel.
646 646
647 647 This stops the channels by joining their threads. If the channels
648 648 were not started, :class:`RuntimeError` will be raised.
649 649 """
650 650 self.xreq_channel.stop()
651 651 self.sub_channel.stop()
652 652 self.rep_channel.stop()
653 653 self.hb_channel.stop()
654 654
655 655 @property
656 656 def channels_running(self):
657 657 """Are all of the channels created and running?"""
658 658 return self.xreq_channel.is_alive() \
659 659 and self.sub_channel.is_alive() \
660 660 and self.rep_channel.is_alive() \
661 661 and self.hb_channel.is_alive()
662 662
663 663 #--------------------------------------------------------------------------
664 664 # Kernel process management methods:
665 665 #--------------------------------------------------------------------------
666 666
667 667 def start_kernel(self, **kw):
668 668 """Starts a kernel process and configures the manager to use it.
669 669
670 670 If random ports (port=0) are being used, this method must be called
671 671 before the channels are created.
672 672
673 673 Parameters:
674 674 -----------
675 675 ipython : bool, optional (default True)
676 676 Whether to use an IPython kernel instead of a plain Python kernel.
677 677 """
678 678 xreq, sub, rep, hb = self.xreq_address, self.sub_address, \
679 679 self.rep_address, self.hb_address
680 680 if xreq[0] != LOCALHOST or sub[0] != LOCALHOST or \
681 681 rep[0] != LOCALHOST or hb[0] != LOCALHOST:
682 682 raise RuntimeError("Can only launch a kernel on localhost."
683 683 "Make sure that the '*_address' attributes are "
684 684 "configured properly.")
685 685
686 686 self._launch_args = kw.copy()
687 687 if kw.pop('ipython', True):
688 from ipkernel import launch_kernel as launch
688 from ipkernel import launch_kernel
689 689 else:
690 from pykernel import launch_kernel as launch
691 self.kernel, xrep, pub, req, hb = launch(
690 from pykernel import launch_kernel
691 self.kernel, xrep, pub, req, hb = launch_kernel(
692 692 xrep_port=xreq[1], pub_port=sub[1],
693 693 req_port=rep[1], hb_port=hb[1], **kw)
694 694 self.xreq_address = (LOCALHOST, xrep)
695 695 self.sub_address = (LOCALHOST, pub)
696 696 self.rep_address = (LOCALHOST, req)
697 697 self.hb_address = (LOCALHOST, hb)
698 698
699 def shutdown_kernel(self):
700 """ Attempts to the stop the kernel process cleanly. If the kernel
701 cannot be stopped, it is killed, if possible.
702 """
703 # Send quit message to kernel. Once we implement kernel-side setattr,
704 # this should probably be done that way, but for now this will do.
705 self.xreq_channel.execute('get_ipython().exit_now=True', silent=True)
706
707 # Don't send any additional kernel kill messages immediately, to give
708 # the kernel a chance to properly execute shutdown actions. Wait for at
709 # most 2s, checking every 0.1s.
710 for i in range(20):
711 if self.is_alive:
712 time.sleep(0.1)
713 else:
714 break
715 else:
716 # OK, we've waited long enough.
717 if self.has_kernel:
718 self.kill_kernel()
719
699 720 def restart_kernel(self):
700 721 """Restarts a kernel with the same arguments that were used to launch
701 722 it. If the old kernel was launched with random ports, the same ports
702 723 will be used for the new kernel.
703 724 """
704 725 if self._launch_args is None:
705 726 raise RuntimeError("Cannot restart the kernel. "
706 727 "No previous call to 'start_kernel'.")
707 728 else:
708 729 if self.has_kernel:
709 730 self.kill_kernel()
710 731 self.start_kernel(**self._launch_args)
711 732
712 733 @property
713 734 def has_kernel(self):
714 735 """Returns whether a kernel process has been specified for the kernel
715 736 manager.
716 737 """
717 738 return self.kernel is not None
718 739
719 740 def kill_kernel(self):
720 741 """ Kill the running kernel. """
721 742 if self.kernel is not None:
722 743 self.kernel.kill()
723 744 self.kernel = None
724 745 else:
725 746 raise RuntimeError("Cannot kill kernel. No kernel is running!")
726 747
727 748 def signal_kernel(self, signum):
728 749 """ Sends a signal to the kernel. """
729 750 if self.kernel is not None:
730 751 self.kernel.send_signal(signum)
731 752 else:
732 753 raise RuntimeError("Cannot signal kernel. No kernel is running!")
733 754
734 755 @property
735 756 def is_alive(self):
736 757 """Is the kernel process still running?"""
737 758 if self.kernel is not None:
738 759 if self.kernel.poll() is None:
739 760 return True
740 761 else:
741 762 return False
742 763 else:
743 764 # We didn't start the kernel with this KernelManager so we don't
744 765 # know if it is running. We should use a heartbeat for this case.
745 766 return True
746 767
747 768 #--------------------------------------------------------------------------
748 769 # Channels used for communication with the kernel:
749 770 #--------------------------------------------------------------------------
750 771
751 772 @property
752 773 def xreq_channel(self):
753 774 """Get the REQ socket channel object to make requests of the kernel."""
754 775 if self._xreq_channel is None:
755 776 self._xreq_channel = self.xreq_channel_class(self.context,
756 777 self.session,
757 778 self.xreq_address)
758 779 return self._xreq_channel
759 780
760 781 @property
761 782 def sub_channel(self):
762 783 """Get the SUB socket channel object."""
763 784 if self._sub_channel is None:
764 785 self._sub_channel = self.sub_channel_class(self.context,
765 786 self.session,
766 787 self.sub_address)
767 788 return self._sub_channel
768 789
769 790 @property
770 791 def rep_channel(self):
771 792 """Get the REP socket channel object to handle stdin (raw_input)."""
772 793 if self._rep_channel is None:
773 794 self._rep_channel = self.rep_channel_class(self.context,
774 795 self.session,
775 796 self.rep_address)
776 797 return self._rep_channel
777 798
778 799 @property
779 800 def hb_channel(self):
780 801 """Get the REP socket channel object to handle stdin (raw_input)."""
781 802 if self._hb_channel is None:
782 803 self._hb_channel = self.hb_channel_class(self.context,
783 804 self.session,
784 805 self.hb_address)
785 806 return self._hb_channel
General Comments 0
You need to be logged in to leave comments. Login now